mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2026-04-02 16:21:58 -04:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17ac19e435 | |||
| ef5089438c | |||
| 1f64e36892 | |||
| 643b15391b | |||
| d8a26f98c0 | |||
| 8fc01cd8fb | |||
| fe3da8c452 |
320
README.md
320
README.md
@@ -1,14 +1,21 @@
|
|||||||
# Redmine QuickBooks Online
|
# Redmine QuickBooks Online Plugin
|
||||||
|
|
||||||
A plugin for Redmine to connect to QuickBooks Online.
|
A plugin for **Redmine** that integrates with **QuickBooks Online (QBO)** to automatically create **Time Activity entries** from billable hours logged on Issues.
|
||||||
|
|
||||||
The goal of this project is to allow Redmine to connect with QuickBooks Online to create Time Activity Entries for billable hours logged when an Issue is closed.
|
When an Issue associated with a Customer is closed, the plugin generates corresponding Time Activities in QuickBooks based on the Redmine Time Entries recorded for that Issue.
|
||||||
|
|
||||||
## Disclaimer
|
---
|
||||||
|
|
||||||
**Note:** Although the core functionality is complete, this project is still under development and the master branch may be unstable. Tags should be stable and are recommended.
|
# Disclaimer
|
||||||
|
|
||||||
## Compatibility
|
The core functionality is implemented, but the project is **under active development**.
|
||||||
|
|
||||||
|
The `master` branch may contain unstable changes.
|
||||||
|
For production deployments, **use a tagged release**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Compatibility
|
||||||
|
|
||||||
| Plugin Version | Redmine Version |
|
| Plugin Version | Redmine Version |
|
||||||
| :--- | :--- |
|
| :--- | :--- |
|
||||||
@@ -17,85 +24,244 @@ The goal of this project is to allow Redmine to connect with QuickBooks Online t
|
|||||||
| Version 1.0.0+ | Redmine 4 |
|
| Version 1.0.0+ | Redmine 4 |
|
||||||
| Version 0.8.1 | Redmine 3 |
|
| Version 0.8.1 | Redmine 3 |
|
||||||
|
|
||||||
## Features
|
---
|
||||||
|
|
||||||
* **Customer Assignment:** Issues can be assigned to a Customer via a dropdown in the edit Issue form.
|
# Features
|
||||||
* Once a customer is attached to an Issue, you can attach an Estimate to the issue via a dropdown menu.
|
|
||||||
* **Employee Mapping:** An Employee is assigned to a Redmine User via a dropdown in the User Administration page.
|
|
||||||
* **Automatic Billing:** If an Issue has been assigned a Customer, the following happens when the Issue is closed:
|
|
||||||
* A new Time Activity will be billed against the Customer assigned to the issue for each Redmine Time Entry.
|
|
||||||
* Time Entries will be totalled up by Activity name. This allows billing for different activities without having to create separate Issues.
|
|
||||||
* The Time Activity names are used to dynamically lookup Items in QuickBooks.
|
|
||||||
* If there are no Items that match the Activity name, it will be skipped and will not be billed to the Customer.
|
|
||||||
* Labor Rates are set by the corresponding Item in QuickBooks.
|
|
||||||
* **Customer Management:** Customers can be created via the New Customer Page.
|
|
||||||
* Customers can be searched by name or phone number.
|
|
||||||
* Basic information for the Customer can be viewed/edited via the Customer page.
|
|
||||||
* **Webhook Support:**
|
|
||||||
* **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).
|
|
||||||
* **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
|
## Issue Billing Integration
|
||||||
|
|
||||||
* Sign up to become a developer for Intuit: https://developer.intuit.com/
|
* Assign a **QuickBooks Customer** to a Redmine Issue.
|
||||||
* Create your own application to obtain your API keys.
|
|
||||||
* Set up the webhook service to `https://redmine.yourdomain.com/qbo/webhook`
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
1. **Clone the plugin:**
|
|
||||||
Clone this repo into your plugin folder and checkout a tagged version.
|
|
||||||
```bash
|
|
||||||
cd path/to/redmine/plugins
|
|
||||||
git clone git@github.com:rickbarrette/redmine_qbo.git
|
|
||||||
cd redmine_qbo
|
|
||||||
git checkout <tag>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Install dependencies:** *Crucial for Redmine 6 / Rails 7 compatibility.*
|
|
||||||
|
|
||||||
Bash
|
* Optionally associate a **QuickBooks Estimate** with the Issue.
|
||||||
|
|
||||||
```
|
|
||||||
bundle install
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Migrate your database:**
|
|
||||||
|
|
||||||
Bash
|
|
||||||
|
|
||||||
```
|
|
||||||
bundle exec rake redmine:plugins:migrate RAILS_ENV=production
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Restart Redmine:** You must restart your Redmine server instance for the plugin and hooks to load.
|
|
||||||
|
|
||||||
5. **Configuration:**
|
|
||||||
|
|
||||||
* Navigate to the plugin configuration page (`Administration > Plugins > Configure`).
|
|
||||||
|
|
||||||
* Supply your own OAuth Key & Secret.
|
|
||||||
|
|
||||||
* After saving the Key & Secret, click the **Authenticate** link on the configuration page to connect to QBO.
|
|
||||||
|
|
||||||
6. **User Mapping:**
|
|
||||||
|
|
||||||
* Assign an Employee to each of your users via the **User Administration Page**.
|
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
* Automatically associates a **QuickBooks Invoice** with the Issue.
|
||||||
|
|
||||||
|
|
||||||
To enable automatic Time Activity entries for an Issue, you simply need to assign a Customer to an Issue via the dropdowns in the issue creation/update form.
|
---
|
||||||
|
|
||||||
**Note:** After the initial synchronization, this plugin will receive push notifications via Intuit's webhook service.
|
## Automatic Time Activity Creation
|
||||||
|
|
||||||
## Companion Plugin Hooks
|
When an Issue with an assigned Customer is closed:
|
||||||
* :pdf_left, { issue: issue }
|
|
||||||
* :pdf_right, { issue: issue }
|
* A **Time Activity** is created in QuickBooks for each relevant Redmine Time Entry.
|
||||||
* :process_invoice_custom_fields, { issue: issue, invoice: invoice }
|
|
||||||
* :show_customer_view_right, {customer: @customer}
|
* Time Entries are **grouped by Activity name**.
|
||||||
|
|
||||||
|
* Activity names are used to **dynamically match Items in QuickBooks**.
|
||||||
|
|
||||||
|
* If no matching Item exists, the activity is **skipped**.
|
||||||
|
|
||||||
|
* **Labor rates** are determined by the associated QuickBooks Item.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Employee Mapping
|
||||||
|
|
||||||
|
Redmine Users can be mapped to **QuickBooks Employees** through the **User Administration** page.
|
||||||
|
|
||||||
|
This ensures Time Activities are recorded under the correct employee in QuickBooks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Customer Management
|
||||||
|
|
||||||
|
The plugin provides basic Customer management:
|
||||||
|
|
||||||
|
* Create Customers directly from Redmine
|
||||||
|
|
||||||
|
* Search Customers by **name or phone number**
|
||||||
|
|
||||||
|
* View and edit Customer information
|
||||||
|
|
||||||
|
|
||||||
|
Customers are synchronized with QuickBooks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Webhook Support
|
||||||
|
|
||||||
|
The plugin listens for **QuickBooks webhook events**.
|
||||||
|
|
||||||
|
Supported automation:
|
||||||
|
|
||||||
|
### Invoice Linking
|
||||||
|
|
||||||
|
Invoices containing an Issue reference (e.g. `#123`) automatically attach to the corresponding Issue.
|
||||||
|
|
||||||
|
### Custom Field Synchronization
|
||||||
|
|
||||||
|
Invoice custom fields can be mapped to Issue custom fields.
|
||||||
|
|
||||||
|
Example use case:
|
||||||
|
|
||||||
|
* Mileage In/Out recorded in Redmine
|
||||||
|
|
||||||
|
* Automatically synchronized to the QuickBooks invoice.
|
||||||
|
|
||||||
|
|
||||||
|
### Customer Synchronization
|
||||||
|
|
||||||
|
Customer records are automatically updated in the local database when changes occur in QuickBooks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plugin Hooks
|
||||||
|
|
||||||
|
The plugin exposes several hooks for extending functionality through companion plugins.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
`redmine_qbo_vehicles`
|
||||||
|
Adds support for tracking **customer vehicles** associated with Issues.
|
||||||
|
|
||||||
|
Available hooks:
|
||||||
|
|
||||||
|
|Type|Hook|Note
|
||||||
|
|--|--|--|
|
||||||
|
View Hook|:pdf_left, { issue: issue } | Used to add text to left side of PDF
|
||||||
|
View Hook|:pdf_right, { issue: issue } | Used to add text to right side of PDF
|
||||||
|
Hook|process_invoice_custom_fields, { issue: issue, invoice: invoice } | Used to process invoice custom fields
|
||||||
|
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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Prerequisites
|
||||||
|
|
||||||
|
Before installing the plugin:
|
||||||
|
|
||||||
|
1. Create a QuickBooks developer account:
|
||||||
|
|
||||||
|
|
||||||
|
[https://developer.intuit.com/](https://developer.intuit.com/)
|
||||||
|
|
||||||
|
2. Create an **Intuit application** to obtain:
|
||||||
|
|
||||||
|
|
||||||
|
* Client ID
|
||||||
|
|
||||||
|
* Client Secret
|
||||||
|
|
||||||
|
|
||||||
|
3. Configure the QuickBooks webhook endpoint:
|
||||||
|
|
||||||
|
|
||||||
|
https://redmine.yourdomain.com/qbo/webhook
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
## 1\. Clone the Plugin
|
||||||
|
|
||||||
|
Install the plugin into your Redmine plugins directory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/redmine/plugins
|
||||||
|
git clone https://github.com/rickbarrette/redmine_qbo.git
|
||||||
|
cd redmine_qbo
|
||||||
|
git checkout <tag>
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a **tagged release** for stability.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2\. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bundle install
|
||||||
|
```
|
||||||
|
|
||||||
|
Required for **Redmine 6 / Rails 7 compatibility**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3\. Run Database Migrations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bundle exec rake redmine:plugins:migrate RAILS_ENV=production
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4\. Restart Redmine
|
||||||
|
|
||||||
|
Restart your Redmine server so the plugin and hooks are loaded.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
1. Navigate to:
|
||||||
|
|
||||||
|
|
||||||
|
Administration → Plugins → Configure
|
||||||
|
|
||||||
|
2. Enter your **QuickBooks Client ID and Client Secret**.
|
||||||
|
|
||||||
|
3. Save the configuration.
|
||||||
|
|
||||||
|
4. Click **Authenticate** to complete the OAuth connection with QuickBooks Online.
|
||||||
|
|
||||||
|
|
||||||
|
Once authentication succeeds, the plugin performs an **initial synchronization**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# User Mapping
|
||||||
|
|
||||||
|
Each Redmine user must be mapped to a QuickBooks Employee.
|
||||||
|
|
||||||
|
Navigate to:
|
||||||
|
|
||||||
|
Administration → Users
|
||||||
|
|
||||||
|
Then assign the corresponding **QuickBooks Employee** to each user.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
To enable automatic billing:
|
||||||
|
|
||||||
|
1. Assign a **Customer** to an Issue.
|
||||||
|
|
||||||
|
2. Log billable time using **Redmine Time Entries**.
|
||||||
|
|
||||||
|
3. Close the Issue.
|
||||||
|
|
||||||
|
|
||||||
|
When the Issue is closed, the plugin automatically generates the corresponding **Time Activity entries in QuickBooks Online**.
|
||||||
|
|
||||||
|
After the initial synchronization, the plugin receives updates through **Intuit webhooks**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
### Time Activities Not Created
|
||||||
|
|
||||||
|
Verify that:
|
||||||
|
|
||||||
|
* The Issue has a **Customer assigned**
|
||||||
|
|
||||||
|
* Time Entries exist for the Issue
|
||||||
|
|
||||||
|
* Activity names match **QuickBooks Item names**
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Webhooks Not Triggering
|
||||||
|
|
||||||
|
Ensure the QuickBooks webhook endpoint is reachable:
|
||||||
|
|
||||||
|
https://redmine.yourdomain.com/qbo/webhook
|
||||||
|
|
||||||
|
Also verify webhook configuration in the Intuit developer dashboard.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,8 @@ class BillIssueTimeJob < ActiveJob::Base
|
|||||||
issue = Issue.find(issue_id)
|
issue = Issue.find(issue_id)
|
||||||
|
|
||||||
log "Starting billing for issue ##{issue.id}"
|
log "Starting billing for issue ##{issue.id}"
|
||||||
|
|
||||||
issue.with_lock do
|
issue.with_lock do
|
||||||
unbilled_entries = issue.time_entries.where(billed: [false, nil]).lock
|
unbilled_entries = issue.time_entries.where(billed: [false, nil]).lock
|
||||||
|
|
||||||
return if unbilled_entries.blank?
|
return if unbilled_entries.blank?
|
||||||
|
|
||||||
totals = aggregate_hours(unbilled_entries)
|
totals = aggregate_hours(unbilled_entries)
|
||||||
@@ -28,8 +26,6 @@ class BillIssueTimeJob < ActiveJob::Base
|
|||||||
log "Aggregated hours for billing: #{totals.inspect}"
|
log "Aggregated hours for billing: #{totals.inspect}"
|
||||||
|
|
||||||
qbo = QboConnectionService.current!
|
qbo = QboConnectionService.current!
|
||||||
raise "No QBO configuration found" unless qbo
|
|
||||||
|
|
||||||
qbo.perform_authenticated_request do |access_token|
|
qbo.perform_authenticated_request do |access_token|
|
||||||
create_time_activities(issue, totals, access_token, qbo)
|
create_time_activities(issue, totals, access_token, qbo)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
jobs.each { |job| job.perform_later(full_sync: true) }
|
||||||
end
|
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
|
||||||
|
|||||||
@@ -30,29 +30,30 @@ class SyncServiceBase
|
|||||||
@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)
|
||||||
|
|
||||||
page = 1
|
|
||||||
loop do
|
|
||||||
collection = fetch_page(service, page, full_sync)
|
|
||||||
entries = Array(collection&.entries)
|
|
||||||
break if entries.empty?
|
|
||||||
|
|
||||||
entries.each { |remote| persist(remote) }
|
query = build_query(full_sync)
|
||||||
|
|
||||||
break if entries.size < PAGE_SIZE
|
service.query_in_batches(query, per_page: self.class::PAGE_SIZE) do |batch|
|
||||||
page += 1
|
entries = Array(batch)
|
||||||
|
log "Processing batch of #{entries.size} #{@entity.name}"
|
||||||
|
|
||||||
|
entries.each do |remote|
|
||||||
|
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,8 +61,22 @@ 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
|
||||||
|
|
||||||
# Determine if a remote entity should be deleted locally (e.g. if it's marked inactive in QBO)
|
# Determine if a remote entity should be deleted locally (e.g. if it's marked inactive in QBO)
|
||||||
@@ -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,24 +101,20 @@ 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}"
|
||||||
|
attach_documents(local, remote)
|
||||||
# Handle attaching documents if applicable to invoices
|
|
||||||
attach_documents(local, remote)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue => e
|
rescue => e
|
||||||
log "Failed to sync #{@entity.name} #{remote.id}: #{e.message}"
|
log "Failed to sync #{@entity.name} #{remote.id}: #{e.message}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# This method should be implemented in subclasses to map remote attributes to local model
|
# This method should be implemented in subclasses to map remote attributes to local model
|
||||||
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