mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2025-11-09 09:24:23 -05:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7839116134 | |||
| b3a809ab1c | |||
| 3a0e58c3da | |||
| 26433c9020 | |||
| 6dbf84f401 | |||
| 3220ff728f | |||
| d8d1942673 | |||
| 8e329b2dd2 | |||
| 3622f8cad7 | |||
| 0513763607 | |||
| b7e3ea9e3d | |||
| 3ea2cd14d1 | |||
| 7b7875991f | |||
| b1a106d4d8 | |||
| 0281d86f1a | |||
| 2231156873 | |||
| ecc8930bec | |||
| 5814740a5d | |||
| 25159c760a | |||
| 3ff9132acb | |||
| b5f00f254c | |||
| 70f2c473d5 | |||
| b3b11d726d | |||
| f97d5bc731 | |||
| 49507d06c7 | |||
| 5d928c486f | |||
| 0485e9d64c | |||
| cc0839204e | |||
| 760a85a1da |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
.bundle
|
.bundle
|
||||||
.config
|
.config
|
||||||
|
.dockerrc
|
||||||
|
.vscode
|
||||||
Gemfile.lock
|
Gemfile.lock
|
||||||
|
|||||||
2
Gemfile
2
Gemfile
@@ -1,7 +1,7 @@
|
|||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
gem 'quickbooks-ruby'
|
gem 'quickbooks-ruby'
|
||||||
gem 'oauth2', '1.4.7'
|
gem 'oauth2'
|
||||||
gem 'roxml'
|
gem 'roxml'
|
||||||
gem 'nhtsa_vin'
|
gem 'nhtsa_vin'
|
||||||
gem 'will_paginate'
|
gem 'will_paginate'
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -8,7 +8,10 @@ The goal of this project is to allow Redmine to connect with Quickbooks Online t
|
|||||||
|
|
||||||
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 & the master branch may be unstable. Tags should be stable and are recommended
|
||||||
|
|
||||||
Use tags Version 1.0.0+ for Redmine 4+ and Version 0.8.1 for Redine 3
|
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
|
||||||
|
|
||||||
#### Features
|
#### Features
|
||||||
* Issues can be assigned to a Customer via drop down in the edit Issue form
|
* Issues can be assigned to a Customer via drop down in the edit Issue form
|
||||||
@@ -36,10 +39,12 @@ Use tags Version 1.0.0+ for Redmine 4+ and Version 0.8.1 for Redine 3
|
|||||||
|
|
||||||
## The Install
|
## The Install
|
||||||
|
|
||||||
1. To install, clone this repo into your plugin folder
|
1. To install, clone this repo into your plugin folder & checkout a tagged version
|
||||||
|
|
||||||
`git clone git@github.com:rickbarrette/redmine_qbo.git`
|
`git clone git@github.com:rickbarrette/redmine_qbo.git`
|
||||||
|
|
||||||
then
|
then
|
||||||
|
|
||||||
`git checkout <tag>`
|
`git checkout <tag>`
|
||||||
|
|
||||||
2. Migrate your database
|
2. Migrate your database
|
||||||
@@ -59,11 +64,8 @@ Use tags Version 1.0.0+ for Redmine 4+ and Version 0.8.1 for Redine 3
|
|||||||
Note: After the inital synchronization, this plugin will recieve push notifications via Intuit's webhook service.
|
Note: After the inital synchronization, this plugin will recieve push notifications via Intuit's webhook service.
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
* Customer link option to allow for temporary sharing of an issue
|
|
||||||
* Add Setting for Sandbox Mode
|
* Add Setting for Sandbox Mode
|
||||||
* Seperate Vehicles into a seperate plugin (I use redmine for my automotive shop management 😉)
|
* Seperate Vehicles into a seperate plugin (I use redmine for my automotive shop management 😉)
|
||||||
* Make HTML Pretty (It's ugly right now but it works)
|
|
||||||
* Intergrate Customer Search into Redmine Search
|
|
||||||
* MORE Stuff as I make it up...
|
* MORE Stuff as I make it up...
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ class CustomersController < ApplicationController
|
|||||||
@billing_address = address_to_s(@customer.billing_address)
|
@billing_address = address_to_s(@customer.billing_address)
|
||||||
@shipping_address = address_to_s(@customer.shipping_address)
|
@shipping_address = address_to_s(@customer.shipping_address)
|
||||||
@closed_issues = (@issues - @issues.open)
|
@closed_issues = (@issues - @issues.open)
|
||||||
|
@hours = 0
|
||||||
|
@closed_hours = 0
|
||||||
|
@issues.open.each { |i| @hours+= i.total_spent_hours }
|
||||||
|
@closed_issues.each { |i| @closed_hours+= i.total_spent_hours }
|
||||||
rescue
|
rescue
|
||||||
render_404
|
render_404
|
||||||
end
|
end
|
||||||
@@ -156,9 +160,11 @@ class CustomersController < ApplicationController
|
|||||||
|
|
||||||
User.current = User.find_by lastname: 'Anonymous'
|
User.current = User.find_by lastname: 'Anonymous'
|
||||||
|
|
||||||
@token = CustomerToken.where("token = ? and expires_at > ?", params[:token], Time.now)
|
@token = CustomerToken.find_by token: params[:token]
|
||||||
@token = @token.first
|
begin
|
||||||
if @token
|
@token.destroy if @token.expired?
|
||||||
|
raise "Token Expired" if @token.destroyed
|
||||||
|
|
||||||
session[:token] = @token.token
|
session[:token] = @token.token
|
||||||
@issue = Issue.find @token.issue_id
|
@issue = Issue.find @token.issue_id
|
||||||
@journals = @issue.journals.
|
@journals = @issue.journals.
|
||||||
@@ -179,7 +185,7 @@ class CustomersController < ApplicationController
|
|||||||
@priorities = IssuePriority.active
|
@priorities = IssuePriority.active
|
||||||
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
|
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
|
||||||
@relation = IssueRelation.new
|
@relation = IssueRelation.new
|
||||||
else
|
rescue
|
||||||
render_403
|
render_403
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -212,14 +218,14 @@ class CustomersController < ApplicationController
|
|||||||
# format a quickbooks address to a human readable string
|
# format a quickbooks address to a human readable string
|
||||||
def address_to_s (address)
|
def address_to_s (address)
|
||||||
return if address.nil?
|
return if address.nil?
|
||||||
string = address.line1
|
string = address.line1 if address.line1
|
||||||
string << "\n" + address.line2 if address.line2
|
string << "\n" + address.line2 if address.line2
|
||||||
string << "\n" + address.line3 if address.line3
|
string << "\n" + address.line3 if address.line3
|
||||||
string << "\n" + address.line4 if address.line4
|
string << "\n" + address.line4 if address.line4
|
||||||
string << "\n" + address.line5 if address.line5
|
string << "\n" + address.line5 if address.line5
|
||||||
string << " " + address.city
|
string << " " + address.city if address.city
|
||||||
string << ", " + address.country_sub_division_code
|
string << ", " + address.country_sub_division_code if address.country_sub_division_code
|
||||||
string << " " + address.postal_code
|
string << " " + address.postal_code if address.postal_code
|
||||||
return string
|
return string
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -12,17 +12,23 @@ class EstimateController < ApplicationController
|
|||||||
|
|
||||||
include AuthHelper
|
include AuthHelper
|
||||||
|
|
||||||
before_action :require_user
|
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
|
||||||
|
estimate = Estimate.find_by_id(params[:id]) if params[:id]
|
||||||
|
estimate = Estimate.find_by_doc_number(params[:search]) if params[:search]
|
||||||
|
return estimate
|
||||||
|
end
|
||||||
|
|
||||||
#
|
#
|
||||||
# Downloads and forwards the estimate pdf
|
# Downloads and forwards the estimate pdf
|
||||||
#
|
#
|
||||||
def show
|
def show
|
||||||
e = Estimate.find_by_id(params[:id]) if params[:id]
|
estimate = get_estimate
|
||||||
e = Estimate.find_by_doc_number(params[:search]) if params[:search]
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
send_data e.pdf, filename: "estimate #{e.doc_number}.pdf", :disposition => 'inline', :type => "application/pdf"
|
send_data estimate.pdf, filename: "estimate #{estimate.doc_number}.pdf", :disposition => 'inline', :type => "application/pdf"
|
||||||
rescue
|
rescue
|
||||||
redirect_to :back, :flash => { :error => "Estimate not found" }
|
redirect_to :back, :flash => { :error => "Estimate not found" }
|
||||||
end
|
end
|
||||||
@@ -32,11 +38,10 @@ class EstimateController < ApplicationController
|
|||||||
# Downloads estimate by document number
|
# Downloads estimate by document number
|
||||||
#
|
#
|
||||||
def doc
|
def doc
|
||||||
e = Estimate.find_by_doc_number(params[:id]) if params[:id]
|
estimate = get_estimate
|
||||||
e = Estimate.find_by_doc_number(params[:search]) if params[:search]
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
send_data e.pdf, filename: "estimate #{e.doc_number}.pdf", :disposition => 'inline', :type => "application/pdf"
|
send_data estimate.pdf, filename: "estimate #{estimate.doc_number}.pdf", :disposition => 'inline', :type => "application/pdf"
|
||||||
rescue
|
rescue
|
||||||
redirect_to :back, :flash => { :error => "Estimate not found" }
|
redirect_to :back, :flash => { :error => "Estimate not found" }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class QboController < ApplicationController
|
|||||||
qbo.expire = 1.hour.from_now.utc
|
qbo.expire = 1.hour.from_now.utc
|
||||||
|
|
||||||
if qbo.save!
|
if qbo.save!
|
||||||
redirect_to sync_path, :flash => { :notice => "Successfully connected to Quickbooks" }
|
redirect_to qbo_sync_path, :flash => { :notice => "Successfully connected to Quickbooks" }
|
||||||
else
|
else
|
||||||
redirect_to plugin_settings_path(:redmine_qbo), :flash => { :error => "Error" }
|
redirect_to plugin_settings_path(:redmine_qbo), :flash => { :error => "Error" }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class Customer < ActiveRecord::Base
|
|||||||
|
|
||||||
# returns a human readable string
|
# returns a human readable string
|
||||||
def to_s
|
def to_s
|
||||||
return name
|
return "#{self[:name]} - #{phone_number.split(//).last(4).join unless phone_number.nil?}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Convenience Method
|
# Convenience Method
|
||||||
|
|||||||
@@ -11,17 +11,51 @@
|
|||||||
class CustomerToken < ActiveRecord::Base
|
class CustomerToken < ActiveRecord::Base
|
||||||
unloadable
|
unloadable
|
||||||
has_many :issues
|
has_many :issues
|
||||||
validates_presence_of :expires_at, :issue_id
|
validates_presence_of :issue_id
|
||||||
before_create :generate_token
|
before_create :generate_token, :generate_expire_date
|
||||||
|
attr_accessor :destroyed
|
||||||
|
after_destroy :mark_as_destroyed
|
||||||
|
|
||||||
OAUTH_CONSUMER_SECRET = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret'] || 'CONFIGURE__' + SecureRandom.uuid
|
OAUTH_CONSUMER_SECRET = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret'] || 'CONFIGURE__' + SecureRandom.uuid
|
||||||
|
|
||||||
|
# generates a random token using the plugin setting settingsOAuthConsumerSecret for salt
|
||||||
def generate_token
|
def generate_token
|
||||||
self.token = SecureRandom.base64(15).tr('+/=lIO0', OAUTH_CONSUMER_SECRET)
|
self.token = SecureRandom.base64(15).tr('+/=lIO0', OAUTH_CONSUMER_SECRET)
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_expired_tokens
|
# generates an expiring date
|
||||||
CustomerToken.where("expires_at < ?", Time.now).destroy_all
|
def generate_expire_date
|
||||||
|
self.expires_at = Time.now + 1.month
|
||||||
|
end
|
||||||
|
|
||||||
|
# set destroyed flag
|
||||||
|
def mark_as_destroyed
|
||||||
|
self.destroyed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
# purge expired tokens
|
||||||
|
def self.remove_expired_tokens
|
||||||
|
where("expires_at < ?", Time.now).destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
|
# has the token expired?
|
||||||
|
def expired?
|
||||||
|
self.expires_at < Time.now
|
||||||
|
end
|
||||||
|
|
||||||
|
# Getter convenience method for tokens
|
||||||
|
def self.get_token(issue)
|
||||||
|
|
||||||
|
# check to see if token exists & if it is expired
|
||||||
|
token = find_by_issue_id issue.id
|
||||||
|
unless token.nil?
|
||||||
|
return token unless token.expired?
|
||||||
|
# remove expired tokens
|
||||||
|
token.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
# only create new token if we have an issue to attach it to
|
||||||
|
return create(:issue_id => issue.id) if User.current.logged?
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ class Estimate < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
# process an estimate into the database
|
# process an estimate into the database
|
||||||
def self.process_estimate(estimate)
|
def self.process_estimate(qbo_estimate)
|
||||||
logger.info "Processing estimate #{estimate.id}"
|
logger.info "Processing estimate #{qbo_estimate.id}"
|
||||||
estimate = find_or_create_by(id: estimate.id)
|
estimate = find_or_create_by(id: qbo_estimate.id)
|
||||||
estimate.doc_number = estimate.doc_number
|
estimate.doc_number = qbo_estimate.doc_number
|
||||||
estimate.customer_id = estimate.customer_ref.value
|
estimate.customer_id = qbo_estimate.customer_ref.value
|
||||||
estimate.id = estimate.id
|
estimate.id = qbo_estimate.id
|
||||||
estimate.txn_date = estimate.txn_date
|
estimate.txn_date = qbo_estimate.txn_date
|
||||||
estimate.save!
|
estimate.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -70,26 +70,26 @@ class Invoice < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
# processes the invoice into the database
|
# processes the invoice into the database
|
||||||
def self.process_invoice(invoice)
|
def self.process_invoice(i)
|
||||||
logger.info "Processing invoice #{invoice.id}"
|
logger.info "Processing invoice #{i.id}"
|
||||||
|
|
||||||
# Load the invoice into the database
|
# Load the invoice into the database
|
||||||
invoice = Invoice.find_or_create_by(id: invoice.id)
|
invoice = Invoice.find_or_create_by(id: i.id)
|
||||||
invoice.doc_number = invoice.doc_number
|
invoice.doc_number = i.doc_number
|
||||||
invoice.id = invoice.id
|
invoice.id = i.id
|
||||||
invoice.customer_id = invoice.customer_ref
|
invoice.customer_id = i.customer_ref
|
||||||
invoice.txn_date = invoice.txn_date
|
invoice.txn_date = i.txn_date
|
||||||
invoice.save!
|
invoice.save!
|
||||||
|
|
||||||
# Scan the private notes for hashtags and attach to the applicable issues
|
# Scan the private notes for hashtags and attach to the applicable issues
|
||||||
if not invoice.private_note.nil?
|
if not i.private_note.nil?
|
||||||
invoice.private_note.scan(/#(\w+)/).flatten.each { |issue|
|
i.private_note.scan(/#(\w+)/).flatten.each { |issue|
|
||||||
attach_to_issue(Issue.find_by_id(issue.to_i), invoice)
|
attach_to_issue(Issue.find_by_id(issue.to_i), invoice)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Scan the line items for hashtags and attach to the applicable issues
|
# Scan the line items for hashtags and attach to the applicable issues
|
||||||
invoice.line_items.each { |line|
|
i.line_items.each { |line|
|
||||||
if line.description
|
if line.description
|
||||||
line.description.scan(/#(\w+)/).flatten.each { |issue|
|
line.description.scan(/#(\w+)/).flatten.each { |issue|
|
||||||
attach_to_issue(Issue.find_by_id(issue.to_i), invoice)
|
attach_to_issue(Issue.find_by_id(issue.to_i), invoice)
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
<h3><%=@issues.open.count%> <%=t(:label_open_issues)%>:</h3>
|
<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)%>:</h3>
|
<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} %>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<p style="float: right;"> <%= copy_object_url_link(request.url) %> </p>
|
||||||
|
|
||||||
<h2><%= issue_heading(@issue) %></h2>
|
<h2><%= issue_heading(@issue) %></h2>
|
||||||
|
|
||||||
<div class="<%= @issue.css_classes %> details">
|
<div class="<%= @issue.css_classes %> details">
|
||||||
@@ -20,13 +22,13 @@
|
|||||||
rows.left l(:field_status), @issue.status.name, :class => 'status'
|
rows.left l(:field_status), @issue.status.name, :class => 'status'
|
||||||
rows.left l(:field_priority), @issue.priority.name, :class => 'priority'
|
rows.left l(:field_priority), @issue.priority.name, :class => 'priority'
|
||||||
unless @issue.disabled_core_fields.include?('assigned_to_id')
|
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 ? link_to_user(@issue.assigned_to) : "-"), :class => 'assigned-to'
|
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
|
end
|
||||||
unless @issue.disabled_core_fields.include?('category_id') || (@issue.category.nil? && @issue.project.issue_categories.none?)
|
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'
|
rows.left l(:field_category), (@issue.category ? @issue.category.name : "-"), :class => 'category'
|
||||||
end
|
end
|
||||||
unless @issue.disabled_core_fields.include?('fixed_version_id') || (@issue.fixed_version.nil? && @issue.assignable_versions.none?)
|
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 ? link_to_version(@issue.fixed_version) : "-"), :class => 'fixed-version'
|
rows.left l(:field_fixed_version), (@issue.fixed_version ? @issue.fixed_version : "-"), :class => 'fixed-version'
|
||||||
end
|
end
|
||||||
unless @issue.disabled_core_fields.include?('start_date')
|
unless @issue.disabled_core_fields.include?('start_date')
|
||||||
rows.right l(:field_start_date), format_date(@issue.start_date), :class => 'start-date'
|
rows.right l(:field_start_date), format_date(@issue.start_date), :class => 'start-date'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<%= form_tag("/qbo/estimate/doc", :method => "get", id: "est-search-form") do %>
|
<%= form_tag(estimate_doc_path, :method => "get") do %>
|
||||||
<%= text_field_tag :search, params[:search], placeholder: t(:label_search_estimates), :autocomplete => "off" %>
|
<%= text_field_tag :search, params[:search], placeholder: t(:label_search_estimates), :autocomplete => "off" %>
|
||||||
<%= submit_tag t(:label_search), :formtarget => "_blank" %>
|
<%= submit_tag t(:label_search), :formtarget => "_blank" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -87,4 +87,5 @@ en:
|
|||||||
label_billed_success: "Successfully Billed "
|
label_billed_success: "Successfully Billed "
|
||||||
label_billing_error: "Cannot bill without a customer assigned"
|
label_billing_error: "Cannot bill without a customer assigned"
|
||||||
label_qbo_sync_success: "Successfully synced to Quickbooks"
|
label_qbo_sync_success: "Successfully synced to Quickbooks"
|
||||||
|
label_hours: "Hours"
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ post 'qbo/webhook', :to => 'qbo#webhook'
|
|||||||
|
|
||||||
# Estimate & Invoice PDF
|
# Estimate & Invoice PDF
|
||||||
get 'estimates/:id', :to => 'estimate#show', as: :estimate
|
get 'estimates/:id', :to => 'estimate#show', as: :estimate
|
||||||
get 'estimates/doc/:id', :to => 'estimate#doc', as: :estimate_doc
|
get 'estimates/doc/', :to => 'estimate#doc', as: :estimate_doc
|
||||||
get 'invoices/:id', :to => 'invoice#show', as: :invoice
|
get 'invoices/:id', :to => 'invoice#show', as: :invoice
|
||||||
|
|
||||||
#manual billing
|
#manual billing
|
||||||
|
|||||||
15
db/migrate/036_remove_qbo_time_entries.rb
Normal file
15
db/migrate/036_remove_qbo_time_entries.rb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#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 RemoveQboTimeEntries < ActiveRecord::Migration[5.1]
|
||||||
|
def change
|
||||||
|
rename_column :time_entries, :qbo_billed, :billed
|
||||||
|
end
|
||||||
|
end
|
||||||
8
init.rb
8
init.rb
@@ -1,6 +1,6 @@
|
|||||||
#The MIT License (MIT)
|
#The MIT License (MIT)
|
||||||
#
|
#
|
||||||
#Copyright (c) 2022 rick barrette
|
#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:
|
#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:
|
||||||
#
|
#
|
||||||
@@ -22,11 +22,11 @@ Redmine::Plugin.register :redmine_qbo do
|
|||||||
name 'Redmine Quickbooks Online plugin'
|
name 'Redmine Quickbooks Online plugin'
|
||||||
author 'Rick Barrette'
|
author 'Rick Barrette'
|
||||||
description 'This is a plugin for Redmine to intergrate with Quickbooks Online to allow for seamless intergration CRM and invoicing of completed issues'
|
description 'This is a plugin for Redmine to intergrate with Quickbooks Online to allow for seamless intergration CRM and invoicing of completed issues'
|
||||||
version '1.1.3'
|
version '2.0.0'
|
||||||
url 'https://github.com/rickbarrette/redmine_qbo'
|
url 'https://github.com/rickbarrette/redmine_qbo'
|
||||||
author_url 'http://rickbarrette.org'
|
author_url 'https://barrettefabrication.com'
|
||||||
settings :default => {'empty' => true}, :partial => 'qbo/settings'
|
settings :default => {'empty' => true}, :partial => 'qbo/settings'
|
||||||
requires_redmine :version_or_higher => '4.0.0'
|
requires_redmine :version_or_higher => '5.1.0'
|
||||||
|
|
||||||
# Add safe attributes for core models
|
# Add safe attributes for core models
|
||||||
Issue.safe_attributes 'customer_id'
|
Issue.safe_attributes 'customer_id'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#The MIT License (MIT)
|
#The MIT License (MIT)
|
||||||
#
|
#
|
||||||
#Copyright (c) 2017 rick barrette
|
#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:
|
#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,27 +12,32 @@ require_dependency 'attachments_controller'
|
|||||||
|
|
||||||
module AttachmentsControllerPatch
|
module AttachmentsControllerPatch
|
||||||
|
|
||||||
def self.included(base) # :nodoc:
|
def self.included(base)
|
||||||
base.extend(ClassMethods)
|
|
||||||
|
|
||||||
base.send(:include, InstanceMethods)
|
|
||||||
|
|
||||||
# Same as typing in the class
|
|
||||||
base.class_eval do
|
base.class_eval do
|
||||||
unloadable # Send unloadable so it will not be unloaded in development
|
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
|
||||||
|
# no check needed if user is already logged in
|
||||||
|
return true if User.current.logged?
|
||||||
|
|
||||||
|
# Pull up the attachmet, & verify if we have a valid token for the Issue
|
||||||
|
attachment = Attachment.find(params[:id])
|
||||||
|
token = CustomerToken.where("token = ? and expires_at > ?", session[:token], Time.now)
|
||||||
|
token = token.first
|
||||||
|
unless token.nil?
|
||||||
|
return true if token.issue_id == attachment.container_id
|
||||||
|
end
|
||||||
|
|
||||||
|
require_login if Setting.login_required?
|
||||||
|
end
|
||||||
|
|
||||||
skip_before_action :read_authorize
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
module ClassMethods
|
end
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
module InstanceMethods
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Add module to AttachmentsController
|
# Add module to AttachmentsController
|
||||||
AttachmentsController.send(:include, AttachmentsControllerPatch)
|
AttachmentsController.send(:include, AttachmentsControllerPatch)
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ module IssuePatch
|
|||||||
|
|
||||||
# Create a shareable link for a customer
|
# Create a shareable link for a customer
|
||||||
def share_token
|
def share_token
|
||||||
CustomerToken.create(:expires_at => Time.now + 1.month, :issue_id => id)
|
CustomerToken.get_token self
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
35
lib/issues_controller_patch.rb
Normal file
35
lib/issues_controller_patch.rb
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#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.
|
||||||
|
require_dependency 'issues_controller'
|
||||||
|
|
||||||
|
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_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
|
||||||
|
|
||||||
|
# Add module to IssuessController
|
||||||
|
IssuesController.send(:include, IssuesControllerPatch)
|
||||||
@@ -19,7 +19,11 @@ class IssuesSaveHookListener < Redmine::Hook::ViewListener
|
|||||||
# Called After Issue Saved
|
# Called After Issue Saved
|
||||||
def controller_issues_edit_after_save(context={})
|
def controller_issues_edit_after_save(context={})
|
||||||
issue = context[:issue]
|
issue = context[:issue]
|
||||||
issue.bill_time if issue.status.is_closed?
|
begin
|
||||||
|
issue.bill_time if issue.status.is_closed?
|
||||||
|
rescue
|
||||||
|
# TODO flash[:error] = "Unable to bill, check QBO Auth"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -59,12 +59,5 @@ class IssuesShowHookListener < Redmine::Hook::ViewListener
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
# Display Buttons under the issue above subtasks
|
|
||||||
def view_issues_show_description_bottom(context={})
|
|
||||||
bill_button = button_to I18n.t(:label_bill_time), bill_path( context[:issue].id ), method: :get if User.current.admin?
|
|
||||||
share_button = button_to I18n.t(:label_share), share_path( context[:issue].id ), method: :get if User.current.logged?
|
|
||||||
return "<br/> #{bill_button} #{share_button}"
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#The MIT License (MIT)
|
#The MIT License (MIT)
|
||||||
#
|
#
|
||||||
#Copyright (c) 2022 rick barrette
|
#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:
|
#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 @@
|
|||||||
require_dependency 'redmine/export/pdf'
|
require_dependency 'redmine/export/pdf'
|
||||||
require_dependency 'redmine/export/pdf/issues_pdf_helper'
|
require_dependency 'redmine/export/pdf/issues_pdf_helper'
|
||||||
|
|
||||||
module IssuesPdfHelperPatch
|
module PdfPatch
|
||||||
|
|
||||||
def self.included(base)
|
def self.included(base)
|
||||||
base.send(:include, InstanceMethods)
|
base.send(:include, InstanceMethods)
|
||||||
@@ -256,4 +256,4 @@ module IssuesPdfHelperPatch
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Redmine::Export::PDF::IssuesPdfHelper.send(:include, IssuesPdfHelperPatch)
|
Redmine::Export::PDF::IssuesPdfHelper.send(:include, PdfPatch)
|
||||||
|
|||||||
Reference in New Issue
Block a user