mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2026-04-03 16:51:58 -04:00
Compare commits
8 Commits
1a10360884
...
2026.3.9
| Author | SHA1 | Date | |
|---|---|---|---|
| db3c6021c5 | |||
| b8327be5d6 | |||
| c4e1ece82c | |||
| 9fd7140e4a | |||
| a6c8923ea9 | |||
| eb1174cf7c | |||
| 7993f15441 | |||
| bb57af71ae |
@@ -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
|
||||||
|
|||||||
@@ -14,6 +14,26 @@ class Qbo < ActiveRecord::Base
|
|||||||
include Redmine::I18n
|
include Redmine::I18n
|
||||||
|
|
||||||
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
|
||||||
@@ -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>
|
||||||
|
|||||||
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.9'
|
||||||
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