33 Commits
2.0.0 ... 2.0.3

Author SHA1 Message Date
047296329e 2.0.32.0.3 2023-12-31 16:42:47 -05:00
c8cb74f3d4 Merge branch 'redmine-5' 2023-12-31 16:35:26 -05:00
aceb6cb6b5 fixed typo 2023-12-31 16:26:02 -05:00
9fd1bc9dff Merge branch 'redmine-5' 2023-12-30 23:35:25 -05:00
04391f1c6e 2.0.2 2023-12-30 23:07:17 -05:00
e2bf42e66b Fixed invoice pdf 2023-12-30 23:04:43 -05:00
0c72ca9294 missed this authenticated_request 2023-12-30 23:01:32 -05:00
2985fad77c Fixed typo 2023-12-30 23:01:01 -05:00
02b5fb4d0e Fixed returned variable handling 2023-12-30 22:53:08 -05:00
bf417c163c Rework performing authenticated requests 2023-12-30 22:33:28 -05:00
b35974e455 2.0.1 2023-12-30 20:55:58 -05:00
6d0abf865e 2023 2023-12-30 20:54:40 -05:00
275af9be82 Fixed formatting 2023-12-30 20:53:39 -05:00
f4e44a1975 Remove listen (was used for development env) 2023-12-30 20:29:20 -05:00
81f322b616 Call refresh_token to set token time stamps 2023-12-30 20:26:30 -05:00
f094ef57ec Setter for notes 2023-12-30 20:08:36 -05:00
2e32d8f6e5 Fixed get_base 2023-12-30 19:55:01 -05:00
3e352f270d Added Item 2023-12-30 19:41:02 -05:00
45056e8ff4 Remove unsed columns 2023-12-30 19:28:04 -05:00
b13abe51bf Display token expiration times 2023-12-30 19:27:37 -05:00
c3513427de Used realm_id not comany_id 2023-12-30 19:13:08 -05:00
7a6b6882d2 Update get_base 2023-12-30 18:53:01 -05:00
d6ec34cef9 added realm_id 2023-12-30 12:56:46 -05:00
84dfdd707a fixed token names 2023-12-30 12:46:52 -05:00
517a239485 Started reworking Oauth token sorage 2023-12-30 12:39:51 -05:00
47868051f8 Rails 6.1 Deprecates update_attributes 2023-12-29 20:25:26 -05:00
96e4e9df66 Fixed typo with params 2023-12-29 20:17:46 -05:00
7d510e4028 Added notes to allowed params 2023-12-29 20:06:15 -05:00
6760b29148 Log the time stamp 2023-12-29 19:20:31 -05:00
122063b1d5 Fixed customer typo 2023-12-29 19:14:38 -05:00
b304c3a175 Fixed employee typo 2023-12-29 19:09:24 -05:00
5b89d73c20 Remove QboItem.sync 2023-12-29 18:56:20 -05:00
8380dda25a render 403 when forbidden 2023-12-29 17:06:00 -05:00
18 changed files with 305 additions and 206 deletions

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2016 - 2022 Rick Barrette
Copyright (c) 2016 - 2023 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

View File

@@ -36,7 +36,7 @@ class CustomersController < ApplicationController
autocomplete :customer, :name, :full => true, :extra_data => [:id]
def allowed_params
params.require(:customer).permit(:name, :email, :primary_phone, :mobile_phone, :phone_number)
params.require(:customer).permit(:name, :email, :primary_phone, :mobile_phone, :phone_number, :notes)
end
# getter method for a customer's vehicles
@@ -115,7 +115,7 @@ class CustomersController < ApplicationController
def update
begin
@customer = Customer.find_by_id(params[:id])
if @customer.update_attributes(allowed_params)
if @customer.update(allowed_params)
flash[:notice] = "Customer updated"
redirect_to @customer
else

View File

@@ -1,6 +1,6 @@
#The MIT License (MIT)
#
#Copyright (c) 2022 rick barrette
#Copyright (c) 2023 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:
#
@@ -20,10 +20,13 @@ class InvoiceController < ApplicationController
#
def show
begin
base = Invoice.get_base
invoice = base.fetch_by_id(params[:id])
@pdf = base.pdf(invoice)
send_data @pdf, filename: "invoice #{invoice.doc_number}.pdf", :disposition => 'inline', :type => "application/pdf"
qbo = Qbo.first
qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token)
invoice = service.fetch_by_id(params[:id])
@pdf = service.pdf(invoice)
send_data @pdf, filename: "invoice #{invoice.doc_number}.pdf", :disposition => 'inline', :type => "application/pdf"
end
rescue
redirect_to :back, :flash => { :error => "Invoice not found" }
end

View File

@@ -1,6 +1,6 @@
#The MIT License (MIT)
#
#Copyright (c) 2022 rick barrette
#Copyright (c) 2023 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:
#
@@ -26,7 +26,7 @@ class QboController < ApplicationController
# Called when the user requests that Redmine to connect to QBO
#
def authenticate
oauth2_client = Qbo.get_client
oauth2_client = Qbo.construct_oauth2_client
callback = Setting.host_name + "/qbo/oauth_callback/"
grant_url = oauth2_client.auth_code.authorize_url(redirect_uri: callback, response_type: "code", state: SecureRandom.hex(12), scope: "com.intuit.quickbooks.accounting")
redirect_to grant_url
@@ -37,7 +37,7 @@ class QboController < ApplicationController
#
def oauth_callback
if params[:state].present?
oauth2_client = Qbo.get_client
oauth2_client = Qbo.construct_oauth2_client
# use the state value to retrieve from your backend any information you need to identify the customer in your system
redirect_uri = Setting.host_name + "/qbo/oauth_callback/"
if resp = oauth2_client.auth_code.get_token(params[:code], redirect_uri: redirect_uri)
@@ -47,12 +47,8 @@ class QboController < ApplicationController
# Save the authentication information
qbo = Qbo.new
qbo.company_id = params[:realmId]
# Generate Access Token & Serialize it into the database
access_token = OAuth2::AccessToken.new(oauth2_client, resp.token, refresh_token: resp.refresh_token)
qbo.token = access_token.to_hash
qbo.expire = 1.hour.from_now.utc
qbo.update(oauth2_access_token: resp.token, oauth2_refresh_token: resp.refresh_token, realm_id: params[:realmId])
qbo.refresh_token!
if qbo.save!
redirect_to qbo_sync_path, :flash => { :notice => "Successfully connected to Quickbooks" }
@@ -145,7 +141,6 @@ class QboController < ApplicationController
if Qbo.exists?
Customer.sync
Invoice.sync
QboItem.sync
Employee.sync
Estimate.sync
@@ -155,6 +150,6 @@ class QboController < ApplicationController
ActiveRecord::Base.connection.close
end
redirect_to :home, :flash => { :notice => "Successfully synced to Quickbooks" }
redirect_to :home, :flash => { :notice => "Syncing Quickbooks" }
end
end

View File

@@ -13,7 +13,7 @@ module AuthHelper
def require_user
return unless session[:token].nil?
if !User.current.logged?
render :file => "public/401.html.erb", :status => :unauthorized, :layout =>true
render_403
end
end
@@ -27,14 +27,14 @@ module AuthHelper
def check_permission(permission)
if !allowed_to?(permission)
render :file => "public/401.html.erb", :status => :unauthorized, :layout =>true
render_403
end
end
def global_check_permission(permission)
if !globaly_allowed_to?(permission)
render :file => "public/401.html.erb", :status => :unauthorized, :layout =>true
render_403
end
end

View File

@@ -0,0 +1,80 @@
#The MIT License (MIT)
#
#Copyright (c) 2023 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 QuickbooksOauth
extend ActiveSupport::Concern
OAUTH_CONSUMER_KEY = Setting.plugin_redmine_qbo['settingsOAuthConsumerKey']
OAUTH_CONSUMER_SECRET = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret']
#== Instance Methods
def perform_authenticated_request(&block)
attempts = 0
begin
yield oauth_access_token
rescue OAuth2::Error, Quickbooks::AuthorizationFailure => ex
Rails.logger.info("QuickbooksOauth.perform: #{ex.message}")
# to prevent an infinite loop here keep a counter and bail out after N times...
attempts += 1
raise "QuickbooksOauth:ExceededAuthAttempts" if attempts >= 3
# check if its an invalid_grant first, but assume it is for now
refresh_token!
retry
end
end
def refresh_token!
t = oauth_access_token
refreshed = t.refresh!
if refreshed.params['x_refresh_token_expires_in'].to_i > 0
oauth2_refresh_token_expires_at = Time.now + refreshed.params['x_refresh_token_expires_in'].to_i.seconds
else
oauth2_refresh_token_expires_at = 100.days.from_now
end
update!(
oauth2_access_token: refreshed.token,
oauth2_access_token_expires_at: Time.at(refreshed.expires_at),
oauth2_refresh_token: refreshed.refresh_token,
oauth2_refresh_token_expires_at: oauth2_refresh_token_expires_at
)
end
def oauth_client
self.class.construct_oauth2_client
end
def oauth_access_token
OAuth2::AccessToken.new(oauth_client, oauth2_access_token, refresh_token: oauth2_refresh_token)
end
def consumer
oauth_access_token
end
module ClassMethods
def construct_oauth2_client
options = {
site: "https://appcenter.intuit.com/connect/oauth2",
authorize_url: "https://appcenter.intuit.com/connect/oauth2",
token_url: "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer"
}
OAuth2::Client.new(OAUTH_CONSUMER_KEY, OAUTH_CONSUMER_SECRET, options)
end
end
end

View File

@@ -1,6 +1,6 @@
#The MIT License (MIT)
#
#Copyright (c) 2022 rick barrette
#Copyright (c) 2023 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:
#
@@ -87,6 +87,13 @@ class Customer < ActiveRecord::Base
#update our locally stored number too
update_mobile_phone_number
end
# Convenience Method
# Sets the notes
def notes=(s)
pull unless @details
@details.notes = s
end
# update the localy stored phone number as a plain string with no special chars
def update_phone_number
@@ -135,28 +142,26 @@ class Customer < ActiveRecord::Base
# proforms a bruteforce sync operation
# This needs to be simplified
def self.sync
service = Qbo.get_base(:customer)
# Sync ALL customers if the database is empty
#if count == 0
customers = service.all
#else
# last = Qbo.first.last_sync
# query = "Select Id, DisplayName From Customer"
# query << " Where Metadata.LastUpdatedTime >= '#{last.iso8601}' " if last
# customers = service.query(query)
#end
qbo = Qbo.first
customers = qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token)
service.all
end
customers.each do |customer|
customer = Customer.find_or_create_by(id: customer.id)
if customer.active?
if not customer.name.eql? customer.display_name
customer.name = customer.display_name
customer.id = customer.id
return unless customers
customers.each do |c|
logger.info "Processing customer #{c.id}"
customer = Customer.find_or_create_by(id: c.id)
if c.active?
if not customer.name.eql? c.display_name
customer.name = c.display_name
customer.id = c.id
customer.save_without_push
end
else
if not customer.new_record?
if not c.new_record?
customer.delete
end
end
@@ -172,9 +177,14 @@ class Customer < ActiveRecord::Base
# proforms a bruteforce sync operation
# This needs to be simplified
def self.sync_by_id(id)
service = Qbo.get_base(:customer)
qbo = Qbo.first
customer = qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token)
service.fetch_by_id(id)
end
return unless customer
customer = service.fetch_by_id(id)
customer = Customer.find_or_create_by(id: customer.id)
if customer.active?
if not customer.name.eql? customer.display_name
@@ -192,7 +202,11 @@ class Customer < ActiveRecord::Base
# Push the updates
def save_with_push
begin
@details = Qbo.get_base(:customer).update(@details)
qbo = Qbo.first
@details = qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token)
service.update(@details)
end
#raise "QBO Fault" if @details.fault?
self.id = @details.id
rescue Exception => e
@@ -210,7 +224,11 @@ class Customer < ActiveRecord::Base
def pull
begin
raise Exception unless self.id
@details = Qbo.get_base(:customer).fetch_by_id(self.id)
qbo = Qbo.first
@details = qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token)
service.fetch_by_id(self.id)
end
rescue Exception => e
@details = Quickbooks::Model::Customer.new
end

View File

@@ -1,6 +1,6 @@
#The MIT License (MIT)
#
#Copyright (c) 2022 rick barrette
#Copyright (c) 2023 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:
#
@@ -13,26 +13,35 @@ class Employee < ActiveRecord::Base
has_many :users
validates_presence_of :id, :name
def self.get_base
Qbo.get_base(:employee)
end
def self.sync
employees = get_base.all
qbo = Qbo.first
employees = qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Employee.new(:company_id => qbo.realm_id, :access_token => access_token)
service.all
end
return unless employees
transaction do
# Update the item table
employees.each { |employee|
employee = find_or_create_by(id: employee.id)
employee.name = employee.display_name
employee.id = employee.id
employees.each { |e|
logger.info "Processing employee #{e.id}"
employee = find_or_create_by(id: e.id)
employee.name = e.display_name
employee.id = e.id
employee.save!
}
end
end
def self.sync_by_id(id)
employee = get_base.fetch_by_id(id)
qbo = Qbo.first
employee = qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Employee.new(:company_id => qbo.realm_id, :access_token => access_token)
service.fetch_by_id(id)
end
return unless employee
employee = find_or_create_by(id: employee.id)
employee.name = employee.display_name
employee.id = employee.id

View File

@@ -1,6 +1,6 @@
#The MIT License (MIT)
#
#Copyright (c) 2022 rick barrette
#Copyright (c) 2023 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:
#
@@ -16,15 +16,17 @@ class Estimate < ActiveRecord::Base
validates_presence_of :doc_number, :id
self.primary_key = :id
# return the QBO Estimate service
def self.get_base
Qbo.get_base(:estimate)
end
# sync all estimates
def self.sync
logger.debug "Syncing ALL estimates"
estimates = get_base.all
qbo = Qbo.first
estimates = qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token)
service.all
end
return unless estimates
estimates.each { |estimate|
process_estimate(estimate)
}
@@ -36,17 +38,28 @@ class Estimate < ActiveRecord::Base
# sync only one estimate
def self.sync_by_id(id)
logger.debug "Syncing estimate #{id}"
process_estimate(get_base.fetch_by_id(id))
qbo = Qbo.first
qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token)
process_estimate(service.fetch_by_id(id))
end
end
# update an estimate
def self.update(id)
# Update the item table
estimate = get_base.fetch_by_id(id)
estimate = find_or_create_by(id: id)
estimate.doc_number = estimate.doc_number
estimate.txn_date = estimate.txn_date
estimate.save!
qbo = Qbo.first
estimate = qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token)
service.fetch_by_id(id)
end
return unless estimate
e = find_or_create_by(id: id)
e.doc_number = estimate.doc_number
e.txn_date = estimate.txn_date
e.save!
end
# process an estimate into the database
@@ -62,9 +75,12 @@ class Estimate < ActiveRecord::Base
# download the pdf from quickbooks
def pdf
base = Estimate.get_base
estimate = base.fetch_by_id(id)
return base.pdf(estimate)
qbo = Qbo.first
qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token)
estimate = service.fetch_by_id(id)
service.pdf(estimate)
end
end
# Magic Method
@@ -91,7 +107,11 @@ class Estimate < ActiveRecord::Base
def pull
begin
raise Exception unless self.id
@details = Qbo.get_base(:estimate).fetch_by_id(self.id)
qbo = Qbo.first
@details = qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token)
service(:estimate).fetch_by_id(self.id)
end
rescue Exception => e
@details = Quickbooks::Model::Estimate.new
end

View File

@@ -1,6 +1,6 @@
#The MIT License (MIT)
#
#Copyright (c) 2022 rick barrette
#Copyright (c) 2023 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:
#
@@ -15,11 +15,6 @@ class Invoice < ActiveRecord::Base
validates_presence_of :doc_number, :id, :customer_id, :txn_date
self.primary_key = :id
# Get the quickbooks-ruby base for invoice
def self.get_base
Qbo.get_base(:invoice)
end
# sync ALL the invoices
def self.sync
logger.debug "Syncing all invoices"
@@ -30,11 +25,13 @@ class Invoice < ActiveRecord::Base
# TODO actually do something with the above query
# .all() is never called since count is never initialized
if count == 0
invoices = get_base.all
else
invoices = get_base.query()
qbo = Qbo.first
invoices = qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token)
service.all
end
return unless invoices
invoices.each { | invoice |
process_invoice invoice
@@ -44,8 +41,12 @@ class Invoice < ActiveRecord::Base
#sync by invoice ID
def self.sync_by_id(id)
logger.debug "Syncing invoice #{id}"
invoice = get_base.fetch_by_id(id)
process_invoice invoice
qbo = Qbo.first
qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token)
invoice = service.fetch_by_id(id)
process_invoice invoice
end
end
private
@@ -155,7 +156,11 @@ class Invoice < ActiveRecord::Base
# Push updates
begin
logger.debug "Trying to update invoice"
get_base.update(invoice) if is_changed
qbo = Qbo.first
qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token)
service.update(invoice) if is_changed
end
rescue
# Do nothing, probaly custome field sync confict on the invoice.
# This is a problem with how it's billed
@@ -187,7 +192,11 @@ class Invoice < ActiveRecord::Base
def pull
begin
raise Exception unless self.id
@details = Qbo.get_base(:invoice).fetch_by_id(self.id)
qbo = Qbo.first
@details = qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token)
service.fetch_by_id(self.id)
end
rescue Exception => e
@details = Quickbooks::Model::Invoice.new
end

View File

@@ -1,6 +1,6 @@
#The MIT License (MIT)
#
#Copyright (c) 2022 rick barrette
#Copyright (c) 2023 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:
#
@@ -10,85 +10,15 @@
class Qbo < ActiveRecord::Base
unloadable
validates_presence_of :token, :company_id, :expire
serialize :token
OAUTH_CONSUMER_KEY = Setting.plugin_redmine_qbo['settingsOAuthConsumerKey']
OAUTH_CONSUMER_SECRET = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret']
#
# Getter for quickbooks OAuth2 client
#
def self.get_client
oauth_params = {
site: "https://appcenter.intuit.com/connect/oauth2",
authorize_url: "https://appcenter.intuit.com/connect/oauth2",
token_url: "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer"
}
return OAuth2::Client.new(OAUTH_CONSUMER_KEY, OAUTH_CONSUMER_SECRET, oauth_params)
end
#
# Getter for oauth consumer
#
def self.get_oauth_consumer
# Quickbooks Config Info
return $qb_oauth_consumer
end
#
# Get a quickbooks base service object for type
# @params type of base
#
def self.get_base(type)
# lets getnourbold access token from the database
oauth2_client = get_client
qbo = self.first
access_token = OAuth2::AccessToken.from_hash(oauth2_client, qbo.token)
# check to see if we need to refresh the acesstoken
if qbo.expire.to_time.utc.past?
puts "Updating access token"
new_access_token_object = access_token.refresh!
qbo.token = new_access_token_object.to_hash
qbo.expire = 1.hour.from_now.utc
qbo.save!
access_token = new_access_token_object
else
puts "Using current token"
end
# build the reqiested service
case type
when :item
return Quickbooks::Service::Item.new(:company_id => qbo.company_id, :access_token => access_token)
when :time_activity
return Quickbooks::Service::TimeActivity.new(:company_id => qbo.company_id, :access_token => access_token)
when :customer
return Quickbooks::Service::Customer.new(:company_id => qbo.company_id, :access_token => access_token)
when :invoice
return Quickbooks::Service::Invoice.new(:company_id => qbo.company_id, :access_token => access_token)
when :estimate
return Quickbooks::Service::Estimate.new(:company_id => qbo.company_id, :access_token => access_token)
when :account
return Quickbooks::Service::Account.new(:company_id => qbo.company_id, :access_token => access_token)
when :employee
return Quickbooks::Service::Employee.new(:company_id => qbo.company_id, :access_token => access_token)
else
return access_token
end
end
# Get the QBO account
def self.get_account
first
end
include QuickbooksOauth
# Updates last sync time stamp
def self.update_time_stamp
date = DateTime.now
logger.info "Updating QBO timestamp to #{date}"
qbo = Qbo.first
qbo.last_sync = DateTime.now
qbo.last_sync = date
qbo.save
end

View File

@@ -1 +0,0 @@
<%= flash.now[:error] = t(:label_401) %>

View File

@@ -58,8 +58,13 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
</tr>
<tr>
<th><%=t(:label_oauth_expires)%></th>
<td><%= if Qbo.exists? then Qbo.first.expire end %>
<th><%=t(:label_oauth_expires)%></th>
<td><%= if Qbo.exists? then Qbo.first.oauth2_access_token_expires_at end %>
</tr>
<tr>
<th><%=t(:label_oauth2_refresh_token_expires_at)%></th>
<td><%= if Qbo.exists? then Qbo.first.oauth2_refresh_token_expires_at end %>
</tr>
</tbody>

View File

@@ -25,7 +25,6 @@ en:
label_search_estimates: "Search Estimates"
label_search: "Search"
label_estimates: "Estimates"
label_401: "Not Authorized"
warn_ru_sure: "You sure?"
label_delete: "Delete"
label_edit: "Edit"
@@ -88,4 +87,5 @@ en:
label_billing_error: "Cannot bill without a customer assigned"
label_qbo_sync_success: "Successfully synced to Quickbooks"
label_hours: "Hours"
label_oauth2_refresh_token_expires_at: "Refresh Token Expires At"

View File

@@ -1,6 +1,6 @@
#The MIT License (MIT)
#
#Copyright (c) 2022 rick barrette
#Copyright (c) 2023 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:
#
@@ -26,7 +26,13 @@ class AddTxnDates < ActiveRecord::Migration[5.1]
say "Sync Invoices"
invoices = QboInvoice.get_base.all
qbo = Qbo.first
invoices = qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token)
service.all
end
return unless invoices
invoices.each { |invoice|
# Load the invoice into the database

View File

@@ -0,0 +1,22 @@
#The MIT License (MIT)
#
#Copyright (c) 2023 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 UpdateQboToken < ActiveRecord::Migration[5.1]
def change
add_column :qbos, :oauth2_access_token, :text
add_column :qbos, :oauth2_access_token_expires_at, :datetime
add_column :qbos, :oauth2_refresh_token, :text
add_column :qbos, :oauth2_refresh_token_expires_at, :datetime
add_column :qbos, :realm_id, :text
remove_column :qbos, :company_id
remove_column :qbos, :token
remove_column :qbos, :expire
end
end

View File

@@ -22,7 +22,7 @@ Redmine::Plugin.register :redmine_qbo do
name 'Redmine Quickbooks Online plugin'
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'
version '2.0.0'
version '2.0.3'
url 'https://github.com/rickbarrette/redmine_qbo'
author_url 'https://barrettefabrication.com'
settings :default => {'empty' => true}, :partial => 'qbo/settings'

View File

@@ -1,6 +1,6 @@
#The MIT License (MIT)
#
#Copyright (c) 2022 rick barrette
#Copyright (c) 2023 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:
#
@@ -52,46 +52,49 @@ module IssuePatch
if spent_hours > 0 then
# Prepare to create a new Time Activity
time_service = Qbo.get_base(:time_activity)
item_service = Qbo.get_base(:item)
time_entry = Quickbooks::Model::TimeActivity.new
qbo = Qbo.first
qbo.perform_authenticated_request do |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)
time_entry = Quickbooks::Model::TimeActivity.new
# Lets total up each activity before billing.
# This will simpify the invoicing with a single billable time entry per time activity
h = Hash.new(0)
spent_time.each do |entry|
h[entry.activity.name] += entry.hours
# update time entries billed status
entry.billed = true
entry.save
end
# Now letes upload our totals for each activity as their own billable time entry
h.each do |key, val|
# Lets total up each activity before billing.
# This will simpify the invoicing with a single billable time entry per time activity
h = Hash.new(0)
spent_time.each do |entry|
h[entry.activity.name] += entry.hours
# update time entries billed status
entry.billed = true
entry.save
end
# Convert float spent time to hours and minutes
hours = val.to_i
minutesDecimal = (( val - hours) * 60)
minutes = minutesDecimal.to_i
# Now letes upload our totals for each activity as their own billable time entry
h.each do |key, val|
# Convert float spent time to hours and minutes
hours = val.to_i
minutesDecimal = (( val - hours) * 60)
minutes = minutesDecimal.to_i
# Lets match the activity to an qbo item
item = item_service.query("SELECT * FROM Item WHERE Name = '#{key}' ").first
next if item.nil?
# Create the new billable time entry and upload it
time_entry.description = "#{tracker} ##{id}: #{subject} #{"(Partial @ #{done_ratio}%)" if not closed?}"
time_entry.employee_id = assigned_to.employee_id
time_entry.customer_id = customer_id
time_entry.billable_status = "Billable"
time_entry.hours = hours
time_entry.minutes = minutes
time_entry.name_of = "Employee"
time_entry.txn_date = Date.today
time_entry.hourly_rate = item.unit_price
time_entry.item_id = item.id
time_entry.start_time = start_date
time_entry.end_time = Time.now
time_service.create(time_entry)
# Lets match the activity to an qbo item
item = item_service.query("SELECT * FROM Item WHERE Name = '#{key}' ").first
next if item.nil?
# Create the new billable time entry and upload it
time_entry.description = "#{tracker} ##{id}: #{subject} #{"(Partial @ #{done_ratio}%)" if not closed?}"
time_entry.employee_id = assigned_to.employee_id
time_entry.customer_id = customer_id
time_entry.billable_status = "Billable"
time_entry.hours = hours
time_entry.minutes = minutes
time_entry.name_of = "Employee"
time_entry.txn_date = Date.today
time_entry.hourly_rate = item.unit_price
time_entry.item_id = item.id
time_entry.start_time = start_date
time_entry.end_time = Time.now
time_service.create(time_entry)
end
end
end
end