mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2026-04-03 08:41:58 -04:00
Compare commits
4 Commits
d8a26f98c0
...
2026.3.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 17ac19e435 | |||
| ef5089438c | |||
| 1f64e36892 | |||
| 643b15391b |
14
README.md
14
README.md
@@ -117,12 +117,14 @@ Adds support for tracking **customer vehicles** associated with Issues.
|
|||||||
|
|
||||||
Available hooks:
|
Available hooks:
|
||||||
|
|
||||||
|Type|Hook|
|
|Type|Hook|Note
|
||||||
|--|---|
|
|--|--|--|
|
||||||
View Hook|:pdf\_left, { issue: issue }
|
View Hook|:pdf_left, { issue: issue } | Used to add text to left side of PDF
|
||||||
View Hook|:pdf\_right, { issue: issue }
|
View Hook|:pdf_right, { issue: issue } | Used to add text to right side of PDF
|
||||||
Hook|process\_invoice\_custom\_fields, { issue: issue, invoice: invoice }
|
Hook|process_invoice_custom_fields, { issue: issue, invoice: invoice } | Used to process invoice custom fields
|
||||||
View Hook|:show\_customer\_view\_right, { customer: customer }
|
View Hook|:show_customer_view_right, { customer: customer } | Used to show partials on right side of customer view
|
||||||
|
Hook| :qbo_additional_entities | Used to add additional entites to be processed by the WebhookProcessJob
|
||||||
|
Hook| :qbo_full_sync | Used to add a Class to be called by the QboSyncDispatcher
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,23 @@ class QboSyncDispatcher
|
|||||||
|
|
||||||
# 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. Each job is enqueued with the `full_sync` flag set to true.
|
||||||
def self.full_sync!
|
def self.full_sync!
|
||||||
SYNC_JOBS.each { |job| job.perform_later(full_sync: true) }
|
|
||||||
|
jobs = SYNC_JOBS.dup
|
||||||
|
|
||||||
|
# Allow other plugins to add addtional sync jobs via Hooks
|
||||||
|
Redmine::Hook.call_hook( :qbo_full_sync ).each do |context|
|
||||||
|
next unless context
|
||||||
|
jobs.push context
|
||||||
|
log "Added additionals QBO Sync Job for #{contex.to_s}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
jobs.each { |job| job.perform_later(full_sync: true) }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def self.log(msg)
|
||||||
|
Rails.logger.info "[QboSyncDispatcher] #{msg}"
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
@@ -43,7 +43,14 @@ class WebhookProcessJob < ActiveJob::Base
|
|||||||
name = entity['name']
|
name = entity['name']
|
||||||
id = entity['id']&.to_i
|
id = entity['id']&.to_i
|
||||||
|
|
||||||
return unless ALLOWED_ENTITIES.include?(name)
|
entitys = ALLOWED_ENTITIES.dup
|
||||||
|
# Allow other plugins to add addtional qbo entities via Hooks
|
||||||
|
Redmine::Hook.call_hook( :qbo_additional_entities ).each do |context|
|
||||||
|
next unless context
|
||||||
|
entitys.push context
|
||||||
|
log "Added additional QBO entities: #{context}"
|
||||||
|
end
|
||||||
|
return unless entitys.include?(name)
|
||||||
|
|
||||||
model = name.safe_constantize
|
model = name.safe_constantize
|
||||||
return unless model
|
return unless model
|
||||||
|
|||||||
@@ -31,28 +31,29 @@ class SyncServiceBase
|
|||||||
service_class = "Quickbooks::Service::#{@entity.name}".constantize
|
service_class = "Quickbooks::Service::#{@entity.name}".constantize
|
||||||
service = service_class.new(company_id: @qbo.realm_id, access_token: access_token)
|
service = service_class.new(company_id: @qbo.realm_id, access_token: access_token)
|
||||||
|
|
||||||
page = 1
|
query = build_query(full_sync)
|
||||||
loop do
|
|
||||||
collection = fetch_page(service, page, full_sync)
|
|
||||||
entries = Array(collection&.entries)
|
|
||||||
break if entries.empty?
|
|
||||||
|
|
||||||
entries.each { |remote| persist(remote) }
|
service.query_in_batches(query, per_page: self.class::PAGE_SIZE) do |batch|
|
||||||
|
entries = Array(batch)
|
||||||
|
log "Processing batch of #{entries.size} #{@entity.name}"
|
||||||
|
|
||||||
break if entries.size < PAGE_SIZE
|
entries.each do |remote|
|
||||||
page += 1
|
persist(remote)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
log "#{@entity.name} sync complete"
|
log "#{@entity.name} sync complete"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sync a single entity by its QBO ID, used for webhook updates
|
# Sync a single entity by its QBO ID (webhook usage)
|
||||||
def sync_by_id(id)
|
def sync_by_id(id)
|
||||||
log "Syncing #{@entity.name} with ID #{id}"
|
log "Syncing #{@entity.name} with ID #{id}"
|
||||||
|
|
||||||
@qbo.perform_authenticated_request do |access_token|
|
@qbo.perform_authenticated_request do |access_token|
|
||||||
service_class = "Quickbooks::Service::#{@entity.name}".constantize
|
service_class = "Quickbooks::Service::#{@entity.name}".constantize
|
||||||
service = service_class.new(company_id: @qbo.realm_id, access_token: access_token)
|
service = service_class.new(company_id: @qbo.realm_id, access_token: access_token)
|
||||||
|
|
||||||
remote = service.fetch_by_id(id)
|
remote = service.fetch_by_id(id)
|
||||||
persist(remote)
|
persist(remote)
|
||||||
end
|
end
|
||||||
@@ -60,6 +61,20 @@ class SyncServiceBase
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def build_query(full_sync)
|
||||||
|
if full_sync
|
||||||
|
"SELECT * FROM #{@entity.name} ORDER BY Id"
|
||||||
|
else
|
||||||
|
last_update = @entity.maximum(:updated_at) || 1.year.ago
|
||||||
|
|
||||||
|
<<~SQL.squish
|
||||||
|
SELECT * FROM #{@entity.name}
|
||||||
|
WHERE MetaData.LastUpdatedTime > '#{last_update.utc.iso8601}'
|
||||||
|
ORDER BY MetaData.LastUpdatedTime
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def attach_documents(local, remote)
|
def attach_documents(local, remote)
|
||||||
# Override in subclasses if the entity has attachments (e.g. Invoice)
|
# Override in subclasses if the entity has attachments (e.g. Invoice)
|
||||||
end
|
end
|
||||||
@@ -74,24 +89,6 @@ class SyncServiceBase
|
|||||||
Rails.logger.info "[#{@entity.name}SyncService] #{msg}"
|
Rails.logger.info "[#{@entity.name}SyncService] #{msg}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Fetch a page of entities, either all or only those updated since the last sync
|
|
||||||
def fetch_page(service, page, full_sync)
|
|
||||||
log "Fetching page #{page} of #{@entity.name} from QBO (#{full_sync ? 'full' : 'incremental'} sync)"
|
|
||||||
start_position = (page - 1) * PAGE_SIZE + 1
|
|
||||||
|
|
||||||
if full_sync
|
|
||||||
service.query("SELECT * FROM #{@entity.name} STARTPOSITION #{start_position} MAXRESULTS #{PAGE_SIZE}")
|
|
||||||
else
|
|
||||||
last_update = @entity.maximum(:updated_at) || 1.year.ago
|
|
||||||
service.query(<<~SQL.squish)
|
|
||||||
SELECT * FROM #{@entity.name}
|
|
||||||
WHERE MetaData.LastUpdatedTime > '#{last_update.utc.iso8601}'
|
|
||||||
STARTPOSITION #{start_position}
|
|
||||||
MAXRESULTS #{PAGE_SIZE}
|
|
||||||
SQL
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Create or update a local entity record based on the QBO remote data
|
# Create or update a local entity record based on the QBO remote data
|
||||||
def persist(remote)
|
def persist(remote)
|
||||||
local = @entity.find_or_initialize_by(id: remote.id)
|
local = @entity.find_or_initialize_by(id: remote.id)
|
||||||
@@ -104,14 +101,11 @@ class SyncServiceBase
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
# Map remote attributes to local model fields, this should be implemented in subclasses
|
|
||||||
process_attributes(local, remote)
|
process_attributes(local, remote)
|
||||||
|
|
||||||
if local.changed?
|
if local.changed?
|
||||||
local.save!
|
local.save!
|
||||||
log "Updated #{@entity.name} #{remote.id}"
|
log "Updated #{@entity.name} #{remote.id}"
|
||||||
|
|
||||||
# Handle attaching documents if applicable to invoices
|
|
||||||
attach_documents(local, remote)
|
attach_documents(local, remote)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -123,5 +117,4 @@ class SyncServiceBase
|
|||||||
def process_attributes(local, remote)
|
def process_attributes(local, remote)
|
||||||
raise NotImplementedError, "Subclasses must implement process_attributes"
|
raise NotImplementedError, "Subclasses must implement process_attributes"
|
||||||
end
|
end
|
||||||
|
|
||||||
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.2'
|
version '2026.3.4'
|
||||||
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