mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2026-02-13 09:13:58 -05:00
Compare commits
23 Commits
2026-1.1-n
...
2026.1.5
| Author | SHA1 | Date | |
|---|---|---|---|
| ff358d806e | |||
| b80e1d4e28 | |||
| f24128ef75 | |||
| d3a8c05f50 | |||
| f023cd246d | |||
| b7e18a3c3f | |||
| 67f2dbf4d8 | |||
| 924aa7657b | |||
| 16fe07f177 | |||
| 9257b2f938 | |||
| 0227681e92 | |||
| c034696810 | |||
| ffdabccd84 | |||
| 1f03908040 | |||
| 43a5317b4e | |||
| 4c49ec6890 | |||
| ef7faee685 | |||
| 02b48d2de4 | |||
| e670d99766 | |||
| 241dd594d0 | |||
| b603cb634a | |||
| 1308a05011 | |||
| 334ed60bf7 |
11
README.md
11
README.md
@@ -35,6 +35,7 @@ The goal of this project is to allow Redmine to connect with QuickBooks Online t
|
|||||||
* **Invoices:** Automatically attached to an Issue if a line item contains a hashtag number (e.g., `#123`).
|
* **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).
|
* **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.
|
* **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
|
## Prerequisites
|
||||||
|
|
||||||
@@ -90,11 +91,11 @@ To enable automatic Time Activity entries for an Issue, you simply need to assig
|
|||||||
|
|
||||||
**Note:** After the initial synchronization, this plugin will receive push notifications via Intuit's webhook service.
|
**Note:** After the initial synchronization, this plugin will receive push notifications via Intuit's webhook service.
|
||||||
|
|
||||||
## TODO
|
## Companion Plugin Hooks
|
||||||
|
* :pdf_left, { issue: issue }
|
||||||
* Add hooks to intergrate other plugins, i.e. customer vehicles for example
|
* :pdf_right, { issue: issue }
|
||||||
* MORE Stuff as I make it up...
|
* :process_invoice_custom_fields, { issue: issue, invoice: invoice }
|
||||||
|
* :show_customer_view_right, {customer: @customer}
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ class CustomersController < ApplicationController
|
|||||||
include SortHelper
|
include SortHelper
|
||||||
helper :timelog
|
helper :timelog
|
||||||
|
|
||||||
before_action :add_customer, :only => :new
|
before_action :add_customer, only: :new
|
||||||
before_action :view_customer, :except => [:new, :view]
|
before_action :view_customer, except: [:new, :view]
|
||||||
skip_before_action :verify_authenticity_token, :check_if_login_required, :only => [:view]
|
skip_before_action :verify_authenticity_token, :check_if_login_required, only: [:view]
|
||||||
|
|
||||||
default_search_scope :names
|
default_search_scope :names
|
||||||
|
|
||||||
autocomplete :customer, :name, :full => true, :extra_data => [:id]
|
autocomplete :customer, :name, full: true, extra_data: [:id]
|
||||||
|
|
||||||
def allowed_params
|
def allowed_params
|
||||||
params.require(:customer).permit(:name, :email, :primary_phone, :mobile_phone, :phone_number, :notes)
|
params.require(:customer).permit(:name, :email, :primary_phone, :mobile_phone, :phone_number, :notes)
|
||||||
@@ -53,7 +53,7 @@ class CustomersController < ApplicationController
|
|||||||
# display a list of all customers
|
# display a list of all customers
|
||||||
def index
|
def index
|
||||||
if params[:search]
|
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)
|
if only_one_non_zero?(@customers)
|
||||||
redirect_to @customers.first
|
redirect_to @customers.first
|
||||||
end
|
end
|
||||||
@@ -161,7 +161,7 @@ class CustomersController < ApplicationController
|
|||||||
@issue = Issue.find @token.issue_id
|
@issue = Issue.find @token.issue_id
|
||||||
@journals = @issue.journals.
|
@journals = @issue.journals.
|
||||||
preload(:details).
|
preload(:details).
|
||||||
preload(:user => :email_address).
|
preload(user: :email_address).
|
||||||
reorder(:created_on, :id).to_a
|
reorder(:created_on, :id).to_a
|
||||||
@journals.each_with_index {|j,i| j.indice = i+1}
|
@journals.each_with_index {|j,i| j.indice = i+1}
|
||||||
@journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
|
@journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
|
||||||
@@ -175,7 +175,7 @@ class CustomersController < ApplicationController
|
|||||||
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
|
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
|
||||||
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
|
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
|
||||||
@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
|
||||||
rescue
|
rescue
|
||||||
render_403
|
render_403
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ class EstimateController < ApplicationController
|
|||||||
|
|
||||||
include AuthHelper
|
include AuthHelper
|
||||||
|
|
||||||
before_action :require_user, :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? }
|
skip_before_action :verify_authenticity_token, :check_if_login_required, unless: proc {|c| session[:token].nil? }
|
||||||
|
|
||||||
def get_estimate
|
def get_estimate
|
||||||
# Force sync for estimate by doc number if not found
|
# Force sync for estimate by doc number if not found
|
||||||
@@ -36,9 +36,9 @@ class EstimateController < ApplicationController
|
|||||||
estimate = get_estimate
|
estimate = get_estimate
|
||||||
|
|
||||||
begin
|
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
|
rescue
|
||||||
redirect_to :back, :flash => { :error => "Estimate not found" }
|
redirect_to :back, flash: { error: "Estimate not found" }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -49,9 +49,9 @@ class EstimateController < ApplicationController
|
|||||||
estimate = get_estimate
|
estimate = get_estimate
|
||||||
|
|
||||||
begin
|
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
|
rescue
|
||||||
redirect_to :back, :flash => { :error => "Estimate not found" }
|
redirect_to :back, flash: { error: "Estimate not found" }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ class InvoiceController < ApplicationController
|
|||||||
include AuthHelper
|
include AuthHelper
|
||||||
require 'combine_pdf'
|
require 'combine_pdf'
|
||||||
|
|
||||||
before_action :require_user, :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? }
|
skip_before_action :verify_authenticity_token, :check_if_login_required, unless: proc {|c| session[:token].nil? }
|
||||||
|
|
||||||
#
|
#
|
||||||
# Downloads and forwards the invoice pdf
|
# Downloads and forwards the invoice pdf
|
||||||
@@ -23,7 +23,7 @@ class InvoiceController < ApplicationController
|
|||||||
begin
|
begin
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
qbo.perform_authenticated_request do |access_token|
|
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 multiple id's then pull each pdf & combine them
|
||||||
if params[:invoice_ids]
|
if params[:invoice_ids]
|
||||||
@@ -45,10 +45,10 @@ class InvoiceController < ApplicationController
|
|||||||
ref = invoice.doc_number
|
ref = invoice.doc_number
|
||||||
end
|
end
|
||||||
|
|
||||||
send_data @pdf, filename: "invoice #{ref}.pdf", :disposition => 'inline', :type => "application/pdf"
|
send_data @pdf, filename: "invoice #{ref}.pdf", disposition: 'inline', type: "application/pdf"
|
||||||
end
|
end
|
||||||
rescue
|
rescue
|
||||||
redirect_to :back, :flash => { :error => "Invoice not found" }
|
redirect_to :back, flash: { error: "Invoice not found" }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ class QboController < ApplicationController
|
|||||||
|
|
||||||
include AuthHelper
|
include AuthHelper
|
||||||
|
|
||||||
before_action :require_user, :except => :webhook
|
before_action :require_user, except: :webhook
|
||||||
skip_before_action :verify_authenticity_token, :check_if_login_required, :only => [:webhook]
|
skip_before_action :verify_authenticity_token, :check_if_login_required, only: [:webhook]
|
||||||
|
|
||||||
def allowed_params
|
def allowed_params
|
||||||
params.permit(:code, :state, :realmId, :id)
|
params.permit(:code, :state, :realmId, :id)
|
||||||
@@ -51,9 +51,9 @@ class QboController < ApplicationController
|
|||||||
qbo.refresh_token!
|
qbo.refresh_token!
|
||||||
|
|
||||||
if qbo.save!
|
if qbo.save!
|
||||||
redirect_to qbo_sync_path, :flash => { :notice => I18n.t(:label_connected) }
|
redirect_to qbo_sync_path, flash: { notice: I18n.t(:label_connected) }
|
||||||
else
|
else
|
||||||
redirect_to plugin_settings_path(:redmine_qbo), :flash => { :error => I18n.t(:label_error) }
|
redirect_to plugin_settings_path(:redmine_qbo), flash: { error: I18n.t(:label_error) }
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
@@ -65,9 +65,9 @@ class QboController < ApplicationController
|
|||||||
i = Issue.find_by_id params[:id]
|
i = Issue.find_by_id params[:id]
|
||||||
if i.customer
|
if i.customer
|
||||||
i.bill_time
|
i.bill_time
|
||||||
redirect_to i, :flash => { :notice => I18n.t(:label_billed_success) + i.customer.name }
|
redirect_to i, flash: { notice: I18n.t(:label_billed_success) + i.customer.name }
|
||||||
else
|
else
|
||||||
redirect_to i, :flash => { :error => I18n.t(:label_billing_error) }
|
redirect_to i, flash: { error: I18n.t(:label_billing_error) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ class QboController < ApplicationController
|
|||||||
ActiveRecord::Base.connection.close
|
ActiveRecord::Base.connection.close
|
||||||
end
|
end
|
||||||
# The webhook doesn't require a response but let's make sure we don't send anything
|
# The webhook doesn't require a response but let's make sure we don't send anything
|
||||||
render :nothing => true, status: 200
|
render nothing: true, status: 200
|
||||||
else
|
else
|
||||||
render nothing: true, status: 400
|
render nothing: true, status: 400
|
||||||
end
|
end
|
||||||
@@ -152,6 +152,6 @@ class QboController < ApplicationController
|
|||||||
ActiveRecord::Base.connection.close
|
ActiveRecord::Base.connection.close
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to :home, :flash => { :notice => I18n.t(:label_syncing) }
|
redirect_to :home, flash: { notice: I18n.t(:label_syncing) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ class Customer < ActiveRecord::Base
|
|||||||
# Sync ALL customers if the database is empty
|
# Sync ALL customers if the database is empty
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
customers = qbo.perform_authenticated_request do |access_token|
|
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
|
service.all
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ class Customer < ActiveRecord::Base
|
|||||||
def self.sync_by_id(id)
|
def self.sync_by_id(id)
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
c = qbo.perform_authenticated_request do |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 = Quickbooks::Service::Customer.new(company_id: qbo.realm_id, access_token: access_token)
|
||||||
service.fetch_by_id(id)
|
service.fetch_by_id(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ class Customer < ActiveRecord::Base
|
|||||||
begin
|
begin
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
@details = qbo.perform_authenticated_request do |access_token|
|
@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.update(@details)
|
service.update(@details)
|
||||||
end
|
end
|
||||||
#raise "QBO Fault" if @details.fault?
|
#raise "QBO Fault" if @details.fault?
|
||||||
@@ -227,7 +227,7 @@ class Customer < ActiveRecord::Base
|
|||||||
raise Exception unless self.id
|
raise Exception unless self.id
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
@details = qbo.perform_authenticated_request do |access_token|
|
@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)
|
service.fetch_by_id(self.id)
|
||||||
end
|
end
|
||||||
rescue Exception => e
|
rescue Exception => e
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class CustomerToken < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
# only create new token if we have an issue to attach it to
|
# 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
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,14 +16,13 @@ class Employee < ActiveRecord::Base
|
|||||||
def self.sync
|
def self.sync
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
employees = qbo.perform_authenticated_request do |access_token|
|
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
|
service.all
|
||||||
end
|
end
|
||||||
|
|
||||||
return unless employees
|
return unless employees
|
||||||
|
|
||||||
transaction do
|
transaction do
|
||||||
# Update the item table
|
|
||||||
employees.each { |e|
|
employees.each { |e|
|
||||||
logger.info "Processing employee #{e.id}"
|
logger.info "Processing employee #{e.id}"
|
||||||
employee = find_or_create_by(id: e.id)
|
employee = find_or_create_by(id: e.id)
|
||||||
@@ -37,7 +36,7 @@ class Employee < ActiveRecord::Base
|
|||||||
def self.sync_by_id(id)
|
def self.sync_by_id(id)
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
employee = qbo.perform_authenticated_request do |access_token|
|
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)
|
service.fetch_by_id(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,17 @@ class Estimate < ActiveRecord::Base
|
|||||||
validates_presence_of :doc_number, :id
|
validates_presence_of :doc_number, :id
|
||||||
self.primary_key = :id
|
self.primary_key = :id
|
||||||
|
|
||||||
|
# returns a human readable string
|
||||||
|
def to_s
|
||||||
|
return self[:doc_number]
|
||||||
|
end
|
||||||
|
|
||||||
# sync all estimates
|
# sync all estimates
|
||||||
def self.sync
|
def self.sync
|
||||||
logger.info "Syncing ALL estimates"
|
logger.info "Syncing ALL estimates"
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
estimates = qbo.perform_authenticated_request do |access_token|
|
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
|
service.all
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -39,7 +44,7 @@ class Estimate < ActiveRecord::Base
|
|||||||
logger.info "Syncing estimate #{id}"
|
logger.info "Syncing estimate #{id}"
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
qbo.perform_authenticated_request do |access_token|
|
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))
|
process_estimate(service.fetch_by_id(id))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -49,17 +54,16 @@ class Estimate < ActiveRecord::Base
|
|||||||
logger.info "Syncing estimate by doc number #{number}"
|
logger.info "Syncing estimate by doc number #{number}"
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
qbo.perform_authenticated_request do |access_token|
|
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.find_by( :doc_number, number).first)
|
process_estimate(service.find_by( :doc_number, number).first)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# update an estimate
|
# update an estimate
|
||||||
def self.update(id)
|
def self.update(id)
|
||||||
# Update the item table
|
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
estimate = qbo.perform_authenticated_request do |access_token|
|
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)
|
service.fetch_by_id(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -86,7 +90,7 @@ class Estimate < ActiveRecord::Base
|
|||||||
def pdf
|
def pdf
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
qbo.perform_authenticated_request do |access_token|
|
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)
|
estimate = service.fetch_by_id(id)
|
||||||
service.pdf(estimate)
|
service.pdf(estimate)
|
||||||
end
|
end
|
||||||
@@ -118,7 +122,7 @@ class Estimate < ActiveRecord::Base
|
|||||||
raise Exception unless self.id
|
raise Exception unless self.id
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
@details = qbo.perform_authenticated_request do |access_token|
|
@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)
|
service(:estimate).fetch_by_id(self.id)
|
||||||
end
|
end
|
||||||
rescue Exception => e
|
rescue Exception => e
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ class Invoice < ActiveRecord::Base
|
|||||||
validates_presence_of :doc_number, :id, :customer_id, :txn_date
|
validates_presence_of :doc_number, :id, :customer_id, :txn_date
|
||||||
self.primary_key = :id
|
self.primary_key = :id
|
||||||
|
|
||||||
|
# returns a human readable string
|
||||||
|
def to_s
|
||||||
|
return self[:doc_number]
|
||||||
|
end
|
||||||
|
|
||||||
# sync ALL the invoices
|
# sync ALL the invoices
|
||||||
def self.sync
|
def self.sync
|
||||||
logger.info "Syncing all invoices"
|
logger.info "Syncing all invoices"
|
||||||
@@ -27,7 +32,7 @@ class Invoice < ActiveRecord::Base
|
|||||||
# .all() is never called since count is never initialized
|
# .all() is never called since count is never initialized
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
invoices = qbo.perform_authenticated_request do |access_token|
|
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
|
service.all
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -43,7 +48,7 @@ class Invoice < ActiveRecord::Base
|
|||||||
logger.info "Syncing invoice #{id}"
|
logger.info "Syncing invoice #{id}"
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
qbo.perform_authenticated_request do |access_token|
|
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)
|
invoice = service.fetch_by_id(id)
|
||||||
process_invoice invoice
|
process_invoice invoice
|
||||||
end
|
end
|
||||||
@@ -112,15 +117,15 @@ class Invoice < ActiveRecord::Base
|
|||||||
logger.debug "Calling :process_invoice_custom_fields hook"
|
logger.debug "Calling :process_invoice_custom_fields hook"
|
||||||
context = Redmine::Hook.call_hook :process_invoice_custom_fields, { issue: issue, invoice: invoice }
|
context = Redmine::Hook.call_hook :process_invoice_custom_fields, { issue: issue, invoice: invoice }
|
||||||
|
|
||||||
unless context.nil?
|
# Process updates from the hooks
|
||||||
logger.debug "We have a context!"
|
context.each do |c|
|
||||||
context= context.first
|
unless c.nil?
|
||||||
issue = context[:issue]
|
logger.debug "Invoice.compare_custom_fields: We have a responce from a hook"
|
||||||
invoice = context[:invoice]
|
push_updates c[:invoice] if c[:is_changed]
|
||||||
is_changed = context[:is_changed]
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Custom Values
|
# Process Issue Custom Values
|
||||||
begin
|
begin
|
||||||
value = issue.custom_values.find_by(custom_field_id: CustomField.find_by_name(cf.name).id)
|
value = issue.custom_values.find_by(custom_field_id: CustomField.find_by_name(cf.name).id)
|
||||||
|
|
||||||
@@ -137,13 +142,17 @@ class Invoice < ActiveRecord::Base
|
|||||||
# Nothing to do here, there is no match
|
# Nothing to do here, there is no match
|
||||||
end
|
end
|
||||||
|
|
||||||
# Push updates
|
push_updates invoice if is_changed
|
||||||
|
end
|
||||||
|
|
||||||
|
# pushes invoice updates
|
||||||
|
def self.push_updates(invoice)
|
||||||
begin
|
begin
|
||||||
logger.info "Trying to update invoice"
|
logger.info "Invoice.push_updates"
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
qbo.perform_authenticated_request do |access_token|
|
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.update(invoice) if is_changed
|
service.update invoice
|
||||||
end
|
end
|
||||||
rescue
|
rescue
|
||||||
# Do nothing, probaly custome field sync confict on the invoice.
|
# Do nothing, probaly custome field sync confict on the invoice.
|
||||||
@@ -158,7 +167,7 @@ class Invoice < ActiveRecord::Base
|
|||||||
def pdf
|
def pdf
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
qbo.perform_authenticated_request do |access_token|
|
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)
|
invoice = service.fetch_by_id(id)
|
||||||
return service.pdf(invoice)
|
return service.pdf(invoice)
|
||||||
end
|
end
|
||||||
@@ -188,7 +197,7 @@ class Invoice < ActiveRecord::Base
|
|||||||
raise Exception unless self.id
|
raise Exception unless self.id
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
@details = qbo.perform_authenticated_request do |access_token|
|
@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)
|
service.fetch_by_id(self.id)
|
||||||
end
|
end
|
||||||
rescue Exception => e
|
rescue Exception => e
|
||||||
|
|||||||
@@ -7,28 +7,28 @@
|
|||||||
<div class="clearfix">
|
<div class="clearfix">
|
||||||
<%=t(:label_display_name)%>
|
<%=t(:label_display_name)%>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<%= f.text_field :name, :required => true, :autocomplete => "off" %>
|
<%= f.text_field :name, required: true, autocomplete: "off" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clearfix">
|
<div class="clearfix">
|
||||||
<%=t(:label_primary_phone)%>
|
<%=t(:label_primary_phone)%>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<%= f.telephone_field :primary_phone, :autocomplete => "off" %>
|
<%= f.telephone_field :primary_phone, autocomplete: "off" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clearfix">
|
<div class="clearfix">
|
||||||
<%=t(:label_mobile_phone)%>:
|
<%=t(:label_mobile_phone)%>:
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<%= f.telephone_field :mobile_phone, :autocomplete => "off" %>
|
<%= f.telephone_field :mobile_phone, autocomplete: "off" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clearfix">
|
<div class="clearfix">
|
||||||
<%=t(:label_email)%>:
|
<%=t(:label_email)%>:
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<%= f.email_field :email, :autocomplete => "off" %>
|
<%= f.email_field :email, autocomplete: "off" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -36,13 +36,13 @@
|
|||||||
<%=t(:field_notes)%>:
|
<%=t(:field_notes)%>:
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<p>
|
<p>
|
||||||
<%= content_tag 'span', :id => "issue_description_and_toolbar" do %>
|
<%= content_tag 'span', id: "issue_description_and_toolbar" do %>
|
||||||
<%= f.text_area :notes,
|
<%= f.text_area :notes,
|
||||||
:cols => 60,
|
cols: 60,
|
||||||
:rows => 10,
|
rows: 10,
|
||||||
:accesskey => accesskey(:edit),
|
accesskey: accesskey(:edit),
|
||||||
:class => 'wiki-edit',
|
class: 'wiki-edit',
|
||||||
:no_label => true %>
|
no_label: true %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
<%= wikitoolbar_for 'issue_description' %>
|
<%= wikitoolbar_for 'issue_description' %>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<%= form_tag(customers_path, :method => "get", id: "search-form") do %>
|
<%= form_tag(customers_path, method: "get", id: "search-form") do %>
|
||||||
<%= text_field_tag :search, params[:search], placeholder: t(:label_search_customers), :autocomplete => "off" %>
|
<%= text_field_tag :search, params[:search], placeholder: t(:label_search_customers), autocomplete: "off" %>
|
||||||
<%= submit_tag t(:label_search) %>
|
<%= submit_tag t(:label_search) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= button_to t(:label_new_customer), new_customer_path, method: :get%>
|
<%= button_to t(:label_new_customer), new_customer_path, method: :get%>
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
<h3><%=t(:label_customers)%></h3>
|
<h3><%=t(:label_customers)%></h3>
|
||||||
<%= render :partial => 'customers/search' %>
|
<%= render partial: 'customers/search' %>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<h1><%=t(:label_edit_customer)%></h1>
|
<h1><%=t(:label_edit_customer)%></h1>
|
||||||
<br/>
|
<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? %>
|
<% if @customers.present? %>
|
||||||
<br/>
|
<br/>
|
||||||
<% @customers.each do |c| %>
|
<% @customers.each do |c| %>
|
||||||
@@ -20,5 +20,5 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<%= render :partial => 'qbo/stats' %>
|
<%= render partial: 'qbo/stats' %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<h2><%=t(:label_new_customer)%></h2>
|
<h2><%=t(:label_new_customer)%></h2>
|
||||||
<br/>
|
<br/>
|
||||||
<%= render :partial => 'customers/form' %>
|
<%= render partial: 'customers/form' %>
|
||||||
|
|||||||
@@ -12,12 +12,12 @@
|
|||||||
<div class="splitcontent">
|
<div class="splitcontent">
|
||||||
<div class="splitcontentleft">
|
<div class="splitcontentleft">
|
||||||
<h4><%=t(:field_customer)%>:</h4>
|
<h4><%=t(:field_customer)%>:</h4>
|
||||||
<%= render :partial => 'customers/details', locals: {customer: @customer} %>
|
<%= render partial: 'customers/details', locals: {customer: @customer} %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="splitcontentleft">
|
<div class="splitcontentleft">
|
||||||
<h4><%=t(:label_actions)%>:</h4>
|
<h4><%=t(:label_actions)%>:</h4>
|
||||||
<%= render :partial => 'customers/actions', locals: {customer: @customer} %>
|
<%= render partial: 'customers/actions', locals: {customer: @customer} %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -27,12 +27,12 @@
|
|||||||
<div class="splitcontent">
|
<div class="splitcontent">
|
||||||
<div class="splitcontentleft">
|
<div class="splitcontentleft">
|
||||||
<h4><%=t(:estimates)%>:</h4>
|
<h4><%=t(:estimates)%>:</h4>
|
||||||
<%= render :partial => 'estimates/list', locals: {customer: @customer} %>
|
<%= render partial: 'estimates/list', locals: {customer: @customer} %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="splitcontentleft">
|
<div class="splitcontentleft">
|
||||||
<h4><%=t(:label_invoices)%>:</h4>
|
<h4><%=t(:label_invoices)%>:</h4>
|
||||||
<%= render :partial => 'invoices/list', locals: {customer: @customer} %>
|
<%= render partial: 'invoices/list', locals: {customer: @customer} %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
<h3><%=@issues.open.count%> <%=t(:label_open_issues)%> - <%=@hours.round(1)%> <%=t(:label_hours)%></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)%> - <%= @closed_hours.round(1)%> <%=t(:label_hours)%></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} %>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<div class="<%= @issue.css_classes %> details">
|
<div class="<%= @issue.css_classes %> details">
|
||||||
|
|
||||||
<%= avatar(@issue.author, :size => "50") %>
|
<%= avatar(@issue.author, size: "50") %>
|
||||||
|
|
||||||
<div class="subject">
|
<div class="subject">
|
||||||
<%= render_issue_subject_with_tree(@issue) %>
|
<%= render_issue_subject_with_tree(@issue) %>
|
||||||
@@ -19,39 +19,39 @@
|
|||||||
|
|
||||||
<div class="attributes">
|
<div class="attributes">
|
||||||
<%= issue_fields_rows do |rows|
|
<%= issue_fields_rows do |rows|
|
||||||
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 ? @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 ? @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'
|
||||||
end
|
end
|
||||||
unless @issue.disabled_core_fields.include?('due_date')
|
unless @issue.disabled_core_fields.include?('due_date')
|
||||||
rows.right l(:field_due_date), format_date(@issue.due_date), :class => 'due-date'
|
rows.right l(:field_due_date), format_date(@issue.due_date), class: 'due-date'
|
||||||
end
|
end
|
||||||
unless @issue.disabled_core_fields.include?('done_ratio')
|
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'
|
rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, legend: "#{@issue.done_ratio}%"), class: 'progress'
|
||||||
end
|
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
|
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
|
||||||
end
|
end
|
||||||
#if User.current.allowed_to_view_all_time_entries?(@project)
|
#if User.current.allowed_to_view_all_time_entries?(@project)
|
||||||
if @issue.total_spent_hours > 0
|
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
|
#end
|
||||||
end %>
|
end %>
|
||||||
<%= render_full_width_custom_fields_rows(@issue) %>
|
<%= 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>
|
</div>
|
||||||
|
|
||||||
<% if @issue.description? || @issue.attachments.any? -%>
|
<% if @issue.description? || @issue.attachments.any? -%>
|
||||||
@@ -59,19 +59,19 @@ end %>
|
|||||||
<% if @issue.description? %>
|
<% if @issue.description? %>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<div class="contextual">
|
<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>
|
</div>
|
||||||
|
|
||||||
<p><strong><%=l(:field_description)%></strong></p>
|
<p><strong><%=l(:field_description)%></strong></p>
|
||||||
<div class="wiki">
|
<div class="wiki">
|
||||||
<%= textilizable @issue, :description, :attachments => @issue.attachments %>
|
<%= textilizable @issue, :description, attachments: @issue.attachments %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= link_to_attachments @issue, :thumbnails => true %>
|
<%= link_to_attachments @issue, thumbnails: true %>
|
||||||
<% end -%>
|
<% 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) %>
|
<% if !@issue.leaf? || User.current.allowed_to?(:manage_subtasks, @project) %>
|
||||||
<hr />
|
<hr />
|
||||||
@@ -87,7 +87,7 @@ end %>
|
|||||||
<% if @relations.present? || User.current.allowed_to?(:manage_issue_relations, @project) %>
|
<% if @relations.present? || User.current.allowed_to?(:manage_issue_relations, @project) %>
|
||||||
<hr />
|
<hr />
|
||||||
<div id="relations">
|
<div id="relations">
|
||||||
<%= render :partial => 'issues/relations' %>
|
<%= render partial: 'issues/relations' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
@@ -96,14 +96,14 @@ end %>
|
|||||||
<% if @changesets.present? %>
|
<% if @changesets.present? %>
|
||||||
<div id="issue-changesets">
|
<div id="issue-changesets">
|
||||||
<h3><%=l(:label_associated_revisions)%></h3>
|
<h3><%=l(:label_associated_revisions)%></h3>
|
||||||
<%= render :partial => 'issues/changesets', :locals => { :changesets => @changesets} %>
|
<%= render partial: 'issues/changesets', locals: { changesets: @changesets} %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if @journals.present? %>
|
<% if @journals.present? %>
|
||||||
<div id="history">
|
<div id="history">
|
||||||
<h3><%=l(:label_history)%></h3>
|
<h3><%=l(:label_history)%></h3>
|
||||||
<%= render :partial => 'issues/history', :locals => { :issue => @issue, :journals => @journals } %>
|
<%= render partial: 'issues/history', locals: { issue: @issue, journals: @journals } %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<%= form_tag(estimate_doc_path, :method => "get") 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 %>
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
<h3><%=t(:label_estimates) %></h3>
|
<h3><%=t(:label_estimates) %></h3>
|
||||||
<%= render :partial => 'estimates/search' %>
|
<%= render partial: 'estimates/search' %>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<label for="issue_customer"><%= t(:customer) %></label>
|
<label for="issue_customer"><%= t(:customer) %></label>
|
||||||
<%= search_customer %>
|
<%= search_customer %>
|
||||||
<%= customer_id %>
|
<%= customer_id %>
|
||||||
<%= link_to t(:label_load_customer), '#', onclick: "#{js_link}; return false;" %>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
<div id="change-<%= journal.id %>" class="<%= journal.css_classes %>">
|
<div id="change-<%= journal.id %>" class="<%= journal.css_classes %>">
|
||||||
<div id="note-<%= journal.indice %>">
|
<div id="note-<%= journal.indice %>">
|
||||||
<div class="contextual">
|
<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>
|
<a href="#note-<%= journal.indice %>" class="journal-link">#<%= journal.indice %></a>
|
||||||
</div>
|
</div>
|
||||||
<h4>
|
<h4>
|
||||||
<%= avatar(journal.user, :size => "24") %>
|
<%= avatar(journal.user, size: "24") %>
|
||||||
<%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %>
|
<%= authoring journal.created_on, journal.user, label: :label_updated_time_by %>
|
||||||
<%= render_private_notes_indicator(journal) %>
|
<%= render_private_notes_indicator(journal) %>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
@@ -26,10 +26,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% 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>
|
||||||
</div>
|
</div>
|
||||||
<%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %>
|
<%= call_hook(:view_issues_history_journal_bottom, { journal: journal }) %>
|
||||||
<% end %>
|
<% 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) %>
|
<% heads_for_wiki_formatter if User.current.allowed_to?(:edit_issue_notes, issue.project) || User.current.allowed_to?(:edit_own_issue_notes, issue.project) %>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<% for issue in issues %>
|
<% 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">
|
<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)) %>
|
<%= link_to(issue.id, issue_path(issue)) %>
|
||||||
</td>
|
</td>
|
||||||
<td class="project"><%= link_to_project(issue.project) %></td>
|
<td class="project"><%= link_to_project(issue.project) %></td>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<% if User.current.logged? %>
|
<% if User.current.logged? %>
|
||||||
|
|
||||||
<%= render :partial => 'customers/sidebar' %>
|
<%= render partial: 'customers/sidebar' %>
|
||||||
<%= render :partial => 'estimates/sidebar' %>
|
<%= render partial: 'estimates/sidebar' %>
|
||||||
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<%= Customer.count %> <%=t(:field_customers)%> - <%= render :partial => 'qbo/last_sync' %>
|
<%= Customer.count %> <%=t(:field_customers)%> - <%= render partial: 'qbo/last_sync' %>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
# Usage I18n.t(:label)
|
# Usage I18n.t(:label)
|
||||||
en:
|
en:
|
||||||
field_customer: "Customer"
|
field_customer: "Customer"
|
||||||
field_item: "Item"
|
|
||||||
field_employee: "Employee"
|
field_employee: "Employee"
|
||||||
field_invoice: "Invoice"
|
field_invoice: "Invoice"
|
||||||
field_estimate: "Estimate"
|
field_estimate: "Estimate"
|
||||||
@@ -54,7 +53,6 @@ en:
|
|||||||
label_customer_count: "Customer Count"
|
label_customer_count: "Customer Count"
|
||||||
label_invoice_count: "Invoice Count"
|
label_invoice_count: "Invoice Count"
|
||||||
label_estimate_count: "Estimate Count"
|
label_estimate_count: "Estimate Count"
|
||||||
label_item_count: "Item Count"
|
|
||||||
label_employee_count: "Employee Count"
|
label_employee_count: "Employee Count"
|
||||||
label_client_id: "Intuit QBO OAuth2 Client ID"
|
label_client_id: "Intuit QBO OAuth2 Client ID"
|
||||||
label_client_secret: "Intuit QBO OAuth2 Client Secret"
|
label_client_secret: "Intuit QBO OAuth2 Client Secret"
|
||||||
|
|||||||
@@ -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.
|
#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
|
#authentication
|
||||||
get 'qbo/authenticate', :to => 'qbo#authenticate'
|
get 'qbo/authenticate', to: 'qbo#authenticate'
|
||||||
get 'qbo/oauth_callback', :to => 'qbo#oauth_callback'
|
get 'qbo/oauth_callback', to: 'qbo#oauth_callback'
|
||||||
|
|
||||||
#manual sync
|
#manual sync
|
||||||
get 'qbo/sync', :to => 'qbo#sync'
|
get 'qbo/sync', to: 'qbo#sync'
|
||||||
|
|
||||||
#webhook
|
#webhook
|
||||||
post 'qbo/webhook', :to => 'qbo#webhook'
|
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/', :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
|
||||||
get 'bill/:id', :to => 'qbo#bill', as: :bill
|
get 'bill/:id', to: 'qbo#bill', as: :bill
|
||||||
|
|
||||||
#customer issue view
|
#customer issue view
|
||||||
get 'customers/view/:token', :to => 'customers#view', as: :view
|
get 'customers/view/:token', to: 'customers#view', as: :view
|
||||||
get 'customers/share/:id', :to => 'customers#share', as: :share
|
get 'customers/share/:id', to: 'customers#share', as: :share
|
||||||
|
|
||||||
#java script routes
|
#java script routes
|
||||||
get 'filter_estimates_by_customer' => 'customers#filter_estimates_by_customer'
|
get 'filter_estimates_by_customer' => 'customers#filter_estimates_by_customer'
|
||||||
get 'filter_invoices_by_customer' => 'customers#filter_invoices_by_customer'
|
get 'filter_invoices_by_customer' => 'customers#filter_invoices_by_customer'
|
||||||
|
|
||||||
resources :customers do
|
resources :customers do
|
||||||
get :autocomplete_customer_name, :on => :collection
|
get :autocomplete_customer_name, on: :collection
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
class CreateQboCustomers < ActiveRecord::Migration[5.1]
|
class CreateQboCustomers < ActiveRecord::Migration[5.1]
|
||||||
def change
|
def change
|
||||||
create_table :qbo_customers, id: false do |t|
|
create_table :qbo_customers, id: false do |t|
|
||||||
t.integer :id, :options => 'PRIMARY KEY'
|
t.integer :id, options: 'PRIMARY KEY'
|
||||||
t.string :name
|
t.string :name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
class CreateQboItems < ActiveRecord::Migration[5.1]
|
class CreateQboItems < ActiveRecord::Migration[5.1]
|
||||||
def change
|
def change
|
||||||
create_table :qbo_items, id: false do |t|
|
create_table :qbo_items, id: false do |t|
|
||||||
t.integer :id, :options => 'PRIMARY KEY'
|
t.integer :id, options: 'PRIMARY KEY'
|
||||||
t.string :name
|
t.string :name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
class CreateQboEmployees < ActiveRecord::Migration[5.1]
|
class CreateQboEmployees < ActiveRecord::Migration[5.1]
|
||||||
def change
|
def change
|
||||||
create_table :qbo_employees, id: false do |t|
|
create_table :qbo_employees, id: false do |t|
|
||||||
t.integer :id, :options => 'PRIMARY KEY'
|
t.integer :id, options: 'PRIMARY KEY'
|
||||||
t.string :name
|
t.string :name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,6 +10,6 @@
|
|||||||
|
|
||||||
class UpdateTimeEntries < ActiveRecord::Migration[5.1]
|
class UpdateTimeEntries < ActiveRecord::Migration[5.1]
|
||||||
def change
|
def change
|
||||||
add_column :time_entries, :qbo_billed, :boolean, :default => false
|
add_column :time_entries, :qbo_billed, :boolean, default: false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
class CreateQboEstimates < ActiveRecord::Migration[5.1]
|
class CreateQboEstimates < ActiveRecord::Migration[5.1]
|
||||||
def change
|
def change
|
||||||
create_table :qbo_estimates, id: false do |t|
|
create_table :qbo_estimates, id: false do |t|
|
||||||
t.integer :id, :options => 'PRIMARY KEY'
|
t.integer :id, options: 'PRIMARY KEY'
|
||||||
t.string :doc_number
|
t.string :doc_number
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
class CreateQboInvoices < ActiveRecord::Migration[5.1]
|
class CreateQboInvoices < ActiveRecord::Migration[5.1]
|
||||||
def change
|
def change
|
||||||
create_table :qbo_invoices, id: false do |t|
|
create_table :qbo_invoices, id: false do |t|
|
||||||
t.integer :id, :options => 'PRIMARY KEY'
|
t.integer :id, options: 'PRIMARY KEY'
|
||||||
t.string :doc_number
|
t.string :doc_number
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
class CreateQboPurchases< ActiveRecord::Migration[5.1]
|
class CreateQboPurchases< ActiveRecord::Migration[5.1]
|
||||||
def change
|
def change
|
||||||
create_table :qbo_purchases, id: false do |t|
|
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.integer :line_id
|
||||||
t.string :description
|
t.string :description
|
||||||
t.integer :customer_id
|
t.integer :customer_id
|
||||||
|
|||||||
@@ -10,12 +10,12 @@
|
|||||||
|
|
||||||
class AddIssuesQboInvoices < ActiveRecord::Migration[5.1]
|
class AddIssuesQboInvoices < ActiveRecord::Migration[5.1]
|
||||||
def self.up
|
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 :issue
|
||||||
t.references :qbo_invoice
|
t.references :qbo_invoice
|
||||||
end
|
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!
|
# 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"
|
execute "insert into issues_qbo_invoices(issue_id, qbo_invoice_id) select id, qbo_invoice_id from issues"
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
#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.
|
|
||||||
|
|
||||||
class UpdateProjects < ActiveRecord::Migration[5.1]
|
|
||||||
def change
|
|
||||||
add_reference :projects, :customer, index: true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -30,7 +30,7 @@ class AddTxnDates < ActiveRecord::Migration[5.1]
|
|||||||
|
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
invoices = qbo.perform_authenticated_request do |access_token|
|
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
|
service.all
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
16
init.rb
16
init.rb
@@ -11,33 +11,31 @@
|
|||||||
Redmine::Plugin.register :redmine_qbo do
|
Redmine::Plugin.register :redmine_qbo do
|
||||||
|
|
||||||
# About
|
# About
|
||||||
name 'Redmine QBO DEVELOPMENT plugin'
|
name 'Redmine QBO 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 '2026.1.1'
|
version '2026.1.5'
|
||||||
url 'https://github.com/rickbarrette/redmine_qbo'
|
url 'https://github.com/rickbarrette/redmine_qbo'
|
||||||
author_url 'https://barrettefabrication.com'
|
author_url 'https://barrettefabrication.com'
|
||||||
settings :default => {'empty' => true}, :partial => 'qbo/settings'
|
settings default: {'empty' => true}, partial: 'qbo/settings'
|
||||||
requires_redmine :version_or_higher => '6.1.0'
|
requires_redmine version_or_higher: '6.1.0'
|
||||||
|
|
||||||
# Add safe attributes for core models
|
# Add safe attributes for core models
|
||||||
Issue.safe_attributes 'customer_id'
|
Issue.safe_attributes 'customer_id'
|
||||||
Issue.safe_attributes 'item_id'
|
|
||||||
Issue.safe_attributes 'estimate_id'
|
Issue.safe_attributes 'estimate_id'
|
||||||
Issue.safe_attributes 'invoice_id'
|
Issue.safe_attributes 'invoice_id'
|
||||||
User.safe_attributes 'employee_id'
|
User.safe_attributes 'employee_id'
|
||||||
TimeEntry.safe_attributes 'billed'
|
TimeEntry.safe_attributes 'billed'
|
||||||
Project.safe_attributes 'customer_id'
|
|
||||||
|
|
||||||
# set per_page globally
|
# set per_page globally
|
||||||
WillPaginate.per_page = 20
|
WillPaginate.per_page = 20
|
||||||
|
|
||||||
# Permissions for security
|
# Permissions for security
|
||||||
permission :view_customers, :customers => :index, :public => false
|
permission :view_customers, customers: :index, public: false
|
||||||
permission :add_customers, :customers => :new, :public => false
|
permission :add_customers, customers: :new, public: false
|
||||||
|
|
||||||
# Register top menu items
|
# 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: 'Customers', if: Proc.new {User.current.logged?}
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -20,55 +20,35 @@ module Hooks
|
|||||||
f = context[:form]
|
f = context[:form]
|
||||||
issue = context[:issue]
|
issue = context[:issue]
|
||||||
|
|
||||||
# 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 = issue.customer ? issue.customer.id : nil
|
|
||||||
selected_estimate = issue.estimate ? issue.estimate.id : nil
|
|
||||||
|
|
||||||
# Gernerate edit.js link
|
|
||||||
js_link = "updateIssueFrom('#{escape_javascript update_issue_form_path(issue.project, issue)}', this)"
|
|
||||||
|
|
||||||
# Load customer information
|
|
||||||
customer = Customer.find_by_id(selected_customer) if selected_customer
|
|
||||||
|
|
||||||
# Customer Name Text Box with database backed autocomplete
|
# Customer Name Text Box with database backed autocomplete
|
||||||
|
# onchange event will update the hidden customer_id field
|
||||||
search_customer = f.autocomplete_field :customer,
|
search_customer = f.autocomplete_field :customer,
|
||||||
autocomplete_customer_name_customers_path,
|
autocomplete_customer_name_customers_path,
|
||||||
:selected => selected_customer,
|
selected: issue.customer,
|
||||||
:onchange => js_link,
|
update_elements: {
|
||||||
:update_elements => {
|
id: '#issue_customer_id',
|
||||||
:id => '#issue_customer_id',
|
value: '#issue_customer'
|
||||||
:value => '#issue_customer'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Customer ID - Hidden Field
|
# 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,
|
customer_id = f.hidden_field :customer_id,
|
||||||
:id => "issue_customer_id",
|
id: "issue_customer_id",
|
||||||
:onchange => js_link
|
onchange: "updateIssueFrom('#{escape_javascript update_issue_form_path(issue.project, issue)}', this)".html_safe
|
||||||
|
|
||||||
# Load estimates
|
# Generate the drop down list of quickbooks estimates owned by the selected customer
|
||||||
if issue.customer
|
select_estimate = f.select :estimate_id,
|
||||||
estimates = customer.estimates.pluck(:doc_number, :id).sort! {|x, y| y <=> x}
|
issue.customer ? issue.customer.estimates.pluck(:doc_number, :id).sort! {|x, y| y <=> x} : [],
|
||||||
else
|
selected: issue.estimate,
|
||||||
estimates = [nil].compact
|
include_blank: true
|
||||||
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
|
# Pass all prebuilt form components to our partial
|
||||||
context[:controller].send(:render_to_string, {
|
context[:controller].send(:render_to_string, {
|
||||||
:partial => 'issues/form_hook',
|
partial: 'issues/form_hook',
|
||||||
locals: {
|
locals: {
|
||||||
search_customer: search_customer,
|
search_customer: search_customer,
|
||||||
customer_id: customer_id,
|
customer_id: customer_id,
|
||||||
js_link: js_link,
|
select_estimate: select_estimate
|
||||||
select_estimate: select_estimate,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ module Hooks
|
|||||||
|
|
||||||
# Called Before Issue Saved
|
# Called Before Issue Saved
|
||||||
def controller_issues_edit_before_save(context={})
|
def controller_issues_edit_before_save(context={})
|
||||||
issue = context[:issue]
|
return context[:issue].subject = context[:issue].subject.titleize
|
||||||
issue.subject = issue.subject.titleize
|
end
|
||||||
|
|
||||||
|
def controller_issues_new_before_save(context={})
|
||||||
|
return context[:issue].subject = context[:issue].subject.titleize
|
||||||
end
|
end
|
||||||
|
|
||||||
# Called After Issue Saved
|
# Called After Issue Saved
|
||||||
|
|||||||
@@ -13,37 +13,24 @@ module Hooks
|
|||||||
class IssuesShowHookListener < Redmine::Hook::ViewListener
|
class IssuesShowHookListener < Redmine::Hook::ViewListener
|
||||||
|
|
||||||
# View Issue
|
# View Issue
|
||||||
# Display the quickbooks contact in the issue
|
# Displays the quickbooks customer, estimate, & invoices attached to the issue
|
||||||
def view_issues_show_details_bottom(context={})
|
def view_issues_show_details_bottom(context={})
|
||||||
issue = context[:issue]
|
issue = context[:issue]
|
||||||
|
|
||||||
# Check to see if there is a quickbooks user attached to the issue
|
# Build a list of invoice links
|
||||||
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 = ""
|
invoice_link = ""
|
||||||
if issue.invoice_ids
|
if issue.invoices
|
||||||
issue.invoice_ids.each do |i|
|
issue.invoices.each do |i|
|
||||||
invoice = Invoice.find i
|
invoice_link += "#{link_to i, i, target: :_blank}<br/>"
|
||||||
invoice_link = invoice_link + link_to( invoice.doc_number, invoice_path( i ), :target => "_blank").to_s + " "
|
|
||||||
invoice_link = invoice_link.html_safe
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context[:controller].send(:render_to_string, {
|
context[:controller].send(:render_to_string, {
|
||||||
:partial => 'issues/show_details',
|
partial: 'issues/show_details',
|
||||||
locals: {
|
locals: {
|
||||||
customer: customer,
|
customer: issue.customer ? link_to(issue.customer): nill,
|
||||||
estimate_link: estimate_link,
|
estimate_link: issue.estimate ? link_to(issue.estimate, issue.estimate, target: :_blank) : nil,
|
||||||
invoice_link: invoice_link,
|
invoice_link: invoice_link.html_safe,
|
||||||
issue: issue
|
issue: issue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
#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 Hooks
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -22,7 +22,7 @@ module Hooks
|
|||||||
@selected = context[:user].employee.id if context[:user].employee
|
@selected = context[:user].employee.id if context[:user].employee
|
||||||
|
|
||||||
# Generate the drop down list of quickbooks contacts
|
# 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>"
|
return "<p>#{context[:form].select :employee_id, Employee.all.pluck(:name, :id), selected: @selected, include_blank: true}</p>"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ module Hooks
|
|||||||
|
|
||||||
class ViewHookListener < Redmine::Hook::ViewListener
|
class ViewHookListener < Redmine::Hook::ViewListener
|
||||||
|
|
||||||
render_on :view_layouts_base_sidebar, :partial => "qbo/sidebar"
|
render_on :view_layouts_base_sidebar, partial: "qbo/sidebar"
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ module Hooks
|
|||||||
|
|
||||||
# Load the javascript to support the autocomplete forms
|
# Load the javascript to support the autocomplete forms
|
||||||
def view_layouts_base_html_head(context = {})
|
def view_layouts_base_html_head(context = {})
|
||||||
js = javascript_include_tag 'application.js', :plugin => 'redmine_qbo'
|
js = javascript_include_tag 'application.js', plugin: 'redmine_qbo'
|
||||||
js += javascript_include_tag 'autocomplete-rails.js', :plugin => 'redmine_qbo'
|
js += javascript_include_tag 'autocomplete-rails.js', plugin: 'redmine_qbo'
|
||||||
js += javascript_include_tag 'checkbox_controller.js', :plugin => 'redmine_qbo'
|
js += javascript_include_tag 'checkbox_controller.js', plugin: 'redmine_qbo'
|
||||||
return js
|
return js
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ module Patches
|
|||||||
# Prepare to create a new Time Activity
|
# Prepare to create a new Time Activity
|
||||||
qbo = Qbo.first
|
qbo = Qbo.first
|
||||||
qbo.perform_authenticated_request do |access_token|
|
qbo.perform_authenticated_request do |access_token|
|
||||||
time_service = Quickbooks::Service::TimeActivity.new(:company_id => qbo.realm_id, :access_token => 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)
|
item_service = Quickbooks::Service::Item.new(company_id: qbo.realm_id, access_token: access_token)
|
||||||
time_entry = Quickbooks::Model::TimeActivity.new
|
time_entry = Quickbooks::Model::TimeActivity.new
|
||||||
|
|
||||||
# Lets total up each activity before billing.
|
# Lets total up each activity before billing.
|
||||||
|
|||||||
@@ -59,8 +59,12 @@ module Patches
|
|||||||
#left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id')
|
#left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id')
|
||||||
|
|
||||||
logger.debug "Calling :pdf_left hook"
|
logger.debug "Calling :pdf_left hook"
|
||||||
context = Redmine::Hook.call_hook :pdf_left, { array: left, issue: issue }
|
left_hook_output = Redmine::Hook.call_hook :pdf_left, { issue: issue }
|
||||||
left << context.first unless context.nil?
|
unless left_hook_output.nil?
|
||||||
|
left_hook_output.each do |l|
|
||||||
|
left.concat l unless l.nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
right = []
|
right = []
|
||||||
right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date')
|
right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date')
|
||||||
@@ -70,8 +74,12 @@ module Patches
|
|||||||
right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project)
|
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"
|
logger.debug "Calling :pdf_right hook"
|
||||||
context = Redmine::Hook.call_hook :pdf_right, { array: right, issue: issue }
|
right_hook_output = Redmine::Hook.call_hook :pdf_right, { issue: issue }
|
||||||
right << context.first unless context.nil?
|
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
|
rows = left.size > right.size ? left.size : right.size
|
||||||
while left.size < rows
|
while left.size < rows
|
||||||
@@ -135,10 +143,10 @@ module Patches
|
|||||||
# Set resize image scale
|
# Set resize image scale
|
||||||
pdf.set_image_scale(1.6)
|
pdf.set_image_scale(1.6)
|
||||||
text = textilizable(issue, :description,
|
text = textilizable(issue, :description,
|
||||||
:only_path => false,
|
only_path: false,
|
||||||
:edit_section_links => false,
|
edit_section_links: false,
|
||||||
:headings => false,
|
headings: false,
|
||||||
:inline_attachments => false
|
inline_attachments: false
|
||||||
)
|
)
|
||||||
pdf.RDMwriteFormattedCell(35+155, 5, '', '', text, issue.attachments, "LRB")
|
pdf.RDMwriteFormattedCell(35+155, 5, '', '', text, issue.attachments, "LRB")
|
||||||
|
|
||||||
@@ -225,10 +233,10 @@ module Patches
|
|||||||
pdf.ln unless journal.details.empty?
|
pdf.ln unless journal.details.empty?
|
||||||
pdf.SetFontStyle('',8)
|
pdf.SetFontStyle('',8)
|
||||||
text = textilizable(journal, :notes,
|
text = textilizable(journal, :notes,
|
||||||
:only_path => false,
|
only_path: false,
|
||||||
:edit_section_links => false,
|
edit_section_links: false,
|
||||||
:headings => false,
|
headings: false,
|
||||||
:inline_attachments => false
|
inline_attachments: false
|
||||||
)
|
)
|
||||||
pdf.RDMwriteFormattedCell(190,5,'','', text, issue.attachments, "")
|
pdf.RDMwriteFormattedCell(190,5,'','', text, issue.attachments, "")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
#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 'project'
|
|
||||||
|
|
||||||
module Patches
|
|
||||||
|
|
||||||
# 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
|
|
||||||
belongs_to :customer, primary_key: :id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
module ClassMethods
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
module InstanceMethods
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
# Add module to Project
|
|
||||||
Project.send(:include, ProjectPatch)
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -18,15 +18,15 @@ module Patches
|
|||||||
def available_columns
|
def available_columns
|
||||||
unless @available_columns
|
unless @available_columns
|
||||||
@available_columns = self.class.available_columns.dup
|
@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(: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(:billed, sortable: "#{TimeEntry.table_name}.billed", groupable: true, caption: :field_billed)
|
||||||
end
|
end
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add customers to filters
|
# Add customers to filters
|
||||||
def initialize_available_filters
|
def initialize_available_filters
|
||||||
#add_available_filter "customer", :type => :text
|
#add_available_filter "customer", type: :text
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ module Patches
|
|||||||
def available_columns
|
def available_columns
|
||||||
unless @available_columns
|
unless @available_columns
|
||||||
@available_columns = self.class.available_columns.dup
|
@available_columns = self.class.available_columns.dup
|
||||||
@available_columns << QueryColumn.new(:billed, :sortable => "#{TimeEntry.table_name}.name", :groupable => true, :caption => :field_billed)
|
@available_columns << QueryColumn.new(:billed, sortable: "#{TimeEntry.table_name}.name", groupable: true, caption: :field_billed)
|
||||||
end
|
end
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add QBO options to the filter
|
# Add QBO options to the filter
|
||||||
def initialize_available_filters
|
def initialize_available_filters
|
||||||
add_available_filter "billed", :type => :boolean
|
add_available_filter "billed", type: :boolean
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
require File.expand_path('../../test_helper', __FILE__)
|
|
||||||
|
|
||||||
class QboItemTest < ActiveSupport::TestCase
|
|
||||||
|
|
||||||
# Replace this with your real tests.
|
|
||||||
def test_truth
|
|
||||||
assert true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Reference in New Issue
Block a user