From 3220ff728f59734679c6bf9d6250bfea9f9b2bfb Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Wed, 6 Apr 2022 12:45:40 -0400 Subject: [PATCH 001/106] Fixed process_estimate that I accidently broke --- app/models/estimate.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/models/estimate.rb b/app/models/estimate.rb index 4d9bf82..541f4bd 100644 --- a/app/models/estimate.rb +++ b/app/models/estimate.rb @@ -50,13 +50,13 @@ class Estimate < ActiveRecord::Base end # process an estimate into the database - def self.process_estimate(estimate) - logger.info "Processing estimate #{estimate.id}" - estimate = find_or_create_by(id: estimate.id) - estimate.doc_number = estimate.doc_number - estimate.customer_id = estimate.customer_ref.value - estimate.id = estimate.id - estimate.txn_date = estimate.txn_date + def self.process_estimate(qbo_estimate) + logger.info "Processing estimate #{qbo_estimate.id}" + estimate = find_or_create_by(id: qbo_estimate.id) + estimate.doc_number = qbo_estimate.doc_number + estimate.customer_id = qbo_estimate.customer_ref.value + estimate.id = qbo_estimate.id + estimate.txn_date = qbo_estimate.txn_date estimate.save! end From 6dbf84f4017f8a35757d4afa3d04230ad444f462 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Wed, 4 May 2022 12:37:31 -0400 Subject: [PATCH 002/106] Added nil check to address_to_s --- app/controllers/customers_controller.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/customers_controller.rb b/app/controllers/customers_controller.rb index fcb7142..353e266 100644 --- a/app/controllers/customers_controller.rb +++ b/app/controllers/customers_controller.rb @@ -214,14 +214,14 @@ class CustomersController < ApplicationController # format a quickbooks address to a human readable string def address_to_s (address) return if address.nil? - string = address.line1 + string = address.line1 if address.line1 string << "\n" + address.line2 if address.line2 string << "\n" + address.line3 if address.line3 string << "\n" + address.line4 if address.line4 string << "\n" + address.line5 if address.line5 - string << " " + address.city - string << ", " + address.country_sub_division_code - string << " " + address.postal_code + string << " " + address.city if address.city + string << ", " + address.country_sub_division_code if address.country_sub_division_code + string << " " + address.postal_code if address.postal_code return string end From 26433c9020e436e13a3101217cd67365c597b4da Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 14 Jan 2023 06:38:48 -0500 Subject: [PATCH 003/106] Added hour totals to customer job history --- app/controllers/customers_controller.rb | 4 ++++ app/views/customers/show.html.erb | 4 ++-- config/locales/en.yml | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/controllers/customers_controller.rb b/app/controllers/customers_controller.rb index 353e266..e8b8ec4 100644 --- a/app/controllers/customers_controller.rb +++ b/app/controllers/customers_controller.rb @@ -93,6 +93,10 @@ class CustomersController < ApplicationController @billing_address = address_to_s(@customer.billing_address) @shipping_address = address_to_s(@customer.shipping_address) @closed_issues = (@issues - @issues.open) + @hours = 0 + @closed_hours = 0 + @issues.open.each { |i| @hours+= i.total_spent_hours } + @closed_issues.each { |i| @closed_hours+= i.total_spent_hours } rescue render_404 end diff --git a/app/views/customers/show.html.erb b/app/views/customers/show.html.erb index 2a7689f..af5d5b7 100644 --- a/app/views/customers/show.html.erb +++ b/app/views/customers/show.html.erb @@ -35,8 +35,8 @@
-

<%=@issues.open.count%> <%=t(:label_open_issues)%>:

+

<%=@issues.open.count%> <%=t(:label_open_issues)%> - <%=@hours.round(1)%> <%=t(:label_hours)%>

<%= render :partial => 'issues/list_simple', locals: {issues: @issues.open} %> -

<%=@closed_issues.count%> <%=t(:label_closed_issues)%>:

+

<%=@closed_issues.count%> <%=t(:label_closed_issues)%> - <%= @closed_hours.round(1)%> <%=t(:label_hours)%>

<%= render :partial => 'issues/list_simple', locals: {issues: @closed_issues} %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 48768cb..c93d22c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -87,4 +87,5 @@ en: label_billed_success: "Successfully Billed " label_billing_error: "Cannot bill without a customer assigned" label_qbo_sync_success: "Successfully synced to Quickbooks" + label_hours: "Hours" \ No newline at end of file From 3a0e58c3dab6398f198b15bcf4c6aa643f067d68 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 22 May 2023 07:34:18 -0400 Subject: [PATCH 004/106] Append last 4 of phone number to customers name --- app/models/customer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/customer.rb b/app/models/customer.rb index ce9ba02..accb7f8 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -23,7 +23,7 @@ class Customer < ActiveRecord::Base # returns a human readable string def to_s - return name + return "#{self[:name]} - #{phone_number.split(//).last(4).join unless phone_number.nil?}" end # Convenience Method From b3a809ab1cd445a3750d104a4ac7f496574b6a22 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Wed, 27 Dec 2023 15:03:08 -0500 Subject: [PATCH 005/106] Redmine 5.1 Update --- Gemfile | 2 +- init.rb | 8 ++++---- lib/pdf_patch.rb | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 9924dce..baf0247 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' gem 'quickbooks-ruby' -gem 'oauth2', '1.4.7' +gem 'oauth2' gem 'roxml' gem 'nhtsa_vin' gem 'will_paginate' diff --git a/init.rb b/init.rb index c79919d..25795c2 100644 --- a/init.rb +++ b/init.rb @@ -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: # @@ -22,11 +22,11 @@ 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 '1.1.6' + version '1.2.0' url 'https://github.com/rickbarrette/redmine_qbo' - author_url 'http://rickbarrette.org' + author_url 'https://barrettefabrication.com' settings :default => {'empty' => true}, :partial => 'qbo/settings' - requires_redmine :version_or_higher => '4.0.0' + requires_redmine :version_or_higher => '5.1.0' # Add safe attributes for core models Issue.safe_attributes 'customer_id' diff --git a/lib/pdf_patch.rb b/lib/pdf_patch.rb index ed659b0..803f045 100644 --- a/lib/pdf_patch.rb +++ b/lib/pdf_patch.rb @@ -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: # @@ -11,7 +11,7 @@ require_dependency 'redmine/export/pdf' require_dependency 'redmine/export/pdf/issues_pdf_helper' -module IssuesPdfHelperPatch +module PdfPatch def self.included(base) base.send(:include, InstanceMethods) @@ -256,4 +256,4 @@ module IssuesPdfHelperPatch end end -Redmine::Export::PDF::IssuesPdfHelper.send(:include, IssuesPdfHelperPatch) +Redmine::Export::PDF::IssuesPdfHelper.send(:include, PdfPatch) From 78391161341248b0569b63321cf98877457b9349 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Wed, 27 Dec 2023 16:31:11 -0500 Subject: [PATCH 006/106] Version 2.0.0 --- README.md | 5 ++++- init.rb | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 06e0968..51ae0a2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,10 @@ The goal of this project is to allow Redmine to connect with Quickbooks Online t Note: Although the core functionality is complete, this project is still under development & the master branch may be unstable. Tags should be stable and are recommended -Use tags Version 1.0.0+ for Redmine 4+ and Version 0.8.1 for Redine 3 +Use tags for the following Redmine Versions +* Version 2.0.0+ for Redmine 5+ +* Version 1.0.0+ for Redmine 4+ +* Version 0.8.1 for Redine 3 #### Features * Issues can be assigned to a Customer via drop down in the edit Issue form diff --git a/init.rb b/init.rb index 25795c2..14be242 100644 --- a/init.rb +++ b/init.rb @@ -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 '1.2.0' + version '2.0.0' url 'https://github.com/rickbarrette/redmine_qbo' author_url 'https://barrettefabrication.com' settings :default => {'empty' => true}, :partial => 'qbo/settings' From 8380dda25a53b40b692e938653c67ae996873563 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Fri, 29 Dec 2023 17:06:00 -0500 Subject: [PATCH 007/106] render 403 when forbidden --- app/helpers/auth_helper.rb | 6 +++--- app/views/public/401.html.erb | 1 - config/locales/en.yml | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) delete mode 100644 app/views/public/401.html.erb diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 42b81a2..7959fec 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -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 diff --git a/app/views/public/401.html.erb b/app/views/public/401.html.erb deleted file mode 100644 index 50381b8..0000000 --- a/app/views/public/401.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= flash.now[:error] = t(:label_401) %> diff --git a/config/locales/en.yml b/config/locales/en.yml index c93d22c..5543f76 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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" From 5b89d73c208c001431c798c54cf4062adb61b85d Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Fri, 29 Dec 2023 18:56:20 -0500 Subject: [PATCH 008/106] Remove QboItem.sync --- app/controllers/qbo_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index 4be1b46..3d7c4cd 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -145,7 +145,6 @@ class QboController < ApplicationController if Qbo.exists? Customer.sync Invoice.sync - QboItem.sync Employee.sync Estimate.sync From b304c3a1759ad081aa082ea4abce920d0b9ebeea Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Fri, 29 Dec 2023 19:09:24 -0500 Subject: [PATCH 009/106] Fixed employee typo --- app/models/employee.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/models/employee.rb b/app/models/employee.rb index efc4a1f..3319558 100644 --- a/app/models/employee.rb +++ b/app/models/employee.rb @@ -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: # @@ -22,10 +22,11 @@ class Employee < ActiveRecord::Base 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 From 122063b1d59f414c360739f3bb7575b523b0b6e2 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Fri, 29 Dec 2023 19:14:38 -0500 Subject: [PATCH 010/106] Fixed customer typo --- app/models/customer.rb | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/models/customer.rb b/app/models/customer.rb index accb7f8..6ace67b 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -147,16 +147,17 @@ class Customer < ActiveRecord::Base # customers = service.query(query) #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 + 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 From 6760b2914852d84b1d9927a515afd94f2a537582 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Fri, 29 Dec 2023 19:20:31 -0500 Subject: [PATCH 011/106] Log the time stamp --- app/models/qbo.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/qbo.rb b/app/models/qbo.rb index 09f3515..337639e 100644 --- a/app/models/qbo.rb +++ b/app/models/qbo.rb @@ -87,8 +87,10 @@ class Qbo < ActiveRecord::Base # 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 From 7d510e4028da2fddc33f7e2ef0609dd1495b1506 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Fri, 29 Dec 2023 20:06:15 -0500 Subject: [PATCH 012/106] Added notes to allowed params --- app/controllers/customers_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/customers_controller.rb b/app/controllers/customers_controller.rb index e8b8ec4..44ee4c8 100644 --- a/app/controllers/customers_controller.rb +++ b/app/controllers/customers_controller.rb @@ -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 From 96e4e9df6644624f69cacd111415d65b8f617198 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Fri, 29 Dec 2023 20:17:46 -0500 Subject: [PATCH 013/106] Fixed typo with params --- app/controllers/customers_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/customers_controller.rb b/app/controllers/customers_controller.rb index 44ee4c8..aca80bc 100644 --- a/app/controllers/customers_controller.rb +++ b/app/controllers/customers_controller.rb @@ -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_attributes(params) flash[:notice] = "Customer updated" redirect_to @customer else From 47868051f897b8a6902722875bdaaaa491247b0a Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Fri, 29 Dec 2023 20:25:26 -0500 Subject: [PATCH 014/106] Rails 6.1 Deprecates update_attributes --- app/controllers/customers_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/customers_controller.rb b/app/controllers/customers_controller.rb index aca80bc..dae292c 100644 --- a/app/controllers/customers_controller.rb +++ b/app/controllers/customers_controller.rb @@ -115,7 +115,7 @@ class CustomersController < ApplicationController def update begin @customer = Customer.find_by_id(params[:id]) - if @customer.update_attributes(params) + if @customer.update(allowed_params) flash[:notice] = "Customer updated" redirect_to @customer else From 517a23948528f863dda5def6218d169091412ae6 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 12:39:51 -0500 Subject: [PATCH 015/106] Started reworking Oauth token sorage --- app/controllers/qbo_controller.rb | 15 ++--- app/models/concerns/quickbooks_oauth.rb | 80 ++++++++++++++++++++++++ app/models/qbo.rb | 81 ++++++------------------- db/migrate/037_update_qbo_token.rb | 18 ++++++ 4 files changed, 122 insertions(+), 72 deletions(-) create mode 100644 app/models/concerns/quickbooks_oauth.rb create mode 100644 db/migrate/037_update_qbo_token.rb diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index 3d7c4cd..f6c8be1 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -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,7 @@ 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(access_token: resp.token, refresh_token: resp.refresh_token, realm_id: params[:realmId]) if qbo.save! redirect_to qbo_sync_path, :flash => { :notice => "Successfully connected to Quickbooks" } @@ -154,6 +149,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 diff --git a/app/models/concerns/quickbooks_oauth.rb b/app/models/concerns/quickbooks_oauth.rb new file mode 100644 index 0000000..9e03a82 --- /dev/null +++ b/app/models/concerns/quickbooks_oauth.rb @@ -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 diff --git a/app/models/qbo.rb b/app/models/qbo.rb index 337639e..7cd9b97 100644 --- a/app/models/qbo.rb +++ b/app/models/qbo.rb @@ -10,31 +10,8 @@ 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 + + include QuickbooksOauth # # Get a quickbooks base service object for type @@ -42,47 +19,27 @@ class Qbo < ActiveRecord::Base # def self.get_base(type) # lets getnourbold access token from the database - oauth2_client = get_client + oauth2_client = construct_oauth2_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 + qbo.perform_authenticated_request do |access_token| + # build the reqiested service + case type + 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 :employee + return Quickbooks::Service::Employee.new(:company_id => qbo.company_id, :access_token => access_token) + else + return access_token + end end - end - - # Get the QBO account - def self.get_account - first end # Updates last sync time stamp diff --git a/db/migrate/037_update_qbo_token.rb b/db/migrate/037_update_qbo_token.rb new file mode 100644 index 0000000..71a3109 --- /dev/null +++ b/db/migrate/037_update_qbo_token.rb @@ -0,0 +1,18 @@ +#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 + end +end From 84dfdd707a6b4e765971cfdff8c3ada77ca242ed Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 12:46:52 -0500 Subject: [PATCH 016/106] fixed token names --- app/controllers/qbo_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index f6c8be1..1ed482d 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -47,7 +47,7 @@ class QboController < ApplicationController # Save the authentication information qbo = Qbo.new - qbo.update(access_token: resp.token, refresh_token: resp.refresh_token, realm_id: params[:realmId]) + qbo.update(oauth2_access_token: resp.token, oauth2_refresh_token: resp.refresh_token, realm_id: params[:realmId]) if qbo.save! redirect_to qbo_sync_path, :flash => { :notice => "Successfully connected to Quickbooks" } From d6ec34cef9c87745a67ca6717513996c69d9c74b Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 12:56:46 -0500 Subject: [PATCH 017/106] added realm_id --- db/migrate/037_update_qbo_token.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/db/migrate/037_update_qbo_token.rb b/db/migrate/037_update_qbo_token.rb index 71a3109..ba1dcce 100644 --- a/db/migrate/037_update_qbo_token.rb +++ b/db/migrate/037_update_qbo_token.rb @@ -14,5 +14,6 @@ class UpdateQboToken < ActiveRecord::Migration[5.1] 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 end end From 7a6b6882d273b0d878fe9b8a445591241db51f79 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 18:53:01 -0500 Subject: [PATCH 018/106] Update get_base --- app/models/qbo.rb | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app/models/qbo.rb b/app/models/qbo.rb index 7cd9b97..7a3daf1 100644 --- a/app/models/qbo.rb +++ b/app/models/qbo.rb @@ -18,28 +18,27 @@ class Qbo < ActiveRecord::Base # @params type of base # def self.get_base(type) - # lets getnourbold access token from the database - oauth2_client = construct_oauth2_client qbo = self.first qbo.perform_authenticated_request do |access_token| # build the reqiested service case type when :time_activity - return Quickbooks::Service::TimeActivity.new(:company_id => qbo.company_id, :access_token => access_token) + base = 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) + base = 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) + base = Quickbooks::Service::Estimate.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) + base = Quickbooks::Service::Employee.new(:company_id => qbo.company_id, :access_token => access_token) else - return access_token + base = access_token end end + return base end # Updates last sync time stamp From c3513427de2f13bc6ba1e0fce3c3fecf06842f5f Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 19:13:08 -0500 Subject: [PATCH 019/106] Used realm_id not comany_id --- app/models/qbo.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/qbo.rb b/app/models/qbo.rb index 7a3daf1..8a665ff 100644 --- a/app/models/qbo.rb +++ b/app/models/qbo.rb @@ -24,15 +24,15 @@ class Qbo < ActiveRecord::Base # build the reqiested service case type when :time_activity - base = Quickbooks::Service::TimeActivity.new(:company_id => qbo.company_id, :access_token => access_token) + base = Quickbooks::Service::TimeActivity.new(:company_id => qbo.realm_id, :access_token => access_token) when :customer - return Quickbooks::Service::Customer.new(:company_id => qbo.company_id, :access_token => access_token) + return Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token) when :invoice - base = Quickbooks::Service::Invoice.new(:company_id => qbo.company_id, :access_token => access_token) + base = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token) when :estimate - base = Quickbooks::Service::Estimate.new(:company_id => qbo.company_id, :access_token => access_token) + base = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token) when :employee - base = Quickbooks::Service::Employee.new(:company_id => qbo.company_id, :access_token => access_token) + base = Quickbooks::Service::Employee.new(:company_id => qbo.realm_id, :access_token => access_token) else base = access_token end From b13abe51bf7c536a468415ee525d0f9a38a8212e Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 19:27:37 -0500 Subject: [PATCH 020/106] Display token expiration times --- app/views/qbo/_settings.html.erb | 6 ++++-- config/locales/en.yml | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/qbo/_settings.html.erb b/app/views/qbo/_settings.html.erb index 5a3a733..79c64fd 100644 --- a/app/views/qbo/_settings.html.erb +++ b/app/views/qbo/_settings.html.erb @@ -58,8 +58,10 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI - <%=t(:label_oauth_expires)%> - <%= if Qbo.exists? then Qbo.first.expire end %> + <%=t(:label_oauth_expires)%> + <%= if Qbo.exists? then Qbo.first.oauth2_access_token_expires_at end %> + <%=t(:label_oauth2_refresh_token_expires_at)%> + <%= if Qbo.exists? then Qbo.first.oauth2_refresh_token_expires_at end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 5543f76..8001ac6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -87,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" \ No newline at end of file From 45056e8ff4153139ac3b2ec8aea5d874e9e54dc1 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 19:28:04 -0500 Subject: [PATCH 021/106] Remove unsed columns --- db/migrate/037_update_qbo_token.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/db/migrate/037_update_qbo_token.rb b/db/migrate/037_update_qbo_token.rb index ba1dcce..b9bedc2 100644 --- a/db/migrate/037_update_qbo_token.rb +++ b/db/migrate/037_update_qbo_token.rb @@ -15,5 +15,8 @@ class UpdateQboToken < ActiveRecord::Migration[5.1] 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 From 3e352f270d0e4bcade674177b59e2f440aa01655 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 19:41:02 -0500 Subject: [PATCH 022/106] Added Item --- app/models/qbo.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/qbo.rb b/app/models/qbo.rb index 8a665ff..9d68f62 100644 --- a/app/models/qbo.rb +++ b/app/models/qbo.rb @@ -33,6 +33,8 @@ class Qbo < ActiveRecord::Base base = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token) when :employee base = Quickbooks::Service::Employee.new(:company_id => qbo.realm_id, :access_token => access_token) + when :item + base = Quickbooks::Service::Item.new(:company_id => qbo.realm_id, :access_token => access_token) else base = access_token end From 2e32d8f6e5f69208d13375a01a495b886dbc2027 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 19:55:01 -0500 Subject: [PATCH 023/106] Fixed get_base --- app/models/qbo.rb | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/models/qbo.rb b/app/models/qbo.rb index 9d68f62..66c9984 100644 --- a/app/models/qbo.rb +++ b/app/models/qbo.rb @@ -24,23 +24,21 @@ class Qbo < ActiveRecord::Base # build the reqiested service case type when :time_activity - base = Quickbooks::Service::TimeActivity.new(:company_id => qbo.realm_id, :access_token => access_token) + return Quickbooks::Service::TimeActivity.new(:company_id => qbo.realm_id, :access_token => access_token) when :customer return Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token) when :invoice - base = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token) + return Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token) when :estimate - base = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token) + return Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token) when :employee - base = Quickbooks::Service::Employee.new(:company_id => qbo.realm_id, :access_token => access_token) + return Quickbooks::Service::Employee.new(:company_id => qbo.realm_id, :access_token => access_token) when :item - base = Quickbooks::Service::Item.new(:company_id => qbo.realm_id, :access_token => access_token) + return Quickbooks::Service::Item.new(:company_id => qbo.realm_id, :access_token => access_token) else - base = access_token + return nil end end - - return base end # Updates last sync time stamp From f094ef57ec7b1758d18444701dee64a2d3ac88c7 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 20:08:36 -0500 Subject: [PATCH 024/106] Setter for notes --- app/models/customer.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/models/customer.rb b/app/models/customer.rb index 6ace67b..1314de8 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -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 From 81f322b616a4ef741ebc45b3099f3ca904948f7f Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 20:26:30 -0500 Subject: [PATCH 025/106] Call refresh_token to set token time stamps --- Gemfile | 1 + app/controllers/qbo_controller.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/Gemfile b/Gemfile index baf0247..d6c7ed4 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,7 @@ gem 'nhtsa_vin' gem 'will_paginate' gem 'rails-jquery-autocomplete' gem 'jquery-ui-rails' +gem 'listen', '~> 3.3' group :assets do gem 'coffee-rails' diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index 1ed482d..b3446b7 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -48,6 +48,7 @@ class QboController < ApplicationController # Save the authentication information qbo = Qbo.new 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" } From f4e44a1975dc04b6efb54de7378ed9eb0c4a34ee Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 20:29:20 -0500 Subject: [PATCH 026/106] Remove listen (was used for development env) --- Gemfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Gemfile b/Gemfile index d6c7ed4..baf0247 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,6 @@ gem 'nhtsa_vin' gem 'will_paginate' gem 'rails-jquery-autocomplete' gem 'jquery-ui-rails' -gem 'listen', '~> 3.3' group :assets do gem 'coffee-rails' From 275af9be82403885116b6985bee3897c9ad95826 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 20:53:39 -0500 Subject: [PATCH 027/106] Fixed formatting --- app/views/qbo/_settings.html.erb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/qbo/_settings.html.erb b/app/views/qbo/_settings.html.erb index 79c64fd..753143a 100644 --- a/app/views/qbo/_settings.html.erb +++ b/app/views/qbo/_settings.html.erb @@ -60,6 +60,9 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI <%=t(:label_oauth_expires)%> <%= if Qbo.exists? then Qbo.first.oauth2_access_token_expires_at end %> + + + <%=t(:label_oauth2_refresh_token_expires_at)%> <%= if Qbo.exists? then Qbo.first.oauth2_refresh_token_expires_at end %> From 6d0abf865e10133f8a73ccb51554985de236255d Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 20:54:40 -0500 Subject: [PATCH 028/106] 2023 --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 5ecf62e..ce662ac 100644 --- a/LICENSE +++ b/LICENSE @@ -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 From b35974e455725445647b3e14d8e1c53011480e09 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 20:55:58 -0500 Subject: [PATCH 029/106] 2.0.1 --- init.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init.rb b/init.rb index 14be242..5430d9b 100644 --- a/init.rb +++ b/init.rb @@ -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.1' url 'https://github.com/rickbarrette/redmine_qbo' author_url 'https://barrettefabrication.com' settings :default => {'empty' => true}, :partial => 'qbo/settings' From bf417c163c3eb869d105f22c156fc487c4ab63ee Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 22:33:28 -0500 Subject: [PATCH 030/106] Rework performing authenticated requests --- app/controllers/invoice_controller.rb | 14 +++-- app/models/customer.rb | 38 ++++++++----- app/models/employee.rb | 20 +++++-- app/models/estimate.rb | 54 ++++++++++++------ app/models/invoice.rb | 41 +++++++++----- app/models/qbo.rb | 30 +--------- db/migrate/032_add_txn_dates.rb | 10 +++- lib/issue_patch.rb | 79 ++++++++++++++------------- 8 files changed, 162 insertions(+), 124 deletions(-) diff --git a/app/controllers/invoice_controller.rb b/app/controllers/invoice_controller.rb index f0a7a58..6bf1e4b 100644 --- a/app/controllers/invoice_controller.rb +++ b/app/controllers/invoice_controller.rb @@ -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,9 +20,15 @@ class InvoiceController < ApplicationController # def show begin - base = Invoice.get_base - invoice = base.fetch_by_id(params[:id]) - @pdf = base.pdf(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(params[:id]) + @pdf = service.pdf(invoice) + end + + return unless @pdf + send_data @pdf, filename: "invoice #{invoice.doc_number}.pdf", :disposition => 'inline', :type => "application/pdf" rescue redirect_to :back, :flash => { :error => "Invoice not found" } diff --git a/app/models/customer.rb b/app/models/customer.rb index 1314de8..04dc6fe 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -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: # @@ -142,17 +142,14 @@ 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 + qbo = Qbo.first + qbo.perform_authenticated_request do |access_token| + service = Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token) 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 + end + + return unless customers customers.each do |c| logger.info "Processing customer #{c.id}" @@ -180,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 + qbo.perform_authenticated_request do |access_token| + service = Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token) + customer = 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 @@ -200,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 + qbo.perform_authenticated_request do |access_token| + service = Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token) + @details = serivce.update(@details) + end #raise "QBO Fault" if @details.fault? self.id = @details.id rescue Exception => e @@ -218,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 + qbo.perform_authenticated_request do |access_token| + service = Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token) + @details = bservice.ase.fetch_by_id(self.id) + end rescue Exception => e @details = Quickbooks::Model::Customer.new end diff --git a/app/models/employee.rb b/app/models/employee.rb index 3319558..54c23ea 100644 --- a/app/models/employee.rb +++ b/app/models/employee.rb @@ -13,12 +13,14 @@ 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 + qbo.perform_authenticated_request do |access_token| + service = Quickbooks::Service::Employee.new(:company_id => qbo.realm_id, :access_token => access_token) + employees = service.all + end + + return unless employees transaction do # Update the item table @@ -33,7 +35,13 @@ class Employee < ActiveRecord::Base end def self.sync_by_id(id) - employee = get_base.fetch_by_id(id) + qbo = Qbo.first + qbo.perform_authenticated_request do |access_token| + service = Quickbooks::Service::Employee.new(:company_id => qbo.realm_id, :access_token => access_token) + employee = 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 diff --git a/app/models/estimate.rb b/app/models/estimate.rb index 541f4bd..641ff39 100644 --- a/app/models/estimate.rb +++ b/app/models/estimate.rb @@ -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 + qbo.perform_authenticated_request do |access_token| + service = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token) + estimates = 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 + 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) + 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) + return 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 + qbo.perform_authenticated_request do |access_token| + service = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token) + @details = service(:estimate).fetch_by_id(self.id) + end rescue Exception => e @details = Quickbooks::Model::Estimate.new end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 8644e72..4b6d570 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -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,17 @@ 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 + qbo.perform_authenticated_request do |access_token| + service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token) + if count == 0 + invoices = service.all + else + invoices = service.query() + end end + + return unless invoices invoices.each { | invoice | process_invoice invoice @@ -44,8 +45,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 +160,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 +196,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 + qbo.perform_authenticated_request do |access_token| + service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token) + @details = service.fetch_by_id(self.id) + end rescue Exception => e @details = Quickbooks::Model::Invoice.new end diff --git a/app/models/qbo.rb b/app/models/qbo.rb index 66c9984..06199b7 100644 --- a/app/models/qbo.rb +++ b/app/models/qbo.rb @@ -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: # @@ -12,34 +12,6 @@ class Qbo < ActiveRecord::Base unloadable include QuickbooksOauth - - # - # Get a quickbooks base service object for type - # @params type of base - # - def self.get_base(type) - qbo = self.first - - qbo.perform_authenticated_request do |access_token| - # build the reqiested service - case type - when :time_activity - return Quickbooks::Service::TimeActivity.new(:company_id => qbo.realm_id, :access_token => access_token) - when :customer - return Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token) - when :invoice - return Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token) - when :estimate - return Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token) - when :employee - return Quickbooks::Service::Employee.new(:company_id => qbo.realm_id, :access_token => access_token) - when :item - return Quickbooks::Service::Item.new(:company_id => qbo.realm_id, :access_token => access_token) - else - return nil - end - end - end # Updates last sync time stamp def self.update_time_stamp diff --git a/db/migrate/032_add_txn_dates.rb b/db/migrate/032_add_txn_dates.rb index c6915c3..5e1a68d 100644 --- a/db/migrate/032_add_txn_dates.rb +++ b/db/migrate/032_add_txn_dates.rb @@ -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 + qbo.perform_authenticated_request do |access_token| + service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token) + invoices = service.all + end + + return unless invoices invoices.each { |invoice| # Load the invoice into the database diff --git a/lib/issue_patch.rb b/lib/issue_patch.rb index b7c1ecd..a804bff 100644 --- a/lib/issue_patch.rb +++ b/lib/issue_patch.rb @@ -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 From 02b5fb4d0e7de91a041b27c976d6c6c844c01192 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 22:53:08 -0500 Subject: [PATCH 031/106] Fixed returned variable handling --- app/controllers/invoice_controller.rb | 4 ++-- app/models/customer.rb | 16 ++++++++-------- app/models/employee.rb | 8 ++++---- app/models/estimate.rb | 10 +++++----- app/models/invoice.rb | 12 ++++-------- db/migrate/032_add_txn_dates.rb | 4 ++-- 6 files changed, 25 insertions(+), 29 deletions(-) diff --git a/app/controllers/invoice_controller.rb b/app/controllers/invoice_controller.rb index 6bf1e4b..d5a93b8 100644 --- a/app/controllers/invoice_controller.rb +++ b/app/controllers/invoice_controller.rb @@ -21,10 +21,10 @@ class InvoiceController < ApplicationController def show begin qbo = Qbo.first - qbo.perform_authenticated_request do |access_token| + @pdf = 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) + service.pdf(invoice) end return unless @pdf diff --git a/app/models/customer.rb b/app/models/customer.rb index 04dc6fe..4ce883c 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -144,9 +144,9 @@ class Customer < ActiveRecord::Base def self.sync # Sync ALL customers if the database is empty qbo = Qbo.first - qbo.perform_authenticated_request do |access_token| + customers = qbo.perform_authenticated_request do |access_token| service = Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token) - customers = service.all + service.all end return unless customers @@ -178,9 +178,9 @@ class Customer < ActiveRecord::Base # This needs to be simplified def self.sync_by_id(id) qbo = Qbo.first - qbo.perform_authenticated_request do |access_token| + customer = qbo.perform_authenticated_request do |access_token| service = Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token) - customer = service.fetch_by_id(id) + service.fetch_by_id(id) end return unless customer @@ -203,9 +203,9 @@ class Customer < ActiveRecord::Base def save_with_push begin qbo = Qbo.first - qbo.perform_authenticated_request do |access_token| + @details = qbo.perform_authenticated_request do |access_token| service = Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token) - @details = serivce.update(@details) + serivce.update(@details) end #raise "QBO Fault" if @details.fault? self.id = @details.id @@ -225,9 +225,9 @@ class Customer < ActiveRecord::Base begin raise Exception unless self.id qbo = Qbo.first - qbo.perform_authenticated_request do |access_token| + @details = qbo.perform_authenticated_request do |access_token| service = Quickbooks::Service::Customer.new(:company_id => qbo.realm_id, :access_token => access_token) - @details = bservice.ase.fetch_by_id(self.id) + bservice.ase.fetch_by_id(self.id) end rescue Exception => e @details = Quickbooks::Model::Customer.new diff --git a/app/models/employee.rb b/app/models/employee.rb index 54c23ea..acf6702 100644 --- a/app/models/employee.rb +++ b/app/models/employee.rb @@ -15,9 +15,9 @@ class Employee < ActiveRecord::Base def self.sync qbo = Qbo.first - qbo.perform_authenticated_request do |access_token| + employees = qbo.perform_authenticated_request do |access_token| service = Quickbooks::Service::Employee.new(:company_id => qbo.realm_id, :access_token => access_token) - employees = service.all + service.all end return unless employees @@ -36,9 +36,9 @@ class Employee < ActiveRecord::Base def self.sync_by_id(id) qbo = Qbo.first - qbo.perform_authenticated_request do |access_token| + employee = qbo.perform_authenticated_request do |access_token| service = Quickbooks::Service::Employee.new(:company_id => qbo.realm_id, :access_token => access_token) - employee = service.fetch_by_id(id) + service.fetch_by_id(id) end return unless employee diff --git a/app/models/estimate.rb b/app/models/estimate.rb index 641ff39..00410d7 100644 --- a/app/models/estimate.rb +++ b/app/models/estimate.rb @@ -20,9 +20,9 @@ class Estimate < ActiveRecord::Base def self.sync logger.debug "Syncing ALL estimates" qbo = Qbo.first - qbo.perform_authenticated_request do |access_token| + estimates = qbo.perform_authenticated_request do |access_token| service = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token) - estimates = service.all + service.all end return unless estimates @@ -79,7 +79,7 @@ class Estimate < ActiveRecord::Base 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) - return service.pdf(estimate) + service.pdf(estimate) end end @@ -108,9 +108,9 @@ class Estimate < ActiveRecord::Base begin raise Exception unless self.id qbo = Qbo.first - qbo.perform_authenticated_request do |access_token| + @details = qbo.perform_authenticated_request do |access_token| service = Quickbooks::Service::Estimate.new(:company_id => qbo.realm_id, :access_token => access_token) - @details = service(:estimate).fetch_by_id(self.id) + service(:estimate).fetch_by_id(self.id) end rescue Exception => e @details = Quickbooks::Model::Estimate.new diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 4b6d570..f66d1e8 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -26,13 +26,9 @@ class Invoice < ActiveRecord::Base # TODO actually do something with the above query # .all() is never called since count is never initialized qbo = Qbo.first - qbo.perform_authenticated_request do |access_token| + invoices = qbo.perform_authenticated_request do |access_token| service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token) - if count == 0 - invoices = service.all - else - invoices = service.query() - end + service.all end return unless invoices @@ -197,9 +193,9 @@ class Invoice < ActiveRecord::Base begin raise Exception unless self.id qbo = Qbo.first - qbo.perform_authenticated_request do |access_token| + @details = qbo.perform_authenticated_request do |access_token| service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token) - @details = service.fetch_by_id(self.id) + service.fetch_by_id(self.id) end rescue Exception => e @details = Quickbooks::Model::Invoice.new diff --git a/db/migrate/032_add_txn_dates.rb b/db/migrate/032_add_txn_dates.rb index 5e1a68d..8379d79 100644 --- a/db/migrate/032_add_txn_dates.rb +++ b/db/migrate/032_add_txn_dates.rb @@ -27,9 +27,9 @@ class AddTxnDates < ActiveRecord::Migration[5.1] say "Sync Invoices" qbo = Qbo.first - qbo.perform_authenticated_request do |access_token| + invoices = qbo.perform_authenticated_request do |access_token| service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token) - invoices = service.all + service.all end return unless invoices From 2985fad77c661f4cee1378c39ca55613218cf6a6 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 23:01:01 -0500 Subject: [PATCH 032/106] Fixed typo --- app/models/customer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/customer.rb b/app/models/customer.rb index 4ce883c..a4a7639 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -227,7 +227,7 @@ class Customer < ActiveRecord::Base 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) - bservice.ase.fetch_by_id(self.id) + service.fetch_by_id(self.id) end rescue Exception => e @details = Quickbooks::Model::Customer.new From 0c72ca92948c3b9452423510f620550d23778fa3 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 23:01:32 -0500 Subject: [PATCH 033/106] missed this authenticated_request --- app/models/estimate.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/estimate.rb b/app/models/estimate.rb index 00410d7..d92c406 100644 --- a/app/models/estimate.rb +++ b/app/models/estimate.rb @@ -49,9 +49,9 @@ class Estimate < ActiveRecord::Base def self.update(id) # Update the item table qbo = Qbo.first - qbo.perform_authenticated_request do |access_token| + estimate = 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.fetch_by_id(id) end return unless estimate From e2bf42e66b0492ad67d0dfb98b897f07c1b04e93 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 23:04:43 -0500 Subject: [PATCH 034/106] Fixed invoice pdf --- app/controllers/invoice_controller.rb | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/controllers/invoice_controller.rb b/app/controllers/invoice_controller.rb index d5a93b8..0f4017a 100644 --- a/app/controllers/invoice_controller.rb +++ b/app/controllers/invoice_controller.rb @@ -21,15 +21,12 @@ class InvoiceController < ApplicationController def show begin qbo = Qbo.first - @pdf = qbo.perform_authenticated_request do |access_token| + 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]) - service.pdf(invoice) + @pdf = service.pdf(invoice) + send_data @pdf, filename: "invoice #{invoice.doc_number}.pdf", :disposition => 'inline', :type => "application/pdf" end - - return unless @pdf - - send_data @pdf, filename: "invoice #{invoice.doc_number}.pdf", :disposition => 'inline', :type => "application/pdf" rescue redirect_to :back, :flash => { :error => "Invoice not found" } end From 04391f1c6e34d1eac7cd6514ca2a67ad8ff94851 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sat, 30 Dec 2023 23:07:17 -0500 Subject: [PATCH 035/106] 2.0.2 --- init.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init.rb b/init.rb index 5430d9b..dddcbb0 100644 --- a/init.rb +++ b/init.rb @@ -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.1' + version '2.0.2' url 'https://github.com/rickbarrette/redmine_qbo' author_url 'https://barrettefabrication.com' settings :default => {'empty' => true}, :partial => 'qbo/settings' From aceb6cb6b5a355a755ca2f746a7e9fd69efd4d51 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sun, 31 Dec 2023 16:26:02 -0500 Subject: [PATCH 036/106] fixed typo --- app/models/customer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/customer.rb b/app/models/customer.rb index a4a7639..b5f5eb6 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -205,7 +205,7 @@ class Customer < ActiveRecord::Base 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) - serivce.update(@details) + service.update(@details) end #raise "QBO Fault" if @details.fault? self.id = @details.id From 047296329e3301aade3ced15629c28bb9e0512ff Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sun, 31 Dec 2023 16:42:47 -0500 Subject: [PATCH 037/106] 2.0.32.0.3 --- init.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init.rb b/init.rb index dddcbb0..8749b7e 100644 --- a/init.rb +++ b/init.rb @@ -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.2' + version '2.0.3' url 'https://github.com/rickbarrette/redmine_qbo' author_url 'https://barrettefabrication.com' settings :default => {'empty' => true}, :partial => 'qbo/settings' From 817a43e8498d1393e8a4393d74d9110b48ea5243 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sun, 7 Jan 2024 20:47:26 -0500 Subject: [PATCH 038/106] Fixed update --- app/controllers/vehicles_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/vehicles_controller.rb b/app/controllers/vehicles_controller.rb index e353c8e..f2bb620 100644 --- a/app/controllers/vehicles_controller.rb +++ b/app/controllers/vehicles_controller.rb @@ -84,7 +84,7 @@ class VehiclesController < ApplicationController @customer = params[:customer] begin @vehicle = Vehicle.find_by_id(params[:id]) - if @vehicle.update_attributes(allowed_params) + if @vehicle.update(allowed_params) flash[:notice] = "Vehicle updated" redirect_to @vehicle else From 0b60a8e41b6013f6850891025bfb7b8250ef9b4a Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sun, 7 Jan 2024 20:53:07 -0500 Subject: [PATCH 039/106] 2.0.4 --- init.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init.rb b/init.rb index 8749b7e..6742e37 100644 --- a/init.rb +++ b/init.rb @@ -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.3' + version '2.0.4' url 'https://github.com/rickbarrette/redmine_qbo' author_url 'https://barrettefabrication.com' settings :default => {'empty' => true}, :partial => 'qbo/settings' From 938999db91cdbdfc3703fb352284ace8dc2e750e Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Thu, 28 Mar 2024 12:54:36 -0400 Subject: [PATCH 040/106] Added quickbooks to customer's name --- app/views/customers/show.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/customers/show.html.erb b/app/views/customers/show.html.erb index af5d5b7..f192c19 100644 --- a/app/views/customers/show.html.erb +++ b/app/views/customers/show.html.erb @@ -1,4 +1,4 @@ -

<%=t(:field_customer)%> #<%= @customer.id %> - <%= @customer.name %>

+

<%=t(:field_customer)%> #<%= @customer.id %> - <%= link_to @customer.to_s, "https://app.qbo.intuit.com/app/customerdetail?nameId=#{@customer.id}", target: :_blank %>

From 7ba4829066661017533a27ef2c4fa403d473ee32 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Thu, 28 Mar 2024 13:51:29 -0400 Subject: [PATCH 041/106] Update Customer Phone Numbers On Sync --- app/models/customer.rb | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/app/models/customer.rb b/app/models/customer.rb index b5f5eb6..0b63386 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -155,11 +155,13 @@ class Customer < ActiveRecord::Base 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 + #if not customer.name.eql? c.display_name customer.name = c.display_name customer.id = c.id + customer.phone_number = c.primary_phone.tr('^0-9', '') + customer.mobile_phone_number = c.mobile_phone.tr('^0-9', '') customer.save_without_push - end + #end else if not c.new_record? customer.delete @@ -178,20 +180,22 @@ class Customer < ActiveRecord::Base # This needs to be simplified def self.sync_by_id(id) qbo = Qbo.first - customer = qbo.perform_authenticated_request do |access_token| + c = 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 + return unless c - 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 + 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.phone_number = c.primary_phone.tr('^0-9', '') + customer.mobile_phone_number = c.mobile_phone.tr('^0-9', '') customer.save_without_push - end + #end else if not customer.new_record? customer.delete From e2f43d398fa5f83a18ef862ad29fc336b7a77593 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Thu, 28 Mar 2024 14:01:18 -0400 Subject: [PATCH 042/106] Nil Checks --- app/models/customer.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/customer.rb b/app/models/customer.rb index 0b63386..2046aba 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -158,8 +158,8 @@ class Customer < ActiveRecord::Base #if not customer.name.eql? c.display_name customer.name = c.display_name customer.id = c.id - customer.phone_number = c.primary_phone.tr('^0-9', '') - customer.mobile_phone_number = c.mobile_phone.tr('^0-9', '') + customer.phone_number = c.primary_phone.tr('^0-9', '') unless c.primary_phone.nil? + customer.mobile_phone_number = c.mobile_phone.tr('^0-9', '') unless c.mobile_phone.nil? customer.save_without_push #end else @@ -192,8 +192,8 @@ class Customer < ActiveRecord::Base #if not customer.name.eql? c.display_name customer.name = c.display_name customer.id = c.id - customer.phone_number = c.primary_phone.tr('^0-9', '') - customer.mobile_phone_number = c.mobile_phone.tr('^0-9', '') + customer.phone_number = c.primary_phone.tr('^0-9', '') unless c.primary_phone.nil? + customer.mobile_phone_number = c.mobile_phone.tr('^0-9', '') unless c.mobile_phone.nil? customer.save_without_push #end else From 8c638179505de721f6b31b7443e1bd91c3ee78aa Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Thu, 28 Mar 2024 14:13:39 -0400 Subject: [PATCH 043/106] Use free_form_number --- app/models/customer.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/customer.rb b/app/models/customer.rb index 2046aba..cb9221a 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -158,8 +158,8 @@ class Customer < ActiveRecord::Base #if not customer.name.eql? c.display_name customer.name = c.display_name customer.id = c.id - customer.phone_number = c.primary_phone.tr('^0-9', '') unless c.primary_phone.nil? - customer.mobile_phone_number = c.mobile_phone.tr('^0-9', '') unless c.mobile_phone.nil? + customer.phone_number = c.primary_phone.free_form_number.tr('^0-9', '') unless c.primary_phone.nil? + customer.mobile_phone_number = c.mobile_phone.free_form_number.tr('^0-9', '') unless c.mobile_phone.nil? customer.save_without_push #end else @@ -192,8 +192,8 @@ class Customer < ActiveRecord::Base #if not customer.name.eql? c.display_name customer.name = c.display_name customer.id = c.id - customer.phone_number = c.primary_phone.tr('^0-9', '') unless c.primary_phone.nil? - customer.mobile_phone_number = c.mobile_phone.tr('^0-9', '') unless c.mobile_phone.nil? + customer.phone_number = c.primary_phone.free_form_number.tr('^0-9', '') unless c.primary_phone.nil? + customer.mobile_phone_number = c.mobile_phone.free_form_number.tr('^0-9', '') unless c.mobile_phone.nil? customer.save_without_push #end else From 040c9204819c718cb354f37950cdc3107143caa0 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Fri, 29 Mar 2024 07:58:26 -0400 Subject: [PATCH 044/106] 2.0.5 --- init.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init.rb b/init.rb index 6742e37..53e4a08 100644 --- a/init.rb +++ b/init.rb @@ -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.4' + version '2.0.5' url 'https://github.com/rickbarrette/redmine_qbo' author_url 'https://barrettefabrication.com' settings :default => {'empty' => true}, :partial => 'qbo/settings' From c14b59008360c88727ec4d7d6a6e9d21cec2cd6e Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Fri, 29 Mar 2024 08:10:05 -0400 Subject: [PATCH 045/106] 2024 Copy Right Update --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index ce662ac..9dd8a74 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 - 2023 Rick Barrette +Copyright (c) 2016 - 2024 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 From c7a313e9ed12e368572ca072130d75b146a70e10 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Wed, 3 Apr 2024 11:47:38 -0400 Subject: [PATCH 046/106] Add customer name to details --- app/views/customers/_details.html.erb | 6 ++++++ config/locales/en.yml | 1 + 2 files changed, 7 insertions(+) diff --git a/app/views/customers/_details.html.erb b/app/views/customers/_details.html.erb index a0239a4..abee3aa 100644 --- a/app/views/customers/_details.html.erb +++ b/app/views/customers/_details.html.erb @@ -1,5 +1,11 @@ + + + + + + diff --git a/config/locales/en.yml b/config/locales/en.yml index 8001ac6..d998aff 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -88,4 +88,5 @@ en: label_qbo_sync_success: "Successfully synced to Quickbooks" label_hours: "Hours" label_oauth2_refresh_token_expires_at: "Refresh Token Expires At" + label_name: "Name" \ No newline at end of file From 472bdec4fa67c4fed927c9d851f332bb5b2738f2 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 19:17:45 -0400 Subject: [PATCH 047/106] Use qbo_authenticate_path --- app/views/qbo/_settings.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/qbo/_settings.html.erb b/app/views/qbo/_settings.html.erb index 753143a..bc2b9d3 100644 --- a/app/views/qbo/_settings.html.erb +++ b/app/views/qbo/_settings.html.erb @@ -15,7 +15,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
<%=t(:label_name)%><%= customer.name %>
<%=t(:label_email)%> <%= customer.email %>
From 95592e542f9c5be07920874c643c17fb7a7fbf6b Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 19:30:51 -0400 Subject: [PATCH 048/106] Use qbo_oauth_callback_path --- app/controllers/qbo_controller.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index b3446b7..442fb43 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -27,8 +27,7 @@ class QboController < ApplicationController # def authenticate 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") + grant_url = oauth2_client.auth_code.authorize_url(redirect_uri: qbo_oauth_callback_path, response_type: "code", state: SecureRandom.hex(12), scope: "com.intuit.quickbooks.accounting") redirect_to grant_url end From 11dbcaf80c85ee6b336de3fda01bcfb4713024c1 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 19:53:51 -0400 Subject: [PATCH 049/106] Use Setting.host_name & path --- app/controllers/qbo_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index 442fb43..c3c10ed 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -27,7 +27,7 @@ class QboController < ApplicationController # def authenticate oauth2_client = Qbo.construct_oauth2_client - grant_url = oauth2_client.auth_code.authorize_url(redirect_uri: qbo_oauth_callback_path, response_type: "code", state: SecureRandom.hex(12), scope: "com.intuit.quickbooks.accounting") + grant_url = oauth2_client.auth_code.authorize_url(redirect_uri: Setting.host_name + qbo_oauth_callback_path, response_type: "code", state: SecureRandom.hex(12), scope: "com.intuit.quickbooks.accounting") redirect_to grant_url end @@ -38,7 +38,7 @@ class QboController < ApplicationController if params[:state].present? 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/" + redirect_uri = Setting.host_name + qbo_oauth_callback_path if resp = oauth2_client.auth_code.get_token(params[:code], redirect_uri: redirect_uri) # Remove the last authentication information From 6c6de0ba860638270dd22feaca40b2f1bdea3c75 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 19:59:26 -0400 Subject: [PATCH 050/106] Added log --- app/controllers/qbo_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index c3c10ed..49a3270 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -26,6 +26,7 @@ class QboController < ApplicationController # Called when the user requests that Redmine to connect to QBO # def authenticate + logger.info "redirect_uri: " + Setting.host_name + qbo_oauth_callback_path oauth2_client = Qbo.construct_oauth2_client grant_url = oauth2_client.auth_code.authorize_url(redirect_uri: Setting.host_name + qbo_oauth_callback_path, response_type: "code", state: SecureRandom.hex(12), scope: "com.intuit.quickbooks.accounting") redirect_to grant_url From 0729d2ac416018982339bb980e247e7c551bdedc Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 20:02:22 -0400 Subject: [PATCH 051/106] added https to redirect_uri --- app/controllers/qbo_controller.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index 49a3270..a2ecb1d 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -26,9 +26,10 @@ class QboController < ApplicationController # Called when the user requests that Redmine to connect to QBO # def authenticate - logger.info "redirect_uri: " + Setting.host_name + qbo_oauth_callback_path + redirect_uri = "https://" + Setting.host_name + qbo_oauth_callback_path + logger.info "redirect_uri: " + redirect_uri oauth2_client = Qbo.construct_oauth2_client - grant_url = oauth2_client.auth_code.authorize_url(redirect_uri: Setting.host_name + qbo_oauth_callback_path, response_type: "code", state: SecureRandom.hex(12), scope: "com.intuit.quickbooks.accounting") + grant_url = oauth2_client.auth_code.authorize_url(redirect_uri: redirect_uri, response_type: "code", state: SecureRandom.hex(12), scope: "com.intuit.quickbooks.accounting") redirect_to grant_url end From 5782cbc166826298a938864e9addcd1eed8b63d7 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 20:04:09 -0400 Subject: [PATCH 052/106] Added https --- app/controllers/qbo_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index a2ecb1d..e34812f 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -40,7 +40,7 @@ class QboController < ApplicationController if params[:state].present? 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_path + redirect_uri = "https://" + Setting.host_name + qbo_oauth_callback_path if resp = oauth2_client.auth_code.get_token(params[:code], redirect_uri: redirect_uri) # Remove the last authentication information From 6f8d28065744f228e0943f95c8f1b0d90585393d Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 20:06:13 -0400 Subject: [PATCH 053/106] 5.2.0 FIXED QBO Authentication --- init.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init.rb b/init.rb index 53e4a08..d74d388 100644 --- a/init.rb +++ b/init.rb @@ -26,7 +26,7 @@ Redmine::Plugin.register :redmine_qbo do url 'https://github.com/rickbarrette/redmine_qbo' author_url 'https://barrettefabrication.com' settings :default => {'empty' => true}, :partial => 'qbo/settings' - requires_redmine :version_or_higher => '5.1.0' + requires_redmine :version_or_higher => '5.2.0' # Add safe attributes for core models Issue.safe_attributes 'customer_id' From 80fc858a3554b01bfd086c02b1ccf4eadf4408f0 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 20:14:02 -0400 Subject: [PATCH 054/106] send back status 200 if request succeeded --- app/controllers/qbo_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index e34812f..93e95d8 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -124,7 +124,7 @@ class QboController < ApplicationController Qbo.update_time_stamp # The webhook doesn't require a response but let's make sure we don't send anything - render :nothing => true + render :nothing => true, status: 200 else render nothing: true, status: 400 end From 4f789080e748ca7d29212c716c592d679c659e5e Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 20:18:22 -0400 Subject: [PATCH 055/106] 2.1.0 Bumped wrong versoin --- init.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/init.rb b/init.rb index d74d388..7afb1d0 100644 --- a/init.rb +++ b/init.rb @@ -22,11 +22,11 @@ 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.5' + version '2.1.0' url 'https://github.com/rickbarrette/redmine_qbo' author_url 'https://barrettefabrication.com' settings :default => {'empty' => true}, :partial => 'qbo/settings' - requires_redmine :version_or_higher => '5.2.0' + requires_redmine :version_or_higher => '5.1.0' # Add safe attributes for core models Issue.safe_attributes 'customer_id' From dd9ac3c481da28a993d06ac4a6e401496cb7abc5 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 22:30:34 -0400 Subject: [PATCH 056/106] Added Estimate.sync_by_doc_number --- app/controllers/estimate_controller.rb | 2 ++ app/models/estimate.rb | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/controllers/estimate_controller.rb b/app/controllers/estimate_controller.rb index fd37dd0..392ad3b 100644 --- a/app/controllers/estimate_controller.rb +++ b/app/controllers/estimate_controller.rb @@ -16,6 +16,8 @@ class EstimateController < ApplicationController skip_before_action :verify_authenticity_token, :check_if_login_required, :unless => proc {|c| session[:token].nil? } def get_estimate + # Force sync for estimate by doc number + Estimate.sync_by_doc_number if params[:search] estimate = Estimate.find_by_id(params[:id]) if params[:id] estimate = Estimate.find_by_doc_number(params[:search]) if params[:search] return estimate diff --git a/app/models/estimate.rb b/app/models/estimate.rb index d92c406..487da7b 100644 --- a/app/models/estimate.rb +++ b/app/models/estimate.rb @@ -18,7 +18,7 @@ class Estimate < ActiveRecord::Base # sync all estimates def self.sync - logger.debug "Syncing ALL estimates" + logger.info "Syncing ALL estimates" 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) @@ -37,13 +37,23 @@ class Estimate < ActiveRecord::Base # sync only one estimate def self.sync_by_id(id) - logger.debug "Syncing estimate #{id}" + logger.info "Syncing estimate #{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 + + # sync only one estimate + def self.sync_by_doc_number(number) + logger.info "Syncing estimate by doc number #{number}" + 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.find_by( :doc_number, number)) + end + end # update an estimate def self.update(id) From 039d1ca9938553c14847690d592504ef0cf43c7c Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 22:31:41 -0400 Subject: [PATCH 057/106] Use Logger.info --- app/controllers/customers_controller.rb | 2 +- app/controllers/qbo_controller.rb | 2 +- app/models/invoice.rb | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/controllers/customers_controller.rb b/app/controllers/customers_controller.rb index dae292c..9d08e4d 100644 --- a/app/controllers/customers_controller.rb +++ b/app/controllers/customers_controller.rb @@ -142,7 +142,7 @@ class CustomersController < ApplicationController def share Thread.new do - logger.debug "Removing expired customer tokens" + logger.info "Removing expired customer tokens" CustomerToken.remove_expired_tokens ActiveRecord::Base.connection.close end diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index 93e95d8..325dbf8 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -97,7 +97,7 @@ class QboController < ApplicationController id = entity['id'].to_i name = entity['name'] - logger.debug "Casting #{name.constantize} to obj" + logger.info "Casting #{name.constantize} to obj" # Magicly initialize the correct class obj = name.constantize diff --git a/app/models/invoice.rb b/app/models/invoice.rb index f66d1e8..f51ef0b 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -17,7 +17,7 @@ class Invoice < ActiveRecord::Base # sync ALL the invoices def self.sync - logger.debug "Syncing all invoices" + logger.info "Syncing all invoices" last = Qbo.first.last_sync query = "SELECT Id, DocNumber FROM Invoice" @@ -40,7 +40,7 @@ class Invoice < ActiveRecord::Base #sync by invoice ID def self.sync_by_id(id) - logger.debug "Syncing invoice #{id}" + logger.info "Syncing invoice #{id}" qbo = Qbo.first qbo.perform_authenticated_request do |access_token| service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token) @@ -58,7 +58,7 @@ class Invoice < ActiveRecord::Base # skip this issue if the issue customer is not the same as the invoice customer return if issue.customer_id != invoice.customer_ref.value.to_i - logger.debug "Attaching invoice #{invoice.id} to issue #{issue.id}" + logger.info "Attaching invoice #{invoice.id} to issue #{issue.id}" invoice = Invoice.find_or_create_by(id: invoice.id) @@ -105,7 +105,7 @@ class Invoice < ActiveRecord::Base # this condions causes an infinite loop as the webhook is called when an invoice is updated # TODO maybe add a cf_sync_confict flag to invoices def self.compare_custom_fields(issue, invoice) - logger.debug "Comparing custom fields" + logger.info "Comparing custom fields" # TODO break if Invoice.find(invoice.id).cf_sync_confict is_changed = false @@ -120,12 +120,12 @@ class Invoice < ActiveRecord::Base # Only update if blank to prevent infite loops # TODO check cf_sync_confict flag once implemented if cf.string_value.to_s.blank? - logger.debug " VIN was blank, updating the invoice vin in quickbooks" + logger.info " VIN was blank, updating the invoice vin in quickbooks" vin = Vehicle.find(issue.vehicles_id).vin break if vin.nil? if not cf.string_value.to_s.eql? vin cf.string_value = vin.to_s - logger.debug "VIN has changed" + logger.info "VIN has changed" is_changed = true end @@ -155,7 +155,7 @@ class Invoice < ActiveRecord::Base # Push updates begin - logger.debug "Trying to update invoice" + logger.info "Trying to update 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) From e99f5d2e5274190e841da9f48de313c1bf92b7bc Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 22:36:44 -0400 Subject: [PATCH 058/106] Added webhook view --- app/views/qbo/webhook.erb | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 app/views/qbo/webhook.erb diff --git a/app/views/qbo/webhook.erb b/app/views/qbo/webhook.erb new file mode 100644 index 0000000..4f09880 --- /dev/null +++ b/app/views/qbo/webhook.erb @@ -0,0 +1,13 @@ + + +

QboController#webhook

From 9e7c1dbfb22438ec47b9a9a45575a53a3d05bb90 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 22:38:16 -0400 Subject: [PATCH 059/106] removed () --- app/models/estimate.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/estimate.rb b/app/models/estimate.rb index 487da7b..3758909 100644 --- a/app/models/estimate.rb +++ b/app/models/estimate.rb @@ -51,7 +51,7 @@ class Estimate < ActiveRecord::Base 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.find_by( :doc_number, number)) + process_estimate(service.find_by :doc_number, number) end end From 9115cc662c5fc7a9ab7bbf0d638ce09c49d9cef1 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 22:39:50 -0400 Subject: [PATCH 060/106] Forgot params[:search] --- app/controllers/estimate_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/estimate_controller.rb b/app/controllers/estimate_controller.rb index 392ad3b..bda75e8 100644 --- a/app/controllers/estimate_controller.rb +++ b/app/controllers/estimate_controller.rb @@ -17,7 +17,7 @@ class EstimateController < ApplicationController def get_estimate # Force sync for estimate by doc number - Estimate.sync_by_doc_number if params[:search] + Estimate.sync_by_doc_number(params[:search]) if params[:search] estimate = Estimate.find_by_id(params[:id]) if params[:id] estimate = Estimate.find_by_doc_number(params[:search]) if params[:search] return estimate From 7eb26facaf857dcc23918f408254674ed81e02b2 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 22:49:20 -0400 Subject: [PATCH 061/106] Use the first result --- app/controllers/estimate_controller.rb | 6 +++++- app/models/estimate.rb | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/controllers/estimate_controller.rb b/app/controllers/estimate_controller.rb index bda75e8..06d4c2b 100644 --- a/app/controllers/estimate_controller.rb +++ b/app/controllers/estimate_controller.rb @@ -17,7 +17,11 @@ class EstimateController < ApplicationController def get_estimate # Force sync for estimate by doc number - Estimate.sync_by_doc_number(params[:search]) if params[:search] + begin + Estimate.sync_by_doc_number(params[:search]) if params[:search] + rescue + logger.info "Estimate.find_by_doc_number failed" + end estimate = Estimate.find_by_id(params[:id]) if params[:id] estimate = Estimate.find_by_doc_number(params[:search]) if params[:search] return estimate diff --git a/app/models/estimate.rb b/app/models/estimate.rb index 3758909..43ebc0a 100644 --- a/app/models/estimate.rb +++ b/app/models/estimate.rb @@ -51,7 +51,7 @@ class Estimate < ActiveRecord::Base 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.find_by :doc_number, number) + process_estimate(service.find_by( :doc_number, number).first) end end From 6e08746611f24e433bd5f9b46385f838355146af Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 22:51:53 -0400 Subject: [PATCH 062/106] 2.1.1 Force Estimate sync by Doc Number when searching --- init.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init.rb b/init.rb index 7afb1d0..4928efb 100644 --- a/init.rb +++ b/init.rb @@ -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.1.0' + version '2.1.1' url 'https://github.com/rickbarrette/redmine_qbo' author_url 'https://barrettefabrication.com' settings :default => {'empty' => true}, :partial => 'qbo/settings' From cf0be2336bf8dae5c4e9bfe7ed19791e77c11de5 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 23:12:20 -0400 Subject: [PATCH 063/106] Removed sync button from sidebar --- app/views/customers/_search.html.erb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/views/customers/_search.html.erb b/app/views/customers/_search.html.erb index 88c3e59..954cdff 100644 --- a/app/views/customers/_search.html.erb +++ b/app/views/customers/_search.html.erb @@ -3,4 +3,3 @@ <%= submit_tag t(:label_search) %> <% end %> <%= 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?%> From ad7417c233af73f7ad37fb2e58a131613b10a15d Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 23:21:56 -0400 Subject: [PATCH 064/106] Moved work into thread to repsond quickly --- app/controllers/qbo_controller.rb | 71 +++++++++++++++++-------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index 325dbf8..c29de84 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -85,43 +85,48 @@ class QboController < ApplicationController # proceed if the request is good if hash.eql? signature - if request.headers['content-type'] == 'application/json' - data = JSON.parse(data) - else - # application/x-www-form-urlencoded - data = params.as_json - end - # Process the information - entities = data['eventNotifications'][0]['dataChangeEvent']['entities'] - entities.each do |entity| - id = entity['id'].to_i - name = entity['name'] - - logger.info "Casting #{name.constantize} to obj" - - # Magicly initialize the correct class - obj = name.constantize - - # for merge events - obj.destroy(entity['deletedId']) if entity['deletedId'] - - #Check to see if we are deleting a record - if entity['operation'].eql? "Delete" - obj.destroy(id) - #if not then update! + Thread.new do + if request.headers['content-type'] == 'application/json' + data = JSON.parse(data) else - begin - obj.sync_by_id(id) - rescue => e - logger.error "Failed to call sync_by_id on obj" - logger.error e.message - logger.error e.backtrace.join("\n") + # application/x-www-form-urlencoded + data = params.as_json + end + # Process the information + entities = data['eventNotifications'][0]['dataChangeEvent']['entities'] + entities.each do |entity| + id = entity['id'].to_i + name = entity['name'] + + logger.info "Casting #{name.constantize} to obj" + + # Magicly initialize the correct class + obj = name.constantize + + # for merge events + obj.destroy(entity['deletedId']) if entity['deletedId'] + + #Check to see if we are deleting a record + if entity['operation'].eql? "Delete" + obj.destroy(id) + #if not then update! + else + begin + obj.sync_by_id(id) + rescue => e + logger.error "Failed to call sync_by_id on obj" + logger.error e.message + logger.error e.backtrace.join("\n") + end end end + + # Record that last time we updated + Qbo.update_time_stamp + end - - # Record that last time we updated - Qbo.update_time_stamp + ActiveRecord::Base.connection.close + end # The webhook doesn't require a response but let's make sure we don't send anything render :nothing => true, status: 200 From 7f0bb3cae75b52fe307f735d651116867aaa4b92 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 23:26:43 -0400 Subject: [PATCH 065/106] Removed extra end --- app/controllers/qbo_controller.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index c29de84..b8ae3d6 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -108,7 +108,7 @@ class QboController < ApplicationController #Check to see if we are deleting a record if entity['operation'].eql? "Delete" - obj.destroy(id) + obj.destroy(id) #if not then update! else begin @@ -121,10 +121,8 @@ class QboController < ApplicationController end end - # Record that last time we updated - Qbo.update_time_stamp - - end + # Record that last time we updated + Qbo.update_time_stamp ActiveRecord::Base.connection.close end From 63218e7f42d7be2a1df35a4f60cda68181097b06 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 19 Aug 2024 23:28:54 -0400 Subject: [PATCH 066/106] Fixed formating --- app/controllers/qbo_controller.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index b8ae3d6..c43193b 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -121,11 +121,10 @@ class QboController < ApplicationController end end - # Record that last time we updated - Qbo.update_time_stamp - ActiveRecord::Base.connection.close - end - + # Record that last time we updated + Qbo.update_time_stamp + ActiveRecord::Base.connection.close + end # The webhook doesn't require a response but let's make sure we don't send anything render :nothing => true, status: 200 else From 4fb424faa8c0b89f23663b9dcdc97856cbf7ff13 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Tue, 20 Aug 2024 07:14:37 -0400 Subject: [PATCH 067/106] Only sync by doc number if not in database --- app/controllers/estimate_controller.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/controllers/estimate_controller.rb b/app/controllers/estimate_controller.rb index 06d4c2b..f7ffbfa 100644 --- a/app/controllers/estimate_controller.rb +++ b/app/controllers/estimate_controller.rb @@ -16,12 +16,15 @@ class EstimateController < ApplicationController skip_before_action :verify_authenticity_token, :check_if_login_required, :unless => proc {|c| session[:token].nil? } def get_estimate - # Force sync for estimate by doc number - begin - Estimate.sync_by_doc_number(params[:search]) if params[:search] - rescue - logger.info "Estimate.find_by_doc_number failed" + # Force sync for estimate by doc number if not found + if Estimate.find_by_doc_number(params[:search]).nil? + begin + Estimate.sync_by_doc_number(params[:search]) if params[:search] + rescue + logger.info "Estimate.find_by_doc_number failed" + end end + estimate = Estimate.find_by_id(params[:id]) if params[:id] estimate = Estimate.find_by_doc_number(params[:search]) if params[:search] return estimate From 74f7ba41dfefa8d20b65c274c5725aab2feb1ba6 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Wed, 21 Aug 2024 21:39:50 -0400 Subject: [PATCH 068/106] Add Appointment Link --- app/views/customers/_details.html.erb | 2 ++ config/locales/en.yml | 1 + 2 files changed, 3 insertions(+) diff --git a/app/views/customers/_details.html.erb b/app/views/customers/_details.html.erb index abee3aa..13963ba 100644 --- a/app/views/customers/_details.html.erb +++ b/app/views/customers/_details.html.erb @@ -44,6 +44,8 @@
<%= button_to t(:label_edit_customer), edit_customer_path(customer), method: :get%> + + <%= link_to t(:label_appointment), "https://calendar.google.com/calendar/render?action=TEMPLATE&text=#{customer.name}+-&details=#{customer.primary_phone}&dates=#{Time.now.strftime("%Y%m%d")}T090000/#{Time.now.strftime("%Y%m%d")}T170000", target: :_blank %>


diff --git a/config/locales/en.yml b/config/locales/en.yml index d998aff..8b872ed 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -89,4 +89,5 @@ en: label_hours: "Hours" label_oauth2_refresh_token_expires_at: "Refresh Token Expires At" label_name: "Name" + label_appointment: "Add Appointment" \ No newline at end of file From d1f6ccd9cbfbf81bf515a1092ea9c7bfc9cb7fb3 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Mon, 26 Aug 2024 07:51:38 -0400 Subject: [PATCH 069/106] Create _actions.html.erb --- app/views/customers/_actions.html.erb | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/views/customers/_actions.html.erb diff --git a/app/views/customers/_actions.html.erb b/app/views/customers/_actions.html.erb new file mode 100644 index 0000000..d9074d3 --- /dev/null +++ b/app/views/customers/_actions.html.erb @@ -0,0 +1,5 @@ +<%= link_to t(:label_appointment), "https://calendar.google.com/calendar/render?action=TEMPLATE&text=#{@customer.name}+-&details=#{@customer.primary_phone}&dates=#{Time.now.strftime("%Y%m%d")}T090000/#{Time.now.strftime("%Y%m%d")}T170000", target: :_blank %> + +<%= link_to t(:label_appointment), "https://qbo.intuit.com/app/estimate?nameId=#{@customer.id}", target: :_blank %> + +<%= button_to t(:label_edit_customer), edit_customer_path(@customer), method: :get%> From 3b6c0d4a70a03fab0a42f9c352d27bd9e44d7319 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Mon, 26 Aug 2024 07:52:27 -0400 Subject: [PATCH 070/106] Removed Action links --- app/views/customers/_details.html.erb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/views/customers/_details.html.erb b/app/views/customers/_details.html.erb index 13963ba..a15761b 100644 --- a/app/views/customers/_details.html.erb +++ b/app/views/customers/_details.html.erb @@ -41,11 +41,6 @@ <%= customer.notes %> - -
- <%= button_to t(:label_edit_customer), edit_customer_path(customer), method: :get%> - - <%= link_to t(:label_appointment), "https://calendar.google.com/calendar/render?action=TEMPLATE&text=#{customer.name}+-&details=#{customer.primary_phone}&dates=#{Time.now.strftime("%Y%m%d")}T090000/#{Time.now.strftime("%Y%m%d")}T170000", target: :_blank %> -
+

From e04d363e4225594eb2df045407d3dd0b370ce979 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Mon, 26 Aug 2024 07:56:06 -0400 Subject: [PATCH 071/106] Added label for actions --- config/locales/en.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 8b872ed..674d96f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -90,4 +90,5 @@ en: label_oauth2_refresh_token_expires_at: "Refresh Token Expires At" label_name: "Name" label_appointment: "Add Appointment" - \ No newline at end of file + label_actions: "Actions" + From 395e0117fbcd00ad3ce5cdd843a7a594fd08ec08 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Mon, 26 Aug 2024 07:57:10 -0400 Subject: [PATCH 072/106] Update _actions.html.erb --- app/views/customers/_actions.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/customers/_actions.html.erb b/app/views/customers/_actions.html.erb index d9074d3..a240659 100644 --- a/app/views/customers/_actions.html.erb +++ b/app/views/customers/_actions.html.erb @@ -1,5 +1,5 @@ <%= link_to t(:label_appointment), "https://calendar.google.com/calendar/render?action=TEMPLATE&text=#{@customer.name}+-&details=#{@customer.primary_phone}&dates=#{Time.now.strftime("%Y%m%d")}T090000/#{Time.now.strftime("%Y%m%d")}T170000", target: :_blank %> -<%= link_to t(:label_appointment), "https://qbo.intuit.com/app/estimate?nameId=#{@customer.id}", target: :_blank %> +<%= link_to t(:label_create_estimate), "https://qbo.intuit.com/app/estimate?nameId=#{@customer.id}", target: :_blank %> <%= button_to t(:label_edit_customer), edit_customer_path(@customer), method: :get%> From 7f3a94229a77a37142ef9d59e198b484bdcb2f27 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Mon, 26 Aug 2024 07:58:05 -0400 Subject: [PATCH 073/106] Create Estimate --- config/locales/en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 674d96f..506e18d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -91,4 +91,4 @@ en: label_name: "Name" label_appointment: "Add Appointment" label_actions: "Actions" - + label_create_estimate: "Create Estimate" From 6464e1cbc63e53de103712181525f8da512d6283 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Mon, 26 Aug 2024 07:58:40 -0400 Subject: [PATCH 074/106] Added actions --- app/views/customers/show.html.erb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/views/customers/show.html.erb b/app/views/customers/show.html.erb index f192c19..d541c90 100644 --- a/app/views/customers/show.html.erb +++ b/app/views/customers/show.html.erb @@ -5,9 +5,17 @@
-

<%=t(:label_details)%>:

+
+
+

<%=t(:label_details)%>:

+ <%= render :partial => 'customers/details', locals: {customer: @customer} %> +
- <%= render :partial => 'customers/details', locals: {customer: @customer} %> +
+

<%=t(:label_actions)%>:

+ <%= render :partial => 'customers/actions', locals: {customer: @customer} %> +
+
From a5de879260e17851c52f5d60d7f019021dbf65f4 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Mon, 26 Aug 2024 08:41:56 -0400 Subject: [PATCH 075/106] Fixed formatting --- app/views/customers/_actions.html.erb | 6 ++++++ app/views/customers/show.html.erb | 11 +++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/views/customers/_actions.html.erb b/app/views/customers/_actions.html.erb index a240659..af481c8 100644 --- a/app/views/customers/_actions.html.erb +++ b/app/views/customers/_actions.html.erb @@ -1,5 +1,11 @@ <%= link_to t(:label_appointment), "https://calendar.google.com/calendar/render?action=TEMPLATE&text=#{@customer.name}+-&details=#{@customer.primary_phone}&dates=#{Time.now.strftime("%Y%m%d")}T090000/#{Time.now.strftime("%Y%m%d")}T170000", target: :_blank %> +
+
+ <%= link_to t(:label_create_estimate), "https://qbo.intuit.com/app/estimate?nameId=#{@customer.id}", target: :_blank %> +
+
+ <%= button_to t(:label_edit_customer), edit_customer_path(@customer), method: :get%> diff --git a/app/views/customers/show.html.erb b/app/views/customers/show.html.erb index d541c90..97bf7fa 100644 --- a/app/views/customers/show.html.erb +++ b/app/views/customers/show.html.erb @@ -5,9 +5,13 @@
+

<%=t(:label_details)%>:

+ + +
-

<%=t(:label_details)%>:

+

<%=t(:label_customer)%>:

<%= render :partial => 'customers/details', locals: {customer: @customer} %>
@@ -15,7 +19,10 @@

<%=t(:label_actions)%>:

<%= render :partial => 'customers/actions', locals: {customer: @customer} %>
-
+
+ + +
From 9b69d3f728fa142fbb9ca2ef324d9edd15df2523 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Mon, 26 Aug 2024 11:00:50 -0400 Subject: [PATCH 076/106] Added link to customer profile for appointments --- app/views/customers/_actions.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/customers/_actions.html.erb b/app/views/customers/_actions.html.erb index af481c8..63a4643 100644 --- a/app/views/customers/_actions.html.erb +++ b/app/views/customers/_actions.html.erb @@ -1,4 +1,4 @@ -<%= link_to t(:label_appointment), "https://calendar.google.com/calendar/render?action=TEMPLATE&text=#{@customer.name}+-&details=#{@customer.primary_phone}&dates=#{Time.now.strftime("%Y%m%d")}T090000/#{Time.now.strftime("%Y%m%d")}T170000", target: :_blank %> +<%= link_to t(:label_appointment), "https://calendar.google.com/calendar/render?action=TEMPLATE&text=#{@customer.name}+-&details=#{ link_to "Customer Details", "https://#{Setting.host_name}#{customer_path @customer.id}"}%0A#{@customer.primary_phone}&dates=#{Time.now.strftime("%Y%m%d")}T090000/#{Time.now.strftime("%Y%m%d")}T170000", target: :_blank %>

From 9ac1261ed0b5e4d20c77086f704aa569b1b79cd0 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Fri, 30 Aug 2024 09:19:58 -0400 Subject: [PATCH 077/106] Sort by id not doc_number This fixes the bug where documents were displayed out of order --- app/views/estimates/_list.html.erb | 2 +- app/views/invoices/_list.html.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/estimates/_list.html.erb b/app/views/estimates/_list.html.erb index 334fd84..d078a69 100644 --- a/app/views/estimates/_list.html.erb +++ b/app/views/estimates/_list.html.erb @@ -1,6 +1,6 @@ <% if @customer.present? %> - <% @customer.estimates.order(doc_number: :desc).each do |estimate| %> + <% @customer.estimates.order(id: :desc).each do |estimate| %>
<%= link_to "##{estimate.doc_number}", estimate_path(estimate), target: :_blank %> <%= estimate.txn_date %>
diff --git a/app/views/invoices/_list.html.erb b/app/views/invoices/_list.html.erb index e6510c6..212eb8d 100644 --- a/app/views/invoices/_list.html.erb +++ b/app/views/invoices/_list.html.erb @@ -1,6 +1,6 @@ <% if @customer.present? %> - <% @customer.invoices.order(doc_number: :desc).each do |invoice| %> + <% @customer.invoices.order(id: :desc).each do |invoice| %>
<%= link_to "##{invoice.doc_number}", invoice_path(invoice), target: :_blank %> <%= invoice.txn_date %>
From dac9a7c7564284ddff0f696ed46416c1400385b0 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Thu, 12 Dec 2024 06:00:36 -0500 Subject: [PATCH 078/106] Started Support for Redmine 6 --- app/controllers/customers_controller.rb | 3 +- app/controllers/estimate_controller.rb | 3 +- app/controllers/invoice_controller.rb | 3 +- app/controllers/qbo_controller.rb | 3 +- app/controllers/vehicles_controller.rb | 3 +- app/models/customer.rb | 3 +- app/models/customer_token.rb | 4 +- app/models/employee.rb | 4 +- app/models/estimate.rb | 3 +- app/models/invoice.rb | 4 +- app/models/qbo.rb | 3 +- app/models/vehicle.rb | 4 +- db/migrate/032_add_txn_dates.rb | 57 ++++++++++++++----------- db/migrate/033_update_vehicles_trim.rb | 25 ++++++----- lib/attachments_controller_patch.rb | 3 +- lib/issue_patch.rb | 3 +- lib/issues_controller_patch.rb | 3 +- lib/pdf_patch.rb | 4 +- lib/project_patch.rb | 3 +- lib/user_patch.rb | 3 +- 20 files changed, 66 insertions(+), 75 deletions(-) diff --git a/app/controllers/customers_controller.rb b/app/controllers/customers_controller.rb index 9d08e4d..66c0253 100644 --- a/app/controllers/customers_controller.rb +++ b/app/controllers/customers_controller.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2022 rick barrette +#Copyright (c) 2024 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,7 +10,6 @@ # This controller class will handle map management class CustomersController < ApplicationController - unloadable include AuthHelper helper :issues diff --git a/app/controllers/estimate_controller.rb b/app/controllers/estimate_controller.rb index f7ffbfa..437d5b7 100644 --- a/app/controllers/estimate_controller.rb +++ b/app/controllers/estimate_controller.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2022 rick barrette +#Copyright (c) 2024 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: # @@ -8,7 +8,6 @@ # #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 EstimateController < ApplicationController - unloadable include AuthHelper diff --git a/app/controllers/invoice_controller.rb b/app/controllers/invoice_controller.rb index 0f4017a..c193fcd 100644 --- a/app/controllers/invoice_controller.rb +++ b/app/controllers/invoice_controller.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2023 rick barrette +#Copyright (c) 2024 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: # @@ -8,7 +8,6 @@ # #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 InvoiceController < ApplicationController - unloadable include AuthHelper diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index c43193b..7ef9703 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2023 rick barrette +#Copyright (c) 2024 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: # @@ -9,7 +9,6 @@ #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 QboController < ApplicationController - unloadable require 'openssl' diff --git a/app/controllers/vehicles_controller.rb b/app/controllers/vehicles_controller.rb index f2bb620..0830acd 100644 --- a/app/controllers/vehicles_controller.rb +++ b/app/controllers/vehicles_controller.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2022 rick barrette +#Copyright (c) 2024 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,7 +10,6 @@ # This controller class will handle map management class VehiclesController < ApplicationController - unloadable include AuthHelper diff --git a/app/models/customer.rb b/app/models/customer.rb index cb9221a..557e823 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2023 rick barrette +#Copyright (c) 2024 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: # @@ -9,7 +9,6 @@ #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 Customer < ActiveRecord::Base - unloadable has_many :issues has_many :purchases diff --git a/app/models/customer_token.rb b/app/models/customer_token.rb index b3af4c4..90f7475 100644 --- a/app/models/customer_token.rb +++ b/app/models/customer_token.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2022 rick barrette +#Copyright (c) 2024 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: # @@ -9,7 +9,7 @@ #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 CustomerToken < ActiveRecord::Base - unloadable + has_many :issues validates_presence_of :issue_id before_create :generate_token, :generate_expire_date diff --git a/app/models/employee.rb b/app/models/employee.rb index acf6702..ab35227 100644 --- a/app/models/employee.rb +++ b/app/models/employee.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2023 rick barrette +#Copyright (c) 2024 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: # @@ -9,7 +9,7 @@ #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 Employee < ActiveRecord::Base - unloadable + has_many :users validates_presence_of :id, :name diff --git a/app/models/estimate.rb b/app/models/estimate.rb index 43ebc0a..80c5c70 100644 --- a/app/models/estimate.rb +++ b/app/models/estimate.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2023 rick barrette +#Copyright (c) 2024 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: # @@ -9,7 +9,6 @@ #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 Estimate < ActiveRecord::Base - unloadable has_and_belongs_to_many :issues belongs_to :customer diff --git a/app/models/invoice.rb b/app/models/invoice.rb index f51ef0b..d4aef06 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2023 rick barrette +#Copyright (c) 2024 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: # @@ -9,7 +9,7 @@ #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 Invoice < ActiveRecord::Base - unloadable + has_and_belongs_to_many :issues belongs_to :customer validates_presence_of :doc_number, :id, :customer_id, :txn_date diff --git a/app/models/qbo.rb b/app/models/qbo.rb index 06199b7..3392d30 100644 --- a/app/models/qbo.rb +++ b/app/models/qbo.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2023 rick barrette +#Copyright (c) 2024 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: # @@ -9,7 +9,6 @@ #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 Qbo < ActiveRecord::Base - unloadable include QuickbooksOauth diff --git a/app/models/vehicle.rb b/app/models/vehicle.rb index bc99259..4a61117 100644 --- a/app/models/vehicle.rb +++ b/app/models/vehicle.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2022 rick barrette +#Copyright (c) 2024 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,8 +10,6 @@ class Vehicle < ActiveRecord::Base - unloadable - belongs_to :customer has_many :issues, :foreign_key => 'vehicles_id' diff --git a/db/migrate/032_add_txn_dates.rb b/db/migrate/032_add_txn_dates.rb index 8379d79..a95a27f 100644 --- a/db/migrate/032_add_txn_dates.rb +++ b/db/migrate/032_add_txn_dates.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2023 rick barrette +#Copyright (c) 2024 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: # @@ -9,41 +9,46 @@ #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 AddTxnDates < ActiveRecord::Migration[5.1] + def change - add_column :qbo_invoices, :txn_date, :date - add_column :qbo_estimates, :txn_date, :date + begin + add_column :qbo_invoices, :txn_date, :date + add_column :qbo_estimates, :txn_date, :date - reversible do |direction| - direction.up { - break unless Qbo.first + reversible do |direction| + direction.up { + break unless Qbo.first - QboEstimate.reset_column_information - QboInvoice.reset_column_information + QboEstimate.reset_column_information + QboInvoice.reset_column_information - say "Sync Estimates" + say "Sync Estimates" - QboEstimate.sync + QboEstimate.sync - say "Sync Invoices" + say "Sync Invoices" - 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 + 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 + return unless invoices - invoices.each { |invoice| - # Load the invoice into the database - qbo_invoice = QboInvoice.find_or_create_by(id: invoice.id) - qbo_invoice.doc_number = invoice.doc_number - qbo_invoice.id = invoice.id - qbo_invoice.customer_id = invoice.customer_ref - qbo_invoice.txn_date = invoice.txn_date - qbo_invoice.save! + invoices.each { |invoice| + # Load the invoice into the database + qbo_invoice = QboInvoice.find_or_create_by(id: invoice.id) + qbo_invoice.doc_number = invoice.doc_number + qbo_invoice.id = invoice.id + qbo_invoice.customer_id = invoice.customer_ref + qbo_invoice.txn_date = invoice.txn_date + qbo_invoice.save! + } } - } + end + rescue + logger.error "AddTxnDates Failed" end end diff --git a/db/migrate/033_update_vehicles_trim.rb b/db/migrate/033_update_vehicles_trim.rb index c94f05b..dc5ace2 100644 --- a/db/migrate/033_update_vehicles_trim.rb +++ b/db/migrate/033_update_vehicles_trim.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2022 rick barrette +#Copyright (c) 2024 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,19 +10,22 @@ class UpdateVehiclesTrim < ActiveRecord::Migration[5.1] def change - add_column :vehicles, :doors, :text - add_column :vehicles, :trim, :text + begin + add_column :vehicles, :doors, :text + add_column :vehicles, :trim, :text - reversible do |direction| - direction.up { - - # Update local vehicle database by forcing a save, look at before_save - vehicles = Vehicle.all - vehicles.each { |vehicle| - vehicle.save! - } + reversible do |direction| + direction.up { + # Update local vehicle database by forcing a save, look at before_save + vehicles = Vehicle.all + vehicles.each { |vehicle| + vehicle.save! + } } + end + rescue + logger.error "Failed to update vehicles" end end diff --git a/lib/attachments_controller_patch.rb b/lib/attachments_controller_patch.rb index 37e2eaf..69d2a7f 100644 --- a/lib/attachments_controller_patch.rb +++ b/lib/attachments_controller_patch.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2022 rick barrette +#Copyright (c) 2024 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,7 +15,6 @@ module AttachmentsControllerPatch def self.included(base) base.class_eval do - unloadable # Send unloadable so it will not be unloaded in development # check if login is globally required to access the application def check_if_login_required diff --git a/lib/issue_patch.rb b/lib/issue_patch.rb index a804bff..d5293b8 100644 --- a/lib/issue_patch.rb +++ b/lib/issue_patch.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2023 rick barrette +#Copyright (c) 2024 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: # @@ -21,7 +21,6 @@ module IssuePatch # Same as typing in the class base.class_eval do - unloadable # Send unloadable so it will not be unloaded in development belongs_to :customer, primary_key: :id belongs_to :customer_token, primary_key: :id belongs_to :estimate, primary_key: :id diff --git a/lib/issues_controller_patch.rb b/lib/issues_controller_patch.rb index 98019f0..85b3d70 100644 --- a/lib/issues_controller_patch.rb +++ b/lib/issues_controller_patch.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2022 rick barrette +#Copyright (c) 2024 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: # @@ -23,7 +23,6 @@ module IssuesControllerPatch def self.included(base) base.class_eval do - unloadable # Send unloadable so it will not be unloaded in development helper Helper end diff --git a/lib/pdf_patch.rb b/lib/pdf_patch.rb index 803f045..274bf57 100644 --- a/lib/pdf_patch.rb +++ b/lib/pdf_patch.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2023 rick barrette +#Copyright (c) 2024 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,10 +16,8 @@ module PdfPatch def self.included(base) base.send(:include, InstanceMethods) base.class_eval do - unloadable # Send unloadable so it will not be unloaded in development alias_method :issue_to_pdf, :issue_to_pdf_with_patch alias_method :issue_to_pdf_with_patch, :issue_to_pdf - end end diff --git a/lib/project_patch.rb b/lib/project_patch.rb index 907031c..925bde6 100644 --- a/lib/project_patch.rb +++ b/lib/project_patch.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2017 rick barrette +#Copyright (c) 2024 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: # @@ -21,7 +21,6 @@ module ProjectPatch # Same as typing in the class base.class_eval do - unloadable # Send unloadable so it will not be unloaded in development belongs_to :customer, primary_key: :id belongs_to :vehicle, primary_key: :id end diff --git a/lib/user_patch.rb b/lib/user_patch.rb index 3a6e494..5b78a74 100644 --- a/lib/user_patch.rb +++ b/lib/user_patch.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2022 rick barrette +#Copyright (c) 2024 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,7 +20,6 @@ module UserPatch # Same as typing in the class base.class_eval do - unloadable # Send unloadable so it will not be unloaded in development belongs_to :employee, primary_key: :id end end From 1a37926628a751d2317fc4f093a98d5f345908da Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Thu, 19 Dec 2024 09:36:23 -0500 Subject: [PATCH 079/106] Log error not info --- app/models/concerns/quickbooks_oauth.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/concerns/quickbooks_oauth.rb b/app/models/concerns/quickbooks_oauth.rb index 9e03a82..6691cce 100644 --- a/app/models/concerns/quickbooks_oauth.rb +++ b/app/models/concerns/quickbooks_oauth.rb @@ -21,7 +21,7 @@ module QuickbooksOauth begin yield oauth_access_token rescue OAuth2::Error, Quickbooks::AuthorizationFailure => ex - Rails.logger.info("QuickbooksOauth.perform: #{ex.message}") + Rails.logger.error("QuickbooksOauth.perform: #{ex.message}") # to prevent an infinite loop here keep a counter and bail out after N times... attempts += 1 From 9779437c007e7cb8c6dc639df2b75c881649d51f Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Thu, 19 Dec 2024 09:41:53 -0500 Subject: [PATCH 080/106] Log token refresh --- app/models/concerns/quickbooks_oauth.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/models/concerns/quickbooks_oauth.rb b/app/models/concerns/quickbooks_oauth.rb index 6691cce..3892780 100644 --- a/app/models/concerns/quickbooks_oauth.rb +++ b/app/models/concerns/quickbooks_oauth.rb @@ -36,6 +36,7 @@ module QuickbooksOauth end def refresh_token! + Rails.logger.info("QuickbooksOauth.refresh_token!") t = oauth_access_token refreshed = t.refresh! @@ -45,6 +46,8 @@ module QuickbooksOauth oauth2_refresh_token_expires_at = 100.days.from_now end + Rails.logger.info("QuickbooksOauth.refresh_token!: #{oauth2_refresh_token_expires_at}") + update!( oauth2_access_token: refreshed.token, oauth2_access_token_expires_at: Time.at(refreshed.expires_at), From f6da031e72c71699636c2467c88bedeaa3327239 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Sun, 15 Jun 2025 18:59:00 -0400 Subject: [PATCH 081/106] include Redmine::I18n --- app/models/qbo.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/qbo.rb b/app/models/qbo.rb index 3392d30..f5450c7 100644 --- a/app/models/qbo.rb +++ b/app/models/qbo.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2024 rick barrette +#Copyright (c) 2016 - 2025 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: # @@ -11,6 +11,7 @@ class Qbo < ActiveRecord::Base include QuickbooksOauth + include Redmine::I18n # Updates last sync time stamp def self.update_time_stamp From 60857e9dca9e7f8afd6e922e8e2e51f6a519cd40 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 16 Jun 2025 22:15:42 -0400 Subject: [PATCH 082/106] generate redirect_uri protocol based one site settings --- app/controllers/qbo_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index 7ef9703..bf3f3ac 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -25,8 +25,8 @@ class QboController < ApplicationController # Called when the user requests that Redmine to connect to QBO # def authenticate - redirect_uri = "https://" + Setting.host_name + qbo_oauth_callback_path - logger.info "redirect_uri: " + redirect_uri + redirect_uri = "#{Setting.protocol}://#{Setting.host_name + qbo_oauth_callback_path}" + logger.info "redirect_uri: #{redirect_uri}" oauth2_client = Qbo.construct_oauth2_client grant_url = oauth2_client.auth_code.authorize_url(redirect_uri: redirect_uri, response_type: "code", state: SecureRandom.hex(12), scope: "com.intuit.quickbooks.accounting") redirect_to grant_url From 43c7374c42ff8d76b70b3040abd1392d39e36044 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 16 Jun 2025 22:16:58 -0400 Subject: [PATCH 083/106] Load oauth key & secret when constructing client, not on application start up --- app/models/concerns/quickbooks_oauth.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/models/concerns/quickbooks_oauth.rb b/app/models/concerns/quickbooks_oauth.rb index 3892780..d99344d 100644 --- a/app/models/concerns/quickbooks_oauth.rb +++ b/app/models/concerns/quickbooks_oauth.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2023 rick barrette +#Copyright (c) 2016 - 2025 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: # @@ -11,9 +11,6 @@ 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) @@ -71,12 +68,16 @@ module QuickbooksOauth module ClassMethods def construct_oauth2_client + + oauth_consumer_key = Setting.plugin_redmine_qbo['settingsOAuthConsumerKey'] + oauth_consumer_secret = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret'] + 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) + OAuth2::Client.new(oauth_consumer_key, oauth_consumer_secret, options) end end From 4d85c2487204304b4f7e6d3e50409f4e9ecf425b Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 16 Jun 2025 22:30:30 -0400 Subject: [PATCH 084/106] generate redirect_uri protocol based one site settings --- app/controllers/qbo_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index bf3f3ac..c62c937 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2024 rick barrette +#Copyright (c) 2016 - 2025 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: # @@ -39,7 +39,7 @@ class QboController < ApplicationController if params[:state].present? 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 = "https://" + Setting.host_name + qbo_oauth_callback_path + redirect_uri = "#{Setting.protocol}://#{Setting.host_name + qbo_oauth_callback_path}" if resp = oauth2_client.auth_code.get_token(params[:code], redirect_uri: redirect_uri) # Remove the last authentication information From 166a9ee31b98161be955d8deae28b33b0f31402a Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 16 Jun 2025 22:54:47 -0400 Subject: [PATCH 085/106] Removed has_many purchases, table doesn't exist anymore --- app/models/customer.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/models/customer.rb b/app/models/customer.rb index 557e823..d4f8031 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2024 rick barrette +#Copyright (c) 2016 - 2025 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: # @@ -11,7 +11,6 @@ class Customer < ActiveRecord::Base has_many :issues - has_many :purchases has_many :invoices has_many :estimates has_many :vehicles From f22795ac901942a4db7ff2e88d8e3467d58066ba Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 16 Jun 2025 22:56:54 -0400 Subject: [PATCH 086/106] Moved strings for notices to en.yml --- app/controllers/qbo_controller.rb | 10 +++++----- config/locales/en.yml | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index c62c937..c3c86d3 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -51,9 +51,9 @@ class QboController < ApplicationController qbo.refresh_token! if qbo.save! - redirect_to qbo_sync_path, :flash => { :notice => "Successfully connected to Quickbooks" } + redirect_to qbo_sync_path, :flash => { :notice => I18n.t(:label_connected) } else - redirect_to plugin_settings_path(:redmine_qbo), :flash => { :error => "Error" } + redirect_to plugin_settings_path(:redmine_qbo), :flash => { :error => I18n.t(:label_error) } end end @@ -65,9 +65,9 @@ class QboController < ApplicationController i = Issue.find_by_id params[:id] if i.customer i.bill_time - redirect_to i, :flash => { :notice => "Successfully Billed #{i.customer.name}" } + redirect_to i, :flash => { :notice => I18n.t(:label_billed_success) + i.customer.name } else - redirect_to i, :flash => { :error => "Cannot bill without a customer assigned" } + redirect_to i, :flash => { :error => I18n.t(:label_billing_error) } end end @@ -152,6 +152,6 @@ class QboController < ApplicationController ActiveRecord::Base.connection.close end - redirect_to :home, :flash => { :notice => "Syncing Quickbooks" } + redirect_to :home, :flash => { :notice => I18n.t(:label_syncing) } end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 506e18d..81a598e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -92,3 +92,4 @@ en: label_appointment: "Add Appointment" label_actions: "Actions" label_create_estimate: "Create Estimate" + label_syncing: "Syncing Quickbooks" From 19733c3f8c6fe32b554bf459c361255c2e400e39 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Mon, 16 Jun 2025 23:41:30 -0400 Subject: [PATCH 087/106] Added gem rexml --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index baf0247..caa0961 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,7 @@ gem 'nhtsa_vin' gem 'will_paginate' gem 'rails-jquery-autocomplete' gem 'jquery-ui-rails' +gem 'rexml' group :assets do gem 'coffee-rails' From eba3f529f84db5ece23e739593e0ea3a0fb576a8 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Tue, 8 Jul 2025 20:19:19 -0400 Subject: [PATCH 088/106] Set version & requirements --- init.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/init.rb b/init.rb index 4928efb..8523b8e 100644 --- a/init.rb +++ b/init.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2023 rick barrette +#Copyright (c) 2016-2025 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: # @@ -22,11 +22,11 @@ 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.1.1' + version '2025.7.1' url 'https://github.com/rickbarrette/redmine_qbo' author_url 'https://barrettefabrication.com' settings :default => {'empty' => true}, :partial => 'qbo/settings' - requires_redmine :version_or_higher => '5.1.0' + requires_redmine :version_or_higher => '6.0.0' # Add safe attributes for core models Issue.safe_attributes 'customer_id' From 55d00f90059956a799193cf36c94528c5d588549 Mon Sep 17 00:00:00 2001 From: Ricky Barrette Date: Tue, 8 Jul 2025 21:01:48 -0400 Subject: [PATCH 089/106] Added sandbox to settings --- app/models/concerns/quickbooks_oauth.rb | 4 ++++ app/views/qbo/_settings.html.erb | 7 +++++++ config/locales/en.yml | 1 + init.rb | 3 --- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/models/concerns/quickbooks_oauth.rb b/app/models/concerns/quickbooks_oauth.rb index d99344d..90d593c 100644 --- a/app/models/concerns/quickbooks_oauth.rb +++ b/app/models/concerns/quickbooks_oauth.rb @@ -72,6 +72,10 @@ module QuickbooksOauth oauth_consumer_key = Setting.plugin_redmine_qbo['settingsOAuthConsumerKey'] oauth_consumer_secret = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret'] + # Are we are playing in the sandbox? + Quickbooks.sandbox_mode = Setting.plugin_redmine_qbo['sandbox'] ? true : false + logger.info "Sandbox mode: #{Quickbooks.sandbox_mode}" + options = { site: "https://appcenter.intuit.com/connect/oauth2", authorize_url: "https://appcenter.intuit.com/connect/oauth2", diff --git a/app/views/qbo/_settings.html.erb b/app/views/qbo/_settings.html.erb index bc2b9d3..d75609a 100644 --- a/app/views/qbo/_settings.html.erb +++ b/app/views/qbo/_settings.html.erb @@ -57,6 +57,13 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI + + <%=t(:label_sandbox)%> + + <%= check_box_tag 'settings[sandbox]', @settings['sandbox'], @settings['sandbox'] %> + + + <%=t(:label_oauth_expires)%> <%= if Qbo.exists? then Qbo.first.oauth2_access_token_expires_at end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 81a598e..0db3fc6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -93,3 +93,4 @@ en: label_actions: "Actions" label_create_estimate: "Create Estimate" label_syncing: "Syncing Quickbooks" + label_sandbox: "Sandbox" diff --git a/init.rb b/init.rb index 8523b8e..fa577ca 100644 --- a/init.rb +++ b/init.rb @@ -39,9 +39,6 @@ Redmine::Plugin.register :redmine_qbo do Project.safe_attributes 'customer_id' Project.safe_attributes 'vehicle_id' - # We are playing in the sandbox - #Quickbooks.sandbox_mode = true - # set per_page globally WillPaginate.per_page = 20 From 704dff2a72c1493d7ae7e57d6d0c6df18b985ac7 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Mon, 19 Jan 2026 19:33:29 -0500 Subject: [PATCH 090/106] Multiple Invoices to PDF --- Gemfile | 1 + app/controllers/invoice_controller.rb | 31 ++++++++++++++++++++++----- app/views/invoices/_list.html.erb | 17 ++++++++++++--- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index caa0961..ebd7f88 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gem 'will_paginate' gem 'rails-jquery-autocomplete' gem 'jquery-ui-rails' gem 'rexml' +gem 'combine_pdf' group :assets do gem 'coffee-rails' diff --git a/app/controllers/invoice_controller.rb b/app/controllers/invoice_controller.rb index c193fcd..52edbc2 100644 --- a/app/controllers/invoice_controller.rb +++ b/app/controllers/invoice_controller.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2024 rick barrette +#Copyright (c) 2026 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,7 +10,8 @@ class InvoiceController < ApplicationController include AuthHelper - + require 'combine_pdf' + before_action :require_user, :unless => proc {|c| session[:token].nil? } skip_before_action :verify_authenticity_token, :check_if_login_required, :unless => proc {|c| session[:token].nil? } @@ -18,13 +19,33 @@ class InvoiceController < ApplicationController # Downloads and forwards the invoice pdf # def show + logger.info("Processing request for URL: #{request.original_url}") begin 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" + + # If multiple id's then pull each pdf & combine them + if params[:item_ids] + logger.info("Grabbing pdfs for " + params[:item_ids].join(', ')) + ref = "" + params[:item_ids].each do |i| + logger.info("processing " + i) + invoice = service.fetch_by_id(i) + ref += " #{invoice.doc_number}" + @pdf << CombinePDF.parse(service.pdf(invoice)) unless @pdf.nil? + if @pdf.nil? + @pdf = CombinePDF.parse(service.pdf(invoice)) + end + end + @pdf = @pdf.to_pdf + else + invoice = service.fetch_by_id(params[:id]) + @pdf = service.pdf(invoice) + ref = invoice.doc_number + end + + send_data @pdf, filename: "invoice #{ref}.pdf", :disposition => 'inline', :type => "application/pdf" end rescue redirect_to :back, :flash => { :error => "Invoice not found" } diff --git a/app/views/invoices/_list.html.erb b/app/views/invoices/_list.html.erb index 212eb8d..a38b3db 100644 --- a/app/views/invoices/_list.html.erb +++ b/app/views/invoices/_list.html.erb @@ -1,9 +1,20 @@ <% if @customer.present? %> - <% @customer.invoices.order(id: :desc).each do |invoice| %> -
- <%= link_to "##{invoice.doc_number}", invoice_path(invoice), target: :_blank %> <%= invoice.txn_date %> + <%= form_with(url: invoice_path, method: :get) do |form| %> + +
+ <%= check_box_tag "select_all", "1", false, id: "select-all-batches" %> + <%= label_tag "select-all-batches", "Select All Items" %>
+ + <% @customer.invoices.order(id: :desc).each do |invoice| %> +
+ <%= check_box_tag "item_ids[]", invoice.id, false,data: { checkbox_select_all_target: "checkbox", class: "item-checkbox" } %> + <%= link_to "##{invoice.doc_number}", invoice_path(invoice), target: :_blank %> <%= invoice.txn_date %> +
+ <% end %> + + <%= form.submit "Bulk PDF" %> <% end %> <% else %> From bcdd515cf1cb48bcd9002c8cbedfd797423f4d2c Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Mon, 19 Jan 2026 19:34:10 -0500 Subject: [PATCH 091/106] Remove view_layouts_base_body_bottom --- lib/header_footer_hook_listener.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/header_footer_hook_listener.rb b/lib/header_footer_hook_listener.rb index 5dc165f..de70328 100644 --- a/lib/header_footer_hook_listener.rb +++ b/lib/header_footer_hook_listener.rb @@ -8,11 +8,6 @@ # #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 HeaderFooterHookListener < Redmine::Hook::ViewListener - - def view_layouts_base_html_head(context = {}) - #nothing - end - def view_layouts_base_body_bottom(context = {}) return "" end From 5649ba05cda2c60f662640fdcdbdc7c9b24ccee5 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Mon, 19 Jan 2026 19:34:59 -0500 Subject: [PATCH 092/106] Added Checkbox Controller javascript --- assets/javascripts/checkbox_controller.js | 23 +++++++++++++++++++++++ lib/issues_form_hook_listener.rb | 6 ++++-- 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 assets/javascripts/checkbox_controller.js diff --git a/assets/javascripts/checkbox_controller.js b/assets/javascripts/checkbox_controller.js new file mode 100644 index 0000000..9bccee3 --- /dev/null +++ b/assets/javascripts/checkbox_controller.js @@ -0,0 +1,23 @@ +document.addEventListener("turbo:load", () => { + const selectAllBox = document.getElementById("select-all-batches"); + const checkboxes = document.querySelectorAll(".item-checkbox"); + + if (selectAllBox) { + selectAllBox.addEventListener("change", function() { + checkboxes.forEach((checkbox) => { + checkbox.checked = this.checked; + }); + }); + + // Optional: Uncheck "Select All" if an individual box is unchecked + checkboxes.forEach((checkbox) => { + checkbox.addEventListener("change", () => { + if (!checkbox.checked) { + selectAllBox.checked = false; + } else if (Array.from(checkboxes).every(c => c.checked)) { + selectAllBox.checked = true; + } + }); + }); + } +}); \ No newline at end of file diff --git a/lib/issues_form_hook_listener.rb b/lib/issues_form_hook_listener.rb index e3d279a..07e4d85 100644 --- a/lib/issues_form_hook_listener.rb +++ b/lib/issues_form_hook_listener.rb @@ -12,8 +12,10 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener # Load the javascript to support the autocomplete forms def view_layouts_base_html_head(context = {}) - js = javascript_include_tag 'application', :plugin => 'redmine_qbo' - js += javascript_include_tag 'autocomplete-rails', :plugin => 'redmine_qbo' + logger.info("IssuesFormHookListener.view_layouts_base_html_head") + js = javascript_include_tag 'application.js', :plugin => 'redmine_qbo' + js += javascript_include_tag 'autocomplete-rails.js', :plugin => 'redmine_qbo' + js += javascript_include_tag 'checkbox_controller.js', :plugin => 'redmine_qbo' return js end From b712c328ba83d167b6ed0166cd231f62f8257f9c Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Tue, 20 Jan 2026 08:11:53 -0500 Subject: [PATCH 093/106] Remove logging --- lib/issues_form_hook_listener.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/issues_form_hook_listener.rb b/lib/issues_form_hook_listener.rb index 07e4d85..195abe3 100644 --- a/lib/issues_form_hook_listener.rb +++ b/lib/issues_form_hook_listener.rb @@ -12,7 +12,6 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener # Load the javascript to support the autocomplete forms def view_layouts_base_html_head(context = {}) - logger.info("IssuesFormHookListener.view_layouts_base_html_head") js = javascript_include_tag 'application.js', :plugin => 'redmine_qbo' js += javascript_include_tag 'autocomplete-rails.js', :plugin => 'redmine_qbo' js += javascript_include_tag 'checkbox_controller.js', :plugin => 'redmine_qbo' From 7d644f06199d78723fbd94496ec49c0f9b9fd7f8 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Tue, 20 Jan 2026 08:12:34 -0500 Subject: [PATCH 094/106] Dynamically load all Hooks & Patches --- init.rb | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/init.rb b/init.rb index fa577ca..cb9f834 100644 --- a/init.rb +++ b/init.rb @@ -8,14 +8,6 @@ # #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. -# Dynamically load all Hooks & Patches -ActiveSupport::Reloader.to_prepare do - Dir::foreach(File.join(File.dirname(__FILE__), 'lib')) do |file| - next unless /\.rb$/ =~ file - require_dependency file - end -end - Redmine::Plugin.register :redmine_qbo do # About @@ -26,7 +18,7 @@ Redmine::Plugin.register :redmine_qbo do url 'https://github.com/rickbarrette/redmine_qbo' author_url 'https://barrettefabrication.com' settings :default => {'empty' => true}, :partial => 'qbo/settings' - requires_redmine :version_or_higher => '6.0.0' + requires_redmine :version_or_higher => '6.1.0' # Add safe attributes for core models Issue.safe_attributes 'customer_id' @@ -52,3 +44,9 @@ Redmine::Plugin.register :redmine_qbo do menu :top_menu, :vehicles, { :controller => :vehicles, :action => :index }, :caption => 'Vehicles', :if => Proc.new { User.current.logged? } end + +# Dynamically load all Hooks & Patches +Dir::foreach(File.join(File.dirname(__FILE__), 'lib')) do |file| + next unless file.end_with?('.rb') + require_relative "lib/#{file.delete_suffix(".rb") }" +end From 0647b7708fd65361b965cbd134f8b536608c28c7 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Tue, 20 Jan 2026 13:49:38 -0500 Subject: [PATCH 095/106] Udated README & Copyright Date --- LICENSE | 2 +- README.md | 151 ++++++++++++++++++++++++++++++++---------------------- init.rb | 4 +- 3 files changed, 94 insertions(+), 63 deletions(-) diff --git a/LICENSE b/LICENSE index 9dd8a74..526207b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 - 2024 Rick Barrette +Copyright (c) 2016 - 2026 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 diff --git a/README.md b/README.md index 51ae0a2..8a373d9 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,112 @@ -# Redmine Quickbooks Online +# Redmine QuickBooks Online -A plugin for Redmine to connect to Quickbooks Online +A plugin for Redmine to connect to QuickBooks Online. -The goal of this project is to allow Redmine to connect with Quickbooks Online to create Time Activity Entries for billable hours loged when an Issue is closed. +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. -#### Disclaimer +## Disclaimer -Note: Although the core functionality is complete, this project is still under development & the master branch may be unstable. Tags should be stable and are recommended +**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. -Use tags for the following Redmine Versions -* Version 2.0.0+ for Redmine 5+ -* Version 1.0.0+ for Redmine 4+ -* Version 0.8.1 for Redine 3 +## Compatibility -#### Features -* Issues can be assigned to a Customer via drop down in the edit Issue form - - Once a customer is attached to an Issue, you can attach an Estimate to the issue via a drop down menu -* Employee is assigned to a user via a drop down in the user admistration page. -* IF an Issue has been assined a Customer when an Issue is closed the following will happen: - - A new Time Activity will be billed agaist the Customer assinged to the issue for each Redmine Time Entery. - + Time Entries will be totalled up by Activity name. This will allow billing for diffrent activities without having to create seperate Issues. - + The Time Activity names are used to dynamically lookup Items in Quickbooks. - + IF there isn't any Items that match the Activity name it will be skipped, and will not be billed to the Customer - - Labor Rates are set by corresponding the Item in Quickbooks -* 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/edit via the Customer page -* Webhook Support - - Invoices are automaticly attached to an Issue if a line item has a hashtag number in a Line Item - + Invoice Custom Fields are matched Issue Custom Fileds and are automaticly updated in Quickbooks. For example, this is usefull for extracting the Mileage In / Out from the Issue and updating the Invoice with the information. - - Customers are automaticly updated in local database +| Plugin Version | Redmine Version | +| :--- | :--- | +| Version 2026.1.0+ | Redmine 6.1 | +| Version 2.0.0+ | Redmine 5 | +| Version 1.0.0+ | Redmine 4 | +| Version 0.8.1 | Redmine 3 | + +## Features + +* **Customer Assignment:** Issues can be assigned to a Customer via a dropdown in the edit Issue form. + * 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. ## Prerequisites -* Sign up to become a developer for Intuit https://developer.intuit.com/ -* Create your own aplication to obtain your API keys -* Set up webhook service to https://redmine.yourdomain.com/qbo/webhook +* Sign up to become a developer for Intuit: https://developer.intuit.com/ +* Create your own application to obtain your API keys. +* Set up the webhook service to `https://redmine.yourdomain.com/qbo/webhook` -## The Install +## Installation -1. To install, clone this repo into your plugin folder & checkout a tagged version +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 + ``` - `git clone git@github.com:rickbarrette/redmine_qbo.git` - - then - - `git checkout ` - -2. Migrate your database - - `rake redmine:plugins:migrate RAILS_ENV=production` - -3. Navigate to the plugin configuration page and suppy your own OAuth key & secret. - -4. After saving your key & secret, you need to click on the Authenticate link on the plugin configuration page to authenticate with QBO. - -5. Assign an Employee to each of your users via the User Administration Page +2. **Install dependencies:** *Crucial for Redmine 6 / Rails 7 compatibility.* + + Bash + + ``` + 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 - To enable automatic Time Activity entries for an Issue , you need only to assign a Customer to an Issue via drop downs in the issue creation/update form. +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 inital synchronization, this plugin will recieve push notifications via Intuit's webhook service. +**Note:** After the initial synchronization, this plugin will receive push notifications via Intuit's webhook service. ## TODO - * Add Setting for Sandbox Mode - * Seperate Vehicles into a seperate plugin (I use redmine for my automotive shop management 😉) - * MORE Stuff as I make it up... + +* Add Setting for Sandbox Mode + +* Separate Vehicles into a separate plugin (I use Redmine for my automotive shop management 😉) + +* MORE Stuff as I make it up... + ## License -The MIT License (MIT) - -Copyright (c) 2016 - 2022 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. +> The MIT License (MIT) +> +> Copyright (c) 2016 - 2026 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. \ No newline at end of file diff --git a/init.rb b/init.rb index cb9f834..3372729 100644 --- a/init.rb +++ b/init.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2016-2025 rick barrette +#Copyright (c) 2016-2026 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: # @@ -14,7 +14,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 '2025.7.1' + version '2026.1.0' url 'https://github.com/rickbarrette/redmine_qbo' author_url 'https://barrettefabrication.com' settings :default => {'empty' => true}, :partial => 'qbo/settings' From f74f3ad72e33beb79f6f313d4277ca6bb077a3b4 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Tue, 20 Jan 2026 20:49:03 -0500 Subject: [PATCH 096/106] Select All Invoices For Bulk PDF --- app/controllers/invoice_controller.rb | 6 ++--- app/views/invoices/_list.html.erb | 8 +++--- assets/javascripts/checkbox_controller.js | 32 +++++++++-------------- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/app/controllers/invoice_controller.rb b/app/controllers/invoice_controller.rb index 52edbc2..2291abc 100644 --- a/app/controllers/invoice_controller.rb +++ b/app/controllers/invoice_controller.rb @@ -26,10 +26,10 @@ class InvoiceController < ApplicationController service = Quickbooks::Service::Invoice.new(:company_id => qbo.realm_id, :access_token => access_token) # If multiple id's then pull each pdf & combine them - if params[:item_ids] - logger.info("Grabbing pdfs for " + params[:item_ids].join(', ')) + if params[:invoice_ids] + logger.info("Grabbing pdfs for " + params[:invoice_ids].join(', ')) ref = "" - params[:item_ids].each do |i| + params[:invoice_ids].each do |i| logger.info("processing " + i) invoice = service.fetch_by_id(i) ref += " #{invoice.doc_number}" diff --git a/app/views/invoices/_list.html.erb b/app/views/invoices/_list.html.erb index a38b3db..2332277 100644 --- a/app/views/invoices/_list.html.erb +++ b/app/views/invoices/_list.html.erb @@ -3,13 +3,15 @@ <%= form_with(url: invoice_path, method: :get) do |form| %>
- <%= check_box_tag "select_all", "1", false, id: "select-all-batches" %> - <%= label_tag "select-all-batches", "Select All Items" %> + <%= check_box_tag "select-all-invoices", "1", false, id: "select-all-invoices" %> + <%= label_tag "select-all-invoices", "Select All Invioces" %>
+
+ <% @customer.invoices.order(id: :desc).each do |invoice| %>
- <%= check_box_tag "item_ids[]", invoice.id, false,data: { checkbox_select_all_target: "checkbox", class: "item-checkbox" } %> + <%= check_box_tag "invoice_ids[]", invoice.id, false, class: "invoice-checkbox" %> <%= link_to "##{invoice.doc_number}", invoice_path(invoice), target: :_blank %> <%= invoice.txn_date %>
<% end %> diff --git a/assets/javascripts/checkbox_controller.js b/assets/javascripts/checkbox_controller.js index 9bccee3..f7d298c 100644 --- a/assets/javascripts/checkbox_controller.js +++ b/assets/javascripts/checkbox_controller.js @@ -1,23 +1,17 @@ -document.addEventListener("turbo:load", () => { - const selectAllBox = document.getElementById("select-all-batches"); - const checkboxes = document.querySelectorAll(".item-checkbox"); +document.addEventListener('DOMContentLoaded', () => { + const select_all_invoice = document.getElementById('select-all-invoices'); + const invoices = document.querySelectorAll('.invoice-checkbox'); - if (selectAllBox) { - selectAllBox.addEventListener("change", function() { - checkboxes.forEach((checkbox) => { - checkbox.checked = this.checked; - }); - }); - - // Optional: Uncheck "Select All" if an individual box is unchecked - checkboxes.forEach((checkbox) => { - checkbox.addEventListener("change", () => { - if (!checkbox.checked) { - selectAllBox.checked = false; - } else if (Array.from(checkboxes).every(c => c.checked)) { - selectAllBox.checked = true; - } - }); + if (select_all_invoice) { + select_all_invoice.addEventListener('change', (e) => { + invoices.forEach(item => item.checked = e.target.checked); }); } + + invoices.forEach(item => { + item.addEventListener('change', () => { + const allChecked = Array.from(invoices).every(i => i.checked); + select_all_invoice.checked = allChecked; + }); + }); }); \ No newline at end of file From b54eb86b7f83fb21b12f039766f8fc636213ff0b Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Tue, 20 Jan 2026 21:02:40 -0500 Subject: [PATCH 097/106] Moved javascript into ViewLayoutsHookListener --- lib/issues_form_hook_listener.rb | 8 -------- lib/view_layouts_hook_listener.rb | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 lib/view_layouts_hook_listener.rb diff --git a/lib/issues_form_hook_listener.rb b/lib/issues_form_hook_listener.rb index 195abe3..98ee0fe 100644 --- a/lib/issues_form_hook_listener.rb +++ b/lib/issues_form_hook_listener.rb @@ -10,14 +10,6 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener - # Load the javascript to support the autocomplete forms - def view_layouts_base_html_head(context = {}) - js = javascript_include_tag 'application.js', :plugin => 'redmine_qbo' - js += javascript_include_tag 'autocomplete-rails.js', :plugin => 'redmine_qbo' - js += javascript_include_tag 'checkbox_controller.js', :plugin => 'redmine_qbo' - return js - end - # Edit Issue Form # Here we build the required form components before passing them to a partial view formatting. def view_issues_form_details_bottom(context={}) diff --git a/lib/view_layouts_hook_listener.rb b/lib/view_layouts_hook_listener.rb new file mode 100644 index 0000000..29fdbba --- /dev/null +++ b/lib/view_layouts_hook_listener.rb @@ -0,0 +1,21 @@ +#The MIT License (MIT) +# +#Copyright (c) 2026 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 ViewLayoutsHookListener < Redmine::Hook::ViewListener + + # Load the javascript to support the autocomplete forms + def view_layouts_base_html_head(context = {}) + js = javascript_include_tag 'application.js', :plugin => 'redmine_qbo' + js += javascript_include_tag 'autocomplete-rails.js', :plugin => 'redmine_qbo' + js += javascript_include_tag 'checkbox_controller.js', :plugin => 'redmine_qbo' + return js + end + +end \ No newline at end of file From 72ec89292f308e8a15cb4c2cb23ea4d306518d16 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Tue, 20 Jan 2026 21:15:25 -0500 Subject: [PATCH 098/106] Added onclick to load customer link --- app/views/issues/_form_hook.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/issues/_form_hook.html.erb b/app/views/issues/_form_hook.html.erb index c1833dc..03b6094 100644 --- a/app/views/issues/_form_hook.html.erb +++ b/app/views/issues/_form_hook.html.erb @@ -2,7 +2,7 @@ <%= search_customer %> <%= customer_id %> - <%= link_to_function(t(:label_load_customer), "updateIssueFrom('/issues/#{context[:issue].id}/edit.js', this)") %> + <%= link_to t(:label_load_customer), '#', onclick: "updateIssueFrom('/issues/#{context[:issue].id}/edit.js', this); return false;" %>

From f741ce5dc934de386676ed336fbf109b3fb5f40c Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Tue, 20 Jan 2026 21:40:17 -0500 Subject: [PATCH 099/106] Fixed displaying of notes --- app/views/customers/_details.html.erb | 13 ++++++++++++- app/views/customers/_form.html.erb | 3 +-- app/views/issues/_show_details.html.erb | 6 +++++- app/views/vehicles/_details.html.erb | 10 +++++++++- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/app/views/customers/_details.html.erb b/app/views/customers/_details.html.erb index a15761b..41a8738 100644 --- a/app/views/customers/_details.html.erb +++ b/app/views/customers/_details.html.erb @@ -38,8 +38,19 @@ <%=t(:field_notes)%> - <%= customer.notes %> + +

+          <%= customer.notes %>
+        
+ + + +
diff --git a/app/views/customers/_form.html.erb b/app/views/customers/_form.html.erb index 56a69f2..3314742 100644 --- a/app/views/customers/_form.html.erb +++ b/app/views/customers/_form.html.erb @@ -36,8 +36,7 @@ <%=t(:field_notes)%>:

- <%= link_to_function content_tag(:span, l(:button_edit), :class => 'icon icon-edit'), '$(this).hide(); $("#issue_description_and_toolbar").show()' unless @customer.new_record? %> - <%= content_tag 'span', :id => "issue_description_and_toolbar", :style => (@customer.new_record? ? nil : 'display:none') do %> + <%= content_tag 'span', :id => "issue_description_and_toolbar" do %> <%= f.text_area :notes, :cols => 60, :rows => 10, diff --git a/app/views/issues/_show_details.html.erb b/app/views/issues/_show_details.html.erb index be53694..0b053e9 100644 --- a/app/views/issues/_show_details.html.erb +++ b/app/views/issues/_show_details.html.erb @@ -29,7 +29,11 @@

<%=t(:field_notes)%>:
-
<%=notes%>
+
+
+          <%=notes%>
+        
+
diff --git a/app/views/vehicles/_details.html.erb b/app/views/vehicles/_details.html.erb index b8293e1..d5aaa0a 100644 --- a/app/views/vehicles/_details.html.erb +++ b/app/views/vehicles/_details.html.erb @@ -32,9 +32,17 @@

<%=t(:field_notes)%>:

- <%= vehicle.notes %> +
+          <%= vehicle.notes %>
+        
+ +
From 20d9f0a84ebcc3839267e8cc124fe8f895bf4a67 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Tue, 20 Jan 2026 21:41:58 -0500 Subject: [PATCH 100/106] update readme --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 8a373d9..8e6187f 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,6 @@ To enable automatic Time Activity entries for an Issue, you simply need to assig **Note:** After the initial synchronization, this plugin will receive push notifications via Intuit's webhook service. ## TODO - -* Add Setting for Sandbox Mode * Separate Vehicles into a separate plugin (I use Redmine for my automotive shop management 😉) From 623510b474fae7d9797d79b1eff274381dbd975f Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Wed, 21 Jan 2026 12:20:43 -0500 Subject: [PATCH 101/106] Finaly got the issue form javascrip reloading to work --- app/views/issues/_form_hook.html.erb | 2 +- lib/issues_form_hook_listener.rb | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/views/issues/_form_hook.html.erb b/app/views/issues/_form_hook.html.erb index 03b6094..52a1c7b 100644 --- a/app/views/issues/_form_hook.html.erb +++ b/app/views/issues/_form_hook.html.erb @@ -2,7 +2,7 @@ <%= search_customer %> <%= customer_id %> - <%= link_to t(:label_load_customer), '#', onclick: "updateIssueFrom('/issues/#{context[:issue].id}/edit.js', this); return false;" %> + <%= link_to t(:label_load_customer), '#', onclick: "#{js_link}; return false;" %>

diff --git a/lib/issues_form_hook_listener.rb b/lib/issues_form_hook_listener.rb index 98ee0fe..2a5c2e7 100644 --- a/lib/issues_form_hook_listener.rb +++ b/lib/issues_form_hook_listener.rb @@ -14,6 +14,7 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener # Here we build the required form components before passing them to a partial view formatting. def view_issues_form_details_bottom(context={}) f = context[:form] + issue = context[:issue] # check project level customer ownership first # This is done to preload customer information if the entire project is dedicated to a customer @@ -23,9 +24,12 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener end # Check to see if the issue already belongs to a customer - selected_customer = context[:issue].customer ? context[:issue].customer.id : nil - selected_estimate = context[:issue].estimate ? context[:issue].estimate.id : nil - selected_vehicle = context[:issue].vehicles_id ? context[:issue].vehicles_id : nil + selected_customer = issue.customer ? issue.customer.id : nil + selected_estimate = issue.estimate ? issue.estimate.id : nil + selected_vehicle = issue.vehicles_id ? issue.vehicles_id : nil + + # Gernerate edit.js link + js_link = issue.new_record? ? "updateIssueFrom('/projects/rmt/issues/new.js', this)" : "updateIssueFrom('/issues/#{issue.id}/edit.js', this)" # Load customer information customer = Customer.find_by_id(selected_customer) if selected_customer @@ -34,7 +38,7 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener search_customer = f.autocomplete_field :customer, autocomplete_customer_name_customers_path, :selected => selected_customer, - :onchange => "updateIssueFrom('/issues/#{context[:issue].id}/edit.js', this)", + :onchange => js_link, :update_elements => { :id => '#issue_customer_id', :value => '#issue_customer' @@ -43,10 +47,10 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener # Customer ID - Hidden Field customer_id = f.hidden_field :customer_id, :id => "issue_customer_id", - :onchange => "updateIssueFrom('/issues/#{context[:issue].id}/edit.js', this)" + :onchange => js_link # Load estimates & vehicles - if context[:issue].customer + if issue.customer if customer.vehicles vehicles = customer.vehicles.pluck(:name, :id) else @@ -68,7 +72,7 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener locals: { search_customer: search_customer, customer_id: customer_id, - context: context, + js_link: js_link, select_estimate: select_estimate, vehicle: vehicle } From 14f411c2e180412bc84d0813438204e4b4318d06 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Wed, 21 Jan 2026 19:35:08 -0500 Subject: [PATCH 102/106] Moved hooks & patches into sperate folders --- init.rb | 12 +- .../header_footer_hook_listener.rb | 13 +- lib/hooks/issues_form_hook_listener.rb | 86 ++++++ lib/{ => hooks}/issues_save_hook_listener.rb | 32 ++- lib/hooks/issues_show_hook_listener.rb | 67 +++++ lib/hooks/projects_form_hook_listener.rb | 40 +++ lib/{ => hooks}/users_show_hook_listener.rb | 28 +- lib/{ => hooks}/view_hook_listener.rb | 12 +- lib/{ => hooks}/view_layouts_hook_listener.rb | 18 +- lib/issue_patch.rb | 110 -------- lib/issues_form_hook_listener.rb | 82 ------ lib/issues_show_hook_listener.rb | 63 ----- .../attachments_controller_patch.rb | 44 +-- lib/patches/issue_patch.rb | 115 ++++++++ lib/{ => patches}/issues_controller_patch.rb | 38 +-- lib/patches/pdf_patch.rb | 261 ++++++++++++++++++ .../project_patch.rb} | 82 +++--- lib/{ => patches}/query_patch.rb | 40 +-- lib/{ => patches}/time_entry_query_patch.rb | 36 +-- .../user_patch.rb} | 81 +++--- lib/pdf_patch.rb | 257 ----------------- lib/projects_form_hook_listener.rb | 36 --- 22 files changed, 811 insertions(+), 742 deletions(-) rename lib/{ => hooks}/header_footer_hook_listener.rb (80%) create mode 100644 lib/hooks/issues_form_hook_listener.rb rename lib/{ => hooks}/issues_save_hook_listener.rb (68%) create mode 100644 lib/hooks/issues_show_hook_listener.rb create mode 100644 lib/hooks/projects_form_hook_listener.rb rename lib/{ => hooks}/users_show_hook_listener.rb (65%) rename lib/{ => hooks}/view_hook_listener.rb (87%) rename lib/{ => hooks}/view_layouts_hook_listener.rb (70%) delete mode 100644 lib/issue_patch.rb delete mode 100644 lib/issues_form_hook_listener.rb delete mode 100644 lib/issues_show_hook_listener.rb rename lib/{ => patches}/attachments_controller_patch.rb (57%) create mode 100644 lib/patches/issue_patch.rb rename lib/{ => patches}/issues_controller_patch.rb (64%) create mode 100644 lib/patches/pdf_patch.rb rename lib/{user_patch.rb => patches/project_patch.rb} (70%) rename lib/{ => patches}/query_patch.rb (59%) rename lib/{ => patches}/time_entry_query_patch.rb (65%) rename lib/{project_patch.rb => patches/user_patch.rb} (66%) delete mode 100644 lib/pdf_patch.rb delete mode 100644 lib/projects_form_hook_listener.rb diff --git a/init.rb b/init.rb index 3372729..8b84924 100644 --- a/init.rb +++ b/init.rb @@ -45,8 +45,10 @@ Redmine::Plugin.register :redmine_qbo do end -# Dynamically load all Hooks & Patches -Dir::foreach(File.join(File.dirname(__FILE__), 'lib')) do |file| - next unless file.end_with?('.rb') - require_relative "lib/#{file.delete_suffix(".rb") }" -end +# Dynamically load all Hooks & Patches recursively +base_dir = File.join(File.dirname(__FILE__), 'lib') + +# '**' looks inside subdirectories, '*.rb' matches Ruby files +Dir.glob(File.join(base_dir, '**', '*.rb')).sort.each do |file| + require file +end \ No newline at end of file diff --git a/lib/header_footer_hook_listener.rb b/lib/hooks/header_footer_hook_listener.rb similarity index 80% rename from lib/header_footer_hook_listener.rb rename to lib/hooks/header_footer_hook_listener.rb index de70328..edb87aa 100644 --- a/lib/header_footer_hook_listener.rb +++ b/lib/hooks/header_footer_hook_listener.rb @@ -7,8 +7,13 @@ #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 HeaderFooterHookListener < Redmine::Hook::ViewListener - def view_layouts_base_body_bottom(context = {}) - return "

" + +module Hooks + + class HeaderFooterHookListener < Redmine::Hook::ViewListener + def view_layouts_base_body_bottom(context = {}) + return "" + end end -end + +end \ No newline at end of file diff --git a/lib/hooks/issues_form_hook_listener.rb b/lib/hooks/issues_form_hook_listener.rb new file mode 100644 index 0000000..f93f741 --- /dev/null +++ b/lib/hooks/issues_form_hook_listener.rb @@ -0,0 +1,86 @@ +#The MIT License (MIT) +# +#Copyright (c) 2022 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 Hooks + + class IssuesFormHookListener < Redmine::Hook::ViewListener + + # Edit Issue Form + # Here we build the required form components before passing them to a partial view formatting. + def view_issues_form_details_bottom(context={}) + f = context[:form] + issue = context[:issue] + + # check project level customer ownership first + # This is done to preload customer information if the entire project is dedicated to a customer + if context[:project] + selected_customer = context[:project].customer ? context[:project].customer.id : nil + selected_vehicle = context[:project].vehicle ? context[:project].vehicle.id : nil + end + + # Check to see if the issue already belongs to a customer + selected_customer = issue.customer ? issue.customer.id : nil + selected_estimate = issue.estimate ? issue.estimate.id : nil + selected_vehicle = issue.vehicles_id ? issue.vehicles_id : nil + + # Gernerate edit.js link + js_link = issue.new_record? ? "updateIssueFrom('/projects/rmt/issues/new.js', this)" : "updateIssueFrom('/issues/#{issue.id}/edit.js', this)" + + # Load customer information + customer = Customer.find_by_id(selected_customer) if selected_customer + + # Customer Name Text Box with database backed autocomplete + search_customer = f.autocomplete_field :customer, + autocomplete_customer_name_customers_path, + :selected => selected_customer, + :onchange => js_link, + :update_elements => { + :id => '#issue_customer_id', + :value => '#issue_customer' + } + + # Customer ID - Hidden Field + customer_id = f.hidden_field :customer_id, + :id => "issue_customer_id", + :onchange => js_link + + # Load estimates & vehicles + if issue.customer + if customer.vehicles + vehicles = customer.vehicles.pluck(:name, :id) + else + vehicles = [nil].compact + end + estimates = customer.estimates.pluck(:doc_number, :id).sort! {|x, y| y <=> x} + else + vehicles = [nil].compact + estimates = [nil].compact + end + + # Generate the drop down list of quickbooks estimates & vehicles + select_estimate = f.select :estimate_id, estimates, :selected => selected_estimate, include_blank: true + vehicle = f.select :vehicles_id, vehicles, :selected => selected_vehicle, include_blank: true + + # Pass all prebuilt form components to our partial + context[:controller].send(:render_to_string, { + :partial => 'issues/form_hook', + locals: { + search_customer: search_customer, + customer_id: customer_id, + js_link: js_link, + select_estimate: select_estimate, + vehicle: vehicle + } + } + ) + end + end + +end \ No newline at end of file diff --git a/lib/issues_save_hook_listener.rb b/lib/hooks/issues_save_hook_listener.rb similarity index 68% rename from lib/issues_save_hook_listener.rb rename to lib/hooks/issues_save_hook_listener.rb index f67c9c1..e85756c 100644 --- a/lib/issues_save_hook_listener.rb +++ b/lib/hooks/issues_save_hook_listener.rb @@ -8,22 +8,26 @@ # #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 IssuesSaveHookListener < Redmine::Hook::ViewListener +module Hooks - # Called Before Issue Saved - def controller_issues_edit_before_save(context={}) - issue = context[:issue] - issue.subject = issue.subject.titleize - end + class IssuesSaveHookListener < Redmine::Hook::ViewListener - # Called After Issue Saved - def controller_issues_edit_after_save(context={}) - issue = context[:issue] - begin - issue.bill_time if issue.status.is_closed? - rescue - # TODO flash[:error] = "Unable to bill, check QBO Auth" + # Called Before Issue Saved + def controller_issues_edit_before_save(context={}) + issue = context[:issue] + issue.subject = issue.subject.titleize end + + # Called After Issue Saved + def controller_issues_edit_after_save(context={}) + issue = context[:issue] + begin + issue.bill_time if issue.status.is_closed? + rescue + # TODO flash[:error] = "Unable to bill, check QBO Auth" + end + end + end -end +end \ No newline at end of file diff --git a/lib/hooks/issues_show_hook_listener.rb b/lib/hooks/issues_show_hook_listener.rb new file mode 100644 index 0000000..a7e2658 --- /dev/null +++ b/lib/hooks/issues_show_hook_listener.rb @@ -0,0 +1,67 @@ +#The MIT License (MIT) +# +#Copyright (c) 2022 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 Hooks + + class IssuesShowHookListener < Redmine::Hook::ViewListener + + # View Issue + # Display the quickbooks contact in the issue + def view_issues_show_details_bottom(context={}) + issue = context[:issue] + + # Check to see if there is a quickbooks user attached to the issue + if issue.customer + customer = link_to issue.customer.name, customer_path( issue.customer.id ) + end + + # Estimate Number + if issue.estimate + estimate = issue.estimate.doc_number + estimate_link = link_to estimate, estimate_path( issue.estimate.id ), :target => "_blank" + end + + # Invoice Number + invoice_link = "" + if issue.invoice_ids + issue.invoice_ids.each do |i| + invoice = Invoice.find i + invoice_link = invoice_link + link_to( invoice.doc_number, invoice_path( i ), :target => "_blank").to_s + " " + invoice_link = invoice_link.html_safe + end + end + + begin + v = Vehicle.find(issue.vehicles_id) + vehicle = link_to v.to_s, vehicle_path( v.id ) + vin = v.vin + notes = v.notes + rescue + #do nothing + end + + split_vin = vin.scan(/.{1,9}/) if vin + + context[:controller].send(:render_to_string, { + :partial => 'issues/show_details', + locals: { + customer: customer, + estimate_link: estimate_link, + invoice_link: invoice_link, + vehicle: vehicle, + split_vin: split_vin, + notes: notes + } + }) + end + + end + +end \ No newline at end of file diff --git a/lib/hooks/projects_form_hook_listener.rb b/lib/hooks/projects_form_hook_listener.rb new file mode 100644 index 0000000..184c9a5 --- /dev/null +++ b/lib/hooks/projects_form_hook_listener.rb @@ -0,0 +1,40 @@ +#The MIT License (MIT) +# +#Copyright (c) 2017 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 Hooks + + class ProjectsFormHookListener < Redmine::Hook::ViewListener + + # Edit Project Form + def view_projects_form(context={}) + f = context[:form] + + # Check to see if there is a quickbooks user attached to the issue + selected_customer = context[:project].customer ? context[:project].customer : nil + selected_vehicle = context[:project].vehicle_id ? context[:project].vehicle_id : nil + + # Load customer information + customer = Customer.find_by_id(selected_customer) if selected_customer + search_customer = f.autocomplete_field :customer, autocomplete_customer_name_customers_path, :selected => selected_customer, :update_elements => {:id => '#project_customer_id', :value => '#project_customer'} + customer_id = f.hidden_field :customer_id, :id => "project_customer_id" + + if context[:project].customer + vehicles = customer.vehicles.pluck(:name, :id).sort! + else + vehicles = [nil].compact + end + + vehicle = f.select :vehicle_id, vehicles, :selected => selected_vehicle, include_blank: true + + return "

#{search_customer} #{customer_id}

#{vehicle}

" + end + end + +end \ No newline at end of file diff --git a/lib/users_show_hook_listener.rb b/lib/hooks/users_show_hook_listener.rb similarity index 65% rename from lib/users_show_hook_listener.rb rename to lib/hooks/users_show_hook_listener.rb index 025b889..5f0e2d4 100644 --- a/lib/users_show_hook_listener.rb +++ b/lib/hooks/users_show_hook_listener.rb @@ -8,18 +8,22 @@ # #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 UsersShowHookListener < Redmine::Hook::ViewListener +module Hooks - # View User - def view_users_form(context={}) - - # Update the users - #Employee.update_all - - # Check to see if there is a quickbooks user attached to the issue - @selected = context[:user].employee.id if context[:user].employee + class UsersShowHookListener < Redmine::Hook::ViewListener - # Generate the drop down list of quickbooks contacts - return "

#{context[:form].select :employee_id, Employee.all.pluck(:name, :id), :selected => @selected, include_blank: true}

" + # View User + def view_users_form(context={}) + + # Update the users + #Employee.update_all + + # Check to see if there is a quickbooks user attached to the issue + @selected = context[:user].employee.id if context[:user].employee + + # Generate the drop down list of quickbooks contacts + return "

#{context[:form].select :employee_id, Employee.all.pluck(:name, :id), :selected => @selected, include_blank: true}

" + end end -end + +end \ No newline at end of file diff --git a/lib/view_hook_listener.rb b/lib/hooks/view_hook_listener.rb similarity index 87% rename from lib/view_hook_listener.rb rename to lib/hooks/view_hook_listener.rb index f7cb0fe..3f1ba5e 100644 --- a/lib/view_hook_listener.rb +++ b/lib/hooks/view_hook_listener.rb @@ -8,8 +8,12 @@ # #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 ViewHookListener < Redmine::Hook::ViewListener +module Hooks - render_on :view_layouts_base_sidebar, :partial => "qbo/sidebar" - -end + class ViewHookListener < Redmine::Hook::ViewListener + + render_on :view_layouts_base_sidebar, :partial => "qbo/sidebar" + + end + +end \ No newline at end of file diff --git a/lib/view_layouts_hook_listener.rb b/lib/hooks/view_layouts_hook_listener.rb similarity index 70% rename from lib/view_layouts_hook_listener.rb rename to lib/hooks/view_layouts_hook_listener.rb index 29fdbba..01271d3 100644 --- a/lib/view_layouts_hook_listener.rb +++ b/lib/hooks/view_layouts_hook_listener.rb @@ -8,14 +8,18 @@ # #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 ViewLayoutsHookListener < Redmine::Hook::ViewListener +module Hooks + + class ViewLayoutsHookListener < Redmine::Hook::ViewListener + + # Load the javascript to support the autocomplete forms + def view_layouts_base_html_head(context = {}) + js = javascript_include_tag 'application.js', :plugin => 'redmine_qbo' + js += javascript_include_tag 'autocomplete-rails.js', :plugin => 'redmine_qbo' + js += javascript_include_tag 'checkbox_controller.js', :plugin => 'redmine_qbo' + return js + end - # Load the javascript to support the autocomplete forms - def view_layouts_base_html_head(context = {}) - js = javascript_include_tag 'application.js', :plugin => 'redmine_qbo' - js += javascript_include_tag 'autocomplete-rails.js', :plugin => 'redmine_qbo' - js += javascript_include_tag 'checkbox_controller.js', :plugin => 'redmine_qbo' - return js end end \ No newline at end of file diff --git a/lib/issue_patch.rb b/lib/issue_patch.rb deleted file mode 100644 index d5293b8..0000000 --- a/lib/issue_patch.rb +++ /dev/null @@ -1,110 +0,0 @@ -#The MIT License (MIT) -# -#Copyright (c) 2024 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. - -require_dependency 'issue' - -# Patches Redmine's Issues dynamically. -# Adds a relationships -module IssuePatch - - def self.included(base) # :nodoc: - base.extend(ClassMethods) - - base.send(:include, InstanceMethods) - - # Same as typing in the class - base.class_eval do - belongs_to :customer, primary_key: :id - belongs_to :customer_token, primary_key: :id - belongs_to :estimate, primary_key: :id - has_and_belongs_to_many :invoices - belongs_to :vehicle, primary_key: :id - end - - end - - module ClassMethods - - end - - module InstanceMethods - - # Create billable time entries - def bill_time - - # Check to see if we have everything we need to bill the customer - return if assigned_to.nil? - return unless Qbo.first - return unless customer - - # Get unbilled time entries - spent_time = time_entries.where(billed: [false, nil]) - spent_hours ||= spent_time.sum(:hours) || 0 - - if spent_hours > 0 then - - # Prepare to create a new Time Activity - 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| - - # 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) - end - end - end - end - end - - # Create a shareable link for a customer - def share_token - CustomerToken.get_token self - end - -end - -# Add module to Issue -Issue.send(:include, IssuePatch) diff --git a/lib/issues_form_hook_listener.rb b/lib/issues_form_hook_listener.rb deleted file mode 100644 index 2a5c2e7..0000000 --- a/lib/issues_form_hook_listener.rb +++ /dev/null @@ -1,82 +0,0 @@ -#The MIT License (MIT) -# -#Copyright (c) 2022 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 IssuesFormHookListener < Redmine::Hook::ViewListener - - # Edit Issue Form - # Here we build the required form components before passing them to a partial view formatting. - def view_issues_form_details_bottom(context={}) - f = context[:form] - issue = context[:issue] - - # check project level customer ownership first - # This is done to preload customer information if the entire project is dedicated to a customer - if context[:project] - selected_customer = context[:project].customer ? context[:project].customer.id : nil - selected_vehicle = context[:project].vehicle ? context[:project].vehicle.id : nil - end - - # Check to see if the issue already belongs to a customer - selected_customer = issue.customer ? issue.customer.id : nil - selected_estimate = issue.estimate ? issue.estimate.id : nil - selected_vehicle = issue.vehicles_id ? issue.vehicles_id : nil - - # Gernerate edit.js link - js_link = issue.new_record? ? "updateIssueFrom('/projects/rmt/issues/new.js', this)" : "updateIssueFrom('/issues/#{issue.id}/edit.js', this)" - - # Load customer information - customer = Customer.find_by_id(selected_customer) if selected_customer - - # Customer Name Text Box with database backed autocomplete - search_customer = f.autocomplete_field :customer, - autocomplete_customer_name_customers_path, - :selected => selected_customer, - :onchange => js_link, - :update_elements => { - :id => '#issue_customer_id', - :value => '#issue_customer' - } - - # Customer ID - Hidden Field - customer_id = f.hidden_field :customer_id, - :id => "issue_customer_id", - :onchange => js_link - - # Load estimates & vehicles - if issue.customer - if customer.vehicles - vehicles = customer.vehicles.pluck(:name, :id) - else - vehicles = [nil].compact - end - estimates = customer.estimates.pluck(:doc_number, :id).sort! {|x, y| y <=> x} - else - vehicles = [nil].compact - estimates = [nil].compact - end - - # Generate the drop down list of quickbooks estimates & vehicles - select_estimate = f.select :estimate_id, estimates, :selected => selected_estimate, include_blank: true - vehicle = f.select :vehicles_id, vehicles, :selected => selected_vehicle, include_blank: true - - # Pass all prebuilt form components to our partial - context[:controller].send(:render_to_string, { - :partial => 'issues/form_hook', - locals: { - search_customer: search_customer, - customer_id: customer_id, - js_link: js_link, - select_estimate: select_estimate, - vehicle: vehicle - } - } - ) - end -end diff --git a/lib/issues_show_hook_listener.rb b/lib/issues_show_hook_listener.rb deleted file mode 100644 index 8a61e27..0000000 --- a/lib/issues_show_hook_listener.rb +++ /dev/null @@ -1,63 +0,0 @@ -#The MIT License (MIT) -# -#Copyright (c) 2022 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 IssuesShowHookListener < Redmine::Hook::ViewListener - - # View Issue - # Display the quickbooks contact in the issue - def view_issues_show_details_bottom(context={}) - issue = context[:issue] - - # Check to see if there is a quickbooks user attached to the issue - if issue.customer - customer = link_to issue.customer.name, customer_path( issue.customer.id ) - end - - # Estimate Number - if issue.estimate - estimate = issue.estimate.doc_number - estimate_link = link_to estimate, estimate_path( issue.estimate.id ), :target => "_blank" - end - - # Invoice Number - invoice_link = "" - if issue.invoice_ids - issue.invoice_ids.each do |i| - invoice = Invoice.find i - invoice_link = invoice_link + link_to( invoice.doc_number, invoice_path( i ), :target => "_blank").to_s + " " - invoice_link = invoice_link.html_safe - end - end - - begin - v = Vehicle.find(issue.vehicles_id) - vehicle = link_to v.to_s, vehicle_path( v.id ) - vin = v.vin - notes = v.notes - rescue - #do nothing - end - - split_vin = vin.scan(/.{1,9}/) if vin - - context[:controller].send(:render_to_string, { - :partial => 'issues/show_details', - locals: { - customer: customer, - estimate_link: estimate_link, - invoice_link: invoice_link, - vehicle: vehicle, - split_vin: split_vin, - notes: notes - } - }) - end - -end diff --git a/lib/attachments_controller_patch.rb b/lib/patches/attachments_controller_patch.rb similarity index 57% rename from lib/attachments_controller_patch.rb rename to lib/patches/attachments_controller_patch.rb index 69d2a7f..e43b690 100644 --- a/lib/attachments_controller_patch.rb +++ b/lib/patches/attachments_controller_patch.rb @@ -10,33 +10,37 @@ require_dependency 'attachments_controller' -module AttachmentsControllerPatch +module Patches - def self.included(base) + module AttachmentsControllerPatch - base.class_eval do + def self.included(base) - # check if login is globally required to access the application - def check_if_login_required - # no check needed if user is already logged in - return true if User.current.logged? - - # Pull up the attachmet, & verify if we have a valid token for the Issue - attachment = Attachment.find(params[:id]) - token = CustomerToken.where("token = ? and expires_at > ?", session[:token], Time.now) - token = token.first - unless token.nil? - return true if token.issue_id == attachment.container_id + base.class_eval do + + # check if login is globally required to access the application + def check_if_login_required + # no check needed if user is already logged in + return true if User.current.logged? + + # Pull up the attachmet, & verify if we have a valid token for the Issue + attachment = Attachment.find(params[:id]) + token = CustomerToken.where("token = ? and expires_at > ?", session[:token], Time.now) + token = token.first + unless token.nil? + return true if token.issue_id == attachment.container_id + end + + require_login if Setting.login_required? end - require_login if Setting.login_required? end - + end - end + end -end + # Add module to AttachmentsController + AttachmentsController.send(:include, AttachmentsControllerPatch) -# Add module to AttachmentsController -AttachmentsController.send(:include, AttachmentsControllerPatch) +end \ No newline at end of file diff --git a/lib/patches/issue_patch.rb b/lib/patches/issue_patch.rb new file mode 100644 index 0000000..4e7ce37 --- /dev/null +++ b/lib/patches/issue_patch.rb @@ -0,0 +1,115 @@ +#The MIT License (MIT) +# +#Copyright (c) 2024 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. + +require_dependency 'issue' + +module Patches + + + # Patches Redmine's Issues dynamically. + # Adds a relationships + module IssuePatch + + def self.included(base) # :nodoc: + base.extend(ClassMethods) + + base.send(:include, InstanceMethods) + + # Same as typing in the class + base.class_eval do + belongs_to :customer, primary_key: :id + belongs_to :customer_token, primary_key: :id + belongs_to :estimate, primary_key: :id + has_and_belongs_to_many :invoices + belongs_to :vehicle, primary_key: :id + end + + end + + module ClassMethods + + end + + module InstanceMethods + + # Create billable time entries + def bill_time + + # Check to see if we have everything we need to bill the customer + return if assigned_to.nil? + return unless Qbo.first + return unless customer + + # Get unbilled time entries + spent_time = time_entries.where(billed: [false, nil]) + spent_hours ||= spent_time.sum(:hours) || 0 + + if spent_hours > 0 then + + # Prepare to create a new Time Activity + 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| + + # 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) + end + end + end + end + end + + # Create a shareable link for a customer + def share_token + CustomerToken.get_token self + end + + end + + # Add module to Issue + Issue.send(:include, IssuePatch) + +end \ No newline at end of file diff --git a/lib/issues_controller_patch.rb b/lib/patches/issues_controller_patch.rb similarity index 64% rename from lib/issues_controller_patch.rb rename to lib/patches/issues_controller_patch.rb index 85b3d70..083cbdb 100644 --- a/lib/issues_controller_patch.rb +++ b/lib/patches/issues_controller_patch.rb @@ -9,26 +9,30 @@ #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. require_dependency 'issues_controller' -module IssuesControllerPatch +module Patches - module Helper - def watcher_link(issue, user) - link = +'' - link << link_to(I18n.t(:label_bill_time), bill_path( issue.id ), method: :get, class: 'icon icon-email-add') if user.admin? - link << link_to(I18n.t(:label_share), share_path( issue.id ), method: :get, target: :_blank, class: 'icon icon-shared') if user.logged? - link.html_safe + super - end - end + module IssuesControllerPatch - def self.included(base) - - base.class_eval do - helper Helper + module Helper + def watcher_link(issue, user) + link = +'' + link << link_to(I18n.t(:label_bill_time), bill_path( issue.id ), method: :get, class: 'icon icon-email-add') if user.admin? + link << link_to(I18n.t(:label_share), share_path( issue.id ), method: :get, target: :_blank, class: 'icon icon-shared') if user.logged? + link.html_safe + super + end end - end + def self.included(base) -end + base.class_eval do + helper Helper + end -# Add module to IssuessController -IssuesController.send(:include, IssuesControllerPatch) + end + + end + + # Add module to IssuessController + IssuesController.send(:include, IssuesControllerPatch) + +end \ No newline at end of file diff --git a/lib/patches/pdf_patch.rb b/lib/patches/pdf_patch.rb new file mode 100644 index 0000000..5bd3ec2 --- /dev/null +++ b/lib/patches/pdf_patch.rb @@ -0,0 +1,261 @@ +#The MIT License (MIT) +# +#Copyright (c) 2024 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. + +require_dependency 'redmine/export/pdf' +require_dependency 'redmine/export/pdf/issues_pdf_helper' + +module Patches + + module PdfPatch + + def self.included(base) + base.send(:include, InstanceMethods) + base.class_eval do + alias_method :issue_to_pdf, :issue_to_pdf_with_patch + alias_method :issue_to_pdf_with_patch, :issue_to_pdf + end + end + + module InstanceMethods + + def issue_to_pdf_with_patch(issue, assoc={}) + pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language) + pdf.set_title("#{issue.project} - #{issue.tracker} ##{issue.id}") + pdf.alias_nb_pages + pdf.footer_date = format_date(Date.today) + pdf.add_page + pdf.SetFontStyle('B',11) + buf = "#{issue.project} - #{issue.tracker} ##{issue.id}" + pdf.RDMMultiCell(190, 5, buf) + pdf.SetFontStyle('',8) + base_x = pdf.get_x + i = 1 + issue.ancestors.visible.each do |ancestor| + pdf.set_x(base_x + i) + buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}" + pdf.RDMMultiCell(190 - i, 5, buf) + i += 1 if i < 35 + end + pdf.SetFontStyle('B',11) + pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s) + pdf.SetFontStyle('',8) + pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}") + pdf.ln + + customer = issue.customer.name if issue.customer + left = [] + left << [l(:field_status), issue.status] + left << [l(:field_priority), issue.priority] + left << [l(:field_customer), customer] + left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id') + #left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id') + #left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id') + + v = Vehicle.find_by_id(issue.vehicles_id) + vehicle = v ? v.to_s : nil + vin = v ? v.vin : nil + notes = v ? v.notes : nil + left << [l(:field_vehicles), vehicle] + left << [l(:field_vin), vin ? vin.gsub(/(.{9})/, '\1 ') : nil] + #left << [l(:field_notes), notes] + + right = [] + right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date') + right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date') + right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio') + right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours') + right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project) + right << [l(:field_notes), notes] + + rows = left.size > right.size ? left.size : right.size + while left.size < rows + left << nil + end + while right.size < rows + right << nil + end + + half = (issue.visible_custom_field_values.size / 2.0).ceil + issue.visible_custom_field_values.each_with_index do |custom_value, i| + (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)] + end + + if pdf.get_rtl + border_first_top = 'RT' + border_last_top = 'LT' + border_first = 'R' + border_last = 'L' + else + border_first_top = 'LT' + border_last_top = 'RT' + border_first = 'L' + border_last = 'R' + end + + rows = left.size > right.size ? left.size : right.size + rows.times do |i| + heights = [] + pdf.SetFontStyle('B',9) + item = left[i] + heights << pdf.get_string_height(35, item ? "#{item.first}:" : "") + item = right[i] + heights << pdf.get_string_height(35, item ? "#{item.first}:" : "") + pdf.SetFontStyle('',9) + item = left[i] + heights << pdf.get_string_height(60, item ? item.last.to_s : "") + item = right[i] + heights << pdf.get_string_height(60, item ? item.last.to_s : "") + height = heights.max + + item = left[i] + pdf.SetFontStyle('B',9) + pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0) + pdf.SetFontStyle('',9) + pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 0) + + item = right[i] + pdf.SetFontStyle('B',9) + pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0) + pdf.SetFontStyle('',9) + pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 2) + + pdf.set_x(base_x) + end + + pdf.SetFontStyle('B',9) + pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1) + pdf.SetFontStyle('',9) + + # Set resize image scale + pdf.set_image_scale(1.6) + text = textilizable(issue, :description, + :only_path => false, + :edit_section_links => false, + :headings => false, + :inline_attachments => false + ) + pdf.RDMwriteFormattedCell(35+155, 5, '', '', text, issue.attachments, "LRB") + + unless issue.leaf? + truncate_length = (!is_cjk? ? 90 : 65) + pdf.SetFontStyle('B',9) + pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR") + pdf.ln + issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level| + buf = "#{child.tracker} # #{child.id}: #{child.subject}". + truncate(truncate_length) + level = 10 if level >= 10 + pdf.SetFontStyle('',8) + pdf.RDMCell(35+135,5, (level >=1 ? " " * level : "") + buf, border_first) + pdf.SetFontStyle('B',8) + pdf.RDMCell(20,5, child.status.to_s, border_last) + pdf.ln + end + end + + relations = issue.relations.select { |r| r.other_issue(issue).visible? } + unless relations.empty? + truncate_length = (!is_cjk? ? 80 : 60) + pdf.SetFontStyle('B',9) + pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR") + pdf.ln + relations.each do |relation| + buf = relation.to_s(issue) {|other| + text = "" + if Setting.cross_project_issue_relations? + text += "#{relation.other_issue(issue).project} - " + end + text += "#{other.tracker} ##{other.id}: #{other.subject}" + text + } + buf = buf.truncate(truncate_length) + pdf.SetFontStyle('', 8) + pdf.RDMCell(35+155-60, 5, buf, border_first) + pdf.SetFontStyle('B',8) + pdf.RDMCell(20,5, relation.other_issue(issue).status.to_s, "") + pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "") + pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), border_last) + pdf.ln + end + end + pdf.RDMCell(190,5, "", "T") + pdf.ln + + if issue.changesets.any? && + User.current.allowed_to?(:view_changesets, issue.project) + pdf.SetFontStyle('B',9) + pdf.RDMCell(190,5, l(:label_associated_revisions), "B") + pdf.ln + for changeset in issue.changesets + pdf.SetFontStyle('B',8) + csstr = "#{l(:label_revision)} #{changeset.format_identifier} - " + csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s + pdf.RDMCell(190, 5, csstr) + pdf.ln + unless changeset.comments.blank? + pdf.SetFontStyle('',8) + pdf.RDMwriteHTMLCell(190,5,'','', + changeset.comments.to_s, issue.attachments, "") + end + pdf.ln + end + end + + if assoc[:journals].present? + pdf.SetFontStyle('B',9) + pdf.RDMCell(190,5, l(:label_history), "B") + pdf.ln + assoc[:journals].each do |journal| + pdf.SetFontStyle('B',8) + title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}" + title << " (#{l(:field_private_notes)})" if journal.private_notes? + pdf.RDMCell(190,5, title) + pdf.ln + pdf.SetFontStyle('I',8) + details_to_strings(journal.visible_details, true).each do |string| + pdf.RDMMultiCell(190,5, "- " + string) + end + if journal.notes? + pdf.ln unless journal.details.empty? + pdf.SetFontStyle('',8) + text = textilizable(journal, :notes, + :only_path => false, + :edit_section_links => false, + :headings => false, + :inline_attachments => false + ) + pdf.RDMwriteFormattedCell(190,5,'','', text, issue.attachments, "") + end + pdf.ln + end + end + + if issue.attachments.any? + pdf.SetFontStyle('B',9) + pdf.RDMCell(190,5, l(:label_attachment_plural), "B") + pdf.ln + for attachment in issue.attachments + pdf.SetFontStyle('',8) + pdf.RDMCell(80,5, attachment.filename) + pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R") + pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R") + pdf.RDMCell(65,5, attachment.author.name,0,0,"R") + pdf.ln + end + end + pdf.output + end + + end + end + + Redmine::Export::PDF::IssuesPdfHelper.send(:include, PdfPatch) + +end \ No newline at end of file diff --git a/lib/user_patch.rb b/lib/patches/project_patch.rb similarity index 70% rename from lib/user_patch.rb rename to lib/patches/project_patch.rb index 5b78a74..88c591f 100644 --- a/lib/user_patch.rb +++ b/lib/patches/project_patch.rb @@ -1,38 +1,44 @@ -#The MIT License (MIT) -# -#Copyright (c) 2024 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. - -require_dependency 'user' - -# Patches Redmine's User dynamically. -# Adds a relationships -module UserPatch - def self.included(base) # :nodoc: - base.extend(ClassMethods) - - base.send(:include, InstanceMethods) - - # Same as typing in the class - base.class_eval do - belongs_to :employee, primary_key: :id - end - end - - module ClassMethods - - end - - module InstanceMethods - - end - -end - -# Add module to Issue -User.send(:include, UserPatch) +#The MIT License (MIT) +# +#Copyright (c) 2024 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. + +require_dependency 'project' + +module Patches + + # Patches Redmine's Projects dynamically. + # Adds a relationships + module ProjectPatch + + def self.included(base) # :nodoc: + base.extend(ClassMethods) + + base.send(:include, InstanceMethods) + + # Same as typing in the class + base.class_eval do + belongs_to :customer, primary_key: :id + belongs_to :vehicle, primary_key: :id + end + end + + end + + module ClassMethods + + end + + module InstanceMethods + + end + + # Add module to Project + Project.send(:include, ProjectPatch) + +end \ No newline at end of file diff --git a/lib/query_patch.rb b/lib/patches/query_patch.rb similarity index 59% rename from lib/query_patch.rb rename to lib/patches/query_patch.rb index b4a0e56..8dbf5eb 100644 --- a/lib/query_patch.rb +++ b/lib/patches/query_patch.rb @@ -10,25 +10,29 @@ require_dependency 'issue_query' -module QueryPatch - - # Add qbo options to the aviable columns - def available_columns - unless @available_columns - @available_columns = self.class.available_columns.dup - @available_columns << QueryColumn.new(:customer, :sortable => "#{Issue.table_name}.customer_id", :groupable => true, :caption => :field_customer) - @available_columns << QueryColumn.new(:billed, :sortable => "#{TimeEntry.table_name}.billed", :groupable => true, :caption => :field_billed) +module Patches + + module QueryPatch + + # Add qbo options to the aviable columns + def available_columns + unless @available_columns + @available_columns = self.class.available_columns.dup + @available_columns << QueryColumn.new(:customer, :sortable => "#{Issue.table_name}.customer_id", :groupable => true, :caption => :field_customer) + @available_columns << QueryColumn.new(:billed, :sortable => "#{TimeEntry.table_name}.billed", :groupable => true, :caption => :field_billed) + end + super end - super - end - - # Add customers to filters - def initialize_available_filters - #add_available_filter "customer", :type => :text - super + + # Add customers to filters + def initialize_available_filters + #add_available_filter "customer", :type => :text + super + end + end -end + # Add module to Issue + IssueQuery.send(:prepend, QueryPatch) -# Add module to Issue -IssueQuery.send(:prepend, QueryPatch) +end \ No newline at end of file diff --git a/lib/time_entry_query_patch.rb b/lib/patches/time_entry_query_patch.rb similarity index 65% rename from lib/time_entry_query_patch.rb rename to lib/patches/time_entry_query_patch.rb index dce2bac..591aade 100644 --- a/lib/time_entry_query_patch.rb +++ b/lib/patches/time_entry_query_patch.rb @@ -10,24 +10,28 @@ require_dependency 'time_entry_query' -module TimeEntryQueryPatch +module Patches - # Add QBO options to columns - def available_columns - unless @available_columns - @available_columns = self.class.available_columns.dup - @available_columns << QueryColumn.new(:billed, :sortable => "#{TimeEntry.table_name}.name", :groupable => true, :caption => :field_billed) + module TimeEntryQueryPatch + + # Add QBO options to columns + def available_columns + unless @available_columns + @available_columns = self.class.available_columns.dup + @available_columns << QueryColumn.new(:billed, :sortable => "#{TimeEntry.table_name}.name", :groupable => true, :caption => :field_billed) + end + super end - super - end - - # Add QBO options to the filter - def initialize_available_filters - add_available_filter "billed", :type => :boolean - super + + # Add QBO options to the filter + def initialize_available_filters + add_available_filter "billed", :type => :boolean + super + end + end -end + # Add module to TimeEntryQuery + TimeEntryQuery.send(:prepend, QueryPatch) -# Add module to TimeEntryQuery -TimeEntryQuery.send(:prepend, QueryPatch) +end \ No newline at end of file diff --git a/lib/project_patch.rb b/lib/patches/user_patch.rb similarity index 66% rename from lib/project_patch.rb rename to lib/patches/user_patch.rb index 925bde6..d717c77 100644 --- a/lib/project_patch.rb +++ b/lib/patches/user_patch.rb @@ -1,39 +1,42 @@ -#The MIT License (MIT) -# -#Copyright (c) 2024 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. - -require_dependency 'project' - -# Patches Redmine's Projects dynamically. -# Adds a relationships -module ProjectPatch - - def self.included(base) # :nodoc: - base.extend(ClassMethods) - - base.send(:include, InstanceMethods) - - # Same as typing in the class - base.class_eval do - belongs_to :customer, primary_key: :id - belongs_to :vehicle, primary_key: :id - end - end -end - -module ClassMethods - -end - -module InstanceMethods - -end - -# Add module to Project -Project.send(:include, ProjectPatch) +#The MIT License (MIT) +# +#Copyright (c) 2024 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. + +require_dependency 'user' + +module Patches + + # Patches Redmine's User dynamically. + # Adds a relationships + module UserPatch + def self.included(base) # :nodoc: + base.extend(ClassMethods) + + base.send(:include, InstanceMethods) + + # Same as typing in the class + base.class_eval do + belongs_to :employee, primary_key: :id + end + end + + module ClassMethods + + end + + module InstanceMethods + + end + + end + + # Add module to Issue + User.send(:include, UserPatch) + +end \ No newline at end of file diff --git a/lib/pdf_patch.rb b/lib/pdf_patch.rb deleted file mode 100644 index 274bf57..0000000 --- a/lib/pdf_patch.rb +++ /dev/null @@ -1,257 +0,0 @@ -#The MIT License (MIT) -# -#Copyright (c) 2024 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. - -require_dependency 'redmine/export/pdf' -require_dependency 'redmine/export/pdf/issues_pdf_helper' - -module PdfPatch - - def self.included(base) - base.send(:include, InstanceMethods) - base.class_eval do - alias_method :issue_to_pdf, :issue_to_pdf_with_patch - alias_method :issue_to_pdf_with_patch, :issue_to_pdf - end - end - - module InstanceMethods - - def issue_to_pdf_with_patch(issue, assoc={}) - pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language) - pdf.set_title("#{issue.project} - #{issue.tracker} ##{issue.id}") - pdf.alias_nb_pages - pdf.footer_date = format_date(Date.today) - pdf.add_page - pdf.SetFontStyle('B',11) - buf = "#{issue.project} - #{issue.tracker} ##{issue.id}" - pdf.RDMMultiCell(190, 5, buf) - pdf.SetFontStyle('',8) - base_x = pdf.get_x - i = 1 - issue.ancestors.visible.each do |ancestor| - pdf.set_x(base_x + i) - buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}" - pdf.RDMMultiCell(190 - i, 5, buf) - i += 1 if i < 35 - end - pdf.SetFontStyle('B',11) - pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s) - pdf.SetFontStyle('',8) - pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}") - pdf.ln - - customer = issue.customer.name if issue.customer - left = [] - left << [l(:field_status), issue.status] - left << [l(:field_priority), issue.priority] - left << [l(:field_customer), customer] - left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id') - #left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id') - #left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id') - - v = Vehicle.find_by_id(issue.vehicles_id) - vehicle = v ? v.to_s : nil - vin = v ? v.vin : nil - notes = v ? v.notes : nil - left << [l(:field_vehicles), vehicle] - left << [l(:field_vin), vin ? vin.gsub(/(.{9})/, '\1 ') : nil] - #left << [l(:field_notes), notes] - - right = [] - right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date') - right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date') - right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio') - right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours') - right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project) - right << [l(:field_notes), notes] - - rows = left.size > right.size ? left.size : right.size - while left.size < rows - left << nil - end - while right.size < rows - right << nil - end - - half = (issue.visible_custom_field_values.size / 2.0).ceil - issue.visible_custom_field_values.each_with_index do |custom_value, i| - (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)] - end - - if pdf.get_rtl - border_first_top = 'RT' - border_last_top = 'LT' - border_first = 'R' - border_last = 'L' - else - border_first_top = 'LT' - border_last_top = 'RT' - border_first = 'L' - border_last = 'R' - end - - rows = left.size > right.size ? left.size : right.size - rows.times do |i| - heights = [] - pdf.SetFontStyle('B',9) - item = left[i] - heights << pdf.get_string_height(35, item ? "#{item.first}:" : "") - item = right[i] - heights << pdf.get_string_height(35, item ? "#{item.first}:" : "") - pdf.SetFontStyle('',9) - item = left[i] - heights << pdf.get_string_height(60, item ? item.last.to_s : "") - item = right[i] - heights << pdf.get_string_height(60, item ? item.last.to_s : "") - height = heights.max - - item = left[i] - pdf.SetFontStyle('B',9) - pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0) - pdf.SetFontStyle('',9) - pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 0) - - item = right[i] - pdf.SetFontStyle('B',9) - pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0) - pdf.SetFontStyle('',9) - pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 2) - - pdf.set_x(base_x) - end - - pdf.SetFontStyle('B',9) - pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1) - pdf.SetFontStyle('',9) - - # Set resize image scale - pdf.set_image_scale(1.6) - text = textilizable(issue, :description, - :only_path => false, - :edit_section_links => false, - :headings => false, - :inline_attachments => false - ) - pdf.RDMwriteFormattedCell(35+155, 5, '', '', text, issue.attachments, "LRB") - - unless issue.leaf? - truncate_length = (!is_cjk? ? 90 : 65) - pdf.SetFontStyle('B',9) - pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR") - pdf.ln - issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level| - buf = "#{child.tracker} # #{child.id}: #{child.subject}". - truncate(truncate_length) - level = 10 if level >= 10 - pdf.SetFontStyle('',8) - pdf.RDMCell(35+135,5, (level >=1 ? " " * level : "") + buf, border_first) - pdf.SetFontStyle('B',8) - pdf.RDMCell(20,5, child.status.to_s, border_last) - pdf.ln - end - end - - relations = issue.relations.select { |r| r.other_issue(issue).visible? } - unless relations.empty? - truncate_length = (!is_cjk? ? 80 : 60) - pdf.SetFontStyle('B',9) - pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR") - pdf.ln - relations.each do |relation| - buf = relation.to_s(issue) {|other| - text = "" - if Setting.cross_project_issue_relations? - text += "#{relation.other_issue(issue).project} - " - end - text += "#{other.tracker} ##{other.id}: #{other.subject}" - text - } - buf = buf.truncate(truncate_length) - pdf.SetFontStyle('', 8) - pdf.RDMCell(35+155-60, 5, buf, border_first) - pdf.SetFontStyle('B',8) - pdf.RDMCell(20,5, relation.other_issue(issue).status.to_s, "") - pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "") - pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), border_last) - pdf.ln - end - end - pdf.RDMCell(190,5, "", "T") - pdf.ln - - if issue.changesets.any? && - User.current.allowed_to?(:view_changesets, issue.project) - pdf.SetFontStyle('B',9) - pdf.RDMCell(190,5, l(:label_associated_revisions), "B") - pdf.ln - for changeset in issue.changesets - pdf.SetFontStyle('B',8) - csstr = "#{l(:label_revision)} #{changeset.format_identifier} - " - csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s - pdf.RDMCell(190, 5, csstr) - pdf.ln - unless changeset.comments.blank? - pdf.SetFontStyle('',8) - pdf.RDMwriteHTMLCell(190,5,'','', - changeset.comments.to_s, issue.attachments, "") - end - pdf.ln - end - end - - if assoc[:journals].present? - pdf.SetFontStyle('B',9) - pdf.RDMCell(190,5, l(:label_history), "B") - pdf.ln - assoc[:journals].each do |journal| - pdf.SetFontStyle('B',8) - title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}" - title << " (#{l(:field_private_notes)})" if journal.private_notes? - pdf.RDMCell(190,5, title) - pdf.ln - pdf.SetFontStyle('I',8) - details_to_strings(journal.visible_details, true).each do |string| - pdf.RDMMultiCell(190,5, "- " + string) - end - if journal.notes? - pdf.ln unless journal.details.empty? - pdf.SetFontStyle('',8) - text = textilizable(journal, :notes, - :only_path => false, - :edit_section_links => false, - :headings => false, - :inline_attachments => false - ) - pdf.RDMwriteFormattedCell(190,5,'','', text, issue.attachments, "") - end - pdf.ln - end - end - - if issue.attachments.any? - pdf.SetFontStyle('B',9) - pdf.RDMCell(190,5, l(:label_attachment_plural), "B") - pdf.ln - for attachment in issue.attachments - pdf.SetFontStyle('',8) - pdf.RDMCell(80,5, attachment.filename) - pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R") - pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R") - pdf.RDMCell(65,5, attachment.author.name,0,0,"R") - pdf.ln - end - end - pdf.output - end - - end -end - -Redmine::Export::PDF::IssuesPdfHelper.send(:include, PdfPatch) diff --git a/lib/projects_form_hook_listener.rb b/lib/projects_form_hook_listener.rb deleted file mode 100644 index ef04179..0000000 --- a/lib/projects_form_hook_listener.rb +++ /dev/null @@ -1,36 +0,0 @@ -#The MIT License (MIT) -# -#Copyright (c) 2017 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 ProjectsFormHookListener < Redmine::Hook::ViewListener - - # Edit Project Form - def view_projects_form(context={}) - f = context[:form] - - # Check to see if there is a quickbooks user attached to the issue - selected_customer = context[:project].customer ? context[:project].customer : nil - selected_vehicle = context[:project].vehicle_id ? context[:project].vehicle_id : nil - - # Load customer information - customer = Customer.find_by_id(selected_customer) if selected_customer - search_customer = f.autocomplete_field :customer, autocomplete_customer_name_customers_path, :selected => selected_customer, :update_elements => {:id => '#project_customer_id', :value => '#project_customer'} - customer_id = f.hidden_field :customer_id, :id => "project_customer_id" - - if context[:project].customer - vehicles = customer.vehicles.pluck(:name, :id).sort! - else - vehicles = [nil].compact - end - - vehicle = f.select :vehicle_id, vehicles, :selected => selected_vehicle, include_blank: true - - return "

#{search_customer} #{customer_id}

#{vehicle}

" - end -end From 6cd782543050b313ed27413aa559b8864ac35983 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Wed, 21 Jan 2026 20:28:54 -0500 Subject: [PATCH 103/106] Added pdf method --- app/models/invoice.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/models/invoice.rb b/app/models/invoice.rb index d4aef06..3598c2b 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -170,6 +170,16 @@ class Invoice < ActiveRecord::Base end end + # download the pdf from quickbooks + def 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(id) + return service.pdf(invoice) + end + end + # Magic Method # Maps Get/Set methods to QBO invoice object def method_missing(sym, *arguments) From c85e45b544d300823f6e9fa4b980ccd4e560204a Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Wed, 21 Jan 2026 20:29:38 -0500 Subject: [PATCH 104/106] Print attached estimate --- lib/patches/pdf_patch.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/patches/pdf_patch.rb b/lib/patches/pdf_patch.rb index 5bd3ec2..f5cfc9a 100644 --- a/lib/patches/pdf_patch.rb +++ b/lib/patches/pdf_patch.rb @@ -250,7 +250,15 @@ module Patches pdf.ln end end - pdf.output + + # Check to see if there is an estimate attached, then combine them + if issue.estimate + pdf = CombinePDF.parse(pdf.output, allow_optional_content: true) + pdf << CombinePDF.parse(issue.estimate.pdf) + return pdf.to_pdf + end + + return pdf.output end end From 2aeb3fa028df8490181d067a07ebd9386d4a8e61 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Wed, 21 Jan 2026 20:40:06 -0500 Subject: [PATCH 105/106] Updated copyright dates --- app/controllers/customers_controller.rb | 2 +- app/controllers/estimate_controller.rb | 2 +- app/controllers/invoice_controller.rb | 2 +- app/controllers/qbo_controller.rb | 2 +- app/controllers/vehicles_controller.rb | 2 +- app/helpers/auth_helper.rb | 2 +- app/models/concerns/quickbooks_oauth.rb | 2 +- app/models/customer.rb | 2 +- app/models/customer_token.rb | 2 +- app/models/employee.rb | 2 +- app/models/estimate.rb | 2 +- app/models/invoice.rb | 2 +- app/models/qbo.rb | 2 +- app/models/vehicle.rb | 2 +- app/views/qbo/_settings.html.erb | 2 +- app/views/qbo/authenticate.html.erb | 2 +- app/views/qbo/oauth_callback.html.erb | 2 +- app/views/qbo/webhook.erb | 2 +- config/locales/en.yml | 2 +- config/routes.rb | 2 +- db/migrate/001_create_qbos.rb | 2 +- db/migrate/002_create_qbo_customers.rb | 2 +- db/migrate/003_update_issues.rb | 2 +- db/migrate/004_create_qbo_items.rb | 2 +- db/migrate/005_create_qbo_employees.rb | 2 +- db/migrate/006_update_users.rb | 2 +- db/migrate/007_update_time_entries.rb | 2 +- db/migrate/008_create_qbo_estimates.rb | 2 +- db/migrate/009_update_qbos.rb | 2 +- db/migrate/010_update_issues_with_estimates.rb | 2 +- db/migrate/011_create_qbo_invoices.rb | 2 +- db/migrate/012_update_issues_with_invoices.rb | 2 +- db/migrate/013_create_qbo_purchases.rb | 2 +- db/migrate/014_update_customers.rb | 2 +- db/migrate/015_update_qbo_purchases.rb | 2 +- db/migrate/016_create_vehicles.rb | 2 +- db/migrate/017_update_issues_with_vehicles.rb | 2 +- db/migrate/018_update_vehicles.rb | 2 +- db/migrate/019_qbocustomers_to_customers.rb | 2 +- db/migrate/020_update_qbos_time_stamp.rb | 2 +- db/migrate/021_add_issues_qbo_invoices.rb | 2 +- db/migrate/022_update_issues_remove_invoice.rb | 2 +- db/migrate/023_create_customer_tokens.rb | 2 +- db/migrate/024_update_invoices_and_estimates.rb | 2 +- db/migrate/025_update_projects.rb | 2 +- db/migrate/026_create_line_items.rb | 2 +- db/migrate/027_add_customers_phone_number.rb | 2 +- db/migrate/028_add_customers_mobile_phone_number.rb | 2 +- db/migrate/029_update_qbos_types.rb | 2 +- db/migrate/030_update_qbos_token.rb | 2 +- db/migrate/031_remove_qbos_keys.rb | 2 +- db/migrate/032_add_txn_dates.rb | 2 +- db/migrate/033_update_vehicles_trim.rb | 2 +- db/migrate/034_remove_qbo_items.rb | 2 +- db/migrate/035_drop_qbo_prefix.rb | 2 +- db/migrate/036_remove_qbo_time_entries.rb | 2 +- db/migrate/037_update_qbo_token.rb | 2 +- init.rb | 2 +- lib/hooks/header_footer_hook_listener.rb | 2 +- lib/hooks/issues_form_hook_listener.rb | 2 +- lib/hooks/issues_save_hook_listener.rb | 2 +- lib/hooks/issues_show_hook_listener.rb | 2 +- lib/hooks/projects_form_hook_listener.rb | 2 +- lib/hooks/users_show_hook_listener.rb | 2 +- lib/hooks/view_hook_listener.rb | 2 +- lib/hooks/view_layouts_hook_listener.rb | 2 +- lib/patches/attachments_controller_patch.rb | 2 +- lib/patches/issue_patch.rb | 2 +- lib/patches/issues_controller_patch.rb | 2 +- lib/patches/pdf_patch.rb | 2 +- lib/patches/project_patch.rb | 2 +- lib/patches/query_patch.rb | 2 +- lib/patches/time_entry_query_patch.rb | 2 +- lib/patches/user_patch.rb | 2 +- test/functional/qbo_controller_test.rb | 2 +- test/unit/qbo_customers_test.rb | 2 +- test/unit/qbo_test.rb | 2 +- 77 files changed, 77 insertions(+), 77 deletions(-) diff --git a/app/controllers/customers_controller.rb b/app/controllers/customers_controller.rb index 66c0253..e199dcf 100644 --- a/app/controllers/customers_controller.rb +++ b/app/controllers/customers_controller.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2024 rick barrette +#Copyright (c) 2016 - 2026 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: # diff --git a/app/controllers/estimate_controller.rb b/app/controllers/estimate_controller.rb index 437d5b7..8190c24 100644 --- a/app/controllers/estimate_controller.rb +++ b/app/controllers/estimate_controller.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2024 rick barrette +#Copyright (c) 2016 - 2026 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: # diff --git a/app/controllers/invoice_controller.rb b/app/controllers/invoice_controller.rb index 2291abc..a86666c 100644 --- a/app/controllers/invoice_controller.rb +++ b/app/controllers/invoice_controller.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2026 rick barrette +#Copyright (c) 2016 - 2026 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: # diff --git a/app/controllers/qbo_controller.rb b/app/controllers/qbo_controller.rb index c3c86d3..1acd248 100644 --- a/app/controllers/qbo_controller.rb +++ b/app/controllers/qbo_controller.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2016 - 2025 rick barrette +#Copyright (c) 2016 - 2026 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: # diff --git a/app/controllers/vehicles_controller.rb b/app/controllers/vehicles_controller.rb index 0830acd..25fe322 100644 --- a/app/controllers/vehicles_controller.rb +++ b/app/controllers/vehicles_controller.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2024 rick barrette +#Copyright (c) 2016 - 2026 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: # diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 7959fec..bdc662a 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2017 rick barrette +#Copyright (c) 2016 - 2026 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: # diff --git a/app/models/concerns/quickbooks_oauth.rb b/app/models/concerns/quickbooks_oauth.rb index 90d593c..2508f00 100644 --- a/app/models/concerns/quickbooks_oauth.rb +++ b/app/models/concerns/quickbooks_oauth.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2016 - 2025 rick barrette +#Copyright (c) 2016 - 2026 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: # diff --git a/app/models/customer.rb b/app/models/customer.rb index d4f8031..e574801 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2016 - 2025 rick barrette +#Copyright (c) 2016 - 2026 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: # diff --git a/app/models/customer_token.rb b/app/models/customer_token.rb index 90f7475..96e9d48 100644 --- a/app/models/customer_token.rb +++ b/app/models/customer_token.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2024 rick barrette +#Copyright (c) 2016 - 2026 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: # diff --git a/app/models/employee.rb b/app/models/employee.rb index ab35227..32ded44 100644 --- a/app/models/employee.rb +++ b/app/models/employee.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2024 rick barrette +#Copyright (c) 2016 - 2026 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: # diff --git a/app/models/estimate.rb b/app/models/estimate.rb index 80c5c70..20e8c38 100644 --- a/app/models/estimate.rb +++ b/app/models/estimate.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2024 rick barrette +#Copyright (c) 2016 - 2026 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: # diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 3598c2b..5821b03 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2024 rick barrette +#Copyright (c) 2016 - 2026 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: # diff --git a/app/models/qbo.rb b/app/models/qbo.rb index f5450c7..82c371e 100644 --- a/app/models/qbo.rb +++ b/app/models/qbo.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2016 - 2025 rick barrette +#Copyright (c) 2016 - 2026 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: # diff --git a/app/models/vehicle.rb b/app/models/vehicle.rb index 4a61117..ae5adfc 100644 --- a/app/models/vehicle.rb +++ b/app/models/vehicle.rb @@ -1,6 +1,6 @@ #The MIT License (MIT) # -#Copyright (c) 2024 rick barrette +#Copyright (c) 2016 - 2026 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: # diff --git a/app/views/qbo/_settings.html.erb b/app/views/qbo/_settings.html.erb index d75609a..d120f31 100644 --- a/app/views/qbo/_settings.html.erb +++ b/app/views/qbo/_settings.html.erb @@ -1,7 +1,7 @@