Compare commits

...

12 Commits

Author SHA1 Message Date
6434eea906 2026.2.12 2026-02-21 08:24:36 -05:00
9b656534ae Sanitize search, no little bobby tables 2026-02-21 08:23:58 -05:00
659a1fbcf0 2026.2.11 2026-02-20 19:11:31 -05:00
4dc1f5d0bd Enhance billing functionality in IssuePatch with detailed logging and self-references 2026-02-20 09:47:47 -05:00
02f34582f4 2026.2.10
Addressed the Bullet (the N+1 query detector) warning to include customers
2026-02-16 18:56:09 -05:00
2f9ef6304f scope.includes(:customer) 2026-02-16 18:53:29 -05:00
886d5f4ace 2026.2.9 2026-02-16 08:15:46 -05:00
1ade938eb3 Fixed Querying issues by customer name 2026-02-16 08:13:57 -05:00
3111f391f3 Filter by customer works now 2026-02-15 21:34:22 -05:00
d2b9113914 2026.2.8 2026-02-14 18:57:22 -05:00
447e048819 updated screensots 2026-02-14 09:32:40 -05:00
e7dfc3f2ad added sync estimates by id 2026-02-14 08:25:02 -05:00
11 changed files with 68 additions and 16 deletions

BIN
Screenshots/issue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 KiB

BIN
Screenshots/issue_form.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

After

Width:  |  Height:  |  Size: 672 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

After

Width:  |  Height:  |  Size: 538 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 512 KiB

View File

@@ -15,18 +15,31 @@ class EstimateController < ApplicationController
skip_before_action :verify_authenticity_token, :check_if_login_required, unless: proc {|c| session[:token].nil? } skip_before_action :verify_authenticity_token, :check_if_login_required, unless: proc {|c| session[:token].nil? }
def get_estimate def get_estimate
e = Estimate.find_by_doc_number(params[:search]) if params[:search]
e = Estimate.find_by_id(params[:id]) if params[:id]
# Force sync for estimate by doc number if not found # Force sync for estimate by doc number if not found
if Estimate.find_by_doc_number(params[:search]).nil? if e.nil? && params[:search]
begin begin
Estimate.sync_by_doc_number(params[:search]) if params[:search] Estimate.sync_by_doc_number(params[:search])
e = Estimate.find_by_doc_number(params[:search])
rescue rescue
logger.info "Estimate.find_by_doc_number failed" logger.info "Estimate.find_by_doc_number failed"
end end
end end
estimate = Estimate.find_by_id(params[:id]) if params[:id] # Force sync for estimate by id if not found
estimate = Estimate.find_by_doc_number(params[:search]) if params[:search] if e.nil? && params[:id]
return estimate begin
Estimate.sync_by_id(params[:id])
e = Estimate.find_by_id(params[:id])
rescue
logger.info "Estimate.find_by_id failed"
end
end
return e
end end
# #

View File

@@ -169,6 +169,7 @@ class Customer < ActiveRecord::Base
# Searchs the database for a customer by name or phone number with out special chars # Searchs the database for a customer by name or phone number with out special chars
def self.search(search) def self.search(search)
search = sanitize_sql_like(search)
customers = where("name LIKE ? OR phone_number LIKE ? OR mobile_phone_number LIKE ?", "%#{search}%", "%#{search}%", "%#{search}%") customers = where("name LIKE ? OR phone_number LIKE ? OR mobile_phone_number LIKE ?", "%#{search}%", "%#{search}%", "%#{search}%")
return customers.order(:name) return customers.order(:name)
end end

View File

@@ -14,7 +14,7 @@ Redmine::Plugin.register :redmine_qbo do
name 'Redmine QBO plugin' name 'Redmine QBO plugin'
author 'Rick Barrette' author 'Rick Barrette'
description 'A pluging for Redmine to connect with QuickBooks Online to create Time Activity Entries for billable hours logged when an Issue is closed' description 'A pluging for Redmine to connect with QuickBooks Online to create Time Activity Entries for billable hours logged when an Issue is closed'
version '2026.2.7' version '2026.2.12'
url 'https://github.com/rickbarrette/redmine_qbo' url 'https://github.com/rickbarrette/redmine_qbo'
author_url 'https://barrettefabrication.com' author_url 'https://barrettefabrication.com'
settings default: {empty: true}, partial: 'qbo/settings' settings default: {empty: true}, partial: 'qbo/settings'

View File

@@ -25,7 +25,7 @@ module RedmineQbo
# Same as typing in the class # Same as typing in the class
base.class_eval do base.class_eval do
belongs_to :customer, primary_key: :id belongs_to :customer, class_name: 'Customer', foreign_key: :customer_id, optional: true
belongs_to :customer_token, primary_key: :id belongs_to :customer_token, primary_key: :id
belongs_to :estimate, primary_key: :id belongs_to :estimate, primary_key: :id
has_and_belongs_to_many :invoices has_and_belongs_to_many :invoices
@@ -43,15 +43,19 @@ module RedmineQbo
# Create billable time entries # Create billable time entries
def bill_time def bill_time
logger.debug "QBO: Billing time for issue ##{id}" logger.debug "QBO: Billing time for issue ##{self.id}"
return false if assigned_to.nil? logger.debug "Issue is closed? #{self.closed?}"
return false if self.assigned_to.nil?
return false unless Qbo.first return false unless Qbo.first
return false unless customer return false unless self.customer
Thread.new do Thread.new do
spent_time = time_entries.where(billed: [false, nil]) spent_time = self.time_entries.where(billed: [false, nil])
spent_hours ||= spent_time.sum(:hours) || 0 spent_hours ||= spent_time.sum(:hours) || 0
logger.debug "Issue has spent hours: #{spent_hours}"
if spent_hours > 0 then if spent_hours > 0 then
# Prepare to create a new Time Activity # Prepare to create a new Time Activity
@@ -73,20 +77,23 @@ module RedmineQbo
# Now letes upload our totals for each activity as their own billable time entry # Now letes upload our totals for each activity as their own billable time entry
h.each do |key, val| h.each do |key, val|
logger.debug "Processing activity '#{key}' with #{val.to_i} hours for issue ##{self.id}"
# Convert float spent time to hours and minutes # Convert float spent time to hours and minutes
hours = val.to_i hours = val.to_i
minutesDecimal = (( val - hours) * 60) minutesDecimal = (( val - hours) * 60)
minutes = minutesDecimal.to_i minutes = minutesDecimal.to_i
logger.debug "Converted #{val.to_i} hours to #{hours} hours and #{minutes} minutes"
# Lets match the activity to an qbo item # Lets match the activity to an qbo item
item = item_service.query("SELECT * FROM Item WHERE Name = '#{key}' ").first item = item_service.query("SELECT * FROM Item WHERE Name = '#{key}' ").first
next if item.nil? next if item.nil?
# Create the new billable time entry and upload it # Create the new billable time entry and upload it
time_entry.description = "#{tracker} ##{id}: #{subject} #{"(Partial @ #{done_ratio}%)" if not closed?}" time_entry.description = "#{self.tracker} ##{self.id}: #{self.subject} #{"(Partial @ #{self.done_ratio}%)" unless self.closed?}"
time_entry.employee_id = assigned_to.employee_id time_entry.employee_id = self.assigned_to.employee_id
time_entry.customer_id = customer_id time_entry.customer_id = self.customer_id
time_entry.billable_status = "Billable" time_entry.billable_status = "Billable"
time_entry.hours = hours time_entry.hours = hours
time_entry.minutes = minutes time_entry.minutes = minutes

View File

@@ -14,6 +14,20 @@ module RedmineQbo
module Patches module Patches
module QueryPatch module QueryPatch
def base_scope
scope = super
if filters['customer_name'].present?
scope = scope.left_outer_joins(:customer)
end
if has_column?(:customer) || filters['customer_name'].present?
scope = scope.includes(:customer)
end
scope
end
# Add qbo options to the aviable columns # Add qbo options to the aviable columns
def available_columns def available_columns
unless @available_columns unless @available_columns
@@ -26,10 +40,27 @@ module RedmineQbo
# Add customers to filters # Add customers to filters
def initialize_available_filters def initialize_available_filters
#add_available_filter "customer", type: :text #add_available_filter "customer_id", type: :list, name: l(:field_customer), :values => lambda {Customer.pluck(:name, :id).map {|name, id| [name, id.to_s]}}
add_available_filter( 'customer_name', type: :text, name: l(:field_customer))
super super
end end
def sql_for_customer_name_field(field, operator, value)
pattern = "%#{value.first}%"
sql = case operator
when '~'
"#{Customer.table_name}.name LIKE ?"
when '!~'
"#{Customer.table_name}.name NOT LIKE ?"
else
return nil
end
Issue.joins(:customer).sanitize_sql_for_conditions([sql, pattern])
end
end end
# Add module to Issue # Add module to Issue