mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2026-02-13 01:03:59 -05:00
refoactored hooks and patches under RedmineQbo module
This commit is contained in:
@@ -1,58 +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 Hooks
|
|
||||||
|
|
||||||
class IssuesFormHookListener < Redmine::Hook::ViewListener
|
|
||||||
|
|
||||||
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={})
|
|
||||||
f = context[:form]
|
|
||||||
issue = context[:issue]
|
|
||||||
|
|
||||||
# Customer Name Text Box with database backed autocomplete
|
|
||||||
# onchange event will update the hidden customer_id field
|
|
||||||
search_customer = f.autocomplete_field :customer,
|
|
||||||
autocomplete_customer_name_customers_path,
|
|
||||||
selected: issue.customer,
|
|
||||||
update_elements: {
|
|
||||||
id: '#issue_customer_id',
|
|
||||||
value: '#issue_customer'
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Generate the drop down list of quickbooks estimates owned by the selected customer
|
|
||||||
select_estimate = f.select :estimate_id,
|
|
||||||
issue.customer ? issue.customer.estimates.pluck(:doc_number, :id).sort! {|x, y| y <=> x} : [],
|
|
||||||
selected: issue.estimate ? issue.estimate.id : nil,
|
|
||||||
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,
|
|
||||||
select_estimate: select_estimate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,122 +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.
|
|
||||||
|
|
||||||
require_dependency 'issue'
|
|
||||||
|
|
||||||
module Patches
|
|
||||||
|
|
||||||
# Patches Redmine's Issues dynamically.
|
|
||||||
# Adds relationships for customers, estimates, invoices, customer_tokens
|
|
||||||
# Adds before and after save hooks
|
|
||||||
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
|
|
||||||
before_save :titlize_subject
|
|
||||||
after_save :bill_time
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
module ClassMethods
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
module InstanceMethods
|
|
||||||
|
|
||||||
# Create billable time entries
|
|
||||||
def bill_time
|
|
||||||
logger.debug "QBO: Billing time for issue ##{id} - #{subject}"
|
|
||||||
return unless status.is_closed?
|
|
||||||
return if assigned_to.nil?
|
|
||||||
return unless Qbo.first
|
|
||||||
return unless customer
|
|
||||||
|
|
||||||
Thread.new do
|
|
||||||
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
|
|
||||||
end
|
|
||||||
|
|
||||||
# Create a shareable link for a customer
|
|
||||||
def share_token
|
|
||||||
CustomerToken.get_token self
|
|
||||||
end
|
|
||||||
|
|
||||||
# Titleize the subject before save
|
|
||||||
def titlize_subject
|
|
||||||
self.subject = self.subject.titleize
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
# Add module to Issue
|
|
||||||
Issue.send(:include, IssuePatch)
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,276 +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.
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
logger.debug "Calling :pdf_left hook"
|
|
||||||
left_hook_output = Redmine::Hook.call_hook :pdf_left, { issue: issue }
|
|
||||||
unless left_hook_output.nil?
|
|
||||||
left_hook_output.each do |l|
|
|
||||||
left.concat l unless l.nil?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
logger.debug "Calling :pdf_right hook"
|
|
||||||
right_hook_output = Redmine::Hook.call_hook :pdf_right, { issue: issue }
|
|
||||||
unless right_hook_output.nil?
|
|
||||||
right_hook_output.each do |r|
|
|
||||||
right.concat r unless r.nil?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
end
|
|
||||||
|
|
||||||
Redmine::Export::PDF::IssuesPdfHelper.send(:include, PdfPatch)
|
|
||||||
|
|
||||||
end
|
|
||||||
60
lib/redmine_qbo/hooks/issues_form_hook_listener.rb
Normal file
60
lib/redmine_qbo/hooks/issues_form_hook_listener.rb
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#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 IssuesFormHookListener < Redmine::Hook::ViewListener
|
||||||
|
|
||||||
|
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={})
|
||||||
|
f = context[:form]
|
||||||
|
issue = context[:issue]
|
||||||
|
|
||||||
|
# Customer Name Text Box with database backed autocomplete
|
||||||
|
# onchange event will update the hidden customer_id field
|
||||||
|
search_customer = f.autocomplete_field :customer,
|
||||||
|
autocomplete_customer_name_customers_path,
|
||||||
|
selected: issue.customer,
|
||||||
|
update_elements: {
|
||||||
|
id: '#issue_customer_id',
|
||||||
|
value: '#issue_customer'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Generate the drop down list of quickbooks estimates owned by the selected customer
|
||||||
|
select_estimate = f.select :estimate_id,
|
||||||
|
issue.customer ? issue.customer.estimates.pluck(:doc_number, :id).sort! {|x, y| y <=> x} : [],
|
||||||
|
selected: issue.estimate ? issue.estimate.id : nil,
|
||||||
|
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,
|
||||||
|
select_estimate: select_estimate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,34 +8,36 @@
|
|||||||
#
|
#
|
||||||
#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 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
|
module RedmineQbo
|
||||||
|
module Hooks
|
||||||
|
|
||||||
class IssuesShowHookListener < Redmine::Hook::ViewListener
|
class IssuesShowHookListener < Redmine::Hook::ViewListener
|
||||||
|
|
||||||
# View Issue
|
# View Issue
|
||||||
# Displays the quickbooks customer, estimate, & invoices attached to the issue
|
# Displays the quickbooks customer, estimate, & invoices attached to the issue
|
||||||
def view_issues_show_details_bottom(context={})
|
def view_issues_show_details_bottom(context={})
|
||||||
issue = context[:issue]
|
issue = context[:issue]
|
||||||
|
|
||||||
# Build a list of invoice links
|
# Build a list of invoice links
|
||||||
invoice_link = ""
|
invoice_link = ""
|
||||||
if issue.invoices
|
if issue.invoices
|
||||||
issue.invoices.each do |i|
|
issue.invoices.each do |i|
|
||||||
invoice_link += "#{link_to i, i, target: :_blank}<br/>"
|
invoice_link += "#{link_to i, i, target: :_blank}<br/>"
|
||||||
|
end
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
@@ -8,30 +8,24 @@
|
|||||||
#
|
#
|
||||||
#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 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 'time_entry_query'
|
module RedmineQbo
|
||||||
|
module Hooks
|
||||||
|
|
||||||
module Patches
|
class UsersShowHookListener < Redmine::Hook::ViewListener
|
||||||
|
|
||||||
module TimeEntryQueryPatch
|
# View User
|
||||||
|
def view_users_form(context={})
|
||||||
|
|
||||||
# Add QBO options to columns
|
# Update the users
|
||||||
def available_columns
|
#Employee.update_all
|
||||||
unless @available_columns
|
|
||||||
@available_columns = self.class.available_columns.dup
|
# Check to see if there is a quickbooks user attached to the issue
|
||||||
@available_columns << QueryColumn.new(:billed, sortable: "#{TimeEntry.table_name}.name", groupable: true, caption: :field_billed)
|
@selected = context[:user].employee.id if context[:user].employee
|
||||||
|
|
||||||
|
# Generate the drop down list of quickbooks contacts
|
||||||
|
return "<p>#{context[:form].select :employee_id, Employee.all.pluck(:name, :id), selected: @selected, include_blank: true}</p>"
|
||||||
end
|
end
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
# Add QBO options to the filter
|
|
||||||
def initialize_available_filters
|
|
||||||
add_available_filter "billed", type: :boolean
|
|
||||||
super
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add module to TimeEntryQuery
|
|
||||||
TimeEntryQuery.send(:prepend, QueryPatch)
|
|
||||||
|
|
||||||
end
|
end
|
||||||
@@ -8,20 +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.
|
#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
|
module RedmineQbo
|
||||||
|
module Hooks
|
||||||
|
|
||||||
class ViewHookListener < Redmine::Hook::ViewListener
|
class ViewHookListener < Redmine::Hook::ViewListener
|
||||||
|
|
||||||
# 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
|
js = javascript_include_tag 'application.js', plugin: :redmine_qbo
|
||||||
js += javascript_include_tag 'autocomplete-rails.js', plugin: :redmine_qbo
|
js += javascript_include_tag 'autocomplete-rails.js', plugin: :redmine_qbo
|
||||||
js += javascript_include_tag 'checkbox_controller.js', plugin: :redmine_qbo
|
js += javascript_include_tag 'checkbox_controller.js', plugin: :redmine_qbo
|
||||||
return js
|
return js
|
||||||
|
end
|
||||||
|
|
||||||
|
render_on :view_layouts_base_sidebar, partial: "qbo/sidebar"
|
||||||
|
render_on :view_layouts_base_body_bottom, partial: "qbo/footer"
|
||||||
end
|
end
|
||||||
|
|
||||||
render_on :view_layouts_base_sidebar, partial: "qbo/sidebar"
|
|
||||||
render_on :view_layouts_base_body_bottom, partial: "qbo/footer"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
@@ -10,37 +10,39 @@
|
|||||||
|
|
||||||
require_dependency 'attachments_controller'
|
require_dependency 'attachments_controller'
|
||||||
|
|
||||||
module Patches
|
module RedmineQbo
|
||||||
|
module Patches
|
||||||
|
|
||||||
module AttachmentsControllerPatch
|
module AttachmentsControllerPatch
|
||||||
|
|
||||||
def self.included(base)
|
def self.included(base)
|
||||||
|
|
||||||
base.class_eval do
|
base.class_eval do
|
||||||
|
|
||||||
# check if login is globally required to access the application
|
# check if login is globally required to access the application
|
||||||
def check_if_login_required
|
def check_if_login_required
|
||||||
# no check needed if user is already logged in
|
# no check needed if user is already logged in
|
||||||
return true if User.current.logged?
|
return true if User.current.logged?
|
||||||
|
|
||||||
# Pull up the attachmet, & verify if we have a valid token for the Issue
|
# Pull up the attachmet, & verify if we have a valid token for the Issue
|
||||||
attachment = Attachment.find(params[:id])
|
attachment = Attachment.find(params[:id])
|
||||||
token = CustomerToken.where("token = ? and expires_at > ?", session[:token], Time.now)
|
token = CustomerToken.where("token = ? and expires_at > ?", session[:token], Time.now)
|
||||||
token = token.first
|
token = token.first
|
||||||
unless token.nil?
|
unless token.nil?
|
||||||
return true if token.issue_id == attachment.container_id
|
return true if token.issue_id == attachment.container_id
|
||||||
|
end
|
||||||
|
|
||||||
|
require_login if Setting.login_required?
|
||||||
end
|
end
|
||||||
|
|
||||||
require_login if Setting.login_required?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Add module to AttachmentsController
|
||||||
|
AttachmentsController.send(:include, AttachmentsControllerPatch)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add module to AttachmentsController
|
|
||||||
AttachmentsController.send(:include, AttachmentsControllerPatch)
|
|
||||||
|
|
||||||
end
|
end
|
||||||
124
lib/redmine_qbo/patches/issue_patch.rb
Normal file
124
lib/redmine_qbo/patches/issue_patch.rb
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#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.
|
||||||
|
|
||||||
|
require_dependency 'issue'
|
||||||
|
|
||||||
|
module RedmineQbo
|
||||||
|
module Patches
|
||||||
|
|
||||||
|
# Patches Redmine's Issues dynamically.
|
||||||
|
# Adds relationships for customers, estimates, invoices, customer_tokens
|
||||||
|
# Adds before and after save hooks
|
||||||
|
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
|
||||||
|
before_save :titlize_subject
|
||||||
|
after_save :bill_time
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
module InstanceMethods
|
||||||
|
|
||||||
|
# Create billable time entries
|
||||||
|
def bill_time
|
||||||
|
logger.debug "QBO: Billing time for issue ##{id} - #{subject}"
|
||||||
|
return unless status.is_closed?
|
||||||
|
return if assigned_to.nil?
|
||||||
|
return unless Qbo.first
|
||||||
|
return unless customer
|
||||||
|
|
||||||
|
Thread.new do
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create a shareable link for a customer
|
||||||
|
def share_token
|
||||||
|
CustomerToken.get_token self
|
||||||
|
end
|
||||||
|
|
||||||
|
# Titleize the subject before save
|
||||||
|
def titlize_subject
|
||||||
|
self.subject = self.subject.titleize
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
# Add module to Issue
|
||||||
|
Issue.send(:include, IssuePatch)
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,30 +9,31 @@
|
|||||||
#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 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'
|
require_dependency 'issues_controller'
|
||||||
|
|
||||||
module Patches
|
module RedmineQbo
|
||||||
|
module Patches
|
||||||
|
module IssuesControllerPatch
|
||||||
|
|
||||||
module IssuesControllerPatch
|
module Helper
|
||||||
|
def watcher_link(issue, user)
|
||||||
module Helper
|
link = +''
|
||||||
def watcher_link(issue, user)
|
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_share), share_path( issue.id ), method: :get, target: :_blank, class: 'icon icon-shared') if user.logged?
|
||||||
link << link_to(I18n.t(:label_bill_time), bill_path( issue.id ), method: :get, class: 'icon icon-email-add') if user.admin?
|
link.html_safe + super
|
||||||
link << link_to(I18n.t(:label_share), share_path( issue.id ), method: :get, target: :_blank, class: 'icon icon-shared') if user.logged?
|
end
|
||||||
link.html_safe + super
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def self.included(base)
|
def self.included(base)
|
||||||
|
|
||||||
|
base.class_eval do
|
||||||
|
helper Helper
|
||||||
|
end
|
||||||
|
|
||||||
base.class_eval do
|
|
||||||
helper Helper
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Add module to IssuessController
|
||||||
|
IssuesController.send(:include, IssuesControllerPatch)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add module to IssuessController
|
|
||||||
IssuesController.send(:include, IssuesControllerPatch)
|
|
||||||
|
|
||||||
end
|
end
|
||||||
277
lib/redmine_qbo/patches/pdf_patch.rb
Normal file
277
lib/redmine_qbo/patches/pdf_patch.rb
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
#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.
|
||||||
|
|
||||||
|
require_dependency 'redmine/export/pdf'
|
||||||
|
require_dependency 'redmine/export/pdf/issues_pdf_helper'
|
||||||
|
|
||||||
|
module RedmineQbo
|
||||||
|
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)
|
||||||
|
|
||||||
|
logger.debug "Calling :pdf_left hook"
|
||||||
|
left_hook_output = Redmine::Hook.call_hook :pdf_left, { issue: issue }
|
||||||
|
unless left_hook_output.nil?
|
||||||
|
left_hook_output.each do |l|
|
||||||
|
left.concat l unless l.nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
logger.debug "Calling :pdf_right hook"
|
||||||
|
right_hook_output = Redmine::Hook.call_hook :pdf_right, { issue: issue }
|
||||||
|
unless right_hook_output.nil?
|
||||||
|
right_hook_output.each do |r|
|
||||||
|
right.concat r unless r.nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
end
|
||||||
|
|
||||||
|
Redmine::Export::PDF::IssuesPdfHelper.send(:include, PdfPatch)
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,29 +10,30 @@
|
|||||||
|
|
||||||
require_dependency 'issue_query'
|
require_dependency 'issue_query'
|
||||||
|
|
||||||
module Patches
|
module RedmineQbo
|
||||||
|
module Patches
|
||||||
|
module QueryPatch
|
||||||
|
|
||||||
module QueryPatch
|
# Add qbo options to the aviable columns
|
||||||
|
def available_columns
|
||||||
# Add qbo options to the aviable columns
|
unless @available_columns
|
||||||
def available_columns
|
@available_columns = self.class.available_columns.dup
|
||||||
unless @available_columns
|
@available_columns << QueryColumn.new(:customer, sortable: "#{Issue.table_name}.customer_id", groupable: true, caption: :field_customer)
|
||||||
@available_columns = self.class.available_columns.dup
|
@available_columns << QueryColumn.new(:billed, sortable: "#{TimeEntry.table_name}.billed", groupable: true, caption: :field_billed)
|
||||||
@available_columns << QueryColumn.new(:customer, sortable: "#{Issue.table_name}.customer_id", groupable: true, caption: :field_customer)
|
end
|
||||||
@available_columns << QueryColumn.new(:billed, sortable: "#{TimeEntry.table_name}.billed", groupable: true, caption: :field_billed)
|
super
|
||||||
end
|
end
|
||||||
super
|
|
||||||
|
# Add customers to filters
|
||||||
|
def initialize_available_filters
|
||||||
|
#add_available_filter "customer", type: :text
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add customers to filters
|
# Add module to Issue
|
||||||
def initialize_available_filters
|
IssueQuery.send(:prepend, QueryPatch)
|
||||||
#add_available_filter "customer", type: :text
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add module to Issue
|
|
||||||
IssueQuery.send(:prepend, QueryPatch)
|
|
||||||
|
|
||||||
end
|
end
|
||||||
@@ -8,22 +8,31 @@
|
|||||||
#
|
#
|
||||||
#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 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
|
require_dependency 'time_entry_query'
|
||||||
|
|
||||||
class UsersShowHookListener < Redmine::Hook::ViewListener
|
module RedmineQbo
|
||||||
|
module Patches
|
||||||
|
module TimeEntryQueryPatch
|
||||||
|
|
||||||
# View User
|
# Add QBO options to columns
|
||||||
def view_users_form(context={})
|
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
|
||||||
|
|
||||||
# Update the users
|
# Add QBO options to the filter
|
||||||
#Employee.update_all
|
def initialize_available_filters
|
||||||
|
add_available_filter "billed", type: :boolean
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
# 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 "<p>#{context[:form].select :employee_id, Employee.all.pluck(:name, :id), selected: @selected, include_blank: true}</p>"
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
|
# Add module to TimeEntryQuery
|
||||||
|
TimeEntryQuery.send(:prepend, QueryPatch)
|
||||||
|
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -10,33 +10,35 @@
|
|||||||
|
|
||||||
require_dependency 'user'
|
require_dependency 'user'
|
||||||
|
|
||||||
module Patches
|
module RedmineQbo
|
||||||
|
module Patches
|
||||||
|
|
||||||
# Patches Redmine's User dynamically.
|
# Patches Redmine's User dynamically.
|
||||||
# Adds a relationships
|
# Adds a relationships
|
||||||
module UserPatch
|
module UserPatch
|
||||||
def self.included(base) # :nodoc:
|
def self.included(base) # :nodoc:
|
||||||
base.extend(ClassMethods)
|
base.extend(ClassMethods)
|
||||||
|
|
||||||
base.send(:include, InstanceMethods)
|
base.send(:include, InstanceMethods)
|
||||||
|
|
||||||
# Same as typing in the class
|
# Same as typing in the class
|
||||||
base.class_eval do
|
base.class_eval do
|
||||||
belongs_to :employee, primary_key: :id
|
belongs_to :employee, primary_key: :id
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
module ClassMethods
|
module ClassMethods
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
module InstanceMethods
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
module InstanceMethods
|
# Add module to Issue
|
||||||
|
User.send(:include, UserPatch)
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add module to Issue
|
|
||||||
User.send(:include, UserPatch)
|
|
||||||
|
|
||||||
end
|
end
|
||||||
Reference in New Issue
Block a user