Compare commits

..

45 Commits

Author SHA1 Message Date
139f5dd618 render partial on footer 2026-02-13 22:39:54 -05:00
9c11704d03 Added label_create_estimate 2026-02-13 20:45:59 -05:00
2ae53adf08 added needed trailing space 2026-02-13 20:37:42 -05:00
877c1b78a5 removed old comment 2026-02-13 18:38:42 -05:00
1d47703206 fixed indentiation 2026-02-13 18:37:12 -05:00
a069556ed9 Alphabetized: All keys are now in A-Z order for easier maintenance.
Branding: Changed "Quickbooks" to QuickBooks (capitalized B) to match official branding.

Grammar:

warn_ru_sure: Changed "You sure?" to "Are you sure?"

label_no_customers: Refined to "There are no customers matching the search term(s)."

label_billing_error: Added punctuation and clarified the phrasing.

Cleanup: Removed unnecessary spaces inside quotes (e.g., " Make" and "Matching ").
2026-02-13 18:33:48 -05:00
359c582e22 Fixed partial billing and added flash messages 2026-02-13 18:25:07 -05:00
e63b9e4217 use safe_join 2026-02-13 07:32:20 -05:00
6fd355d8cc 2026.2.7 2026-02-12 19:00:55 -05:00
e6b57392d1 Merge branch '422' 2026-02-12 18:59:32 -05:00
331c1eabeb seems to work without overiding the main issues _form 2026-02-11 19:59:41 -05:00
167385bb99 override issues form to add passing @project to issues hook 2026-02-11 19:31:01 -05:00
11b9876d4f Removed unused controller_issues_new_before_save that was used for finding the 422 error 2026-02-11 08:09:00 -05:00
9cf72821b0 2026.2.6 2026-02-11 08:05:28 -05:00
57adcce431 Refactor JavaScript path handling for issue form updates to help prevent 422 errors on new issue creation 2026-02-11 07:59:18 -05:00
7fdb15f7e8 more logging 2026-02-10 22:09:16 -05:00
6e11e05a24 2026.2.5 2026-02-09 21:54:01 -05:00
a6751d3f41 simplified appointment link javascript 2026-02-09 21:43:29 -05:00
8944e92ffc Use html data attributes 2026-02-09 20:52:27 -05:00
f0c0a42c96 2026.2.4 2026-02-09 20:04:47 -05:00
a4b51457bb moved controller_issues_new_before_save nito issues_hook_listener 2026-02-09 20:04:16 -05:00
fb4a883b43 Added logging 2026-02-09 14:31:56 -05:00
c24ec93335 force a tracker if still nil 2026-02-09 14:21:50 -05:00
df49964bf9 added tracker nil check 2026-02-09 14:19:31 -05:00
502ba94465 Readded missing controller_issues_new_before_save 2026-02-09 14:12:01 -05:00
ff038fe5ae removed method: :get from estimate link 2026-02-09 10:41:48 -05:00
3eed122598 fixed typo 2026-02-09 10:38:10 -05:00
d8d34540a9 use link_to, as button_to doesn't allow for openign links in a new tab 2026-02-09 10:35:12 -05:00
c01cc5ca97 2026.2.3 2026-02-09 09:36:50 -05:00
6a2f7a1146 initialize string link 2026-02-09 09:21:42 -05:00
f4c844f097 2026.2.2 2026-02-08 19:59:28 -05:00
1135c69e1b Don't change link text 2026-02-08 19:53:50 -05:00
ef86d222cb Removed change link button 2026-02-08 19:52:17 -05:00
be88a601ae Merge branch 'dev' into js 2026-02-08 19:50:55 -05:00
e6c4e81df2 Remove unused add_appointment method from CustomersController 2026-02-08 19:48:01 -05:00
f4a979672f update link on change 2026-02-08 19:45:58 -05:00
8a4d64ffc0 Update appointment link with selected invoice links 2026-02-08 18:25:20 -05:00
ac05d38763 2026.2.1 2026-02-08 13:15:20 -05:00
548dc4fba8 Implement issue creation error handling; add project validation and refactored issue hooks 2026-02-08 13:07:28 -05:00
7a73b7e8a9 refactor error handling in issue creation; remove unused reload_new_issue method 2026-02-08 10:58:39 -05:00
b38bd951f7 fiex typo tracker not project 2026-02-08 10:02:42 -05:00
0e3318efdd Added prefilters to help locate 422 on issue creation.
This is an effort to figure out why I get 422 Unprocessable Entity errors sometimes when creating new issues.
2026-02-08 09:58:34 -05:00
d063494bd2 removed empty link string 2026-02-06 23:00:26 -05:00
e35a2148eb 2026.2.0 2026-02-06 19:37:32 -05:00
5d7d9a81bb inital start of using javascript to update the appointment link with selected document links 2026-02-06 17:58:02 -05:00
13 changed files with 173 additions and 150 deletions

View File

@@ -64,10 +64,15 @@ class QboController < ApplicationController
def bill
i = Issue.find_by_id params[:id]
if i.customer
i.bill_time
redirect_to i, flash: { notice: I18n.t(:label_billed_success) + i.customer.name }
billed = i.bill_time
if i.bill_time
redirect_to i, flash: { notice: I18n.t( :label_billed_success ) + i.customer.name }
else
redirect_to i, flash: { error: I18n.t(:label_billing_error) }
end
else
redirect_to i, flash: { error: I18n.t(:label_billing_error) }
redirect_to i, flash: { error: I18n.t(:label_billing_error_no_customer) }
end
end

View File

@@ -1,9 +1,9 @@
<%= link_to t(:label_appointment), "https://calendar.google.com/calendar/render?action=TEMPLATE&text=#{@customer.name}+-&details=#{ link_to t(: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 %>
<%= link_to t(:label_appointment), "https://calendar.google.com/calendar/render?action=TEMPLATE&text=#{@customer.name}+-&details=#{ link_to t(:customer_details), "https://#{Setting.host_name}#{customer_path @customer.id}"}%0A#{@customer.primary_phone}%3Cbr/%3E+&dates=#{Time.now.strftime("%Y%m%d")}T090000/#{Time.now.strftime("%Y%m%d")}T170000", target: :_blank, id: :appointment_link %>
<br/>
<br/>
<%= button_to t(:label_create_estimate), "https://qbo.intuit.com/app/estimate?nameId=#{@customer.id}", target: :_blank, method: :get %>
<%= link_to t(:label_create_estimate), "https://qbo.intuit.com/app/estimate?nameId=#{@customer.id}", target: :_blank %>
<br/>
<br/>

View File

@@ -2,6 +2,7 @@
<% estimates.sort.reverse.each do |estimate| %>
<div class="row">
<%= check_box_tag "estimate_ids[]", estimate.id, false, onchange: "updateLink()", data: { url: estimate_path(estimate), text: "Estimate ##{estimate.to_s}" }, class: "estimate-checkbox appointment" %>
<b><%= link_to "##{estimate.doc_number}", estimate_path(estimate), target: :_blank %></b> <%= estimate.txn_date %>
</div>
<% end %>

View File

@@ -12,7 +12,7 @@
<% invoices.sort.reverse.each do |invoice| %>
<div class="row">
<%= check_box_tag "invoice_ids[]", invoice.id, false, class: "invoice-checkbox" if invoices.count > 1 %>
<%= check_box_tag "invoice_ids[]", invoice.id, false, onchange: "updateLink()", data: { url: invoice_path(invoice), text: "Invoice ##{invoice.to_s}" }, class: "invoice-checkbox appointment" if invoices.count > 1 %>
<b><%= link_to "##{invoice.doc_number}", invoice_path(invoice), target: :_blank %></b> <%= invoice.txn_date %>
</div>
<% end %>

View File

@@ -1,3 +1,3 @@
"<div id='footer' align='center'>
<b><%=I18n.translate(:label_last_sync)%>: </b> <%=Qbo.last_sync if Qbo.exists?%>
</div>"
<div id='footer' align='center'>
<%= render partial: 'qbo/last_sync' %>
</div>

View File

@@ -0,0 +1,20 @@
function updateLink() {
console.log("updateLink called");
const linkElement = document.getElementById("appointment_link");
const regex = /((?:<br\/>|%3Cbr\/?%3E))([\s\S]*?)(&dates)/gi;
linkElement.href = linkElement.href.replace(regex, `$1${getSelectedDocs()}$3`);
}
function getSelectedDocs() {
const appointent_extras = document.querySelectorAll('.appointment');
let output = '';
for (const item of appointent_extras) {
if (item.checked) {
console.log(`Checked item: ${item.dataset.text} with URL: ${item.dataset.url}`);
output += `%0A`+ encodeURIComponent(`<a href="${window.location.origin}${item.dataset.url}">${item.dataset.text}</a>`) +`%0A`;
}
}
return output;
}

View File

@@ -11,92 +11,95 @@
# English strings go here for Rails i18n
# Usage I18n.t(:label)
en:
field_customer: "Customer"
field_employee: "Employee"
field_invoice: "Invoice"
field_estimate: "Estimate"
field_notes: "Notes"
button_bulk_pdf: "Bulk PDF"
customer_details: "Customer Details"
field_billed: "Billed"
label_week: "Week"
label_search_estimates: "Search Estimates"
label_search: "Search"
label_estimates: "Estimates"
warn_ru_sure: "You sure?"
label_delete: "Delete"
label_edit: "Edit"
label_year: "Year"
label_make: " Make"
label_model: "Model"
label_no_customers: "There are no customers containing the term(s)"
label_matching: "Matching "
label_open_issues: "Open Issues"
label_closed_issues: "Closed Issues"
label_sync: "Sync"
label_new_customer: "New Customer"
label_search_customers: "Search Customers"
label_customers: "Customers"
label_edit_customer: "Edit Customer"
label_email: "Email"
label_primary_phone: "Primary Phone"
label_mobile_phone: "Mobile Phone"
label_billing_address: "Billing Address"
label_shipping_address: "Shipping Address"
field_customer: "Customer"
field_customers: "Customers"
field_employee: "Employee"
field_estimate: "Estimate"
field_invoice: "Invoice"
field_notes: "Notes"
label_account_balance: "Account Balance"
label_balance_with_jobs: "Balance With Jobs"
label_display_name: "Display Name"
label_details: "Details"
label_customer_link_expires: "This customer link expires in"
label_actions: "Actions"
label_amount: "Amount"
label_deposit_into: "Deposit to Account"
label_last_sync: "Last Sync"
label_redmine_qbo: "Redmine Quickbooks"
label_customer_count: "Customer Count"
label_invoice_count: "Invoice Count"
label_estimate_count: "Estimate Count"
label_employee_count: "Employee Count"
label_appointment: "Add Appointment"
label_balance_with_jobs: "Balance With Jobs"
label_bill_time: "Bill Time"
label_billing_address: "Billing Address"
label_billing_error: "Customer could not be billed. Check for Customer or Assignee and try again."
label_billing_error_no_customer: "Cannot bill without an assigned customer."
label_billed_success: "Successfully billed "
label_client_id: "Intuit QBO OAuth2 Client ID"
label_client_secret: "Intuit QBO OAuth2 Client Secret"
label_webhook_token: "Intuit QBO Webhook Token"
label_oauth_expires: "OAuth2 Access Token Expires At"
label_oauth_note: "Note: You need to authenticate with Quickbooks after saving your key and secret above"
field_customers: "Customers"
label_closed_issues: "Closed Issues"
label_connected: "Successfully connected to QuickBooks"
label_create_estimate: "Create Estimate"
label_customer_count: "Customer Count"
label_customer_link_expires: "This customer link expires in"
label_customers: "Customers"
label_delete: "Delete"
label_deposit_into: "Deposit to Account"
label_details: "Details"
label_display_name: "Display Name"
label_door: "Door"
label_edit: "Edit"
label_edit_customer: "Edit Customer"
label_email: "Email"
label_employee_count: "Employee Count"
label_error: "Error"
label_estimate_404: "Estimate not found"
label_estimate_count: "Estimate Count"
label_estimates: "Estimates"
label_hours: "Hours"
label_invoice_404: "Invoice not found"
label_invoice_count: "Invoice Count"
label_invoices: "Invoices"
label_last_sync: "Last Sync"
label_load_customer: "Load Customer"
label_make: "Make"
label_matching: "Matching"
label_mobile_phone: "Mobile Phone"
label_model: "Model"
label_name: "Name"
label_new_customer: "New Customer"
label_no_customers: "There are no customers matching the search term(s)."
label_no_estimates: "No Estimates"
label_no_invoices: "No Invoices"
label_invoices: "Invoices"
label_load_customer: "Load Customer"
label_door: "Door"
label_trim: "Trim"
label_bill_time: "Bill Time"
label_share: "Share"
label_sync_now: "Sync Now"
label_invoice_404: "Invoice not found"
label_estimate_404: "Estimate not found"
label_connected: "Successfully connected to Quickbooks"
label_error: "Error"
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"
label_oauth2_refresh_token_expires_at: "Refresh Token Expires At"
label_name: "Name"
label_appointment: "Add Appointment"
label_actions: "Actions"
label_create_estimate: "Create Estimate"
label_syncing: "Syncing Quickbooks"
label_oauth_expires: "OAuth2 Access Token Expires At"
label_oauth_note: "Note: You need to authenticate with QuickBooks after saving your key and secret above."
label_open_issues: "Open Issues"
label_primary_phone: "Primary Phone"
label_qbo_sync_success: "Successfully synced to QuickBooks"
label_redmine_qbo: "Redmine QuickBooks"
label_sandbox: "Sandbox"
button_bulk_pdf: "Bulk PDF"
label_search: "Search"
label_search_customers: "Search Customers"
label_search_estimates: "Search Estimates"
label_select_all: "Select All"
notice_customer_created: "Customer created in Quickbooks"
notice_customer_updated: "Customer updated in Quickbooks"
notice_customer_not_found: "Customer not found in Quickbooks"
notice_customer_not_deleted: "Customer could not be deleted in Quickbooks"
notice_customer_deleted: "Customer deleted in Quickbooks"
notice_estimate_created: "Estimate created in Quickbooks"
notice_estimate_updated: "Estimate updated in Quickbooks"
label_share: "Share"
label_shipping_address: "Shipping Address"
label_sync: "Sync"
label_sync_now: "Sync Now"
label_syncing: "Syncing QuickBooks"
label_trim: "Trim"
label_webhook_token: "Intuit QBO Webhook Token"
label_week: "Week"
label_year: "Year"
notice_customer_created: "Customer created in QuickBooks"
notice_customer_deleted: "Customer deleted in QuickBooks"
notice_customer_not_deleted: "Customer could not be deleted in QuickBooks"
notice_customer_not_found: "Customer not found in QuickBooks"
notice_customer_updated: "Customer updated in QuickBooks"
notice_error_project_nil: "The issue's project is nil. Set project to:"
notice_error_tracker_nil: "The issue's tracker is nil. Set tracker to:"
notice_estimate_created: "Estimate created in QuickBooks"
notice_estimate_not_found: "Estimate not found"
notice_invoice_created: "Invoice created in Quickbooks"
notice_invoice_updated: "Invoice updated in Quickbooks"
notice_estimate_updated: "Estimate updated in QuickBooks"
notice_forbidden: "You do not have permission to access this resource."
notice_invoice_created: "Invoice created in QuickBooks"
notice_invoice_not_found: "Invoice not found"
notice_forbidden: "You do not have permission to access this resource"
notice_invoice_updated: "Invoice updated in QuickBooks"
notice_issue_not_found: "Issue not found"
customer_details: "Customer Details"
warn_ru_sure: "Are you sure?"

View File

@@ -14,7 +14,7 @@ Redmine::Plugin.register :redmine_qbo do
name 'Redmine QBO plugin'
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'
version '2026.1.9'
version '2026.2.7'
url 'https://github.com/rickbarrette/redmine_qbo'
author_url 'https://barrettefabrication.com'
settings default: {empty: true}, partial: 'qbo/settings'

View File

@@ -10,16 +10,19 @@
module RedmineQbo
module Hooks
class IssuesHookListener < Redmine::Hook::ViewListener
class IssuesFormHookListener < Redmine::Hook::ViewListener
include IssuesHelper
include IssuesHelper
# 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={})
Rails.logger.debug "RedmineQbo::Hooks::IssuesHookListener.view_issues_form_details_bottom: Building form components for quickbooks customer, estimate, and invoice data"
f = context[:form]
issue = context[:issue]
project = context[:project]
Rails.logger.debug issue.inspect
Rails.logger.debug project.inspect
# Customer Name Text Box with database backed autocomplete
# onchange event will update the hidden customer_id field
@@ -31,11 +34,23 @@ module RedmineQbo
value: '#issue_customer'
}
# We need to handle 3 cases for the onchange event of the customer name field:
# 1. New issue Withough project: /issues/new.js
# 2. New issue With project: /projects/rmt/issues/new.js
# 3. Existing issue: /issues/<ID>/edit.js
# The built in helper update_issue_form_path requires a project object to determine the correct path for new vs existing issues,
# but it doesn't work for issue.project when creating new issues not in a project i.e. http://redmine.domain.com/issues/new .
# So we need to figure out how to get a the @project from the controller calling the hook.
#
# If this is not handled correctly, it leads to a 422 error when creating a new issue and selecting a customer.
js_path = "updateIssueFrom('#{escape_javascript update_issue_form_path(project, issue)}', this)"
Rails.logger.debug js_path
# This hidden field is used for the customer ID for the issue
# the onchange event will reload the issue form via ajax to update the available estimates
customer_id = f.hidden_field :customer_id,
id: "issue_customer_id",
onchange: "updateIssueFrom('#{escape_javascript update_issue_form_path(issue.project, issue)}', this)".html_safe
onchange: js_path.html_safe
# Generate the drop down list of quickbooks estimates owned by the selected customer
select_estimate = f.select :estimate_id,
@@ -54,7 +69,31 @@ module RedmineQbo
}
)
end
end
end
# View Issue
# Displays the quickbooks customer, estimate, & invoices attached to the issue
def view_issues_show_details_bottom(context={})
issue = context[:issue]
# Build a list of invoice links
invoice_link = ""
if issue.invoices
issue.invoices.each do |i|
invoice_link += "#{link_to i, i, target: :_blank}<br/>"
end
end
context[:controller].send(:render_to_string, {
partial: 'issues/show_details',
locals: {
customer: issue.customer ? link_to(issue.customer) : nil,
estimate_link: issue.estimate ? link_to(issue.estimate, issue.estimate, target: :_blank) : nil,
invoice_link: invoice_link.html_safe,
issue: issue
}
})
end
end
end
end

View File

@@ -1,43 +0,0 @@
#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.
module RedmineQbo
module Hooks
class IssuesShowHookListener < Redmine::Hook::ViewListener
# View Issue
# Displays the quickbooks customer, estimate, & invoices attached to the issue
def view_issues_show_details_bottom(context={})
issue = context[:issue]
# Build a list of invoice links
invoice_link = ""
if issue.invoices
issue.invoices.each do |i|
invoice_link += "#{link_to i, i, target: :_blank}<br/>"
end
end
context[:controller].send(:render_to_string, {
partial: 'issues/show_details',
locals: {
customer: issue.customer ? link_to(issue.customer) : nil,
estimate_link: issue.estimate ? link_to(issue.estimate, issue.estimate, target: :_blank) : nil,
invoice_link: invoice_link.html_safe,
issue: issue
}
})
end
end
end
end

View File

@@ -15,10 +15,11 @@ module RedmineQbo
# 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
safe_join([
javascript_include_tag( 'application.js', plugin: :redmine_qbo),
javascript_include_tag( 'autocomplete-rails.js', plugin: :redmine_qbo),
javascript_include_tag( 'checkbox_controller.js', plugin: :redmine_qbo)
])
end
render_on :view_layouts_base_sidebar, partial: "qbo/sidebar"

View File

@@ -44,10 +44,9 @@ module RedmineQbo
# Create billable time entries
def bill_time
logger.debug "QBO: Billing time for issue ##{id}"
return unless status.is_closed?
return if assigned_to.nil?
return unless Qbo.first
return unless customer
return false if assigned_to.nil?
return false unless Qbo.first
return false unless customer
Thread.new do
spent_time = time_entries.where(billed: [false, nil])
@@ -102,6 +101,7 @@ module RedmineQbo
end
end
end
return true
end
end

View File

@@ -15,22 +15,19 @@ module RedmineQbo
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 = 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
def self.included(base)
base.class_eval do
helper Helper
end
end
end
end
end
# Add module to IssuessController
IssuesController.send(:include, IssuesControllerPatch)