mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2026-02-13 09:13:58 -05:00
Compare commits
188 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fd355d8cc | |||
| e6b57392d1 | |||
| 331c1eabeb | |||
| 167385bb99 | |||
| 11b9876d4f | |||
| 9cf72821b0 | |||
| 57adcce431 | |||
| 7fdb15f7e8 | |||
| 6e11e05a24 | |||
| a6751d3f41 | |||
| 8944e92ffc | |||
| f0c0a42c96 | |||
| a4b51457bb | |||
| fb4a883b43 | |||
| c24ec93335 | |||
| df49964bf9 | |||
| 502ba94465 | |||
| ff038fe5ae | |||
| 3eed122598 | |||
| d8d34540a9 | |||
| c01cc5ca97 | |||
| 6a2f7a1146 | |||
| f4c844f097 | |||
| 1135c69e1b | |||
| ef86d222cb | |||
| be88a601ae | |||
| e6c4e81df2 | |||
| f4a979672f | |||
| 8a4d64ffc0 | |||
| ac05d38763 | |||
| 548dc4fba8 | |||
| 7a73b7e8a9 | |||
| b38bd951f7 | |||
| 0e3318efdd | |||
| d063494bd2 | |||
| e35a2148eb | |||
| c8f115ae02 | |||
| d59e52b111 | |||
| 2c3548d1ac | |||
| d80007bc84 | |||
| 5d7d9a81bb | |||
| b030f85b74 | |||
| 2f0ee6a6d6 | |||
| 637cfa89b4 | |||
| c36f4c905b | |||
| 83fb20044d | |||
| 928e632dd3 | |||
| 8b9cf5066e | |||
| 45bfce87d8 | |||
| 6f33e9d23d | |||
| 92460392b9 | |||
| f1bdf59697 | |||
| 60e2f1d2b0 | |||
| 6c9ae82f81 | |||
| 42e4494f6e | |||
| 7e0b2c9d09 | |||
| 5ca68b01b6 | |||
| ebd4fa7363 | |||
| e6818958ae | |||
| 5b31459629 | |||
| 92de2928f6 | |||
| a8af180de2 | |||
| e621dc9e3a | |||
| c3d7c1c867 | |||
| defeec7f8e | |||
| 37c302e274 | |||
| 006e907b35 | |||
| f1f77a8022 | |||
| ff358d806e | |||
| b80e1d4e28 | |||
| f24128ef75 | |||
| d3a8c05f50 | |||
| f023cd246d | |||
| b7e18a3c3f | |||
| 67f2dbf4d8 | |||
| 924aa7657b | |||
| 16fe07f177 | |||
| 9257b2f938 | |||
| 0227681e92 | |||
| c034696810 | |||
| ffdabccd84 | |||
| 1f03908040 | |||
| 43a5317b4e | |||
| 4c49ec6890 | |||
| ef7faee685 | |||
| 02b48d2de4 | |||
| e670d99766 | |||
| 241dd594d0 | |||
| b603cb634a | |||
| 1308a05011 | |||
| 334ed60bf7 | |||
| d63bf809f2 | |||
| 31406af681 | |||
| 479be461a6 | |||
| c1af031d22 | |||
| a741cd0217 | |||
| 4ae9374401 | |||
| b096244454 | |||
| 4983cd661c | |||
| 5f6fb4af27 | |||
| 2f2c74403f | |||
| 43579d73e5 | |||
| a90d6b839f | |||
| e76f977ca8 | |||
| 7f821d241c | |||
| 1bc9227c7f | |||
| 3c2f1d0edd | |||
| 35e303d54b | |||
| 2aeb3fa028 | |||
| c85e45b544 | |||
| 6cd7825430 | |||
| 14f411c2e1 | |||
| 623510b474 | |||
| 20d9f0a84e | |||
| f741ce5dc9 | |||
| 72ec89292f | |||
| b54eb86b7f | |||
| f74f3ad72e | |||
| 0647b7708f | |||
| 7d644f0619 | |||
| b712c328ba | |||
| 5649ba05cd | |||
| bcdd515cf1 | |||
| 704dff2a72 | |||
| 55d00f9005 | |||
| eba3f529f8 | |||
| f0a3b0193c | |||
| 19733c3f8c | |||
| f22795ac90 | |||
| 166a9ee31b | |||
| 4d85c24872 | |||
| 43c7374c42 | |||
| 60857e9dca | |||
| d38f0d6ac1 | |||
| f6da031e72 | |||
| 9779437c00 | |||
| 1a37926628 | |||
| dac9a7c756 | |||
| 9ac1261ed0 | |||
| 9b69d3f728 | |||
| a5de879260 | |||
| 6464e1cbc6 | |||
| 7f3a94229a | |||
| 395e0117fb | |||
| e04d363e42 | |||
| 3b6c0d4a70 | |||
| d1f6ccd9cb | |||
| 74f7ba41df | |||
| 4fb424faa8 | |||
| 63218e7f42 | |||
| 7f0bb3cae7 | |||
| ad7417c233 | |||
| cf0be2336b | |||
| 6e08746611 | |||
| 7eb26facaf | |||
| 9115cc662c | |||
| 9e7c1dbfb2 | |||
| e99f5d2e52 | |||
| 039d1ca993 | |||
| dd9ac3c481 | |||
| 4f789080e7 | |||
| 80fc858a35 | |||
| 6f8d280657 | |||
| 5782cbc166 | |||
| 0729d2ac41 | |||
| 6c6de0ba86 | |||
| 11dbcaf80c | |||
| 95592e542f | |||
| 472bdec4fa | |||
| c7a313e9ed | |||
| c14b590083 | |||
| 040c920481 | |||
| 8c63817950 | |||
| e2f43d398f | |||
| 7ba4829066 | |||
| 938999db91 | |||
| 0b60a8e41b | |||
| 817a43e849 | |||
| 047296329e | |||
| c8cb74f3d4 | |||
| aceb6cb6b5 | |||
| a531ef4f87 | |||
| 1fae647381 | |||
| d1764e2203 | |||
| f830881883 | |||
| fb87e8a33a | |||
| 8bdec410c4 | |||
| dec9eee90b |
2
Gemfile
2
Gemfile
@@ -6,6 +6,8 @@ gem 'roxml'
|
||||
gem 'will_paginate'
|
||||
gem 'rails-jquery-autocomplete'
|
||||
gem 'jquery-ui-rails'
|
||||
gem 'rexml'
|
||||
gem 'combine_pdf'
|
||||
|
||||
group :assets do
|
||||
gem 'coffee-rails'
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 - 2023 Rick Barrette
|
||||
Copyright (c) 2016 - 2026 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
|
||||
|
||||
136
README.md
136
README.md
@@ -1,80 +1,110 @@
|
||||
# Redmine Quickbooks Online
|
||||
# Redmine QuickBooks Online
|
||||
|
||||
A 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 billable hours loged 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 billable hours logged when an Issue is closed.
|
||||
|
||||
#### Disclaimer
|
||||
## Disclaimer
|
||||
|
||||
Note: Although the core functionality is complete, this project is still under development & the master branch may be unstable. Tags should be stable and are recommended
|
||||
**Note:** Although the core functionality is complete, this project is still under development and the master branch may be unstable. Tags should be stable and are recommended.
|
||||
|
||||
Use tags for the following Redmine Versions
|
||||
* Version 2.0.0+ for Redmine 5+
|
||||
* Version 1.0.0+ for Redmine 4+
|
||||
* Version 0.8.1 for Redine 3
|
||||
## Compatibility
|
||||
|
||||
#### Features
|
||||
* Issues can be assigned to a Customer via drop down in the edit Issue form
|
||||
- Once a customer is attached to an Issue, you can attach an Estimate to the issue via a drop down menu
|
||||
* Employee is assigned to a user 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 billed agaist 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 dynamically lookup Items in Quickbooks.
|
||||
+ IF there isn't any Items that match the Activity name it will be skipped, and will not be billed to the Customer
|
||||
- Labor Rates are set by corresponding the Item in Quickbooks
|
||||
* Customers Can be created via the New Customer Page
|
||||
- Customers can be searched by name or phone number
|
||||
- Basic information for the Customer can be viewed/edit via the Customer page
|
||||
* 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
|
||||
| Plugin Version | Redmine Version |
|
||||
| :--- | :--- |
|
||||
| Version 2026.1.0+ | Redmine 6.1 |
|
||||
| Version 2.0.0+ | Redmine 5 |
|
||||
| Version 1.0.0+ | Redmine 4 |
|
||||
| Version 0.8.1 | Redmine 3 |
|
||||
|
||||
## Features
|
||||
|
||||
* **Customer Assignment:** Issues can be assigned to a Customer via a dropdown in the edit Issue form.
|
||||
* Once a customer is attached to an Issue, you can attach an Estimate to the issue via a dropdown menu.
|
||||
* **Employee Mapping:** An Employee is assigned to a Redmine User via a dropdown in the User Administration page.
|
||||
* **Automatic Billing:** If an Issue has been assigned a Customer, the following happens when the Issue is closed:
|
||||
* A new Time Activity will be billed against the Customer assigned to the issue for each Redmine Time Entry.
|
||||
* Time Entries will be totalled up by Activity name. This allows billing for different activities without having to create separate Issues.
|
||||
* The Time Activity names are used to dynamically lookup Items in QuickBooks.
|
||||
* If there are no Items that match the Activity name, it will be skipped and will not be billed to the Customer.
|
||||
* Labor Rates are set by the corresponding Item in QuickBooks.
|
||||
* **Customer Management:** Customers can be created via the New Customer Page.
|
||||
* Customers can be searched by name or phone number.
|
||||
* Basic information for the Customer can be viewed/edited via the Customer page.
|
||||
* **Webhook Support:**
|
||||
* **Invoices:** Automatically attached to an Issue if a line item contains a hashtag number (e.g., `#123`).
|
||||
* **Custom Fields:** Invoice Custom Fields are matched to Issue Custom Fields and are automatically updated in QuickBooks. (Useful for extracting Mileage In/Out from the Issue to update the Invoice).
|
||||
* **Sync:** Customers are automatically updated in the local database.
|
||||
* **Plugin View Hooks** Allows intergration of other features supported by companion plugins, for example [redmine_qbo_vehicles](https://github.com/rickbarrette/redmine_qbo_vehicles) adds customer vehicle interation
|
||||
|
||||
## 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
|
||||
* Sign up to become a developer for Intuit: https://developer.intuit.com/
|
||||
* Create your own application to obtain your API keys.
|
||||
* Set up the webhook service to `https://redmine.yourdomain.com/qbo/webhook`
|
||||
|
||||
## The Install
|
||||
## Installation
|
||||
|
||||
1. To install, clone this repo into your plugin folder & checkout a tagged version
|
||||
1. **Clone the plugin:**
|
||||
Clone this repo into your plugin folder and checkout a tagged version.
|
||||
```bash
|
||||
cd path/to/redmine/plugins
|
||||
git clone git@github.com:rickbarrette/redmine_qbo.git
|
||||
cd redmine_qbo
|
||||
git checkout <tag>
|
||||
```
|
||||
|
||||
`git clone git@github.com:rickbarrette/redmine_qbo.git`
|
||||
2. **Install dependencies:** *Crucial for Redmine 6 / Rails 7 compatibility.*
|
||||
|
||||
then
|
||||
Bash
|
||||
|
||||
`git checkout <tag>`
|
||||
```
|
||||
bundle install
|
||||
```
|
||||
|
||||
2. Migrate your database
|
||||
3. **Migrate your database:**
|
||||
|
||||
`rake redmine:plugins:migrate RAILS_ENV=production`
|
||||
Bash
|
||||
|
||||
3. Navigate to the plugin configuration page and suppy your own OAuth key & secret.
|
||||
```
|
||||
bundle exec rake redmine:plugins:migrate RAILS_ENV=production
|
||||
```
|
||||
|
||||
4. After saving your key & secret, you need to click on the Authenticate link on the plugin configuration page to authenticate with QBO.
|
||||
4. **Restart Redmine:** You must restart your Redmine server instance for the plugin and hooks to load.
|
||||
|
||||
5. **Configuration:**
|
||||
|
||||
* Navigate to the plugin configuration page (`Administration > Plugins > Configure`).
|
||||
|
||||
* Supply your own OAuth Key & Secret.
|
||||
|
||||
* After saving the Key & Secret, click the **Authenticate** link on the configuration page to connect to QBO.
|
||||
|
||||
6. **User Mapping:**
|
||||
|
||||
* Assign an Employee to each of your users via the **User Administration Page**.
|
||||
|
||||
5. Assign an Employee to each of your users via the User Administration Page
|
||||
|
||||
## Usage
|
||||
|
||||
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.
|
||||
To enable automatic Time Activity entries for an Issue, you simply need to assign a Customer to an Issue via the dropdowns in the issue creation/update form.
|
||||
|
||||
Note: After the inital synchronization, this plugin will recieve push notifications via Intuit's webhook service.
|
||||
**Note:** After the initial synchronization, this plugin will receive push notifications via Intuit's webhook service.
|
||||
|
||||
## TODO
|
||||
* Add Setting for Sandbox Mode
|
||||
* MORE Stuff as I make it up...
|
||||
## Companion Plugin Hooks
|
||||
* :pdf_left, { issue: issue }
|
||||
* :pdf_right, { issue: issue }
|
||||
* :process_invoice_custom_fields, { issue: issue, invoice: invoice }
|
||||
* :show_customer_view_right, {customer: @customer}
|
||||
|
||||
## License
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 - 2022 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.
|
||||
> The MIT License (MIT)
|
||||
>
|
||||
> Copyright (c) 2016 - 2026 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.
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
# This controller class will handle map management
|
||||
class CustomersController < ApplicationController
|
||||
unloadable
|
||||
|
||||
include AuthHelper
|
||||
helper :issues
|
||||
@@ -27,13 +26,13 @@ class CustomersController < ApplicationController
|
||||
include SortHelper
|
||||
helper :timelog
|
||||
|
||||
before_action :add_customer, :only => :new
|
||||
before_action :view_customer, :except => [:new, :view]
|
||||
skip_before_action :verify_authenticity_token, :check_if_login_required, :only => [:view]
|
||||
before_action :add_customer, only: :new
|
||||
before_action :view_customer, except: [:new, :view]
|
||||
skip_before_action :verify_authenticity_token, :check_if_login_required, only: [:view]
|
||||
|
||||
default_search_scope :names
|
||||
|
||||
autocomplete :customer, :name, :full => true, :extra_data => [:id]
|
||||
autocomplete :customer, :name, full: true, extra_data: [:id]
|
||||
|
||||
def allowed_params
|
||||
params.require(:customer).permit(:name, :email, :primary_phone, :mobile_phone, :phone_number, :notes)
|
||||
@@ -54,7 +53,7 @@ class CustomersController < ApplicationController
|
||||
# display a list of all customers
|
||||
def index
|
||||
if params[:search]
|
||||
@customers = Customer.search(params[:search]).paginate(:page => params[:page])
|
||||
@customers = Customer.search(params[:search]).paginate(page: params[:page])
|
||||
if only_one_non_zero?(@customers)
|
||||
redirect_to @customers.first
|
||||
end
|
||||
@@ -70,7 +69,7 @@ class CustomersController < ApplicationController
|
||||
def create
|
||||
@customer = Customer.new(allowed_params)
|
||||
if @customer.save
|
||||
flash[:notice] = "New Customer Created"
|
||||
flash[:notice] = t :notice_customer_created
|
||||
redirect_to @customer
|
||||
else
|
||||
flash[:error] = @customer.errors.full_messages.to_sentence
|
||||
@@ -91,6 +90,7 @@ class CustomersController < ApplicationController
|
||||
@issues.open.each { |i| @hours+= i.total_spent_hours }
|
||||
@closed_issues.each { |i| @closed_hours+= i.total_spent_hours }
|
||||
rescue
|
||||
flash[:error] = t :notice_customer_not_found
|
||||
render_404
|
||||
end
|
||||
end
|
||||
@@ -100,6 +100,7 @@ class CustomersController < ApplicationController
|
||||
begin
|
||||
@customer = Customer.find_by_id(params[:id])
|
||||
rescue
|
||||
flash[:error] = t :notice_customer_not_found
|
||||
render_404
|
||||
end
|
||||
end
|
||||
@@ -109,13 +110,14 @@ class CustomersController < ApplicationController
|
||||
begin
|
||||
@customer = Customer.find_by_id(params[:id])
|
||||
if @customer.update(allowed_params)
|
||||
flash[:notice] = "Customer updated"
|
||||
flash[:notice] = t :notice_customer_updated
|
||||
redirect_to @customer
|
||||
else
|
||||
redirect_to edit_customer_path
|
||||
flash[:error] = @customer.errors.full_messages.to_sentence if @customer.errors
|
||||
end
|
||||
rescue
|
||||
flash[:error] = t :notice_customer_not_found
|
||||
render_404
|
||||
end
|
||||
end
|
||||
@@ -124,9 +126,10 @@ class CustomersController < ApplicationController
|
||||
def destroy
|
||||
begin
|
||||
Customer.find_by_id(params[:id]).destroy
|
||||
flash[:notice] = "Customer deleted successfully"
|
||||
flash[:notice] = t :notice_customer_deleted
|
||||
redirect_to action: :index
|
||||
rescue
|
||||
flash[:error] = t :notice_customer_not_deleted
|
||||
render_404
|
||||
end
|
||||
end
|
||||
@@ -135,7 +138,7 @@ class CustomersController < ApplicationController
|
||||
def share
|
||||
|
||||
Thread.new do
|
||||
logger.debug "Removing expired customer tokens"
|
||||
logger.info "Removing expired customer tokens"
|
||||
CustomerToken.remove_expired_tokens
|
||||
ActiveRecord::Base.connection.close
|
||||
end
|
||||
@@ -144,6 +147,7 @@ class CustomersController < ApplicationController
|
||||
issue = Issue.find_by_id(params[:id])
|
||||
redirect_to view_path issue.share_token.token
|
||||
rescue
|
||||
flash[:error] = t :notice_issue_not_found
|
||||
render_404
|
||||
end
|
||||
end
|
||||
@@ -162,7 +166,7 @@ class CustomersController < ApplicationController
|
||||
@issue = Issue.find @token.issue_id
|
||||
@journals = @issue.journals.
|
||||
preload(:details).
|
||||
preload(:user => :email_address).
|
||||
preload(user: :email_address).
|
||||
reorder(:created_on, :id).to_a
|
||||
@journals.each_with_index {|j,i| j.indice = i+1}
|
||||
@journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
|
||||
@@ -176,9 +180,10 @@ class CustomersController < ApplicationController
|
||||
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
|
||||
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
|
||||
@priorities = IssuePriority.active
|
||||
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
|
||||
@time_entry = TimeEntry.new(issue: @issue, project: @issue.project)
|
||||
@relation = IssueRelation.new
|
||||
rescue
|
||||
flash[:error] = t :notice_forbidden
|
||||
render_403
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -8,14 +8,22 @@
|
||||
#
|
||||
#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 EstimateController < ApplicationController
|
||||
unloadable
|
||||
|
||||
include AuthHelper
|
||||
|
||||
before_action :require_user, :unless => proc {|c| session[:token].nil? }
|
||||
skip_before_action :verify_authenticity_token, :check_if_login_required, :unless => proc {|c| session[:token].nil? }
|
||||
before_action :require_user, unless: proc {|c| session[:token].nil? }
|
||||
skip_before_action :verify_authenticity_token, :check_if_login_required, unless: proc {|c| session[:token].nil? }
|
||||
|
||||
def get_estimate
|
||||
# Force sync for estimate by doc number if not found
|
||||
if Estimate.find_by_doc_number(params[:search]).nil?
|
||||
begin
|
||||
Estimate.sync_by_doc_number(params[:search]) if params[:search]
|
||||
rescue
|
||||
logger.info "Estimate.find_by_doc_number failed"
|
||||
end
|
||||
end
|
||||
|
||||
estimate = Estimate.find_by_id(params[:id]) if params[:id]
|
||||
estimate = Estimate.find_by_doc_number(params[:search]) if params[:search]
|
||||
return estimate
|
||||
@@ -28,9 +36,9 @@ class EstimateController < ApplicationController
|
||||
estimate = get_estimate
|
||||
|
||||
begin
|
||||
send_data estimate.pdf, filename: "estimate #{estimate.doc_number}.pdf", :disposition => 'inline', :type => "application/pdf"
|
||||
send_data estimate.pdf, filename: "estimate #{estimate.doc_number}.pdf", disposition: :inline, type: "application/pdf"
|
||||
rescue
|
||||
redirect_to :back, :flash => { :error => "Estimate not found" }
|
||||
redirect_to :back, flash: { error: I18n.t(:notice_estimate_not_found) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,9 +49,9 @@ class EstimateController < ApplicationController
|
||||
estimate = get_estimate
|
||||
|
||||
begin
|
||||
send_data estimate.pdf, filename: "estimate #{estimate.doc_number}.pdf", :disposition => 'inline', :type => "application/pdf"
|
||||
send_data estimate.pdf, filename: "estimate #{estimate.doc_number}.pdf", disposition: :inline, type: "application/pdf"
|
||||
rescue
|
||||
redirect_to :back, :flash => { :error => "Estimate not found" }
|
||||
redirect_to :back, flash: { error: I18n.t(:notice_estimate_not_found) }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2023 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -8,27 +8,47 @@
|
||||
#
|
||||
#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 InvoiceController < ApplicationController
|
||||
unloadable
|
||||
|
||||
include AuthHelper
|
||||
require 'combine_pdf'
|
||||
|
||||
before_action :require_user, :unless => proc {|c| session[:token].nil? }
|
||||
skip_before_action :verify_authenticity_token, :check_if_login_required, :unless => proc {|c| session[:token].nil? }
|
||||
before_action :require_user, unless: proc {|c| session[:token].nil? }
|
||||
skip_before_action :verify_authenticity_token, :check_if_login_required, unless: proc {|c| session[:token].nil? }
|
||||
|
||||
#
|
||||
# Downloads and forwards the invoice pdf
|
||||
#
|
||||
def show
|
||||
logger.info("Processing request for URL: #{request.original_url}")
|
||||
begin
|
||||
qbo = Qbo.first
|
||||
qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
service = Quickbooks::Service::Invoice.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
|
||||
# If multiple id's then pull each pdf & combine them
|
||||
if params[:invoice_ids]
|
||||
logger.info("Grabbing pdfs for " + params[:invoice_ids].join(', '))
|
||||
ref = ""
|
||||
params[:invoice_ids].each do |i|
|
||||
logger.info("processing " + i)
|
||||
invoice = service.fetch_by_id(i)
|
||||
ref += " #{invoice.doc_number}"
|
||||
@pdf << CombinePDF.parse(service.pdf(invoice)) unless @pdf.nil?
|
||||
if @pdf.nil?
|
||||
@pdf = CombinePDF.parse(service.pdf(invoice))
|
||||
end
|
||||
end
|
||||
@pdf = @pdf.to_pdf
|
||||
else
|
||||
invoice = service.fetch_by_id(params[:id])
|
||||
@pdf = service.pdf(invoice)
|
||||
send_data @pdf, filename: "invoice #{invoice.doc_number}.pdf", :disposition => 'inline', :type => "application/pdf"
|
||||
ref = invoice.doc_number
|
||||
end
|
||||
|
||||
send_data @pdf, filename: "invoice #{ref}.pdf", disposition: :inline, type: "application/pdf"
|
||||
end
|
||||
rescue
|
||||
redirect_to :back, :flash => { :error => "Invoice not found" }
|
||||
redirect_to :back, flash: { error: I18n.t(:notice_invoice_not_found) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2023 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -9,14 +9,13 @@
|
||||
#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 QboController < ApplicationController
|
||||
unloadable
|
||||
|
||||
require 'openssl'
|
||||
|
||||
include AuthHelper
|
||||
|
||||
before_action :require_user, :except => :webhook
|
||||
skip_before_action :verify_authenticity_token, :check_if_login_required, :only => [:webhook]
|
||||
before_action :require_user, except: :webhook
|
||||
skip_before_action :verify_authenticity_token, :check_if_login_required, only: [:webhook]
|
||||
|
||||
def allowed_params
|
||||
params.permit(:code, :state, :realmId, :id)
|
||||
@@ -26,9 +25,10 @@ class QboController < ApplicationController
|
||||
# Called when the user requests that Redmine to connect to QBO
|
||||
#
|
||||
def authenticate
|
||||
redirect_uri = "#{Setting.protocol}://#{Setting.host_name + qbo_oauth_callback_path}"
|
||||
logger.info "redirect_uri: #{redirect_uri}"
|
||||
oauth2_client = Qbo.construct_oauth2_client
|
||||
callback = Setting.host_name + "/qbo/oauth_callback/"
|
||||
grant_url = oauth2_client.auth_code.authorize_url(redirect_uri: callback, response_type: "code", state: SecureRandom.hex(12), scope: "com.intuit.quickbooks.accounting")
|
||||
grant_url = oauth2_client.auth_code.authorize_url(redirect_uri: redirect_uri, response_type: "code", state: SecureRandom.hex(12), scope: "com.intuit.quickbooks.accounting")
|
||||
redirect_to grant_url
|
||||
end
|
||||
|
||||
@@ -39,7 +39,7 @@ class QboController < ApplicationController
|
||||
if params[:state].present?
|
||||
oauth2_client = Qbo.construct_oauth2_client
|
||||
# use the state value to retrieve from your backend any information you need to identify the customer in your system
|
||||
redirect_uri = Setting.host_name + "/qbo/oauth_callback/"
|
||||
redirect_uri = "#{Setting.protocol}://#{Setting.host_name + qbo_oauth_callback_path}"
|
||||
if resp = oauth2_client.auth_code.get_token(params[:code], redirect_uri: redirect_uri)
|
||||
|
||||
# Remove the last authentication information
|
||||
@@ -51,9 +51,9 @@ class QboController < ApplicationController
|
||||
qbo.refresh_token!
|
||||
|
||||
if qbo.save!
|
||||
redirect_to qbo_sync_path, :flash => { :notice => "Successfully connected to Quickbooks" }
|
||||
redirect_to qbo_sync_path, flash: { notice: I18n.t(:label_connected) }
|
||||
else
|
||||
redirect_to plugin_settings_path(:redmine_qbo), :flash => { :error => "Error" }
|
||||
redirect_to plugin_settings_path(:redmine_qbo), flash: { error: I18n.t(:label_error) }
|
||||
end
|
||||
|
||||
end
|
||||
@@ -65,9 +65,9 @@ class QboController < ApplicationController
|
||||
i = Issue.find_by_id params[:id]
|
||||
if i.customer
|
||||
i.bill_time
|
||||
redirect_to i, :flash => { :notice => "Successfully Billed #{i.customer.name}" }
|
||||
redirect_to i, flash: { notice: I18n.t(:label_billed_success) + i.customer.name }
|
||||
else
|
||||
redirect_to i, :flash => { :error => "Cannot bill without a customer assigned" }
|
||||
redirect_to i, flash: { error: I18n.t(:label_billing_error) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -84,6 +84,7 @@ class QboController < ApplicationController
|
||||
|
||||
# proceed if the request is good
|
||||
if hash.eql? signature
|
||||
Thread.new do
|
||||
if request.headers['content-type'] == 'application/json'
|
||||
data = JSON.parse(data)
|
||||
else
|
||||
@@ -96,7 +97,7 @@ class QboController < ApplicationController
|
||||
id = entity['id'].to_i
|
||||
name = entity['name']
|
||||
|
||||
logger.debug "Casting #{name.constantize} to obj"
|
||||
logger.info "Casting #{name.constantize} to obj"
|
||||
|
||||
# Magicly initialize the correct class
|
||||
obj = name.constantize
|
||||
@@ -121,9 +122,10 @@ class QboController < ApplicationController
|
||||
|
||||
# Record that last time we updated
|
||||
Qbo.update_time_stamp
|
||||
|
||||
ActiveRecord::Base.connection.close
|
||||
end
|
||||
# The webhook doesn't require a response but let's make sure we don't send anything
|
||||
render :nothing => true
|
||||
render nothing: true, status: 200
|
||||
else
|
||||
render nothing: true, status: 400
|
||||
end
|
||||
@@ -150,6 +152,6 @@ class QboController < ApplicationController
|
||||
ActiveRecord::Base.connection.close
|
||||
end
|
||||
|
||||
redirect_to :home, :flash => { :notice => "Syncing Quickbooks" }
|
||||
redirect_to :home, flash: { notice: I18n.t(:label_syncing) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2017 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -13,6 +13,7 @@ module AuthHelper
|
||||
def require_user
|
||||
return unless session[:token].nil?
|
||||
if !User.current.logged?
|
||||
flash[:error] = t :notice_forbidden
|
||||
render_403
|
||||
end
|
||||
end
|
||||
@@ -27,6 +28,7 @@ module AuthHelper
|
||||
|
||||
def check_permission(permission)
|
||||
if !allowed_to?(permission)
|
||||
flash[:error] = t :notice_forbidden
|
||||
render_403
|
||||
end
|
||||
end
|
||||
@@ -34,6 +36,7 @@ module AuthHelper
|
||||
|
||||
def global_check_permission(permission)
|
||||
if !globaly_allowed_to?(permission)
|
||||
flash[:error] = t :notice_forbidden
|
||||
render_403
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2023 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -11,9 +11,6 @@
|
||||
module QuickbooksOauth
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
OAUTH_CONSUMER_KEY = Setting.plugin_redmine_qbo['settingsOAuthConsumerKey']
|
||||
OAUTH_CONSUMER_SECRET = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret']
|
||||
|
||||
#== Instance Methods
|
||||
|
||||
def perform_authenticated_request(&block)
|
||||
@@ -21,7 +18,7 @@ module QuickbooksOauth
|
||||
begin
|
||||
yield oauth_access_token
|
||||
rescue OAuth2::Error, Quickbooks::AuthorizationFailure => ex
|
||||
Rails.logger.info("QuickbooksOauth.perform: #{ex.message}")
|
||||
Rails.logger.error("QuickbooksOauth.perform: #{ex.message}")
|
||||
|
||||
# to prevent an infinite loop here keep a counter and bail out after N times...
|
||||
attempts += 1
|
||||
@@ -36,6 +33,7 @@ module QuickbooksOauth
|
||||
end
|
||||
|
||||
def refresh_token!
|
||||
Rails.logger.info("QuickbooksOauth.refresh_token!")
|
||||
t = oauth_access_token
|
||||
refreshed = t.refresh!
|
||||
|
||||
@@ -45,6 +43,8 @@ module QuickbooksOauth
|
||||
oauth2_refresh_token_expires_at = 100.days.from_now
|
||||
end
|
||||
|
||||
Rails.logger.info("QuickbooksOauth.refresh_token!: #{oauth2_refresh_token_expires_at}")
|
||||
|
||||
update!(
|
||||
oauth2_access_token: refreshed.token,
|
||||
oauth2_access_token_expires_at: Time.at(refreshed.expires_at),
|
||||
@@ -68,12 +68,20 @@ module QuickbooksOauth
|
||||
module ClassMethods
|
||||
|
||||
def construct_oauth2_client
|
||||
|
||||
oauth_consumer_key = Setting.plugin_redmine_qbo['settingsOAuthConsumerKey']
|
||||
oauth_consumer_secret = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret']
|
||||
|
||||
# Are we are playing in the sandbox?
|
||||
Quickbooks.sandbox_mode = Setting.plugin_redmine_qbo[:sandbox] ? true : false
|
||||
logger.info "Sandbox mode: #{Quickbooks.sandbox_mode}"
|
||||
|
||||
options = {
|
||||
site: "https://appcenter.intuit.com/connect/oauth2",
|
||||
authorize_url: "https://appcenter.intuit.com/connect/oauth2",
|
||||
token_url: "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer"
|
||||
}
|
||||
OAuth2::Client.new(OAUTH_CONSUMER_KEY, OAUTH_CONSUMER_SECRET, options)
|
||||
OAuth2::Client.new(oauth_consumer_key, oauth_consumer_secret, options)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2023 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -9,10 +9,8 @@
|
||||
#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 Customer < ActiveRecord::Base
|
||||
unloadable
|
||||
|
||||
has_many :issues
|
||||
has_many :purchases
|
||||
has_many :invoices
|
||||
has_many :estimates
|
||||
|
||||
@@ -144,7 +142,7 @@ class Customer < ActiveRecord::Base
|
||||
# Sync ALL customers if the database is empty
|
||||
qbo = Qbo.first
|
||||
customers = qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
service = Quickbooks::Service::Customer.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
service.all
|
||||
end
|
||||
|
||||
@@ -154,11 +152,13 @@ class Customer < ActiveRecord::Base
|
||||
logger.info "Processing customer #{c.id}"
|
||||
customer = Customer.find_or_create_by(id: c.id)
|
||||
if c.active?
|
||||
if not customer.name.eql? c.display_name
|
||||
#if not customer.name.eql? c.display_name
|
||||
customer.name = c.display_name
|
||||
customer.id = c.id
|
||||
customer.phone_number = c.primary_phone.free_form_number.tr('^0-9', '') unless c.primary_phone.nil?
|
||||
customer.mobile_phone_number = c.mobile_phone.free_form_number.tr('^0-9', '') unless c.mobile_phone.nil?
|
||||
customer.save_without_push
|
||||
end
|
||||
#end
|
||||
else
|
||||
if not c.new_record?
|
||||
customer.delete
|
||||
@@ -177,20 +177,22 @@ class Customer < ActiveRecord::Base
|
||||
# This needs to be simplified
|
||||
def self.sync_by_id(id)
|
||||
qbo = Qbo.first
|
||||
customer = qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
c = qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Customer.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
service.fetch_by_id(id)
|
||||
end
|
||||
|
||||
return unless customer
|
||||
return unless c
|
||||
|
||||
customer = Customer.find_or_create_by(id: customer.id)
|
||||
if customer.active?
|
||||
if not customer.name.eql? customer.display_name
|
||||
customer.name = customer.display_name
|
||||
customer.id = customer.id
|
||||
customer = Customer.find_or_create_by(id: c.id)
|
||||
if c.active?
|
||||
#if not customer.name.eql? c.display_name
|
||||
customer.name = c.display_name
|
||||
customer.id = c.id
|
||||
customer.phone_number = c.primary_phone.free_form_number.tr('^0-9', '') unless c.primary_phone.nil?
|
||||
customer.mobile_phone_number = c.mobile_phone.free_form_number.tr('^0-9', '') unless c.mobile_phone.nil?
|
||||
customer.save_without_push
|
||||
end
|
||||
#end
|
||||
else
|
||||
if not customer.new_record?
|
||||
customer.delete
|
||||
@@ -203,8 +205,8 @@ class Customer < ActiveRecord::Base
|
||||
begin
|
||||
qbo = Qbo.first
|
||||
@details = qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
serivce.update(@details)
|
||||
service = Quickbooks::Service::Customer.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
service.update(@details)
|
||||
end
|
||||
#raise "QBO Fault" if @details.fault?
|
||||
self.id = @details.id
|
||||
@@ -225,7 +227,7 @@ class Customer < ActiveRecord::Base
|
||||
raise Exception unless self.id
|
||||
qbo = Qbo.first
|
||||
@details = qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
service = Quickbooks::Service::Customer.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
service.fetch_by_id(self.id)
|
||||
end
|
||||
rescue Exception => e
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -9,7 +9,7 @@
|
||||
#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 CustomerToken < ActiveRecord::Base
|
||||
unloadable
|
||||
|
||||
has_many :issues
|
||||
validates_presence_of :issue_id
|
||||
before_create :generate_token, :generate_expire_date
|
||||
@@ -55,7 +55,7 @@ class CustomerToken < ActiveRecord::Base
|
||||
end
|
||||
|
||||
# only create new token if we have an issue to attach it to
|
||||
return create(:issue_id => issue.id) if User.current.logged?
|
||||
return create(issue_id: issue.id) if User.current.logged?
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2023 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -9,21 +9,20 @@
|
||||
#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 Employee < ActiveRecord::Base
|
||||
unloadable
|
||||
|
||||
has_many :users
|
||||
validates_presence_of :id, :name
|
||||
|
||||
def self.sync
|
||||
qbo = Qbo.first
|
||||
employees = qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Employee.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
service = Quickbooks::Service::Employee.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
service.all
|
||||
end
|
||||
|
||||
return unless employees
|
||||
|
||||
transaction do
|
||||
# Update the item table
|
||||
employees.each { |e|
|
||||
logger.info "Processing employee #{e.id}"
|
||||
employee = find_or_create_by(id: e.id)
|
||||
@@ -37,7 +36,7 @@ class Employee < ActiveRecord::Base
|
||||
def self.sync_by_id(id)
|
||||
qbo = Qbo.first
|
||||
employee = qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Employee.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
service = Quickbooks::Service::Employee.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
service.fetch_by_id(id)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2023 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -9,19 +9,23 @@
|
||||
#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 Estimate < ActiveRecord::Base
|
||||
unloadable
|
||||
|
||||
has_and_belongs_to_many :issues
|
||||
belongs_to :customer
|
||||
validates_presence_of :doc_number, :id
|
||||
self.primary_key = :id
|
||||
|
||||
# returns a human readable string
|
||||
def to_s
|
||||
return self[:doc_number]
|
||||
end
|
||||
|
||||
# sync all estimates
|
||||
def self.sync
|
||||
logger.debug "Syncing ALL estimates"
|
||||
logger.info "Syncing ALL estimates"
|
||||
qbo = Qbo.first
|
||||
estimates = qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
service = Quickbooks::Service::Estimate.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
service.all
|
||||
end
|
||||
|
||||
@@ -37,20 +41,29 @@ class Estimate < ActiveRecord::Base
|
||||
|
||||
# sync only one estimate
|
||||
def self.sync_by_id(id)
|
||||
logger.debug "Syncing estimate #{id}"
|
||||
logger.info "Syncing estimate #{id}"
|
||||
qbo = Qbo.first
|
||||
qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
service = Quickbooks::Service::Estimate.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
process_estimate(service.fetch_by_id(id))
|
||||
end
|
||||
end
|
||||
|
||||
# sync only one estimate
|
||||
def self.sync_by_doc_number(number)
|
||||
logger.info "Syncing estimate by doc number #{number}"
|
||||
qbo = Qbo.first
|
||||
qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Estimate.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
process_estimate(service.find_by( :doc_number, number).first)
|
||||
end
|
||||
end
|
||||
|
||||
# update an estimate
|
||||
def self.update(id)
|
||||
# Update the item table
|
||||
qbo = Qbo.first
|
||||
estimate = qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
service = Quickbooks::Service::Estimate.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
service.fetch_by_id(id)
|
||||
end
|
||||
|
||||
@@ -77,7 +90,7 @@ class Estimate < ActiveRecord::Base
|
||||
def pdf
|
||||
qbo = Qbo.first
|
||||
qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
service = Quickbooks::Service::Estimate.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
estimate = service.fetch_by_id(id)
|
||||
service.pdf(estimate)
|
||||
end
|
||||
@@ -109,7 +122,7 @@ class Estimate < ActiveRecord::Base
|
||||
raise Exception unless self.id
|
||||
qbo = Qbo.first
|
||||
@details = qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
service = Quickbooks::Service::Estimate.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
service(:estimate).fetch_by_id(self.id)
|
||||
end
|
||||
rescue Exception => e
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2023 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -9,15 +9,20 @@
|
||||
#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 Invoice < ActiveRecord::Base
|
||||
unloadable
|
||||
|
||||
has_and_belongs_to_many :issues
|
||||
belongs_to :customer
|
||||
validates_presence_of :doc_number, :id, :customer_id, :txn_date
|
||||
self.primary_key = :id
|
||||
|
||||
# returns a human readable string
|
||||
def to_s
|
||||
return self[:doc_number]
|
||||
end
|
||||
|
||||
# sync ALL the invoices
|
||||
def self.sync
|
||||
logger.debug "Syncing all invoices"
|
||||
logger.info "Syncing all invoices"
|
||||
last = Qbo.first.last_sync
|
||||
|
||||
query = "SELECT Id, DocNumber FROM Invoice"
|
||||
@@ -27,7 +32,7 @@ class Invoice < ActiveRecord::Base
|
||||
# .all() is never called since count is never initialized
|
||||
qbo = Qbo.first
|
||||
invoices = qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
service = Quickbooks::Service::Invoice.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
service.all
|
||||
end
|
||||
|
||||
@@ -40,10 +45,10 @@ class Invoice < ActiveRecord::Base
|
||||
|
||||
#sync by invoice ID
|
||||
def self.sync_by_id(id)
|
||||
logger.debug "Syncing invoice #{id}"
|
||||
logger.info "Syncing invoice #{id}"
|
||||
qbo = Qbo.first
|
||||
qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
service = Quickbooks::Service::Invoice.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
invoice = service.fetch_by_id(id)
|
||||
process_invoice invoice
|
||||
end
|
||||
@@ -58,7 +63,7 @@ class Invoice < ActiveRecord::Base
|
||||
# skip this issue if the issue customer is not the same as the invoice customer
|
||||
return if issue.customer_id != invoice.customer_ref.value.to_i
|
||||
|
||||
logger.debug "Attaching invoice #{invoice.id} to issue #{issue.id}"
|
||||
logger.info "Attaching invoice #{invoice.id} to issue #{issue.id}"
|
||||
|
||||
invoice = Invoice.find_or_create_by(id: invoice.id)
|
||||
|
||||
@@ -105,23 +110,22 @@ class Invoice < ActiveRecord::Base
|
||||
# this condions causes an infinite loop as the webhook is called when an invoice is updated
|
||||
# TODO maybe add a cf_sync_confict flag to invoices
|
||||
def self.compare_custom_fields(issue, invoice)
|
||||
|
||||
logger.debug "Comparing custom fields"
|
||||
logger.info "Comparing custom fields"
|
||||
# TODO break if Invoice.find(invoice.id).cf_sync_confict
|
||||
is_changed = false
|
||||
|
||||
logger.debug "Calling :process_invoice_custom_fields hook"
|
||||
context = Redmine::Hook.call_hook :process_invoice_custom_fields, { issue: issue, invoice: invoice }
|
||||
|
||||
unless context.nil?
|
||||
logger.debug "We have a context!"
|
||||
context= context.first
|
||||
issue = context[:issue]
|
||||
invoice = context[:invoice]
|
||||
is_changed = context[:is_changed]
|
||||
# Process updates from the hooks
|
||||
context.each do |c|
|
||||
unless c.nil?
|
||||
logger.debug "Invoice.compare_custom_fields: We have a responce from a hook"
|
||||
push_updates c[:invoice] if c[:is_changed]
|
||||
end
|
||||
end
|
||||
|
||||
# Custom Values
|
||||
# Process Issue Custom Values
|
||||
begin
|
||||
value = issue.custom_values.find_by(custom_field_id: CustomField.find_by_name(cf.name).id)
|
||||
|
||||
@@ -138,13 +142,17 @@ class Invoice < ActiveRecord::Base
|
||||
# Nothing to do here, there is no match
|
||||
end
|
||||
|
||||
# Push updates
|
||||
push_updates invoice if is_changed
|
||||
end
|
||||
|
||||
# pushes invoice updates
|
||||
def self.push_updates(invoice)
|
||||
begin
|
||||
logger.debug "Trying to update invoice"
|
||||
logger.info "Invoice.push_updates"
|
||||
qbo = Qbo.first
|
||||
qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
service.update(invoice) if is_changed
|
||||
service = Quickbooks::Service::Invoice.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
service.update invoice
|
||||
end
|
||||
rescue
|
||||
# Do nothing, probaly custome field sync confict on the invoice.
|
||||
@@ -155,6 +163,16 @@ class Invoice < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
# download the pdf from quickbooks
|
||||
def pdf
|
||||
qbo = Qbo.first
|
||||
qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Invoice.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
invoice = service.fetch_by_id(id)
|
||||
return service.pdf(invoice)
|
||||
end
|
||||
end
|
||||
|
||||
# Magic Method
|
||||
# Maps Get/Set methods to QBO invoice object
|
||||
def method_missing(sym, *arguments)
|
||||
@@ -179,7 +197,7 @@ class Invoice < ActiveRecord::Base
|
||||
raise Exception unless self.id
|
||||
qbo = Qbo.first
|
||||
@details = qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
service = Quickbooks::Service::Invoice.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
service.fetch_by_id(self.id)
|
||||
end
|
||||
rescue Exception => e
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2023 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -9,9 +9,9 @@
|
||||
#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 Qbo < ActiveRecord::Base
|
||||
unloadable
|
||||
|
||||
include QuickbooksOauth
|
||||
include Redmine::I18n
|
||||
|
||||
# Updates last sync time stamp
|
||||
def self.update_time_stamp
|
||||
|
||||
11
app/views/customers/_actions.html.erb
Normal file
11
app/views/customers/_actions.html.erb
Normal file
@@ -0,0 +1,11 @@
|
||||
<%= link_to t(:label_appointment), "https://calendar.google.com/calendar/render?action=TEMPLATE&text=#{@customer.name}+-&details=#{ link_to t(:customer_details), "https://#{Setting.host_name}#{customer_path @customer.id}"}%0A#{@customer.primary_phone}%3Cbr/%3E+&dates=#{Time.now.strftime("%Y%m%d")}T090000/#{Time.now.strftime("%Y%m%d")}T170000", target: :_blank, id: :appointment_link %>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<%= link_to t(:label_create_estimate), "https://qbo.intuit.com/app/estimate?nameId=#{@customer.id}", target: :_blank %>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<%= button_to t(:label_edit_customer), edit_customer_path(@customer), method: :get%>
|
||||
@@ -1,5 +1,11 @@
|
||||
<table>
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<th><%=t(:label_name)%></th>
|
||||
<td><%= customer.name %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th><%=t(:label_email)%></th>
|
||||
<td><%= customer.email %></td>
|
||||
@@ -31,13 +37,24 @@
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th><%=t(:field_notes)%></th>
|
||||
<td><%= customer.notes %></td>
|
||||
<th colspan="2"><h4><%=t(:field_notes)%></hr></th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<pre id="note-display" style="text-align: left; white-space: pre-wrap; font-family: inherit;">
|
||||
<%= customer.notes %>
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<script>
|
||||
const preElement = document.getElementById('note-display');
|
||||
// This takes the text, trims the edges, and puts it back
|
||||
preElement.textContent = preElement.textContent.trim();
|
||||
</script>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="float: right;">
|
||||
<%= button_to t(:label_edit_customer), edit_customer_path(customer), method: :get%>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
@@ -7,28 +7,28 @@
|
||||
<div class="clearfix">
|
||||
<%=t(:label_display_name)%>
|
||||
<div class="input">
|
||||
<%= f.text_field :name, :required => true, :autocomplete => "off" %>
|
||||
<%= f.text_field :name, required: true, autocomplete: "off" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix">
|
||||
<%=t(:label_primary_phone)%>
|
||||
<div class="input">
|
||||
<%= f.telephone_field :primary_phone, :autocomplete => "off" %>
|
||||
<%= f.telephone_field :primary_phone, autocomplete: "off" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix">
|
||||
<%=t(:label_mobile_phone)%>:
|
||||
<div class="input">
|
||||
<%= f.telephone_field :mobile_phone, :autocomplete => "off" %>
|
||||
<%= f.telephone_field :mobile_phone, autocomplete: "off" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix">
|
||||
<%=t(:label_email)%>:
|
||||
<div class="input">
|
||||
<%= f.email_field :email, :autocomplete => "off" %>
|
||||
<%= f.email_field :email, autocomplete: "off" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,17 +36,16 @@
|
||||
<%=t(:field_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 @customer.new_record? %>
|
||||
<%= content_tag 'span', :id => "issue_description_and_toolbar", :style => (@customer.new_record? ? nil : 'display:none') do %>
|
||||
<%= content_tag :span, id: "issue_description_and_toolbar" do %>
|
||||
<%= f.text_area :notes,
|
||||
:cols => 60,
|
||||
:rows => 10,
|
||||
:accesskey => accesskey(:edit),
|
||||
:class => 'wiki-edit',
|
||||
:no_label => true %>
|
||||
cols: 60,
|
||||
rows: 10,
|
||||
accesskey: accesskey(:edit),
|
||||
class: 'wiki-edit',
|
||||
no_label: true %>
|
||||
<% end %>
|
||||
</p>
|
||||
<%= wikitoolbar_for 'issue_description' %>
|
||||
<%= wikitoolbar_for :issue_description %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<%= form_tag(customers_path, :method => "get", id: "search-form") do %>
|
||||
<%= text_field_tag :search, params[:search], placeholder: t(:label_search_customers), :autocomplete => "off" %>
|
||||
<%= form_tag(customers_path, method: "get", id: "search-form") do %>
|
||||
<%= text_field_tag :search, params[:search], placeholder: t(:label_search_customers), autocomplete: "off" %>
|
||||
<%= submit_tag t(:label_search) %>
|
||||
<% end %>
|
||||
<%= button_to t(:label_new_customer), new_customer_path, method: :get%>
|
||||
<%= button_to(t(:label_sync), qbo_sync_path, method: :get) if User.current.admin?%>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
<h3><%=t(:label_customers)%></h3>
|
||||
<%= render :partial => 'customers/search' %>
|
||||
<%= render partial: 'customers/search' %>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<h1><%=t(:label_edit_customer)%></h1>
|
||||
<br/>
|
||||
<%= render :partial => 'customers/form' %>
|
||||
<%= render partial: 'customers/form' %>
|
||||
|
||||
@@ -1 +1 @@
|
||||
$('select#issue_estimate_id').html('<%= j content_tag(:option,'',:value=>"")+options_from_collection_for_select(@filtered_estimates, :id, :doc_number) %>');
|
||||
$('select#issue_estimate_id').html('<%= j content_tag(:option,'',value:"")+options_from_collection_for_select(@filtered_estimates, :id, :doc_number) %>');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<h2><%=t(:field_customers)%> <span style="float:right"> <%= render :partial => 'customers/search' %> </span> </h2>
|
||||
<h2><%=t(:field_customers)%> <span style="float:right"> <%= render partial: 'customers/search' %> </span> </h2>
|
||||
<% if @customers.present? %>
|
||||
<br/>
|
||||
<% @customers.each do |c| %>
|
||||
@@ -20,5 +20,5 @@
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= render :partial => 'qbo/stats' %>
|
||||
<%= render partial: 'qbo/stats' %>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<h2><%=t(:label_new_customer)%></h2>
|
||||
<br/>
|
||||
<%= render :partial => 'customers/form' %>
|
||||
<%= render partial: 'customers/form' %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<h2><%=t(:field_customer)%> #<%= @customer.id %> - <%= @customer.name %> </h2>
|
||||
<h2><%=t(:field_customer)%> #<%= @customer.id %> - <%= link_to @customer.to_s, "https://#{Setting.plugin_redmine_qbo[:sandbox] ? "sandbox" : "app"}.qbo.intuit.com/app/customerdetail?nameId=#{@customer.id}", target: :_blank %> </h2>
|
||||
<div class="issue">
|
||||
|
||||
<div class="splitcontent">
|
||||
@@ -7,24 +7,39 @@
|
||||
|
||||
<h4><%=t(:label_details)%>:</h4>
|
||||
|
||||
<%= render :partial => 'customers/details', locals: {customer: @customer} %>
|
||||
<!-- Customer Info -->
|
||||
|
||||
<div class="splitcontent">
|
||||
<div class="splitcontentleft">
|
||||
<h4><%=t(:field_customer)%>:</h4>
|
||||
<%= render partial: 'customers/details', locals: {customer: @customer} %>
|
||||
</div>
|
||||
|
||||
<div class="splitcontentleft">
|
||||
<h4><%=t(:label_actions)%>:</h4>
|
||||
<%= render partial: 'customers/actions', locals: {customer: @customer} %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- QBO Info -->
|
||||
|
||||
<div class="splitcontent">
|
||||
<div class="splitcontentleft">
|
||||
<h4><%=t(:estimates)%>:</h4>
|
||||
<%= render :partial => 'estimates/list', locals: {customer: @customer} %>
|
||||
<%= render partial: 'estimates/list', locals: {estimates: @customer.estimates} %>
|
||||
</div>
|
||||
|
||||
<div class="splitcontentleft">
|
||||
<h4><%=t(:label_invoices)%>:</h4>
|
||||
<%= render :partial => 'invoices/list', locals: {customer: @customer} %>
|
||||
<%= render partial: 'invoices/list', locals: {invoices: @customer.invoices} %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="splitcontentleft">
|
||||
<% call_hook :show_customer_view_right %>
|
||||
<%= call_hook :show_customer_view_right, {customer: @customer} %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +47,7 @@
|
||||
|
||||
<br/>
|
||||
<h3><%=@issues.open.count%> <%=t(:label_open_issues)%> - <%=@hours.round(1)%> <%=t(:label_hours)%></h3>
|
||||
<%= render :partial => 'issues/list_simple', locals: {issues: @issues.open} %>
|
||||
<%= render partial: 'issues/list_simple', locals: {issues: @issues.open} %>
|
||||
|
||||
<h3><%=@closed_issues.count%> <%=t(:label_closed_issues)%> - <%= @closed_hours.round(1)%> <%=t(:label_hours)%></h3>
|
||||
<%= render :partial => 'issues/list_simple', locals: {issues: @closed_issues} %>
|
||||
<%= render partial: 'issues/list_simple', locals: {issues: @closed_issues} %>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<div class="<%= @issue.css_classes %> details">
|
||||
|
||||
<%= avatar(@issue.author, :size => "50") %>
|
||||
<%= avatar(@issue.author, size: "50") %>
|
||||
|
||||
<div class="subject">
|
||||
<%= render_issue_subject_with_tree(@issue) %>
|
||||
@@ -19,39 +19,39 @@
|
||||
|
||||
<div class="attributes">
|
||||
<%= issue_fields_rows do |rows|
|
||||
rows.left l(:field_status), @issue.status.name, :class => 'status'
|
||||
rows.left l(:field_priority), @issue.priority.name, :class => 'priority'
|
||||
unless @issue.disabled_core_fields.include?('assigned_to_id')
|
||||
rows.left l(:field_assigned_to), avatar(@issue.assigned_to, :size => "14").to_s.html_safe + (@issue.assigned_to ? @issue.assigned_to : "-"), :class => 'assigned-to'
|
||||
rows.left l(:field_status), @issue.status.name, class: :status
|
||||
rows.left l(:field_priority), @issue.priority.name, class: :priority
|
||||
# unless @issue.disabled_core_fields.include?(:assigned_to_id)
|
||||
# rows.left l(:field_assigned_to), avatar(@issue.assigned_to, size: "14").to_s.html_safe + (@issue.assigned_to ? @issue.assigned_to : "-"), class: 'assigned-to'
|
||||
# end
|
||||
unless @issue.disabled_core_fields.include?(:category_id) || (@issue.category.nil? && @issue.project.issue_categories.none?)
|
||||
rows.left l(:field_category), (@issue.category ? @issue.category.name : "-"), class: :category
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('category_id') || (@issue.category.nil? && @issue.project.issue_categories.none?)
|
||||
rows.left l(:field_category), (@issue.category ? @issue.category.name : "-"), :class => 'category'
|
||||
unless @issue.disabled_core_fields.include?(:fixed_version_id) || (@issue.fixed_version.nil? && @issue.assignable_versions.none?)
|
||||
rows.left l(:field_fixed_version), (@issue.fixed_version ? @issue.fixed_version : "-"), class: 'fixed-version'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('fixed_version_id') || (@issue.fixed_version.nil? && @issue.assignable_versions.none?)
|
||||
rows.left l(:field_fixed_version), (@issue.fixed_version ? @issue.fixed_version : "-"), :class => 'fixed-version'
|
||||
unless @issue.disabled_core_fields.include?(:start_date)
|
||||
rows.right l(:field_start_date), format_date(@issue.start_date), class: 'start-date'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('start_date')
|
||||
rows.right l(:field_start_date), format_date(@issue.start_date), :class => 'start-date'
|
||||
unless @issue.disabled_core_fields.include?(:due_date)
|
||||
rows.right l(:field_due_date), format_date(@issue.due_date), class: 'due-date'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('due_date')
|
||||
rows.right l(:field_due_date), format_date(@issue.due_date), :class => 'due-date'
|
||||
unless @issue.disabled_core_fields.include?(:done_ratio)
|
||||
rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, legend: "#{@issue.done_ratio}%"), class: :progress
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('done_ratio')
|
||||
rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :legend => "#{@issue.done_ratio}%"), :class => 'progress'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('estimated_hours')
|
||||
unless @issue.disabled_core_fields.include?(:estimated_hours)
|
||||
if @issue.estimated_hours.present? || @issue.total_estimated_hours.to_f > 0
|
||||
rows.right l(:field_estimated_hours), issue_estimated_hours_details(@issue), :class => 'estimated-hours'
|
||||
rows.right l(:field_estimated_hours), issue_estimated_hours_details(@issue), class: 'estimated-hours'
|
||||
end
|
||||
end
|
||||
#if User.current.allowed_to_view_all_time_entries?(@project)
|
||||
if @issue.total_spent_hours > 0
|
||||
rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time'
|
||||
rows.right l(:label_spent_time), issue_spent_hours_details(@issue), class: 'spent-time'
|
||||
end
|
||||
#end
|
||||
end %>
|
||||
<%= render_full_width_custom_fields_rows(@issue) %>
|
||||
<%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
|
||||
<%= call_hook(:view_issues_show_details_bottom, issue: @issue) %>
|
||||
</div>
|
||||
|
||||
<% if @issue.description? || @issue.attachments.any? -%>
|
||||
@@ -59,19 +59,19 @@ end %>
|
||||
<% if @issue.description? %>
|
||||
<div class="description">
|
||||
<div class="contextual">
|
||||
<%= link_to l(:button_quote), quoted_issue_path(@issue), :remote => true, :method => 'post', :class => 'icon icon-comment' if @issue.notes_addable? %>
|
||||
<%= link_to l(:button_quote), quoted_issue_path(@issue), remote: true, method: :post, class: 'icon icon-comment' if @issue.notes_addable? %>
|
||||
</div>
|
||||
|
||||
<p><strong><%=l(:field_description)%></strong></p>
|
||||
<div class="wiki">
|
||||
<%= textilizable @issue, :description, :attachments => @issue.attachments %>
|
||||
<%= textilizable @issue, :description, attachments: @issue.attachments %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= link_to_attachments @issue, :thumbnails => true %>
|
||||
<%= link_to_attachments @issue, thumbnails: true %>
|
||||
<% end -%>
|
||||
|
||||
<%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
|
||||
<%= call_hook(:view_issues_show_description_bottom, issue: @issue) %>
|
||||
|
||||
<% if !@issue.leaf? || User.current.allowed_to?(:manage_subtasks, @project) %>
|
||||
<hr />
|
||||
@@ -87,7 +87,7 @@ end %>
|
||||
<% if @relations.present? || User.current.allowed_to?(:manage_issue_relations, @project) %>
|
||||
<hr />
|
||||
<div id="relations">
|
||||
<%= render :partial => 'issues/relations' %>
|
||||
<%= render partial: 'issues/relations' %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -96,14 +96,14 @@ end %>
|
||||
<% if @changesets.present? %>
|
||||
<div id="issue-changesets">
|
||||
<h3><%=l(:label_associated_revisions)%></h3>
|
||||
<%= render :partial => 'issues/changesets', :locals => { :changesets => @changesets} %>
|
||||
<%= render partial: 'issues/changesets', locals: { changesets: @changesets} %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @journals.present? %>
|
||||
<div id="history">
|
||||
<h3><%=l(:label_history)%></h3>
|
||||
<%= render :partial => 'issues/history', :locals => { :issue => @issue, :journals => @journals } %>
|
||||
<%= render partial: 'issues/history', locals: { issue: @issue, journals: @journals } %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<% if @customer.present? %>
|
||||
<% unless estimates.empty? %>
|
||||
|
||||
<% @customer.estimates.order(doc_number: :desc).each do |estimate| %>
|
||||
<% estimates.sort.reverse.each do |estimate| %>
|
||||
<div class="row">
|
||||
<%= check_box_tag "estimate_ids[]", estimate.id, false, onchange: "updateLink()", data: { url: estimate_path(estimate), text: "Estimate ##{estimate.to_s}" }, class: "estimate-checkbox appointment" %>
|
||||
<b><%= link_to "##{estimate.doc_number}", estimate_path(estimate), target: :_blank %></b> <%= estimate.txn_date %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%= form_tag(estimate_doc_path, :method => "get") do %>
|
||||
<%= text_field_tag :search, params[:search], placeholder: t(:label_search_estimates), :autocomplete => "off" %>
|
||||
<%= submit_tag t(:label_search), :formtarget => "_blank" %>
|
||||
<%= form_tag(estimate_doc_path, method: "get") do %>
|
||||
<%= text_field_tag :search, params[:search], placeholder: t(:label_search_estimates), autocomplete: "off" %>
|
||||
<%= submit_tag t(:label_search), formtarget: "_blank" %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
<h3><%=t(:label_estimates) %></h3>
|
||||
<%= render :partial => 'estimates/search' %>
|
||||
<%= render partial: 'estimates/search' %>
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
<% if @customer.present? %>
|
||||
<% unless invoices.empty? %>
|
||||
|
||||
<% @customer.invoices.order(doc_number: :desc).each do |invoice| %>
|
||||
<%= form_with(url: invoice_path, method: :get) do |form| %>
|
||||
|
||||
<% if invoices.count > 1 %>
|
||||
<div class="form-check">
|
||||
<%= check_box_tag "select-all-invoices", "1", false, id: "select-all-invoices" %>
|
||||
<%= label_tag "select-all-invoices", t(:label_select_all) %>
|
||||
</div>
|
||||
<hr>
|
||||
<% end %>
|
||||
|
||||
<% invoices.sort.reverse.each do |invoice| %>
|
||||
<div class="row">
|
||||
<%= check_box_tag "invoice_ids[]", invoice.id, false, onchange: "updateLink()", data: { url: invoice_path(invoice), text: "Invoice ##{invoice.to_s}" }, class: "invoice-checkbox appointment" if invoices.count > 1 %>
|
||||
<b><%= link_to "##{invoice.doc_number}", invoice_path(invoice), target: :_blank %></b> <%= invoice.txn_date %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if invoices.count > 1 %>
|
||||
<%= form.submit t(:button_bulk_pdf) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% else %>
|
||||
<p><%=t(:label_no_invoices)%>.</p>
|
||||
<% end %>
|
||||
|
||||
@@ -2,14 +2,8 @@
|
||||
<label for="issue_customer"><%= t(:customer) %></label>
|
||||
<%= search_customer %>
|
||||
<%= customer_id %>
|
||||
<%= link_to_function(t(:label_load_customer), "updateIssueFrom('/issues/#{context[:issue].id}/edit.js', this)") %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= select_estimate %>
|
||||
</p>
|
||||
|
||||
|
||||
<%# <p> %>
|
||||
<%# <%= call_hook here %> %>
|
||||
<%# </p> %>
|
||||
@@ -3,12 +3,12 @@
|
||||
<div id="change-<%= journal.id %>" class="<%= journal.css_classes %>">
|
||||
<div id="note-<%= journal.indice %>">
|
||||
<div class="contextual">
|
||||
<span class="journal-actions"><%= render_journal_actions(issue, journal, :reply_links => reply_links) %></span>
|
||||
<span class="journal-actions"><%= render_journal_actions(issue, journal, reply_links: reply_links) %></span>
|
||||
<a href="#note-<%= journal.indice %>" class="journal-link">#<%= journal.indice %></a>
|
||||
</div>
|
||||
<h4>
|
||||
<%= avatar(journal.user, :size => "24") %>
|
||||
<%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %>
|
||||
<%= avatar(journal.user, size: "24") %>
|
||||
<%= authoring journal.created_on, journal.user, label: :label_updated_time_by %>
|
||||
<%= render_private_notes_indicator(journal) %>
|
||||
</h4>
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= render_notes(issue, journal, :reply_links => reply_links) unless journal.notes.blank? %>
|
||||
<%= render_notes(issue, journal, reply_links: reply_links) unless journal.notes.blank? %>
|
||||
</div>
|
||||
</div>
|
||||
<%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %>
|
||||
<%= call_hook(:view_issues_history_journal_bottom, { journal: journal }) %>
|
||||
<% end %>
|
||||
|
||||
<% heads_for_wiki_formatter if User.current.allowed_to?(:edit_issue_notes, issue.project) || User.current.allowed_to?(:edit_own_issue_notes, issue.project) %>
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<% for issue in issues %>
|
||||
<tr id="issue-<%= h(issue.id) %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
|
||||
<tr id="issue-<%= h(issue.id) %>" class="hascontextmenu <%= cycle(:odd, :even) %> <%= issue.css_classes %>">
|
||||
<td class="id">
|
||||
<%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;', :id => nil) %>
|
||||
<%= check_box_tag("ids[]", issue.id, false, style: 'display:none;', id: nil) %>
|
||||
<%= link_to(issue.id, issue_path(issue)) %>
|
||||
</td>
|
||||
<td class="project"><%= link_to_project(issue.project) %></td>
|
||||
|
||||
@@ -17,6 +17,6 @@
|
||||
</div>
|
||||
|
||||
<div class="splitcontentleft">
|
||||
<%# call_hook here %>
|
||||
<%= call_hook :show_issue_view_right, {issue: issue} %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
3
app/views/qbo/_footer.html.erb
Normal file
3
app/views/qbo/_footer.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
"<div id='footer' align='center'>
|
||||
<b><%=I18n.translate(:label_last_sync)%>: </b> <%=Qbo.last_sync if Qbo.exists?%>
|
||||
</div>"
|
||||
@@ -1,7 +1,7 @@
|
||||
<!--
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2022 rick barrette
|
||||
Copyright (c) 2016 - 2026 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:
|
||||
|
||||
@@ -15,7 +15,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
|
||||
<!-- 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: '<%= Setting.host_name %>/qbo/authenticate'});
|
||||
intuit.ipp.anywhere.setup({menuProxy: '/path/to/blue-dot', grantUrl: '<%= qbo_authenticate_path %>'});
|
||||
</script>
|
||||
|
||||
<table >
|
||||
@@ -57,6 +57,13 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th><%=t(:label_sandbox)%></th>
|
||||
<td>
|
||||
<%= check_box_tag 'settings[sandbox]', @settings[:sandbox], @settings[:sandbox] %>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th><%=t(:label_oauth_expires)%></th>
|
||||
<td><%= if Qbo.exists? then Qbo.first.oauth2_access_token_expires_at end %>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<% if User.current.logged? %>
|
||||
|
||||
<%= render :partial => 'customers/sidebar' %>
|
||||
<%= render :partial => 'estimates/sidebar' %>
|
||||
<%= render partial: 'customers/sidebar' %>
|
||||
<%= render partial: 'estimates/sidebar' %>
|
||||
|
||||
<% end %>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<%= Customer.count %> <%=t(:field_customers)%> - <%= render :partial => 'qbo/last_sync' %>
|
||||
<%= Customer.count %> <%=t(:field_customers)%> - <%= render partial: 'qbo/last_sync' %>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!--
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 rick barrette
|
||||
Copyright (c) 2016 - 2026 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:
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!--
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 rick barrette
|
||||
Copyright (c) 2016 - 2026 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:
|
||||
|
||||
|
||||
13
app/views/qbo/webhook.erb
Normal file
13
app/views/qbo/webhook.erb
Normal file
@@ -0,0 +1,13 @@
|
||||
<!--
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 - 2026 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.
|
||||
-->
|
||||
|
||||
<h2>QboController#webhook</h2>
|
||||
@@ -1,11 +1,22 @@
|
||||
$(function() {
|
||||
$("input#issue_customer_id").on("change", function() {
|
||||
function updateLink() {
|
||||
console.log("updateLink called");
|
||||
const linkElement = document.getElementById("appointment_link");
|
||||
const regex = /((?:<br\/>|%3Cbr\/?%3E))([\s\S]*?)(&dates)/gi;
|
||||
linkElement.href = linkElement.href.replace(regex, `$1${getSelectedDocs()}$3`);
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: "/filter_estimates_by_customer",
|
||||
type: "GET",
|
||||
data: { selected_customer: $("input#issue_customer_id").val() }
|
||||
});
|
||||
});
|
||||
function getSelectedDocs() {
|
||||
const appointent_extras = document.querySelectorAll('.appointment');
|
||||
|
||||
});
|
||||
let output = '';
|
||||
for (const item of appointent_extras) {
|
||||
if (item.checked) {
|
||||
console.log(`Checked item: ${item.dataset.text} with URL: ${item.dataset.url}`);
|
||||
output += `%0A`+ encodeURIComponent(`<a href="${window.location.origin}${item.dataset.url}">${item.dataset.text}</a>`) +`%0A`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// You can return the array or use it as needed
|
||||
return output;
|
||||
}
|
||||
|
||||
17
assets/javascripts/checkbox_controller.js
Normal file
17
assets/javascripts/checkbox_controller.js
Normal file
@@ -0,0 +1,17 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const select_all_invoice = document.getElementById('select-all-invoices');
|
||||
const invoices = document.querySelectorAll('.invoice-checkbox');
|
||||
|
||||
if (select_all_invoice) {
|
||||
select_all_invoice.addEventListener('change', (e) => {
|
||||
invoices.forEach(item => item.checked = e.target.checked);
|
||||
});
|
||||
}
|
||||
|
||||
invoices.forEach(item => {
|
||||
item.addEventListener('change', () => {
|
||||
const allChecked = Array.from(invoices).every(i => i.checked);
|
||||
select_all_invoice.checked = allChecked;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -12,7 +12,6 @@
|
||||
# Usage I18n.t(:label)
|
||||
en:
|
||||
field_customer: "Customer"
|
||||
field_item: "Item"
|
||||
field_employee: "Employee"
|
||||
field_invoice: "Invoice"
|
||||
field_estimate: "Estimate"
|
||||
@@ -54,7 +53,6 @@ en:
|
||||
label_customer_count: "Customer Count"
|
||||
label_invoice_count: "Invoice Count"
|
||||
label_estimate_count: "Estimate Count"
|
||||
label_item_count: "Item Count"
|
||||
label_employee_count: "Employee Count"
|
||||
label_client_id: "Intuit QBO OAuth2 Client ID"
|
||||
label_client_secret: "Intuit QBO OAuth2 Client Secret"
|
||||
@@ -80,4 +78,27 @@ en:
|
||||
label_qbo_sync_success: "Successfully synced to Quickbooks"
|
||||
label_hours: "Hours"
|
||||
label_oauth2_refresh_token_expires_at: "Refresh Token Expires At"
|
||||
|
||||
label_name: "Name"
|
||||
label_appointment: "Add Appointment"
|
||||
label_actions: "Actions"
|
||||
label_create_estimate: "Create Estimate"
|
||||
label_syncing: "Syncing Quickbooks"
|
||||
label_sandbox: "Sandbox"
|
||||
button_bulk_pdf: "Bulk PDF"
|
||||
label_select_all: "Select All"
|
||||
notice_customer_created: "Customer created in Quickbooks"
|
||||
notice_customer_updated: "Customer updated in Quickbooks"
|
||||
notice_customer_not_found: "Customer not found in Quickbooks"
|
||||
notice_customer_not_deleted: "Customer could not be deleted in Quickbooks"
|
||||
notice_customer_deleted: "Customer deleted in Quickbooks"
|
||||
notice_estimate_created: "Estimate created in Quickbooks"
|
||||
notice_estimate_updated: "Estimate updated in Quickbooks"
|
||||
notice_estimate_not_found: "Estimate not found"
|
||||
notice_invoice_created: "Invoice created in Quickbooks"
|
||||
notice_invoice_updated: "Invoice updated in Quickbooks"
|
||||
notice_invoice_not_found: "Invoice not found"
|
||||
notice_forbidden: "You do not have permission to access this resource"
|
||||
notice_issue_not_found: "Issue not found"
|
||||
customer_details: "Customer Details"
|
||||
notice_error_project_nil: "The issue's project is nil, set project to: "
|
||||
notice_error_tracker_nil: "The issue's tracker is nil, set tracker to: "
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -9,31 +9,31 @@
|
||||
#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.
|
||||
|
||||
#authentication
|
||||
get 'qbo/authenticate', :to => 'qbo#authenticate'
|
||||
get 'qbo/oauth_callback', :to => 'qbo#oauth_callback'
|
||||
get 'qbo/authenticate', to: 'qbo#authenticate'
|
||||
get 'qbo/oauth_callback', to: 'qbo#oauth_callback'
|
||||
|
||||
#manual sync
|
||||
get 'qbo/sync', :to => 'qbo#sync'
|
||||
get 'qbo/sync', to: 'qbo#sync'
|
||||
|
||||
#webhook
|
||||
post 'qbo/webhook', :to => 'qbo#webhook'
|
||||
post 'qbo/webhook', to: 'qbo#webhook'
|
||||
|
||||
# Estimate & Invoice PDF
|
||||
get 'estimates/:id', :to => 'estimate#show', as: :estimate
|
||||
get 'estimates/doc/', :to => 'estimate#doc', as: :estimate_doc
|
||||
get 'invoices/:id', :to => 'invoice#show', as: :invoice
|
||||
get 'estimates/:id', to: 'estimate#show', as: :estimate
|
||||
get 'estimates/doc/', to: 'estimate#doc', as: :estimate_doc
|
||||
get 'invoices/:id', to: 'invoice#show', as: :invoice
|
||||
|
||||
#manual billing
|
||||
get 'bill/:id', :to => 'qbo#bill', as: :bill
|
||||
get 'bill/:id', to: 'qbo#bill', as: :bill
|
||||
|
||||
#customer issue view
|
||||
get 'customers/view/:token', :to => 'customers#view', as: :view
|
||||
get 'customers/share/:id', :to => 'customers#share', as: :share
|
||||
get 'customers/view/:token', to: 'customers#view', as: :view
|
||||
get 'customers/share/:id', to: 'customers#share', as: :share
|
||||
|
||||
#java script routes
|
||||
get 'filter_estimates_by_customer' => 'customers#filter_estimates_by_customer'
|
||||
get 'filter_invoices_by_customer' => 'customers#filter_invoices_by_customer'
|
||||
|
||||
resources :customers do
|
||||
get :autocomplete_customer_name, :on => :collection
|
||||
get :autocomplete_customer_name, on: :collection
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -11,7 +11,7 @@
|
||||
class CreateQboCustomers < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
create_table :qbo_customers, id: false do |t|
|
||||
t.integer :id, :options => 'PRIMARY KEY'
|
||||
t.integer :id, options: 'PRIMARY KEY'
|
||||
t.string :name
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -11,7 +11,7 @@
|
||||
class CreateQboItems < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
create_table :qbo_items, id: false do |t|
|
||||
t.integer :id, :options => 'PRIMARY KEY'
|
||||
t.integer :id, options: 'PRIMARY KEY'
|
||||
t.string :name
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -11,7 +11,7 @@
|
||||
class CreateQboEmployees < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
create_table :qbo_employees, id: false do |t|
|
||||
t.integer :id, :options => 'PRIMARY KEY'
|
||||
t.integer :id, options: 'PRIMARY KEY'
|
||||
t.string :name
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -10,6 +10,6 @@
|
||||
|
||||
class UpdateTimeEntries < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
add_column :time_entries, :qbo_billed, :boolean, :default => false
|
||||
add_column :time_entries, :qbo_billed, :boolean, default: false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -11,7 +11,7 @@
|
||||
class CreateQboEstimates < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
create_table :qbo_estimates, id: false do |t|
|
||||
t.integer :id, :options => 'PRIMARY KEY'
|
||||
t.integer :id, options: 'PRIMARY KEY'
|
||||
t.string :doc_number
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -11,7 +11,7 @@
|
||||
class CreateQboInvoices < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
create_table :qbo_invoices, id: false do |t|
|
||||
t.integer :id, :options => 'PRIMARY KEY'
|
||||
t.integer :id, options: 'PRIMARY KEY'
|
||||
t.string :doc_number
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -11,7 +11,7 @@
|
||||
class CreateQboPurchases< ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
create_table :qbo_purchases, id: false do |t|
|
||||
t.integer :id, :options => 'PRIMARY KEY'
|
||||
t.integer :id, options: 'PRIMARY KEY'
|
||||
t.integer :line_id
|
||||
t.string :description
|
||||
t.integer :customer_id
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -10,12 +10,12 @@
|
||||
|
||||
class AddIssuesQboInvoices < ActiveRecord::Migration[5.1]
|
||||
def self.up
|
||||
create_table :issues_qbo_invoices, :id => false do |t|
|
||||
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
|
||||
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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 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 UpdateProjects < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
add_reference :projects, :customer, index: true
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
#The License
|
||||
#
|
||||
#Copyright (c) 2022 Rick Barrette - All Rights Reserved
|
||||
#Copyright (c) 2016 - 2026 Rick Barrette - All Rights Reserved
|
||||
#
|
||||
#Unauthorized copying of this software and associated documentation files (the "Software"), via any medium is strictly prohibited.
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2023 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -9,7 +9,9 @@
|
||||
#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 AddTxnDates < ActiveRecord::Migration[5.1]
|
||||
|
||||
def change
|
||||
begin
|
||||
add_column :qbo_invoices, :txn_date, :date
|
||||
add_column :qbo_estimates, :txn_date, :date
|
||||
|
||||
@@ -28,7 +30,7 @@ class AddTxnDates < ActiveRecord::Migration[5.1]
|
||||
|
||||
qbo = Qbo.first
|
||||
invoices = qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
service = Quickbooks::Service::Invoice.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
service.all
|
||||
end
|
||||
|
||||
@@ -45,6 +47,9 @@ class AddTxnDates < ActiveRecord::Migration[5.1]
|
||||
}
|
||||
}
|
||||
end
|
||||
rescue
|
||||
logger.error "AddTxnDates Failed"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2023 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
|
||||
49
init.rb
49
init.rb
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2023 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -8,46 +8,41 @@
|
||||
#
|
||||
#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.
|
||||
|
||||
# Dynamically load all Hooks & Patches
|
||||
ActiveSupport::Reloader.to_prepare do
|
||||
Dir::foreach(File.join(File.dirname(__FILE__), 'lib')) do |file|
|
||||
next unless /\.rb$/ =~ file
|
||||
require_dependency file
|
||||
end
|
||||
end
|
||||
|
||||
Redmine::Plugin.register :redmine_qbo do
|
||||
|
||||
# About
|
||||
name 'Redmine Quickbooks Online plugin'
|
||||
name 'Redmine QBO 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 '2.0.2'
|
||||
description 'A pluging for Redmine to connect with QuickBooks Online to create Time Activity Entries for billable hours logged when an Issue is closed'
|
||||
version '2026.2.7'
|
||||
url 'https://github.com/rickbarrette/redmine_qbo'
|
||||
author_url 'https://barrettefabrication.com'
|
||||
settings :default => {'empty' => true}, :partial => 'qbo/settings'
|
||||
requires_redmine :version_or_higher => '5.1.0'
|
||||
settings default: {empty: true}, partial: 'qbo/settings'
|
||||
requires_redmine version_or_higher: '6.1.0'
|
||||
|
||||
# Add safe attributes for core models
|
||||
Issue.safe_attributes 'customer_id'
|
||||
Issue.safe_attributes 'item_id'
|
||||
Issue.safe_attributes 'estimate_id'
|
||||
Issue.safe_attributes 'invoice_id'
|
||||
User.safe_attributes 'employee_id'
|
||||
TimeEntry.safe_attributes 'billed'
|
||||
Project.safe_attributes 'customer_id'
|
||||
|
||||
# We are playing in the sandbox
|
||||
#Quickbooks.sandbox_mode = true
|
||||
Issue.safe_attributes :customer_id
|
||||
Issue.safe_attributes :estimate_id
|
||||
Issue.safe_attributes :invoice_id
|
||||
User.safe_attributes :employee_id
|
||||
TimeEntry.safe_attributes :billed
|
||||
|
||||
# set per_page globally
|
||||
WillPaginate.per_page = 20
|
||||
|
||||
# Permissions for security
|
||||
permission :view_customers, :customers => :index, :public => false
|
||||
permission :add_customers, :customers => :new, :public => false
|
||||
permission :view_customers, customers: :index, public: false
|
||||
permission :add_customers, customers: :new, public: false
|
||||
|
||||
# Register top menu items
|
||||
menu :top_menu, :customers, { :controller => :customers, :action => :index }, :caption => 'Customers', :if => Proc.new {User.current.logged?}
|
||||
menu :top_menu, :customers, { controller: :customers, action: :index }, caption: :label_customers, if: Proc.new {User.current.logged?}
|
||||
|
||||
end
|
||||
|
||||
# Dynamically load all Hooks & Patches recursively
|
||||
base_dir = File.join(File.dirname(__FILE__), 'lib')
|
||||
|
||||
# '**' looks inside subdirectories, '*.rb' matches Ruby files
|
||||
Dir.glob(File.join(base_dir, '**', '*.rb')).sort.each do |file|
|
||||
require file
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 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 HeaderFooterHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
def view_layouts_base_html_head(context = {})
|
||||
#nothing
|
||||
end
|
||||
|
||||
def view_layouts_base_body_bottom(context = {})
|
||||
return "<div id='footer' align='center'><b>#{I18n.translate(:label_last_sync)}: </b> #{Qbo.last_sync if Qbo.exists?}</div>"
|
||||
end
|
||||
end
|
||||
@@ -1,110 +0,0 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2023 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.
|
||||
|
||||
require_dependency 'issue'
|
||||
|
||||
# Patches Redmine's Issues dynamically.
|
||||
# Adds a relationships
|
||||
module IssuePatch
|
||||
|
||||
def self.included(base) # :nodoc:
|
||||
base.extend(ClassMethods)
|
||||
|
||||
base.send(:include, InstanceMethods)
|
||||
|
||||
# Same as typing in the class
|
||||
base.class_eval do
|
||||
unloadable # Send unloadable so it will not be unloaded in development
|
||||
belongs_to :customer, primary_key: :id
|
||||
belongs_to :customer_token, primary_key: :id
|
||||
belongs_to :estimate, primary_key: :id
|
||||
has_and_belongs_to_many :invoices
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
|
||||
# Create billable time entries
|
||||
def bill_time
|
||||
|
||||
# Check to see if we have everything we need to bill the customer
|
||||
return if assigned_to.nil?
|
||||
return unless Qbo.first
|
||||
return unless customer
|
||||
|
||||
# Get unbilled time entries
|
||||
spent_time = time_entries.where(billed: [false, nil])
|
||||
spent_hours ||= spent_time.sum(:hours) || 0
|
||||
|
||||
if spent_hours > 0 then
|
||||
|
||||
# Prepare to create a new Time Activity
|
||||
qbo = Qbo.first
|
||||
qbo.perform_authenticated_request do |access_token|
|
||||
time_service = Quickbooks::Service::TimeActivity.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
item_service = Quickbooks::Service::Item.new(:company_id => qbo.realm_id, :access_token => access_token)
|
||||
time_entry = Quickbooks::Model::TimeActivity.new
|
||||
|
||||
# Lets total up each activity before billing.
|
||||
# This will simpify the invoicing with a single billable time entry per time activity
|
||||
h = Hash.new(0)
|
||||
spent_time.each do |entry|
|
||||
h[entry.activity.name] += entry.hours
|
||||
# update time entries billed status
|
||||
entry.billed = true
|
||||
entry.save
|
||||
end
|
||||
|
||||
# Now letes upload our totals for each activity as their own billable time entry
|
||||
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
|
||||
|
||||
# Lets match the activity to an qbo item
|
||||
item = item_service.query("SELECT * FROM Item WHERE Name = '#{key}' ").first
|
||||
next if item.nil?
|
||||
|
||||
# Create the new billable time entry and upload it
|
||||
time_entry.description = "#{tracker} ##{id}: #{subject} #{"(Partial @ #{done_ratio}%)" if not closed?}"
|
||||
time_entry.employee_id = assigned_to.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
|
||||
|
||||
# Create a shareable link for a customer
|
||||
def share_token
|
||||
CustomerToken.get_token self
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Add module to Issue
|
||||
Issue.send(:include, IssuePatch)
|
||||
@@ -1,75 +0,0 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 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 IssuesFormHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
# Load the javascript to support the autocomplete forms
|
||||
def view_layouts_base_html_head(context = {})
|
||||
js = javascript_include_tag 'application', :plugin => 'redmine_qbo'
|
||||
js += javascript_include_tag 'autocomplete-rails', :plugin => 'redmine_qbo'
|
||||
return js
|
||||
end
|
||||
|
||||
# Edit Issue Form
|
||||
# Here we build the required form components before passing them to a partial view formatting.
|
||||
def view_issues_form_details_bottom(context={})
|
||||
f = context[:form]
|
||||
|
||||
# check project level customer ownership first
|
||||
# This is done to preload customer information if the entire project is dedicated to a customer
|
||||
if context[:project]
|
||||
selected_customer = context[:project].customer ? context[:project].customer.id : nil
|
||||
end
|
||||
|
||||
# Check to see if the issue already belongs to a customer
|
||||
selected_customer = context[:issue].customer ? context[:issue].customer.id : nil
|
||||
selected_estimate = context[:issue].estimate ? context[:issue].estimate.id : nil
|
||||
|
||||
# Load customer information
|
||||
customer = Customer.find_by_id(selected_customer) if selected_customer
|
||||
|
||||
# Customer Name Text Box with database backed autocomplete
|
||||
search_customer = f.autocomplete_field :customer,
|
||||
autocomplete_customer_name_customers_path,
|
||||
:selected => selected_customer,
|
||||
:onchange => "updateIssueFrom('/issues/#{context[:issue].id}/edit.js', this)",
|
||||
:update_elements => {
|
||||
:id => '#issue_customer_id',
|
||||
:value => '#issue_customer'
|
||||
}
|
||||
|
||||
# Customer ID - Hidden Field
|
||||
customer_id = f.hidden_field :customer_id,
|
||||
:id => "issue_customer_id",
|
||||
:onchange => "updateIssueFrom('/issues/#{context[:issue].id}/edit.js', this)"
|
||||
|
||||
# Load estimates
|
||||
if context[:issue].customer
|
||||
estimates = customer.estimates.pluck(:doc_number, :id).sort! {|x, y| y <=> x}
|
||||
else
|
||||
estimates = [nil].compact
|
||||
end
|
||||
|
||||
# Generate the drop down list of quickbooks estimates
|
||||
select_estimate = f.select :estimate_id, estimates, :selected => selected_estimate, include_blank: true
|
||||
|
||||
# Pass all prebuilt form components to our partial
|
||||
context[:controller].send(:render_to_string, {
|
||||
:partial => 'issues/form_hook',
|
||||
locals: {
|
||||
search_customer: search_customer,
|
||||
customer_id: customer_id,
|
||||
context: context,
|
||||
select_estimate: select_estimate,
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -1,49 +0,0 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 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 IssuesShowHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
# View Issue
|
||||
# Display the quickbooks contact in the issue
|
||||
def view_issues_show_details_bottom(context={})
|
||||
issue = context[:issue]
|
||||
|
||||
# Check to see if there is a quickbooks user attached to the issue
|
||||
if issue.customer
|
||||
customer = link_to issue.customer.name, customer_path( issue.customer.id )
|
||||
end
|
||||
|
||||
# Estimate Number
|
||||
if issue.estimate
|
||||
estimate = issue.estimate.doc_number
|
||||
estimate_link = link_to estimate, estimate_path( issue.estimate.id ), :target => "_blank"
|
||||
end
|
||||
|
||||
# Invoice Number
|
||||
invoice_link = ""
|
||||
if issue.invoice_ids
|
||||
issue.invoice_ids.each do |i|
|
||||
invoice = Invoice.find i
|
||||
invoice_link = invoice_link + link_to( invoice.doc_number, invoice_path( i ), :target => "_blank").to_s + " "
|
||||
invoice_link = invoice_link.html_safe
|
||||
end
|
||||
end
|
||||
|
||||
context[:controller].send(:render_to_string, {
|
||||
:partial => 'issues/show_details',
|
||||
locals: {
|
||||
customer: customer,
|
||||
estimate_link: estimate_link,
|
||||
invoice_link: invoice_link
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
end
|
||||
256
lib/pdf_patch.rb
256
lib/pdf_patch.rb
@@ -1,256 +0,0 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2023 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.
|
||||
|
||||
require_dependency 'redmine/export/pdf'
|
||||
require_dependency 'redmine/export/pdf/issues_pdf_helper'
|
||||
|
||||
module PdfPatch
|
||||
|
||||
def self.included(base)
|
||||
base.send(:include, InstanceMethods)
|
||||
base.class_eval do
|
||||
unloadable # Send unloadable so it will not be unloaded in development
|
||||
alias_method :issue_to_pdf, :issue_to_pdf_with_patch
|
||||
alias_method :issue_to_pdf_with_patch, :issue_to_pdf
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
|
||||
def issue_to_pdf_with_patch(issue, assoc={})
|
||||
pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language)
|
||||
pdf.set_title("#{issue.project} - #{issue.tracker} ##{issue.id}")
|
||||
pdf.alias_nb_pages
|
||||
pdf.footer_date = format_date(Date.today)
|
||||
pdf.add_page
|
||||
pdf.SetFontStyle('B',11)
|
||||
buf = "#{issue.project} - #{issue.tracker} ##{issue.id}"
|
||||
pdf.RDMMultiCell(190, 5, buf)
|
||||
pdf.SetFontStyle('',8)
|
||||
base_x = pdf.get_x
|
||||
i = 1
|
||||
issue.ancestors.visible.each do |ancestor|
|
||||
pdf.set_x(base_x + i)
|
||||
buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}"
|
||||
pdf.RDMMultiCell(190 - i, 5, buf)
|
||||
i += 1 if i < 35
|
||||
end
|
||||
pdf.SetFontStyle('B',11)
|
||||
pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s)
|
||||
pdf.SetFontStyle('',8)
|
||||
pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}")
|
||||
pdf.ln
|
||||
|
||||
customer = issue.customer.name if issue.customer
|
||||
left = []
|
||||
left << [l(:field_status), issue.status]
|
||||
left << [l(:field_priority), issue.priority]
|
||||
left << [l(:field_customer), customer]
|
||||
left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id')
|
||||
|
||||
logger.debug "Calling :pdf_left hook"
|
||||
context = Redmine::Hook.call_hook :pdf_left, { array: left, issue: issue }
|
||||
left = left + context.first unless context.nil?
|
||||
|
||||
right = []
|
||||
right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date')
|
||||
right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date')
|
||||
right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio')
|
||||
right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours')
|
||||
right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project)
|
||||
|
||||
logger.debug "Calling :pdf_right hook"
|
||||
context = Redmine::Hook.call_hook :pdf_right, { array: right, issue: issue }
|
||||
right = right + context.first unless context.nil?
|
||||
|
||||
rows = left.size > right.size ? left.size : right.size
|
||||
while left.size < rows
|
||||
left << nil
|
||||
end
|
||||
while right.size < rows
|
||||
right << nil
|
||||
end
|
||||
|
||||
half = (issue.visible_custom_field_values.size / 2.0).ceil
|
||||
issue.visible_custom_field_values.each_with_index do |custom_value, i|
|
||||
(i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)]
|
||||
end
|
||||
|
||||
if pdf.get_rtl
|
||||
border_first_top = 'RT'
|
||||
border_last_top = 'LT'
|
||||
border_first = 'R'
|
||||
border_last = 'L'
|
||||
else
|
||||
border_first_top = 'LT'
|
||||
border_last_top = 'RT'
|
||||
border_first = 'L'
|
||||
border_last = 'R'
|
||||
end
|
||||
|
||||
rows = left.size > right.size ? left.size : right.size
|
||||
rows.times do |i|
|
||||
heights = []
|
||||
pdf.SetFontStyle('B',9)
|
||||
item = left[i]
|
||||
heights << pdf.get_string_height(35, item ? "#{item.first}:" : "")
|
||||
item = right[i]
|
||||
heights << pdf.get_string_height(35, item ? "#{item.first}:" : "")
|
||||
pdf.SetFontStyle('',9)
|
||||
item = left[i]
|
||||
heights << pdf.get_string_height(60, item ? item.last.to_s : "")
|
||||
item = right[i]
|
||||
heights << pdf.get_string_height(60, item ? item.last.to_s : "")
|
||||
height = heights.max
|
||||
|
||||
item = left[i]
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0)
|
||||
pdf.SetFontStyle('',9)
|
||||
pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 0)
|
||||
|
||||
item = right[i]
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0)
|
||||
pdf.SetFontStyle('',9)
|
||||
pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 2)
|
||||
|
||||
pdf.set_x(base_x)
|
||||
end
|
||||
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1)
|
||||
pdf.SetFontStyle('',9)
|
||||
|
||||
# Set resize image scale
|
||||
pdf.set_image_scale(1.6)
|
||||
text = textilizable(issue, :description,
|
||||
:only_path => false,
|
||||
:edit_section_links => false,
|
||||
:headings => false,
|
||||
:inline_attachments => false
|
||||
)
|
||||
pdf.RDMwriteFormattedCell(35+155, 5, '', '', text, issue.attachments, "LRB")
|
||||
|
||||
unless issue.leaf?
|
||||
truncate_length = (!is_cjk? ? 90 : 65)
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR")
|
||||
pdf.ln
|
||||
issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
|
||||
buf = "#{child.tracker} # #{child.id}: #{child.subject}".
|
||||
truncate(truncate_length)
|
||||
level = 10 if level >= 10
|
||||
pdf.SetFontStyle('',8)
|
||||
pdf.RDMCell(35+135,5, (level >=1 ? " " * level : "") + buf, border_first)
|
||||
pdf.SetFontStyle('B',8)
|
||||
pdf.RDMCell(20,5, child.status.to_s, border_last)
|
||||
pdf.ln
|
||||
end
|
||||
end
|
||||
|
||||
relations = issue.relations.select { |r| r.other_issue(issue).visible? }
|
||||
unless relations.empty?
|
||||
truncate_length = (!is_cjk? ? 80 : 60)
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR")
|
||||
pdf.ln
|
||||
relations.each do |relation|
|
||||
buf = relation.to_s(issue) {|other|
|
||||
text = ""
|
||||
if Setting.cross_project_issue_relations?
|
||||
text += "#{relation.other_issue(issue).project} - "
|
||||
end
|
||||
text += "#{other.tracker} ##{other.id}: #{other.subject}"
|
||||
text
|
||||
}
|
||||
buf = buf.truncate(truncate_length)
|
||||
pdf.SetFontStyle('', 8)
|
||||
pdf.RDMCell(35+155-60, 5, buf, border_first)
|
||||
pdf.SetFontStyle('B',8)
|
||||
pdf.RDMCell(20,5, relation.other_issue(issue).status.to_s, "")
|
||||
pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "")
|
||||
pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), border_last)
|
||||
pdf.ln
|
||||
end
|
||||
end
|
||||
pdf.RDMCell(190,5, "", "T")
|
||||
pdf.ln
|
||||
|
||||
if issue.changesets.any? &&
|
||||
User.current.allowed_to?(:view_changesets, issue.project)
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMCell(190,5, l(:label_associated_revisions), "B")
|
||||
pdf.ln
|
||||
for changeset in issue.changesets
|
||||
pdf.SetFontStyle('B',8)
|
||||
csstr = "#{l(:label_revision)} #{changeset.format_identifier} - "
|
||||
csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s
|
||||
pdf.RDMCell(190, 5, csstr)
|
||||
pdf.ln
|
||||
unless changeset.comments.blank?
|
||||
pdf.SetFontStyle('',8)
|
||||
pdf.RDMwriteHTMLCell(190,5,'','',
|
||||
changeset.comments.to_s, issue.attachments, "")
|
||||
end
|
||||
pdf.ln
|
||||
end
|
||||
end
|
||||
|
||||
if assoc[:journals].present?
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMCell(190,5, l(:label_history), "B")
|
||||
pdf.ln
|
||||
assoc[:journals].each do |journal|
|
||||
pdf.SetFontStyle('B',8)
|
||||
title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}"
|
||||
title << " (#{l(:field_private_notes)})" if journal.private_notes?
|
||||
pdf.RDMCell(190,5, title)
|
||||
pdf.ln
|
||||
pdf.SetFontStyle('I',8)
|
||||
details_to_strings(journal.visible_details, true).each do |string|
|
||||
pdf.RDMMultiCell(190,5, "- " + string)
|
||||
end
|
||||
if journal.notes?
|
||||
pdf.ln unless journal.details.empty?
|
||||
pdf.SetFontStyle('',8)
|
||||
text = textilizable(journal, :notes,
|
||||
:only_path => false,
|
||||
:edit_section_links => false,
|
||||
:headings => false,
|
||||
:inline_attachments => false
|
||||
)
|
||||
pdf.RDMwriteFormattedCell(190,5,'','', text, issue.attachments, "")
|
||||
end
|
||||
pdf.ln
|
||||
end
|
||||
end
|
||||
|
||||
if issue.attachments.any?
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
|
||||
pdf.ln
|
||||
for attachment in issue.attachments
|
||||
pdf.SetFontStyle('',8)
|
||||
pdf.RDMCell(80,5, attachment.filename)
|
||||
pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
|
||||
pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
|
||||
pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
|
||||
pdf.ln
|
||||
end
|
||||
end
|
||||
pdf.output
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
Redmine::Export::PDF::IssuesPdfHelper.send(:include, PdfPatch)
|
||||
@@ -1,39 +0,0 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2017 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.
|
||||
|
||||
require_dependency 'project'
|
||||
|
||||
# Patches Redmine's Projects dynamically.
|
||||
# Adds a relationships
|
||||
module ProjectPatch
|
||||
|
||||
def self.included(base) # :nodoc:
|
||||
base.extend(ClassMethods)
|
||||
|
||||
base.send(:include, InstanceMethods)
|
||||
|
||||
# Same as typing in the class
|
||||
base.class_eval do
|
||||
unloadable # Send unloadable so it will not be unloaded in development
|
||||
belongs_to :customer, primary_key: :id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
|
||||
end
|
||||
|
||||
# Add module to Project
|
||||
Project.send(:include, ProjectPatch)
|
||||
@@ -1,27 +0,0 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2017 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 ProjectsFormHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
# Edit Project Form
|
||||
def view_projects_form(context={})
|
||||
f = context[:form]
|
||||
|
||||
# Check to see if there is a quickbooks user attached to the issue
|
||||
selected_customer = context[:project].customer ? context[:project].customer : nil
|
||||
|
||||
# Load customer information
|
||||
customer = Customer.find_by_id(selected_customer) if selected_customer
|
||||
search_customer = f.autocomplete_field :customer, autocomplete_customer_name_customers_path, :selected => selected_customer, :update_elements => {:id => '#project_customer_id', :value => '#project_customer'}
|
||||
customer_id = f.hidden_field :customer_id, :id => "project_customer_id"
|
||||
|
||||
return "<p><label for=\"project_customer\">Customer</label>#{search_customer} #{customer_id}</p>"
|
||||
end
|
||||
end
|
||||
99
lib/redmine_qbo/hooks/issues_hook_listener.rb
Normal file
99
lib/redmine_qbo/hooks/issues_hook_listener.rb
Normal file
@@ -0,0 +1,99 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2016 - 2026 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.
|
||||
|
||||
module RedmineQbo
|
||||
module Hooks
|
||||
class IssuesHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
include IssuesHelper
|
||||
|
||||
# Edit Issue Form
|
||||
# Here we build the required form components before passing them to a partial view formatting.
|
||||
def view_issues_form_details_bottom(context={})
|
||||
Rails.logger.debug "RedmineQbo::Hooks::IssuesHookListener.view_issues_form_details_bottom: Building form components for quickbooks customer, estimate, and invoice data"
|
||||
f = context[:form]
|
||||
issue = context[:issue]
|
||||
project = context[:project]
|
||||
Rails.logger.debug issue.inspect
|
||||
Rails.logger.debug project.inspect
|
||||
|
||||
# Customer Name Text Box with database backed autocomplete
|
||||
# onchange event will update the hidden customer_id field
|
||||
search_customer = f.autocomplete_field :customer,
|
||||
autocomplete_customer_name_customers_path,
|
||||
selected: issue.customer,
|
||||
update_elements: {
|
||||
id: '#issue_customer_id',
|
||||
value: '#issue_customer'
|
||||
}
|
||||
|
||||
# We need to handle 3 cases for the onchange event of the customer name field:
|
||||
# 1. New issue Withough project: /issues/new.js
|
||||
# 2. New issue With project: /projects/rmt/issues/new.js
|
||||
# 3. Existing issue: /issues/<ID>/edit.js
|
||||
# The built in helper update_issue_form_path requires a project object to determine the correct path for new vs existing issues,
|
||||
# but it doesn't work for issue.project when creating new issues not in a project i.e. http://redmine.domain.com/issues/new .
|
||||
# So we need to figure out how to get a the @project from the controller calling the hook.
|
||||
#
|
||||
# If this is not handled correctly, it leads to a 422 error when creating a new issue and selecting a customer.
|
||||
js_path = "updateIssueFrom('#{escape_javascript update_issue_form_path(project, issue)}', this)"
|
||||
Rails.logger.debug js_path
|
||||
|
||||
# This hidden field is used for the customer ID for the issue
|
||||
# the onchange event will reload the issue form via ajax to update the available estimates
|
||||
customer_id = f.hidden_field :customer_id,
|
||||
id: "issue_customer_id",
|
||||
onchange: js_path.html_safe
|
||||
|
||||
# Generate the drop down list of quickbooks estimates owned by the selected customer
|
||||
select_estimate = f.select :estimate_id,
|
||||
issue.customer ? issue.customer.estimates.pluck(:doc_number, :id).sort! {|x, y| y <=> x} : [],
|
||||
selected: issue.estimate ? issue.estimate.id : nil,
|
||||
include_blank: true
|
||||
|
||||
# Pass all prebuilt form components to our partial
|
||||
context[:controller].send(:render_to_string, {
|
||||
partial: 'issues/form_hook',
|
||||
locals: {
|
||||
search_customer: search_customer,
|
||||
customer_id: customer_id,
|
||||
select_estimate: select_estimate
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
# View Issue
|
||||
# Displays the quickbooks customer, estimate, & invoices attached to the issue
|
||||
def view_issues_show_details_bottom(context={})
|
||||
issue = context[:issue]
|
||||
|
||||
# Build a list of invoice links
|
||||
invoice_link = ""
|
||||
if issue.invoices
|
||||
issue.invoices.each do |i|
|
||||
invoice_link += "#{link_to i, i, target: :_blank}<br/>"
|
||||
end
|
||||
end
|
||||
|
||||
context[:controller].send(:render_to_string, {
|
||||
partial: 'issues/show_details',
|
||||
locals: {
|
||||
customer: issue.customer ? link_to(issue.customer) : nil,
|
||||
estimate_link: issue.estimate ? link_to(issue.estimate, issue.estimate, target: :_blank) : nil,
|
||||
invoice_link: invoice_link.html_safe,
|
||||
issue: issue
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -8,26 +8,24 @@
|
||||
#
|
||||
#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.
|
||||
|
||||
require_dependency 'time_entry_query'
|
||||
module RedmineQbo
|
||||
module Hooks
|
||||
|
||||
module TimeEntryQueryPatch
|
||||
class UsersShowHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
# Add QBO options to columns
|
||||
def available_columns
|
||||
unless @available_columns
|
||||
@available_columns = self.class.available_columns.dup
|
||||
@available_columns << QueryColumn.new(:billed, :sortable => "#{TimeEntry.table_name}.name", :groupable => true, :caption => :field_billed)
|
||||
# View User
|
||||
def view_users_form(context={})
|
||||
|
||||
# Update the users
|
||||
#Employee.update_all
|
||||
|
||||
# Check to see if there is a quickbooks user attached to the issue
|
||||
@selected = context[:user].employee.id if context[:user].employee
|
||||
|
||||
# Generate the drop down list of quickbooks contacts
|
||||
return "<p>#{context[:form].select :employee_id, Employee.all.pluck(:name, :id), selected: @selected, include_blank: true}</p>"
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
# Add QBO options to the filter
|
||||
def initialize_available_filters
|
||||
add_available_filter "billed", :type => :boolean
|
||||
super
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Add module to TimeEntryQuery
|
||||
TimeEntryQuery.send(:prepend, QueryPatch)
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2017 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -8,22 +8,22 @@
|
||||
#
|
||||
#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 IssuesSaveHookListener < Redmine::Hook::ViewListener
|
||||
module RedmineQbo
|
||||
module Hooks
|
||||
|
||||
# Called Before Issue Saved
|
||||
def controller_issues_edit_before_save(context={})
|
||||
issue = context[:issue]
|
||||
issue.subject = issue.subject.titleize
|
||||
class ViewHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
# Load the javascript to support the autocomplete forms
|
||||
def view_layouts_base_html_head(context = {})
|
||||
js = javascript_include_tag 'application.js', plugin: :redmine_qbo
|
||||
js += javascript_include_tag 'autocomplete-rails.js', plugin: :redmine_qbo
|
||||
js += javascript_include_tag 'checkbox_controller.js', plugin: :redmine_qbo
|
||||
return js
|
||||
end
|
||||
|
||||
# Called After Issue Saved
|
||||
def controller_issues_edit_after_save(context={})
|
||||
issue = context[:issue]
|
||||
begin
|
||||
issue.bill_time if issue.status.is_closed?
|
||||
rescue
|
||||
# TODO flash[:error] = "Unable to bill, check QBO Auth"
|
||||
end
|
||||
render_on :view_layouts_base_sidebar, partial: "qbo/sidebar"
|
||||
render_on :view_layouts_base_body_bottom, partial: "qbo/footer"
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -10,12 +10,14 @@
|
||||
|
||||
require_dependency 'attachments_controller'
|
||||
|
||||
module AttachmentsControllerPatch
|
||||
module RedmineQbo
|
||||
module Patches
|
||||
|
||||
module AttachmentsControllerPatch
|
||||
|
||||
def self.included(base)
|
||||
|
||||
base.class_eval do
|
||||
unloadable # Send unloadable so it will not be unloaded in development
|
||||
|
||||
# check if login is globally required to access the application
|
||||
def check_if_login_required
|
||||
@@ -37,7 +39,10 @@ module AttachmentsControllerPatch
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
# Add module to AttachmentsController
|
||||
AttachmentsController.send(:include, AttachmentsControllerPatch)
|
||||
# Add module to AttachmentsController
|
||||
AttachmentsController.send(:include, AttachmentsControllerPatch)
|
||||
|
||||
end
|
||||
end
|
||||
134
lib/redmine_qbo/patches/issue_patch.rb
Normal file
134
lib/redmine_qbo/patches/issue_patch.rb
Normal file
@@ -0,0 +1,134 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2016 - 2026 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.
|
||||
|
||||
require_dependency 'issue'
|
||||
|
||||
module RedmineQbo
|
||||
module Patches
|
||||
|
||||
# Patches Redmine's Issues dynamically.
|
||||
# Adds relationships for customers, estimates, invoices, customer_tokens
|
||||
# Adds before and after save hooks
|
||||
module IssuePatch
|
||||
|
||||
def self.included(base) # :nodoc:
|
||||
base.extend(ClassMethods)
|
||||
|
||||
base.send(:include, InstanceMethods)
|
||||
|
||||
# Same as typing in the class
|
||||
base.class_eval do
|
||||
belongs_to :customer, primary_key: :id
|
||||
belongs_to :customer_token, primary_key: :id
|
||||
belongs_to :estimate, primary_key: :id
|
||||
has_and_belongs_to_many :invoices
|
||||
before_save :titlize_subject
|
||||
after_save :bill_time
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
|
||||
# Create billable time entries
|
||||
def bill_time
|
||||
logger.debug "QBO: Billing time for issue ##{id}"
|
||||
return unless status.is_closed?
|
||||
return if assigned_to.nil?
|
||||
return unless Qbo.first
|
||||
return unless customer
|
||||
|
||||
Thread.new do
|
||||
spent_time = time_entries.where(billed: [false, nil])
|
||||
spent_hours ||= spent_time.sum(:hours) || 0
|
||||
|
||||
if spent_hours > 0 then
|
||||
|
||||
# Prepare to create a new Time Activity
|
||||
qbo = Qbo.first
|
||||
qbo.perform_authenticated_request do |access_token|
|
||||
time_service = Quickbooks::Service::TimeActivity.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
item_service = Quickbooks::Service::Item.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
time_entry = Quickbooks::Model::TimeActivity.new
|
||||
|
||||
# Lets total up each activity before billing.
|
||||
# This will simpify the invoicing with a single billable time entry per time activity
|
||||
h = Hash.new(0)
|
||||
spent_time.each do |entry|
|
||||
h[entry.activity.name] += entry.hours
|
||||
# update time entries billed status
|
||||
entry.billed = true
|
||||
entry.save
|
||||
end
|
||||
|
||||
# Now letes upload our totals for each activity as their own billable time entry
|
||||
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
|
||||
|
||||
# Lets match the activity to an qbo item
|
||||
item = item_service.query("SELECT * FROM Item WHERE Name = '#{key}' ").first
|
||||
next if item.nil?
|
||||
|
||||
# Create the new billable time entry and upload it
|
||||
time_entry.description = "#{tracker} ##{id}: #{subject} #{"(Partial @ #{done_ratio}%)" if not closed?}"
|
||||
time_entry.employee_id = assigned_to.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
|
||||
end
|
||||
|
||||
# Create a shareable link for a customer
|
||||
def share_token
|
||||
CustomerToken.get_token self
|
||||
end
|
||||
|
||||
# Titleize the subject before save , but keep words containing numbers mixed with letters capitalized
|
||||
def titlize_subject
|
||||
logger.debug "QBO: Titlizing subject for issue ##{self.id}"
|
||||
self.subject = self.subject.split(/\s+/).map do |word|
|
||||
# If word is NOT purely alphanumeric (contains special chars),
|
||||
# or is all upper/lower, we can handle it.
|
||||
# excluding alphanumeric strings with mixed case and numbers (e.g., "ID555ABC") from being altered.
|
||||
if word =~ /[A-Z]/ && word =~ /[0-9]/
|
||||
word
|
||||
else
|
||||
word.downcase
|
||||
word.capitalize
|
||||
end
|
||||
end.join(' ')
|
||||
end
|
||||
end
|
||||
|
||||
# Add module to Issue
|
||||
Issue.send(:include, IssuePatch)
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -9,27 +9,28 @@
|
||||
#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.
|
||||
require_dependency 'issues_controller'
|
||||
|
||||
module IssuesControllerPatch
|
||||
module RedmineQbo
|
||||
module Patches
|
||||
module IssuesControllerPatch
|
||||
|
||||
module Helper
|
||||
def watcher_link(issue, user)
|
||||
link = +''
|
||||
link << link_to(I18n.t(:label_bill_time), bill_path( issue.id ), method: :get, class: 'icon icon-email-add') if user.admin?
|
||||
link = ''
|
||||
link = link_to(I18n.t(:label_bill_time), bill_path( issue.id ), method: :get, class: 'icon icon-email-add') if user.admin?
|
||||
link << link_to(I18n.t(:label_share), share_path( issue.id ), method: :get, target: :_blank, class: 'icon icon-shared') if user.logged?
|
||||
link.html_safe + super
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(base)
|
||||
|
||||
base.class_eval do
|
||||
unloadable # Send unloadable so it will not be unloaded in development
|
||||
helper Helper
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
# Add module to IssuessController
|
||||
IssuesController.send(:include, IssuesControllerPatch)
|
||||
|
||||
# Add module to IssuessController
|
||||
IssuesController.send(:include, IssuesControllerPatch)
|
||||
end
|
||||
end
|
||||
277
lib/redmine_qbo/patches/pdf_patch.rb
Normal file
277
lib/redmine_qbo/patches/pdf_patch.rb
Normal file
@@ -0,0 +1,277 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2016 - 2026 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.
|
||||
|
||||
require_dependency 'redmine/export/pdf'
|
||||
require_dependency 'redmine/export/pdf/issues_pdf_helper'
|
||||
|
||||
module RedmineQbo
|
||||
module Patches
|
||||
module PdfPatch
|
||||
|
||||
def self.included(base)
|
||||
base.send(:include, InstanceMethods)
|
||||
base.class_eval do
|
||||
alias_method :issue_to_pdf, :issue_to_pdf_with_patch
|
||||
alias_method :issue_to_pdf_with_patch, :issue_to_pdf
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
|
||||
def issue_to_pdf_with_patch(issue, assoc={})
|
||||
pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language)
|
||||
pdf.set_title("#{issue.project} - #{issue.tracker} ##{issue.id}")
|
||||
pdf.alias_nb_pages
|
||||
pdf.footer_date = format_date(Date.today)
|
||||
pdf.add_page
|
||||
pdf.SetFontStyle('B',11)
|
||||
buf = "#{issue.project} - #{issue.tracker} ##{issue.id}"
|
||||
pdf.RDMMultiCell(190, 5, buf)
|
||||
pdf.SetFontStyle('',8)
|
||||
base_x = pdf.get_x
|
||||
i = 1
|
||||
issue.ancestors.visible.each do |ancestor|
|
||||
pdf.set_x(base_x + i)
|
||||
buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}"
|
||||
pdf.RDMMultiCell(190 - i, 5, buf)
|
||||
i += 1 if i < 35
|
||||
end
|
||||
pdf.SetFontStyle('B',11)
|
||||
pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s)
|
||||
pdf.SetFontStyle('',8)
|
||||
pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}")
|
||||
pdf.ln
|
||||
|
||||
customer = issue.customer.name if issue.customer
|
||||
left = []
|
||||
left << [l(:field_status), issue.status]
|
||||
left << [l(:field_priority), issue.priority]
|
||||
left << [l(:field_customer), customer]
|
||||
left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?(:assigned_to_id)
|
||||
#left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?(:category_id)
|
||||
#left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?(:fixed_version_id)
|
||||
|
||||
logger.debug "Calling :pdf_left hook"
|
||||
left_hook_output = Redmine::Hook.call_hook :pdf_left, { issue: issue }
|
||||
unless left_hook_output.nil?
|
||||
left_hook_output.each do |l|
|
||||
left.concat l unless l.nil?
|
||||
end
|
||||
end
|
||||
|
||||
right = []
|
||||
right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?(:start_date)
|
||||
right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?(:due_date)
|
||||
right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?(:done_ratio)
|
||||
right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?(:estimated_hours)
|
||||
right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project)
|
||||
|
||||
logger.debug "Calling :pdf_right hook"
|
||||
right_hook_output = Redmine::Hook.call_hook :pdf_right, { issue: issue }
|
||||
unless right_hook_output.nil?
|
||||
right_hook_output.each do |r|
|
||||
right.concat r unless r.nil?
|
||||
end
|
||||
end
|
||||
|
||||
rows = left.size > right.size ? left.size : right.size
|
||||
while left.size < rows
|
||||
left << nil
|
||||
end
|
||||
while right.size < rows
|
||||
right << nil
|
||||
end
|
||||
|
||||
half = (issue.visible_custom_field_values.size / 2.0).ceil
|
||||
issue.visible_custom_field_values.each_with_index do |custom_value, i|
|
||||
(i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)]
|
||||
end
|
||||
|
||||
if pdf.get_rtl
|
||||
border_first_top = 'RT'
|
||||
border_last_top = 'LT'
|
||||
border_first = 'R'
|
||||
border_last = 'L'
|
||||
else
|
||||
border_first_top = 'LT'
|
||||
border_last_top = 'RT'
|
||||
border_first = 'L'
|
||||
border_last = 'R'
|
||||
end
|
||||
|
||||
rows = left.size > right.size ? left.size : right.size
|
||||
rows.times do |i|
|
||||
heights = []
|
||||
pdf.SetFontStyle('B',9)
|
||||
item = left[i]
|
||||
heights << pdf.get_string_height(35, item ? "#{item.first}:" : "")
|
||||
item = right[i]
|
||||
heights << pdf.get_string_height(35, item ? "#{item.first}:" : "")
|
||||
pdf.SetFontStyle('',9)
|
||||
item = left[i]
|
||||
heights << pdf.get_string_height(60, item ? item.last.to_s : "")
|
||||
item = right[i]
|
||||
heights << pdf.get_string_height(60, item ? item.last.to_s : "")
|
||||
height = heights.max
|
||||
|
||||
item = left[i]
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0)
|
||||
pdf.SetFontStyle('',9)
|
||||
pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 0)
|
||||
|
||||
item = right[i]
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0)
|
||||
pdf.SetFontStyle('',9)
|
||||
pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 2)
|
||||
|
||||
pdf.set_x(base_x)
|
||||
end
|
||||
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1)
|
||||
pdf.SetFontStyle('',9)
|
||||
|
||||
# Set resize image scale
|
||||
pdf.set_image_scale(1.6)
|
||||
text = textilizable(issue, :description,
|
||||
only_path: false,
|
||||
edit_section_links: false,
|
||||
headings: false,
|
||||
inline_attachments: false
|
||||
)
|
||||
pdf.RDMwriteFormattedCell(35+155, 5, '', '', text, issue.attachments, "LRB")
|
||||
|
||||
unless issue.leaf?
|
||||
truncate_length = (!is_cjk? ? 90 : 65)
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR")
|
||||
pdf.ln
|
||||
issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
|
||||
buf = "#{child.tracker} # #{child.id}: #{child.subject}".
|
||||
truncate(truncate_length)
|
||||
level = 10 if level >= 10
|
||||
pdf.SetFontStyle('',8)
|
||||
pdf.RDMCell(35+135,5, (level >=1 ? " " * level : "") + buf, border_first)
|
||||
pdf.SetFontStyle('B',8)
|
||||
pdf.RDMCell(20,5, child.status.to_s, border_last)
|
||||
pdf.ln
|
||||
end
|
||||
end
|
||||
|
||||
relations = issue.relations.select { |r| r.other_issue(issue).visible? }
|
||||
unless relations.empty?
|
||||
truncate_length = (!is_cjk? ? 80 : 60)
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR")
|
||||
pdf.ln
|
||||
relations.each do |relation|
|
||||
buf = relation.to_s(issue) {|other|
|
||||
text = ""
|
||||
if Setting.cross_project_issue_relations?
|
||||
text += "#{relation.other_issue(issue).project} - "
|
||||
end
|
||||
text += "#{other.tracker} ##{other.id}: #{other.subject}"
|
||||
text
|
||||
}
|
||||
buf = buf.truncate(truncate_length)
|
||||
pdf.SetFontStyle('', 8)
|
||||
pdf.RDMCell(35+155-60, 5, buf, border_first)
|
||||
pdf.SetFontStyle('B',8)
|
||||
pdf.RDMCell(20,5, relation.other_issue(issue).status.to_s, "")
|
||||
pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "")
|
||||
pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), border_last)
|
||||
pdf.ln
|
||||
end
|
||||
end
|
||||
pdf.RDMCell(190,5, "", "T")
|
||||
pdf.ln
|
||||
|
||||
if issue.changesets.any? &&
|
||||
User.current.allowed_to?(:view_changesets, issue.project)
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMCell(190,5, l(:label_associated_revisions), "B")
|
||||
pdf.ln
|
||||
for changeset in issue.changesets
|
||||
pdf.SetFontStyle('B',8)
|
||||
csstr = "#{l(:label_revision)} #{changeset.format_identifier} - "
|
||||
csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s
|
||||
pdf.RDMCell(190, 5, csstr)
|
||||
pdf.ln
|
||||
unless changeset.comments.blank?
|
||||
pdf.SetFontStyle('',8)
|
||||
pdf.RDMwriteHTMLCell(190,5,'','',
|
||||
changeset.comments.to_s, issue.attachments, "")
|
||||
end
|
||||
pdf.ln
|
||||
end
|
||||
end
|
||||
|
||||
if assoc[:journals].present?
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMCell(190,5, l(:label_history), "B")
|
||||
pdf.ln
|
||||
assoc[:journals].each do |journal|
|
||||
pdf.SetFontStyle('B',8)
|
||||
title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}"
|
||||
title << " (#{l(:field_private_notes)})" if journal.private_notes?
|
||||
pdf.RDMCell(190,5, title)
|
||||
pdf.ln
|
||||
pdf.SetFontStyle('I',8)
|
||||
details_to_strings(journal.visible_details, true).each do |string|
|
||||
pdf.RDMMultiCell(190,5, "- " + string)
|
||||
end
|
||||
if journal.notes?
|
||||
pdf.ln unless journal.details.empty?
|
||||
pdf.SetFontStyle('',8)
|
||||
text = textilizable(journal, :notes,
|
||||
only_path: false,
|
||||
edit_section_links: false,
|
||||
headings: false,
|
||||
inline_attachments: false
|
||||
)
|
||||
pdf.RDMwriteFormattedCell(190,5,'','', text, issue.attachments, "")
|
||||
end
|
||||
pdf.ln
|
||||
end
|
||||
end
|
||||
|
||||
if issue.attachments.any?
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
|
||||
pdf.ln
|
||||
for attachment in issue.attachments
|
||||
pdf.SetFontStyle('',8)
|
||||
pdf.RDMCell(80,5, attachment.filename)
|
||||
pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
|
||||
pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
|
||||
pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
|
||||
pdf.ln
|
||||
end
|
||||
end
|
||||
|
||||
# Check to see if there is an estimate attached, then combine them
|
||||
if issue.estimate
|
||||
pdf = CombinePDF.parse(pdf.output, allow_optional_content: true)
|
||||
pdf << CombinePDF.parse(issue.estimate.pdf)
|
||||
return pdf.to_pdf
|
||||
end
|
||||
|
||||
return pdf.output
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
Redmine::Export::PDF::IssuesPdfHelper.send(:include, PdfPatch)
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -10,25 +10,30 @@
|
||||
|
||||
require_dependency 'issue_query'
|
||||
|
||||
module QueryPatch
|
||||
module RedmineQbo
|
||||
module Patches
|
||||
module QueryPatch
|
||||
|
||||
# Add qbo options to the aviable columns
|
||||
def available_columns
|
||||
unless @available_columns
|
||||
@available_columns = self.class.available_columns.dup
|
||||
@available_columns << QueryColumn.new(:customer, :sortable => "#{Issue.table_name}.customer_id", :groupable => true, :caption => :field_customer)
|
||||
@available_columns << QueryColumn.new(:billed, :sortable => "#{TimeEntry.table_name}.billed", :groupable => true, :caption => :field_billed)
|
||||
@available_columns << QueryColumn.new(:customer, sortable: "#{Issue.table_name}.customer_id", groupable: true, caption: :field_customer)
|
||||
@available_columns << QueryColumn.new(:billed, sortable: "#{TimeEntry.table_name}.billed", groupable: true, caption: :field_billed)
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
# Add customers to filters
|
||||
def initialize_available_filters
|
||||
#add_available_filter "customer", :type => :text
|
||||
#add_available_filter "customer", type: :text
|
||||
super
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
# Add module to Issue
|
||||
IssueQuery.send(:prepend, QueryPatch)
|
||||
# Add module to Issue
|
||||
IssueQuery.send(:prepend, QueryPatch)
|
||||
|
||||
end
|
||||
end
|
||||
38
lib/redmine_qbo/patches/time_entry_query_patch.rb
Normal file
38
lib/redmine_qbo/patches/time_entry_query_patch.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2016 - 2026 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.
|
||||
|
||||
require_dependency 'time_entry_query'
|
||||
|
||||
module RedmineQbo
|
||||
module Patches
|
||||
module TimeEntryQueryPatch
|
||||
|
||||
# Add QBO options to columns
|
||||
def available_columns
|
||||
unless @available_columns
|
||||
@available_columns = self.class.available_columns.dup
|
||||
@available_columns << QueryColumn.new(:billed, sortable: "#{TimeEntry.table_name}.name", groupable: true, caption: :field_billed)
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
# Add QBO options to the filter
|
||||
def initialize_available_filters
|
||||
add_available_filter "billed", type: :boolean
|
||||
super
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Add module to TimeEntryQuery
|
||||
TimeEntryQuery.send(:prepend, QueryPatch)
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2022 rick barrette
|
||||
#Copyright (c) 2016 - 2026 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:
|
||||
#
|
||||
@@ -10,9 +10,12 @@
|
||||
|
||||
require_dependency 'user'
|
||||
|
||||
# Patches Redmine's User dynamically.
|
||||
# Adds a relationships
|
||||
module UserPatch
|
||||
module RedmineQbo
|
||||
module Patches
|
||||
|
||||
# Patches Redmine's User dynamically.
|
||||
# Adds a relationships
|
||||
module UserPatch
|
||||
def self.included(base) # :nodoc:
|
||||
base.extend(ClassMethods)
|
||||
|
||||
@@ -20,7 +23,6 @@ module UserPatch
|
||||
|
||||
# Same as typing in the class
|
||||
base.class_eval do
|
||||
unloadable # Send unloadable so it will not be unloaded in development
|
||||
belongs_to :employee, primary_key: :id
|
||||
end
|
||||
end
|
||||
@@ -33,7 +35,10 @@ module UserPatch
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
# Add module to Issue
|
||||
User.send(:include, UserPatch)
|
||||
# Add module to Issue
|
||||
User.send(:include, UserPatch)
|
||||
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user