Compare commits

..

45 Commits

Author SHA1 Message Date
e7dfc3f2ad added sync estimates by id 2026-02-14 08:25:02 -05:00
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
5d7d9a81bb inital start of using javascript to update the appointment link with selected document links 2026-02-06 17:58:02 -05:00
14 changed files with 191 additions and 155 deletions

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

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

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/>
<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/>
<br/> <br/>

View File

@@ -2,6 +2,7 @@
<% estimates.sort.reverse.each do |estimate| %> <% estimates.sort.reverse.each do |estimate| %>
<div class="row"> <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 %> <b><%= link_to "##{estimate.doc_number}", estimate_path(estimate), target: :_blank %></b> <%= estimate.txn_date %>
</div> </div>
<% end %> <% end %>

View File

@@ -12,7 +12,7 @@
<% invoices.sort.reverse.each do |invoice| %> <% invoices.sort.reverse.each do |invoice| %>
<div class="row"> <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 %> <b><%= link_to "##{invoice.doc_number}", invoice_path(invoice), target: :_blank %></b> <%= invoice.txn_date %>
</div> </div>
<% end %> <% end %>

View File

@@ -1,3 +1,3 @@
"<div id='footer' align='center'> <div id='footer' align='center'>
<b><%=I18n.translate(:label_last_sync)%>: </b> <%=Qbo.last_sync if Qbo.exists?%> <%= render partial: 'qbo/last_sync' %>
</div>" </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 # English strings go here for Rails i18n
# Usage I18n.t(:label) # Usage I18n.t(:label)
en: en:
field_customer: "Customer" button_bulk_pdf: "Bulk PDF"
field_employee: "Employee" customer_details: "Customer Details"
field_invoice: "Invoice"
field_estimate: "Estimate"
field_notes: "Notes"
field_billed: "Billed" field_billed: "Billed"
label_week: "Week" field_customer: "Customer"
label_search_estimates: "Search Estimates" field_customers: "Customers"
label_search: "Search" field_employee: "Employee"
label_estimates: "Estimates" field_estimate: "Estimate"
warn_ru_sure: "You sure?" field_invoice: "Invoice"
label_delete: "Delete" field_notes: "Notes"
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"
label_account_balance: "Account Balance" label_account_balance: "Account Balance"
label_balance_with_jobs: "Balance With Jobs" label_actions: "Actions"
label_display_name: "Display Name"
label_details: "Details"
label_customer_link_expires: "This customer link expires in"
label_amount: "Amount" label_amount: "Amount"
label_deposit_into: "Deposit to Account" label_appointment: "Add Appointment"
label_last_sync: "Last Sync" label_balance_with_jobs: "Balance With Jobs"
label_redmine_qbo: "Redmine Quickbooks" label_bill_time: "Bill Time"
label_customer_count: "Customer Count" label_billing_address: "Billing Address"
label_invoice_count: "Invoice Count" label_billing_error: "Customer could not be billed. Check for Customer or Assignee and try again."
label_estimate_count: "Estimate Count" label_billing_error_no_customer: "Cannot bill without an assigned customer."
label_employee_count: "Employee Count" label_billed_success: "Successfully billed "
label_client_id: "Intuit QBO OAuth2 Client ID" label_client_id: "Intuit QBO OAuth2 Client ID"
label_client_secret: "Intuit QBO OAuth2 Client Secret" label_client_secret: "Intuit QBO OAuth2 Client Secret"
label_webhook_token: "Intuit QBO Webhook Token" label_closed_issues: "Closed Issues"
label_oauth_expires: "OAuth2 Access Token Expires At" label_connected: "Successfully connected to QuickBooks"
label_oauth_note: "Note: You need to authenticate with Quickbooks after saving your key and secret above" label_create_estimate: "Create Estimate"
field_customers: "Customers" 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_estimates: "No Estimates"
label_no_invoices: "No Invoices" 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_oauth2_refresh_token_expires_at: "Refresh Token Expires At"
label_name: "Name" label_oauth_expires: "OAuth2 Access Token Expires At"
label_appointment: "Add Appointment" label_oauth_note: "Note: You need to authenticate with QuickBooks after saving your key and secret above."
label_actions: "Actions" label_open_issues: "Open Issues"
label_create_estimate: "Create Estimate" label_primary_phone: "Primary Phone"
label_syncing: "Syncing Quickbooks" label_qbo_sync_success: "Successfully synced to QuickBooks"
label_redmine_qbo: "Redmine QuickBooks"
label_sandbox: "Sandbox" 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" label_select_all: "Select All"
notice_customer_created: "Customer created in Quickbooks" label_share: "Share"
notice_customer_updated: "Customer updated in Quickbooks" label_shipping_address: "Shipping Address"
notice_customer_not_found: "Customer not found in Quickbooks" label_sync: "Sync"
notice_customer_not_deleted: "Customer could not be deleted in Quickbooks" label_sync_now: "Sync Now"
notice_customer_deleted: "Customer deleted in Quickbooks" label_syncing: "Syncing QuickBooks"
notice_estimate_created: "Estimate created in Quickbooks" label_trim: "Trim"
notice_estimate_updated: "Estimate updated in Quickbooks" 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_estimate_not_found: "Estimate not found"
notice_invoice_created: "Invoice created in Quickbooks" notice_estimate_updated: "Estimate updated in QuickBooks"
notice_invoice_updated: "Invoice 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_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" 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' 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.0' version '2026.2.7'
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

@@ -10,16 +10,19 @@
module RedmineQbo module RedmineQbo
module Hooks module Hooks
class IssuesHookListener < Redmine::Hook::ViewListener
class IssuesFormHookListener < Redmine::Hook::ViewListener
include IssuesHelper include IssuesHelper
# Edit Issue Form # Edit Issue Form
# Here we build the required form components before passing them to a partial view formatting. # Here we build the required form components before passing them to a partial view formatting.
def view_issues_form_details_bottom(context={}) 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] f = context[:form]
issue = context[:issue] issue = context[:issue]
project = context[:project]
Rails.logger.debug issue.inspect
Rails.logger.debug project.inspect
# Customer Name Text Box with database backed autocomplete # Customer Name Text Box with database backed autocomplete
# onchange event will update the hidden customer_id field # onchange event will update the hidden customer_id field
@@ -31,11 +34,23 @@ module RedmineQbo
value: '#issue_customer' 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 # 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 # the onchange event will reload the issue form via ajax to update the available estimates
customer_id = f.hidden_field :customer_id, customer_id = f.hidden_field :customer_id,
id: "issue_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 # Generate the drop down list of quickbooks estimates owned by the selected customer
select_estimate = f.select :estimate_id, select_estimate = f.select :estimate_id,
@@ -54,7 +69,31 @@ module RedmineQbo
} }
) )
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
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 # Load the javascript to support the autocomplete forms
def view_layouts_base_html_head(context = {}) def view_layouts_base_html_head(context = {})
js = javascript_include_tag 'application.js', plugin: :redmine_qbo safe_join([
js += javascript_include_tag 'autocomplete-rails.js', plugin: :redmine_qbo javascript_include_tag( 'application.js', plugin: :redmine_qbo),
js += javascript_include_tag 'checkbox_controller.js', plugin: :redmine_qbo javascript_include_tag( 'autocomplete-rails.js', plugin: :redmine_qbo),
return js javascript_include_tag( 'checkbox_controller.js', plugin: :redmine_qbo)
])
end end
render_on :view_layouts_base_sidebar, partial: "qbo/sidebar" render_on :view_layouts_base_sidebar, partial: "qbo/sidebar"

View File

@@ -44,10 +44,9 @@ 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 ##{id}"
return unless status.is_closed? return false if assigned_to.nil?
return if assigned_to.nil? return false unless Qbo.first
return unless Qbo.first return false unless customer
return unless customer
Thread.new do Thread.new do
spent_time = time_entries.where(billed: [false, nil]) spent_time = time_entries.where(billed: [false, nil])
@@ -102,6 +101,7 @@ module RedmineQbo
end end
end end
end end
return true
end end
end end

View File

@@ -15,21 +15,18 @@ module RedmineQbo
module Helper module Helper
def watcher_link(issue, user) def watcher_link(issue, user)
link = +'' 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_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 << 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 link.html_safe + super
end end
end end
def self.included(base) def self.included(base)
base.class_eval do base.class_eval do
helper Helper helper Helper
end end
end end
end end
# Add module to IssuessController # Add module to IssuessController