mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2026-04-03 16:51:58 -04:00
Compare commits
11 Commits
1a10360884
...
2026.3.10
| Author | SHA1 | Date | |
|---|---|---|---|
| a4f461fd4d | |||
| 3e81d2840a | |||
| c9a5dc20f9 | |||
| db3c6021c5 | |||
| b8327be5d6 | |||
| c4e1ece82c | |||
| 9fd7140e4a | |||
| a6c8923ea9 | |||
| eb1174cf7c | |||
| 7993f15441 | |||
| bb57af71ae |
@@ -32,38 +32,36 @@ class CustomersController < ApplicationController
|
|||||||
|
|
||||||
autocomplete :customer, :name, full: true, extra_data: [:id]
|
autocomplete :customer, :name, full: true, extra_data: [:id]
|
||||||
|
|
||||||
|
def address_to_s(address)
|
||||||
|
return if address.nil?
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
address.line1,
|
||||||
|
address.line2,
|
||||||
|
address.line3,
|
||||||
|
address.line4,
|
||||||
|
address.line5
|
||||||
|
].compact_blank
|
||||||
|
|
||||||
|
city_line = [
|
||||||
|
address.city,
|
||||||
|
address.country_sub_division_code,
|
||||||
|
address.postal_code
|
||||||
|
].compact_blank.join(" ")
|
||||||
|
|
||||||
|
lines << city_line unless city_line.blank?
|
||||||
|
|
||||||
|
lines.join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_customer
|
||||||
|
global_check_permission(:add_customers)
|
||||||
|
end
|
||||||
|
|
||||||
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)
|
||||||
end
|
end
|
||||||
|
|
||||||
# getter method for a customer's invoices
|
|
||||||
# used for customer autocomplete field / issue form
|
|
||||||
def filter_invoices_by_customer
|
|
||||||
@filtered_invoices = Invoice.all.where(customer_id: params[:selected_customer])
|
|
||||||
end
|
|
||||||
|
|
||||||
# getter method for a customer's estimates
|
|
||||||
# used for customer autocomplete field / issue form
|
|
||||||
def filter_estimates_by_customer
|
|
||||||
@filtered_estimates = Estimate.all.where(customer_id: params[:selected_customer])
|
|
||||||
end
|
|
||||||
|
|
||||||
# display a list of all customers
|
|
||||||
def index
|
|
||||||
if params[:search]
|
|
||||||
@customers = Customer.search(params[:search]).order(:name).paginate(page: params[:page])
|
|
||||||
if only_one_non_zero?(@customers)
|
|
||||||
redirect_to @customers.first
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# initialize a new customer
|
|
||||||
def new
|
|
||||||
@customer = Customer.new
|
|
||||||
end
|
|
||||||
|
|
||||||
# create a new customer
|
|
||||||
def create
|
def create
|
||||||
@customer = Customer.new(allowed_params)
|
@customer = Customer.new(allowed_params)
|
||||||
@customer.save
|
@customer.save
|
||||||
@@ -76,7 +74,79 @@ class CustomersController < ApplicationController
|
|||||||
redirect_to new_customer_path
|
redirect_to new_customer_path
|
||||||
end
|
end
|
||||||
|
|
||||||
# display a specific customer
|
def edit
|
||||||
|
@customer = Customer.find_by_id(params[:id])
|
||||||
|
return render_404 unless @customer
|
||||||
|
rescue => e
|
||||||
|
log "Failed to edit customer"
|
||||||
|
flash[:error] = e.message
|
||||||
|
render_404
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_estimates_by_customer
|
||||||
|
@filtered_estimates = Estimate.all.where(customer_id: params[:selected_customer])
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_invoices_by_customer
|
||||||
|
@filtered_invoices = Invoice.all.where(customer_id: params[:selected_customer])
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
if params[:search]
|
||||||
|
@customers = Customer.search(params[:search]).order(:name).paginate(page: params[:page])
|
||||||
|
if only_one_non_zero?(@customers)
|
||||||
|
redirect_to @customers.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_issue_data
|
||||||
|
@journals = @issue.journals.preload(:details).preload(user: :email_address).reorder(:created_on, :id).to_a
|
||||||
|
|
||||||
|
@journals.each_with_index { |j, i| j.indice = i + 1 }
|
||||||
|
@journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
|
||||||
|
Journal.preload_journals_details_custom_fields(@journals)
|
||||||
|
@journals.select! { |journal| journal.notes? || journal.visible_details.any? }
|
||||||
|
@journals.reverse! if User.current.wants_comments_in_reverse_order?
|
||||||
|
|
||||||
|
@changesets = @issue.changesets.visible.preload(:repository, :user).to_a
|
||||||
|
@changesets.reverse! if User.current.wants_comments_in_reverse_order?
|
||||||
|
|
||||||
|
@relations = @issue.relations.select { |r| r.other_issue(@issue)&.visible? }
|
||||||
|
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
|
||||||
|
@priorities = IssuePriority.active
|
||||||
|
@time_entry = TimeEntry.new(issue: @issue, project: @issue.project)
|
||||||
|
@relation = IssueRelation.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def log(msg)
|
||||||
|
Rails.logger.info "[CustomersController] #{msg}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@customer = Customer.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def only_one_non_zero?(array)
|
||||||
|
found_non_zero = false
|
||||||
|
array.each do |val|
|
||||||
|
if val != 0
|
||||||
|
return false if found_non_zero
|
||||||
|
found_non_zero = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
found_non_zero
|
||||||
|
end
|
||||||
|
|
||||||
|
def share
|
||||||
|
issue = Issue.find(params[:id])
|
||||||
|
token = issue.share_token
|
||||||
|
redirect_to view_path(token.token)
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
flash[:error] = t(:notice_issue_not_found)
|
||||||
|
render_404
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@customer = Customer.find_by_id(params[:id])
|
@customer = Customer.find_by_id(params[:id])
|
||||||
return render_404 unless @customer
|
return render_404 unless @customer
|
||||||
@@ -109,17 +179,11 @@ class CustomersController < ApplicationController
|
|||||||
render_404
|
render_404
|
||||||
end
|
end
|
||||||
|
|
||||||
# return an HTML form for editing a customer
|
def sync
|
||||||
def edit
|
Customer.sync
|
||||||
@customer = Customer.find_by_id(params[:id])
|
redirect_to :home, flash: { notice: I18n.t(:label_syncing) }
|
||||||
return render_404 unless @customer
|
|
||||||
rescue => e
|
|
||||||
log "Failed to edit customer"
|
|
||||||
flash[:error] = e.message
|
|
||||||
render_404
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# update a specific customer
|
|
||||||
def update
|
def update
|
||||||
@customer = Customer.find_by_id(params[:id])
|
@customer = Customer.find_by_id(params[:id])
|
||||||
@customer.update(allowed_params)
|
@customer.update(allowed_params)
|
||||||
@@ -131,108 +195,21 @@ class CustomersController < ApplicationController
|
|||||||
redirect_to edit_customer_path
|
redirect_to edit_customer_path
|
||||||
end
|
end
|
||||||
|
|
||||||
# creates new customer view tokens, removes expired tokens & redirects to newly created customer view with new token.
|
|
||||||
def share
|
|
||||||
issue = Issue.find(params[:id])
|
|
||||||
token = issue.share_token
|
|
||||||
redirect_to view_path(token.token)
|
|
||||||
rescue ActiveRecord::RecordNotFound
|
|
||||||
flash[:error] = t(:notice_issue_not_found)
|
|
||||||
render_404
|
|
||||||
end
|
|
||||||
|
|
||||||
# displays an issue for a customer with a provided security CustomerToken
|
|
||||||
def view
|
def view
|
||||||
User.current = User.anonymous
|
User.current = User.anonymous
|
||||||
|
|
||||||
# Load only active, non-expired token
|
|
||||||
@token = CustomerToken.active.find_by(token: params[:token])
|
@token = CustomerToken.active.find_by(token: params[:token])
|
||||||
return render_403 unless @token
|
return render_403 unless @token
|
||||||
|
|
||||||
# Load associated issue
|
|
||||||
@issue = @token.issue
|
@issue = @token.issue
|
||||||
return render_403 unless @issue
|
return render_403 unless @issue
|
||||||
|
|
||||||
# Optional: enforce token belongs to the issue's customer
|
|
||||||
return render_403 unless @issue.customer_id == @token.issue.customer_id
|
return render_403 unless @issue.customer_id == @token.issue.customer_id
|
||||||
|
|
||||||
# Store token in session for subsequent requests if needed
|
|
||||||
session[:token] = @token.token
|
session[:token] = @token.token
|
||||||
|
|
||||||
load_issue_data
|
load_issue_data
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
render_403
|
render_403
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def load_issue_data
|
|
||||||
@journals = @issue.journals.preload(:details).preload(user: :email_address).reorder(:created_on, :id).to_a
|
|
||||||
|
|
||||||
@journals.each_with_index { |j, i| j.indice = i + 1 }
|
|
||||||
@journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
|
|
||||||
Journal.preload_journals_details_custom_fields(@journals)
|
|
||||||
@journals.select! { |journal| journal.notes? || journal.visible_details.any? }
|
|
||||||
@journals.reverse! if User.current.wants_comments_in_reverse_order?
|
|
||||||
|
|
||||||
@changesets = @issue.changesets.visible.preload(:repository, :user).to_a
|
|
||||||
@changesets.reverse! if User.current.wants_comments_in_reverse_order?
|
|
||||||
|
|
||||||
@relations = @issue.relations.select { |r| r.other_issue(@issue)&.visible? }
|
|
||||||
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
|
|
||||||
@priorities = IssuePriority.active
|
|
||||||
@time_entry = TimeEntry.new(issue: @issue, project: @issue.project)
|
|
||||||
@relation = IssueRelation.new
|
|
||||||
end
|
|
||||||
|
|
||||||
# redmine permission - add customers
|
|
||||||
def add_customer
|
|
||||||
global_check_permission(:add_customers)
|
|
||||||
end
|
|
||||||
|
|
||||||
# redmine permission - view customers
|
|
||||||
def view_customer
|
def view_customer
|
||||||
global_check_permission(:view_customers)
|
global_check_permission(:view_customers)
|
||||||
end
|
end
|
||||||
|
|
||||||
# checks to see if there is only one item in an array
|
|
||||||
# @return true if array only has one item
|
|
||||||
def only_one_non_zero?( array )
|
|
||||||
found_non_zero = false
|
|
||||||
array.each do |val|
|
|
||||||
if val!=0
|
|
||||||
return false if found_non_zero
|
|
||||||
found_non_zero = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
found_non_zero
|
|
||||||
end
|
|
||||||
|
|
||||||
# format a quickbooks address to a human readable string
|
|
||||||
def address_to_s(address)
|
|
||||||
return if address.nil?
|
|
||||||
|
|
||||||
lines = [
|
|
||||||
address.line1,
|
|
||||||
address.line2,
|
|
||||||
address.line3,
|
|
||||||
address.line4,
|
|
||||||
address.line5
|
|
||||||
].compact_blank
|
|
||||||
|
|
||||||
city_line = [
|
|
||||||
address.city,
|
|
||||||
address.country_sub_division_code,
|
|
||||||
address.postal_code
|
|
||||||
].compact_blank.join(" ")
|
|
||||||
|
|
||||||
lines << city_line unless city_line.blank?
|
|
||||||
|
|
||||||
lines.join("\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
def log(msg)
|
|
||||||
Rails.logger.info "[CustomersController] #{msg}"
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
@@ -72,6 +72,13 @@ class EstimateController < ApplicationController
|
|||||||
redirect_back fallback_location: root_path, flash: { error: I18n.t(:notice_estimate_not_found) }
|
redirect_back fallback_location: root_path, flash: { error: I18n.t(:notice_estimate_not_found) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sync
|
||||||
|
Estimate.sync
|
||||||
|
redirect_to :home, flash: { notice: I18n.t(:label_syncing) }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
# Logs messages with a consistent prefix for easier debugging.
|
# Logs messages with a consistent prefix for easier debugging.
|
||||||
def log(msg)
|
def log(msg)
|
||||||
Rails.logger.info "[EstimateController] #{msg}"
|
Rails.logger.info "[EstimateController] #{msg}"
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ class InvoiceController < ApplicationController
|
|||||||
redirect_back fallback_location: root_path, flash: { error: I18n.t(:notice_invoice_not_found) }
|
redirect_back fallback_location: root_path, flash: { error: I18n.t(:notice_invoice_not_found) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sync
|
||||||
|
Invoice.sync
|
||||||
|
redirect_to :home, flash: { notice: I18n.t(:label_syncing) }
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Logs messages with a consistent prefix for easier debugging.
|
# Logs messages with a consistent prefix for easier debugging.
|
||||||
|
|||||||
@@ -46,9 +46,13 @@ class QboController < ApplicationController
|
|||||||
redirect_to issue || root_path, flash: { error: e.message }
|
redirect_to issue || root_path, flash: { error: e.message }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Manual sync endpoint to trigger a full synchronization of QuickBooks entities with the local database. Enqueues all relevant sync jobs and redirects to the home page with a notice that syncing has started.
|
# Manual sync endpoint to trigger synchronization of QuickBooks entities
|
||||||
|
# with the local database. Supports full or partial sync depending on
|
||||||
|
# the `full_sync` boolean parameter.
|
||||||
def sync
|
def sync
|
||||||
QboSyncDispatcher.full_sync!
|
full_sync = ActiveModel::Type::Boolean.new.cast(params[:full_sync])
|
||||||
|
QboSyncDispatcher.sync!(full_sync: full_sync)
|
||||||
|
|
||||||
redirect_to :home, flash: { notice: I18n.t(:label_syncing) }
|
redirect_to :home, flash: { notice: I18n.t(:label_syncing) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,17 @@ class QboSyncDispatcher
|
|||||||
Employee
|
Employee
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
# Dispatches all synchronization jobs to perform a full sync of QuickBooks entities with the local database. Each job is enqueued with the `full_sync` flag set to true.
|
# Dispatches all synchronization jobs to perform a full sync of QuickBooks entities with the local database.
|
||||||
def self.full_sync!
|
# Each job is enqueued with the `full_sync` flag set to true.
|
||||||
|
def self.sync!(full_sync: false)
|
||||||
|
log "Manual Sync initated for #{full_sync ? "full sync" : "incremental sync"}"
|
||||||
|
enque_jobs full_sync: full_sync
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Dynamically enques all sync jobs
|
||||||
|
def self.enque_jobs(full_sync: full_sync)
|
||||||
jobs = SYNC_JOBS.dup
|
jobs = SYNC_JOBS.dup
|
||||||
|
|
||||||
# Allow other plugins to add addtional sync jobs via Hooks
|
# Allow other plugins to add addtional sync jobs via Hooks
|
||||||
@@ -29,11 +37,9 @@ class QboSyncDispatcher
|
|||||||
log "Added additionals QBO Sync Job for #{context.to_s}"
|
log "Added additionals QBO Sync Job for #{context.to_s}"
|
||||||
end
|
end
|
||||||
|
|
||||||
jobs.each { |job| QboSyncJob.perform_later(entity: job, full_sync: true) }
|
jobs.each { |job| QboSyncJob.perform_later(entity: job, full_sync: full_sync) }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def self.log(msg)
|
def self.log(msg)
|
||||||
Rails.logger.info "[QboSyncDispatcher] #{msg}"
|
Rails.logger.info "[QboSyncDispatcher] #{msg}"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -33,14 +33,12 @@ class Customer < QboBaseModel
|
|||||||
|
|
||||||
# Returns the customer's email address
|
# Returns the customer's email address
|
||||||
def email
|
def email
|
||||||
details
|
return details&.email_address&.address
|
||||||
return @details&.email_address&.address
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Updates the customer's email address
|
# Updates the customer's email address
|
||||||
def email=(s)
|
def email=(s)
|
||||||
details
|
details.email_address = s
|
||||||
@details.email_address = s
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Customers are not bound by a project
|
# Customers are not bound by a project
|
||||||
@@ -51,22 +49,19 @@ class Customer < QboBaseModel
|
|||||||
|
|
||||||
# returns the customer's mobile phone
|
# returns the customer's mobile phone
|
||||||
def mobile_phone
|
def mobile_phone
|
||||||
details
|
return details&.mobile_phone&.free_form_number
|
||||||
return @details&.mobile_phone&.free_form_number
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Updates the custome's mobile phone number
|
# Updates the custome's mobile phone number
|
||||||
def mobile_phone=(n)
|
def mobile_phone=(n)
|
||||||
details
|
|
||||||
pn = Quickbooks::Model::TelephoneNumber.new
|
pn = Quickbooks::Model::TelephoneNumber.new
|
||||||
pn.free_form_number = n
|
pn.free_form_number = n
|
||||||
@details.mobile_phone = pn
|
details.mobile_phone = pn
|
||||||
end
|
end
|
||||||
|
|
||||||
# Updates Both local DB name & QBO display_name
|
# Updates Both local DB name & QBO display_name
|
||||||
def name=(s)
|
def name=(s)
|
||||||
details
|
details.display_name = s
|
||||||
@details.display_name = s
|
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -78,22 +73,19 @@ class Customer < QboBaseModel
|
|||||||
|
|
||||||
# Sets the notes for the customer
|
# Sets the notes for the customer
|
||||||
def notes=(s)
|
def notes=(s)
|
||||||
details
|
details.notes = s
|
||||||
@details.notes = s
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# returns the customer's primary phone
|
# returns the customer's primary phone
|
||||||
def primary_phone
|
def primary_phone
|
||||||
details
|
return details&.primary_phone&.free_form_number
|
||||||
return @details&.primary_phone&.free_form_number
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Updates the customer's primary phone number
|
# Updates the customer's primary phone number
|
||||||
def primary_phone=(n)
|
def primary_phone=(n)
|
||||||
details
|
|
||||||
pn = Quickbooks::Model::TelephoneNumber.new
|
pn = Quickbooks::Model::TelephoneNumber.new
|
||||||
pn.free_form_number = n
|
pn.free_form_number = n
|
||||||
@details.primary_phone = pn
|
details.primary_phone = pn
|
||||||
end
|
end
|
||||||
|
|
||||||
# Seach for customers by name or phone number
|
# Seach for customers by name or phone number
|
||||||
|
|||||||
@@ -15,6 +15,26 @@ class Qbo < ActiveRecord::Base
|
|||||||
|
|
||||||
validate :single_record_only, on: :create
|
validate :single_record_only, on: :create
|
||||||
|
|
||||||
|
# Returns the last sync time formatted for display. If no sync has occurred, returns a default message.
|
||||||
|
def self.last_sync
|
||||||
|
qbo = QboConnectionService.current!
|
||||||
|
format_time(qbo.last_sync)
|
||||||
|
rescue
|
||||||
|
return I18n.t(:label_qbo_never_synced)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.oauth2_access_token_expires_at
|
||||||
|
QboConnectionService.current!.oauth2_access_token_expires_at
|
||||||
|
rescue
|
||||||
|
return I18n.t(:label_qbo_never_synced)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.oauth2_refresh_token_expires_at
|
||||||
|
QboConnectionService.current!.oauth2_refresh_token_expires_at
|
||||||
|
rescue
|
||||||
|
return I18n.t(:label_qbo_never_synced)
|
||||||
|
end
|
||||||
|
|
||||||
# Updates last sync time stamp
|
# Updates last sync time stamp
|
||||||
def self.update_time_stamp
|
def self.update_time_stamp
|
||||||
date = DateTime.now
|
date = DateTime.now
|
||||||
@@ -24,13 +44,6 @@ class Qbo < ActiveRecord::Base
|
|||||||
qbo.save
|
qbo.save
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the last sync time formatted for display. If no sync has occurred, returns a default message.
|
|
||||||
def self.last_sync
|
|
||||||
qbo = QboConnectionService.current!
|
|
||||||
return I18n.t(:label_qbo_never_synced) unless qbo&.last_sync
|
|
||||||
format_time(qbo.last_sync)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Logs a message with a QBO-specific prefix for easier identification in the logs.
|
# Logs a message with a QBO-specific prefix for easier identification in the logs.
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
|
|
||||||
class QboOauthService
|
class QboOauthService
|
||||||
|
|
||||||
# Generates the QuickBooks OAuth authorization URL with the specified callback URL. The URL includes necessary parameters such as response type, state, and scope.
|
# Generates the QuickBooks OAuth authorization URL with the specified callback URL.
|
||||||
|
# The URL includes necessary parameters such as response type, state, and scope.
|
||||||
def self.authorization_url(callback_url:)
|
def self.authorization_url(callback_url:)
|
||||||
client.auth_code.authorize_url(
|
client.auth_code.authorize_url(
|
||||||
redirect_uri: callback_url,
|
redirect_uri: callback_url,
|
||||||
@@ -20,7 +21,8 @@ class QboOauthService
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Exchanges the authorization code for access and refresh tokens. Creates or replaces the QBO connection record with the new credentials and refreshes the token immediately after creation.
|
# Exchanges the authorization code for access and refresh tokens.
|
||||||
|
# Creates or replaces the QBO connection record with the new credentials and refreshes the token immediately after creation.
|
||||||
def self.exchange!(code:, callback_url:, realm_id:)
|
def self.exchange!(code:, callback_url:, realm_id:)
|
||||||
resp = client.auth_code.get_token(code, redirect_uri: callback_url)
|
resp = client.auth_code.get_token(code, redirect_uri: callback_url)
|
||||||
QboConnectionService.replace!( token: resp.token, refresh_token: resp.refresh_token, realm_id: realm_id )
|
QboConnectionService.replace!( token: resp.token, refresh_token: resp.refresh_token, realm_id: realm_id )
|
||||||
|
|||||||
@@ -3,3 +3,4 @@
|
|||||||
<%= 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%>
|
||||||
|
<%= button_to(t(:label_sync), qbo_sync_path, method: :get) if User.current.admin?%>
|
||||||
|
|||||||
@@ -66,12 +66,12 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th><%=t(:label_oauth_expires)%></th>
|
<th><%=t(:label_oauth_expires)%></th>
|
||||||
<td><%= QboConnectionService.current!&.oauth2_access_token_expires_at %>
|
<td><%= Qbo.oauth2_access_token_expires_at %>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th><%=t(:label_oauth2_refresh_token_expires_at)%></th>
|
<th><%=t(:label_oauth2_refresh_token_expires_at)%></th>
|
||||||
<td><%= QboConnectionService.current!&.oauth2_refresh_token_expires_at %>
|
<td><%= Qbo.oauth2_refresh_token_expires_at %>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -107,5 +107,5 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<b><%=t(:label_last_sync)%> </b> <%= Qbo.last_sync if Qbo.exists? %> <%= link_to t(:label_sync_now), qbo_sync_path %>
|
<b><%=t(:label_last_sync)%> </b> <%= Qbo.last_sync if Qbo.exists? %> <%= link_to t(:label_sync_now), qbo_sync_path(full_sync: true) %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ get 'qbo/oauth_callback', to: 'qbo#oauth_callback'
|
|||||||
|
|
||||||
#manual sync
|
#manual sync
|
||||||
get 'qbo/sync', to: 'qbo#sync'
|
get 'qbo/sync', to: 'qbo#sync'
|
||||||
|
get 'invoices/sync', to: 'invoice#sync'
|
||||||
|
get 'estimates/sync', to: 'estimate#sync'
|
||||||
|
|
||||||
#webhook
|
#webhook
|
||||||
post 'qbo/webhook', to: 'qbo#webhook'
|
post 'qbo/webhook', to: 'qbo#webhook'
|
||||||
@@ -36,4 +38,5 @@ 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
|
||||||
|
get :sync
|
||||||
end
|
end
|
||||||
|
|||||||
2
init.rb
2
init.rb
@@ -14,7 +14,7 @@ Redmine::Plugin.register :redmine_qbo do
|
|||||||
name 'Redmine QBO plugin'
|
name 'Redmine QBO plugin'
|
||||||
author 'Rick Barrette'
|
author 'Rick Barrette'
|
||||||
description 'A pluging for Redmine to connect with QuickBooks Online to create Time Activity Entries for billable hours logged when an Issue is closed'
|
description 'A pluging for Redmine to connect with QuickBooks Online to create Time Activity Entries for billable hours logged when an Issue is closed'
|
||||||
version '2026.3.7'
|
version '2026.3.10'
|
||||||
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'
|
||||||
|
|||||||
Reference in New Issue
Block a user