redmine_rate 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYRIGHT.txt +18 -0
- data/CREDITS.txt +5 -0
- data/GPL.txt +339 -0
- data/README.rdoc +93 -0
- data/Rakefile +37 -0
- data/VERSION +1 -0
- data/app/controllers/rate_caches_controller.rb +35 -0
- data/app/controllers/rates_controller.rb +154 -0
- data/app/models/rate.rb +161 -0
- data/app/views/rate_caches/index.html.erb +21 -0
- data/app/views/rates/_form.html.erb +36 -0
- data/app/views/rates/_list.html.erb +42 -0
- data/app/views/rates/create.js.rjs +2 -0
- data/app/views/rates/create_error.js.rjs +5 -0
- data/app/views/rates/edit.html.erb +3 -0
- data/app/views/rates/index.html.erb +5 -0
- data/app/views/rates/new.html.erb +3 -0
- data/app/views/rates/show.html.erb +23 -0
- data/app/views/users/_membership_rate.html.erb +23 -0
- data/app/views/users/_rates.html.erb +17 -0
- data/assets/images/database_refresh.png +0 -0
- data/config/locales/de.yml +18 -0
- data/config/locales/en.yml +18 -0
- data/config/locales/fr.yml +20 -0
- data/config/locales/ru.yml +9 -0
- data/config/routes.rb +4 -0
- data/init.rb +49 -0
- data/lang/de.yml +9 -0
- data/lang/en.yml +9 -0
- data/lang/fr.yml +8 -0
- data/lib/rate_conversion.rb +16 -0
- data/lib/rate_memberships_hook.rb +15 -0
- data/lib/rate_project_hook.rb +106 -0
- data/lib/rate_sort_helper_patch.rb +102 -0
- data/lib/rate_time_entry_patch.rb +66 -0
- data/lib/rate_users_helper_patch.rb +37 -0
- data/lib/redmine_rate/hooks/plugin_timesheet_view_timesheets_report_header_tags_hook.rb +11 -0
- data/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_group_header_hook.rb +9 -0
- data/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_hook.rb +18 -0
- data/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_sum_hook.rb +17 -0
- data/lib/redmine_rate/hooks/plugin_timesheet_views_timesheets_time_entry_row_class_hook.rb +21 -0
- data/lib/redmine_rate/hooks/timesheet_hook_helper.rb +14 -0
- data/lib/redmine_rate/hooks/view_layouts_base_html_head_hook.rb +9 -0
- data/lib/tasks/cache.rake +13 -0
- data/lib/tasks/data.rake +163 -0
- data/rails/init.rb +1 -0
- data/test/functional/rates_controller_test.rb +401 -0
- data/test/integration/admin_panel_test.rb +81 -0
- data/test/integration/routing_test.rb +16 -0
- data/test/test_helper.rb +43 -0
- data/test/unit/lib/rate_time_entry_patch_test.rb +77 -0
- data/test/unit/lib/rate_users_helper_patch_test.rb +37 -0
- data/test/unit/lib/redmine_rate/hooks/plugin_timesheet_view_timesheets_report_header_tags_hook_test.rb +26 -0
- data/test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_group_header_hook_test.rb +26 -0
- data/test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_hook_test.rb +47 -0
- data/test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_sum_hook_test.rb +48 -0
- data/test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheets_time_entry_row_class_hook_test.rb +60 -0
- data/test/unit/rate_for_test.rb +74 -0
- data/test/unit/rate_test.rb +333 -0
- metadata +137 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
class RateConversion
|
2
|
+
RoundTo = 10
|
3
|
+
|
4
|
+
MemberRateDataFile = "#{RAILS_ROOT}/tmp/budget_member_rate_data.yml"
|
5
|
+
DeliverableDataFile = "#{RAILS_ROOT}/tmp/budget_deliverable_data.yml"
|
6
|
+
VendorInvoiceDataFile = "#{RAILS_ROOT}/tmp/billing_vendor_invoice_data.yml"
|
7
|
+
|
8
|
+
|
9
|
+
def self.compare_values(pre, post, message)
|
10
|
+
pre = pre.to_f.round(RoundTo)
|
11
|
+
post = post.to_f.round(RoundTo)
|
12
|
+
|
13
|
+
puts "ERROR: #{message} (pre: #{pre}, post: #{post})" unless pre == post
|
14
|
+
return pre == post
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class RateMembershipsHook < Redmine::Hook::ViewListener
|
2
|
+
def view_users_memberships_table_header(context={})
|
3
|
+
return content_tag(:th, l(:rate_label_rate) + ' ' + l(:rate_label_currency))
|
4
|
+
end
|
5
|
+
|
6
|
+
def view_users_memberships_table_row(context={})
|
7
|
+
return context[:controller].send(:render_to_string, {
|
8
|
+
:partial => 'users/membership_rate',
|
9
|
+
:locals => {
|
10
|
+
:membership => context[:membership],
|
11
|
+
:user => context[:user]
|
12
|
+
}})
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# Hooks to attach to the Redmine Projects.
|
2
|
+
class RateProjectHook < Redmine::Hook::ViewListener
|
3
|
+
|
4
|
+
def protect_against_forgery?
|
5
|
+
false
|
6
|
+
end
|
7
|
+
|
8
|
+
# Renders an additional table header to the membership setting
|
9
|
+
#
|
10
|
+
# Context:
|
11
|
+
# * :project => Current project
|
12
|
+
#
|
13
|
+
def view_projects_settings_members_table_header(context ={ })
|
14
|
+
return '' unless (User.current.allowed_to?(:view_rate, context[:project]) || User.current.admin?)
|
15
|
+
return "<th>#{l(:rate_label_rate)} #{l(:rate_label_currency)}</td>"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Renders an AJAX from to update the member's billing rate
|
19
|
+
#
|
20
|
+
# Context:
|
21
|
+
# * :project => Current project
|
22
|
+
# * :member => Current Member record
|
23
|
+
#
|
24
|
+
# TODO: Move to a view
|
25
|
+
def view_projects_settings_members_table_row(context = { })
|
26
|
+
member = context[:member]
|
27
|
+
project = context[:project]
|
28
|
+
|
29
|
+
return '' unless (User.current.allowed_to?(:view_rate, project) || User.current.admin?)
|
30
|
+
|
31
|
+
if Object.const_defined? 'Group' # 0.8.x compatibility
|
32
|
+
# Groups cannot have a rate
|
33
|
+
return content_tag(:td,'') if member.principal.is_a? Group
|
34
|
+
rate = Rate.for(member.principal, project)
|
35
|
+
else
|
36
|
+
rate = Rate.for(member.user, project)
|
37
|
+
end
|
38
|
+
|
39
|
+
content = ''
|
40
|
+
|
41
|
+
if rate.nil? || rate.default?
|
42
|
+
if rate && rate.default?
|
43
|
+
content << "<em>#{number_to_currency(rate.amount)}</em> "
|
44
|
+
end
|
45
|
+
|
46
|
+
if (User.current.admin?)
|
47
|
+
|
48
|
+
url = {
|
49
|
+
:controller => 'rates',
|
50
|
+
:action => 'create',
|
51
|
+
:method => :post,
|
52
|
+
:protocol => Setting.protocol,
|
53
|
+
:host => Setting.host_name
|
54
|
+
}
|
55
|
+
# Build a form_remote_tag by hand since this isn't in the scope of a controller
|
56
|
+
# and url_rewriter doesn't like that fact.
|
57
|
+
form = form_tag(url, :onsubmit => remote_function(:url => url,
|
58
|
+
:host => Setting.host_name,
|
59
|
+
:protocol => Setting.protocol,
|
60
|
+
:form => true,
|
61
|
+
:method => 'post',
|
62
|
+
:return => 'false' )+ '; return false;')
|
63
|
+
|
64
|
+
form << text_field(:rate, :amount)
|
65
|
+
form << hidden_field(:rate,:date_in_effect, :value => Date.today.to_s)
|
66
|
+
form << hidden_field(:rate, :project_id, :value => project.id)
|
67
|
+
form << hidden_field(:rate, :user_id, :value => member.user.id)
|
68
|
+
form << hidden_field_tag("back_url", url_for(:controller => 'projects', :action => 'settings', :id => project, :tab => 'members', :protocol => Setting.protocol, :host => Setting.host_name))
|
69
|
+
|
70
|
+
form << submit_tag(l(:rate_label_set_rate), :class => "small")
|
71
|
+
form << "</form>"
|
72
|
+
|
73
|
+
content << form
|
74
|
+
end
|
75
|
+
else
|
76
|
+
if (User.current.admin?)
|
77
|
+
|
78
|
+
content << content_tag(:strong, link_to(number_to_currency(rate.amount), {
|
79
|
+
:controller => 'users',
|
80
|
+
:action => 'edit',
|
81
|
+
:id => member.user,
|
82
|
+
:tab => 'rates',
|
83
|
+
:protocol => Setting.protocol,
|
84
|
+
:host => Setting.host_name
|
85
|
+
}))
|
86
|
+
else
|
87
|
+
content << content_tag(:strong, number_to_currency(rate.amount))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
return content_tag(:td, content, :align => 'left', :id => "rate_#{project.id}_#{member.user.id}" )
|
91
|
+
end
|
92
|
+
|
93
|
+
def model_project_copy_before_save(context = {})
|
94
|
+
source = context[:source_project]
|
95
|
+
destination = context[:destination_project]
|
96
|
+
|
97
|
+
Rate.find(:all, :conditions => {:project_id => source.id}).each do |source_rate|
|
98
|
+
destination_rate = Rate.new
|
99
|
+
|
100
|
+
destination_rate.attributes = source_rate.attributes.except("project_id")
|
101
|
+
destination_rate.project = destination
|
102
|
+
destination_rate.save # Need to save here because there is no relation on project to rate
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module RateSortHelperPatch
|
2
|
+
def self.included(base) # :nodoc:
|
3
|
+
base.send(:include, InstanceMethods)
|
4
|
+
end
|
5
|
+
|
6
|
+
module InstanceMethods
|
7
|
+
# Allows more parameters than the standard sort_header_tag
|
8
|
+
def rate_sort_header_tag(column, options = {})
|
9
|
+
caption = options.delete(:caption) || titleize(Inflector::humanize(column))
|
10
|
+
default_order = options.delete(:default_order) || 'asc'
|
11
|
+
options[:title]= l(:label_sort_by, "\"#{caption}\"") unless options[:title]
|
12
|
+
content_tag('th',
|
13
|
+
rate_sort_link(column,
|
14
|
+
caption,
|
15
|
+
default_order,
|
16
|
+
{ :method => options[:method], :update => options[:update], :user_id => options[:user_id] }),
|
17
|
+
options)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Allows more parameters than the standard sort_link and is hard coded to use
|
21
|
+
# the RatesController and to have an :method and :update options
|
22
|
+
def rate_sort_link(column, caption, default_order, options = { })
|
23
|
+
# 0.8.x compatibility
|
24
|
+
if SortHelper.const_defined? 'SortCriteria'
|
25
|
+
rate_sort_link_trunk_version(column, caption, default_order, options)
|
26
|
+
else
|
27
|
+
rate_sort_link_08_version(column, caption, default_order, options)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
# Trunk version of sort_link. Was modified in r2571 of Redmine
|
33
|
+
def rate_sort_link_trunk_version(column, caption, default_order, options = { })
|
34
|
+
css, order = nil, default_order
|
35
|
+
|
36
|
+
if column.to_s == @sort_criteria.first_key
|
37
|
+
if @sort_criteria.first_asc?
|
38
|
+
css = 'sort asc'
|
39
|
+
order = 'desc'
|
40
|
+
else
|
41
|
+
css = 'sort desc'
|
42
|
+
order = 'asc'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
caption = column.to_s.humanize unless caption
|
46
|
+
|
47
|
+
sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param }
|
48
|
+
# don't reuse params if filters are present
|
49
|
+
url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
|
50
|
+
|
51
|
+
# Add project_id to url_options
|
52
|
+
url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id)
|
53
|
+
|
54
|
+
##### Hard code url to the Rates index
|
55
|
+
url_options[:controller] = 'rates'
|
56
|
+
url_options[:action] = 'index'
|
57
|
+
url_options[:user_id] ||= options[:user_id]
|
58
|
+
#####
|
59
|
+
|
60
|
+
|
61
|
+
link_to_remote(caption,
|
62
|
+
{:update => options[:update] || "content", :url => url_options, :method => options[:method] || :post},
|
63
|
+
{:href => url_for(url_options),
|
64
|
+
:class => css})
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
# 0.8.x branch of sort_link.
|
69
|
+
def rate_sort_link_08_version(column, caption, default_order, options = { })
|
70
|
+
key, order = session[@sort_name][:key], session[@sort_name][:order]
|
71
|
+
if key == column
|
72
|
+
if order.downcase == 'asc'
|
73
|
+
icon = 'sort_asc.png'
|
74
|
+
order = 'desc'
|
75
|
+
else
|
76
|
+
icon = 'sort_desc.png'
|
77
|
+
order = 'asc'
|
78
|
+
end
|
79
|
+
else
|
80
|
+
icon = nil
|
81
|
+
order = default_order
|
82
|
+
end
|
83
|
+
caption = titleize(Inflector::humanize(column)) unless caption
|
84
|
+
|
85
|
+
sort_options = { :sort_key => column, :sort_order => order }
|
86
|
+
# don't reuse params if filters are present
|
87
|
+
url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
|
88
|
+
|
89
|
+
##### Hard code url to the Rates index
|
90
|
+
url_options[:controller] = 'rates'
|
91
|
+
url_options[:action] = 'index'
|
92
|
+
url_options[:user_id] ||= options[:user_id]
|
93
|
+
#####
|
94
|
+
|
95
|
+
link_to_remote(caption,
|
96
|
+
{:update => options[:update] || "content", :url => url_options, :method => options[:method] || :post},
|
97
|
+
{:href => url_for(url_options)}) +
|
98
|
+
(icon ? nbsp(2) + image_tag(icon) : '')
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module RateTimeEntryPatch
|
2
|
+
def self.included(base) # :nodoc:
|
3
|
+
base.extend(ClassMethods)
|
4
|
+
|
5
|
+
base.send(:include, InstanceMethods)
|
6
|
+
|
7
|
+
# Same as typing in the class
|
8
|
+
base.class_eval do
|
9
|
+
unloadable # Send unloadable so it will not be unloaded in development
|
10
|
+
belongs_to :rate
|
11
|
+
|
12
|
+
before_save :cost
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
# Updated the cached cost of all TimeEntries for user and project
|
20
|
+
def update_cost_cache(user, project=nil)
|
21
|
+
c = ARCondition.new
|
22
|
+
c << ["#{TimeEntry.table_name}.user_id = ?", user]
|
23
|
+
c << ["#{TimeEntry.table_name}.project_id = ?", project] if project
|
24
|
+
|
25
|
+
TimeEntry.all(:conditions => c.conditions).each do |time_entry|
|
26
|
+
time_entry.save_cached_cost
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module InstanceMethods
|
32
|
+
# Returns the current cost of the TimeEntry based on it's rate and hours
|
33
|
+
#
|
34
|
+
# Is a read-through cache method
|
35
|
+
def cost
|
36
|
+
unless read_attribute(:cost)
|
37
|
+
if self.rate.nil?
|
38
|
+
amount = Rate.amount_for(self.user, self.project, self.spent_on.to_s)
|
39
|
+
else
|
40
|
+
amount = rate.amount
|
41
|
+
end
|
42
|
+
|
43
|
+
if amount.nil?
|
44
|
+
write_attribute(:cost, 0.0)
|
45
|
+
else
|
46
|
+
# Write the cost to the database for caching
|
47
|
+
update_attribute(:cost, amount.to_f * hours.to_f)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
read_attribute(:cost)
|
52
|
+
end
|
53
|
+
|
54
|
+
def clear_cost_cache
|
55
|
+
write_attribute(:cost, nil)
|
56
|
+
end
|
57
|
+
|
58
|
+
def save_cached_cost
|
59
|
+
clear_cost_cache
|
60
|
+
update_attribute(:cost, cost)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module RateUsersHelperPatch
|
2
|
+
def self.included(base) # :nodoc:
|
3
|
+
base.send(:include, InstanceMethods)
|
4
|
+
base.class_eval do
|
5
|
+
alias_method_chain :user_settings_tabs, :rate_tab
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module InstanceMethods
|
10
|
+
# Adds a rates tab to the user administration page
|
11
|
+
def user_settings_tabs_with_rate_tab
|
12
|
+
tabs = user_settings_tabs_without_rate_tab
|
13
|
+
tabs << { :name => 'rates', :partial => 'users/rates', :label => :rate_label_rate_history}
|
14
|
+
return tabs
|
15
|
+
end
|
16
|
+
|
17
|
+
# Similar to +project_options_for_select+ but allows selecting the active value
|
18
|
+
def project_options_for_select_with_selected(projects, selected = nil)
|
19
|
+
options = content_tag('option', "--- #{l(:rate_label_default)} ---", :value => '')
|
20
|
+
projects_by_root = projects.group_by(&:root)
|
21
|
+
projects_by_root.keys.sort.each do |root|
|
22
|
+
root_selected = (root == selected) ? 'selected' : nil
|
23
|
+
|
24
|
+
options << content_tag('option', h(root.name), :value => root.id, :disabled => (!projects.include?(root)), :selected => root_selected)
|
25
|
+
projects_by_root[root].sort.each do |project|
|
26
|
+
next if project == root
|
27
|
+
child_selected = (project == selected) ? 'selected' : nil
|
28
|
+
|
29
|
+
options << content_tag('option', '» ' + h(project.name), :value => project.id, :selected => child_selected)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
options
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module RedmineRate
|
2
|
+
module Hooks
|
3
|
+
class PluginTimesheetViewTimesheetsReportHeaderTagsHook < Redmine::Hook::ViewListener
|
4
|
+
def plugin_timesheet_view_timesheets_report_header_tags(context={})
|
5
|
+
return content_tag(:style,
|
6
|
+
'tr.missing-rate td.cost { color: red; }',
|
7
|
+
:type => 'text/css')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module RedmineRate
|
2
|
+
module Hooks
|
3
|
+
class PluginTimesheetViewsTimesheetTimeEntryHook < Redmine::Hook::ViewListener
|
4
|
+
include TimesheetHookHelper
|
5
|
+
|
6
|
+
def plugin_timesheet_views_timesheet_time_entry(context={})
|
7
|
+
cost = cost_item(context[:time_entry])
|
8
|
+
if cost
|
9
|
+
td_cell(number_to_currency(cost))
|
10
|
+
else
|
11
|
+
td_cell(' ')
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module RedmineRate
|
2
|
+
module Hooks
|
3
|
+
class PluginTimesheetViewsTimesheetTimeEntrySumHook < Redmine::Hook::ViewListener
|
4
|
+
include TimesheetHookHelper
|
5
|
+
|
6
|
+
def plugin_timesheet_views_timesheet_time_entry_sum(context={})
|
7
|
+
time_entries = context[:time_entries]
|
8
|
+
costs = time_entries.collect {|time_entry| cost_item(time_entry)}.compact.sum
|
9
|
+
if costs >= 0
|
10
|
+
return td_cell(number_to_currency(costs))
|
11
|
+
else
|
12
|
+
return td_cell(' ')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module RedmineRate
|
2
|
+
module Hooks
|
3
|
+
class PluginTimesheetViewsTimesheetsTimeEntryRowClassHook < Redmine::Hook::ViewListener
|
4
|
+
include TimesheetHookHelper
|
5
|
+
|
6
|
+
def plugin_timesheet_views_timesheets_time_entry_row_class(context={})
|
7
|
+
time_entry = context[:time_entry]
|
8
|
+
return "" unless time_entry
|
9
|
+
|
10
|
+
cost = cost_item(time_entry)
|
11
|
+
return "" unless cost # Permissions
|
12
|
+
|
13
|
+
if cost && cost <= 0
|
14
|
+
return "missing-rate"
|
15
|
+
else
|
16
|
+
return ""
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module TimesheetHookHelper
|
2
|
+
# Returns the cost of a time entry, checking user permissions
|
3
|
+
def cost_item(time_entry)
|
4
|
+
if User.current.logged? && (User.current.allowed_to?(:view_rate, time_entry.project) || User.current.admin?)
|
5
|
+
return time_entry.cost
|
6
|
+
else
|
7
|
+
return nil
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def td_cell(html)
|
12
|
+
return content_tag(:td, html, :align => 'right', :class => 'cost')
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module RedmineRate
|
2
|
+
module Hooks
|
3
|
+
class ViewLayoutsBaseHtmlHeadHook < Redmine::Hook::ViewListener
|
4
|
+
def view_layouts_base_html_head(context={})
|
5
|
+
return content_tag(:style, "#admin-menu a.rate-caches { background-image: url('#{image_path('database_refresh.png', :plugin => 'redmine_rate')}'); }", :type => 'text/css')
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
namespace :rate_plugin do
|
2
|
+
namespace :cache do
|
3
|
+
desc "Update Time Entry cost caches for Time Entries without a cost"
|
4
|
+
task :update_cost_cache => :environment do
|
5
|
+
Rate.update_all_time_entries_with_missing_cost
|
6
|
+
end
|
7
|
+
|
8
|
+
desc "Clear and update all Time Entry cost caches"
|
9
|
+
task :refresh_cost_cache => :environment do
|
10
|
+
Rate.update_all_time_entries_to_refresh_cache
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/tasks/data.rake
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
namespace :rate_plugin do
|
2
|
+
desc "Export both the Budget and Billing plugin data to a file"
|
3
|
+
task :pre_install_export => ['budget:pre_install_export', 'billing:pre_install_export']
|
4
|
+
|
5
|
+
desc "Check the export against the migrated Rate data"
|
6
|
+
task :post_install_check => ['budget:post_install_check', 'billing:post_install_check']
|
7
|
+
|
8
|
+
namespace :budget do
|
9
|
+
desc "Export the values of the Budget plugin to a file before installing the rate plugin"
|
10
|
+
task :pre_install_export => :environment do
|
11
|
+
|
12
|
+
unless Redmine::Plugin.registered_plugins[:budget_plugin].version == "0.1.0"
|
13
|
+
puts "ERROR: This task is only needed when upgrading Budget from version 0.1.0 to version 0.2.0"
|
14
|
+
return false
|
15
|
+
end
|
16
|
+
|
17
|
+
rates = ''
|
18
|
+
# Rate for members
|
19
|
+
Member.find(:all, :conditions => ['rate IS NOT NULL']).each do |member|
|
20
|
+
|
21
|
+
rates << {
|
22
|
+
:user_id => member.user_id,
|
23
|
+
:project_id => member.project_id,
|
24
|
+
:rate => member.rate
|
25
|
+
}.to_yaml
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
File.open(RateConversion::MemberRateDataFile, 'w') do |file|
|
30
|
+
file.puts rates
|
31
|
+
end
|
32
|
+
|
33
|
+
# HourlyDeliverable.spent and FixedDeliverable.spent
|
34
|
+
deliverables = ''
|
35
|
+
Deliverable.find(:all).each do |deliverable|
|
36
|
+
deliverables << {
|
37
|
+
:id => deliverable.id,
|
38
|
+
:spent => deliverable.spent
|
39
|
+
}.to_yaml
|
40
|
+
end
|
41
|
+
|
42
|
+
File.open(RateConversion::DeliverableDataFile, 'w') do |file|
|
43
|
+
file.puts deliverables
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
desc "Check the values of the export"
|
48
|
+
task :post_install_check => :environment do
|
49
|
+
|
50
|
+
unless Redmine::Plugin.registered_plugins[:budget_plugin].version == "0.2.0"
|
51
|
+
puts "ERROR: Please upgrade the budget_plugin to 0.2.0 now"
|
52
|
+
return false
|
53
|
+
end
|
54
|
+
|
55
|
+
counter = 0
|
56
|
+
# Member Rates
|
57
|
+
File.open(RateConversion::MemberRateDataFile) do |file|
|
58
|
+
YAML::load_documents(file) { |member_export|
|
59
|
+
user_id = member_export[:user_id]
|
60
|
+
project_id = member_export[:project_id]
|
61
|
+
rate = Rate.find_by_user_id_and_project_id(user_id, project_id)
|
62
|
+
|
63
|
+
if rate.nil?
|
64
|
+
puts "ERROR: No Rate found for User: #{user_id}, Project: #{project_id}"
|
65
|
+
counter += 1
|
66
|
+
else
|
67
|
+
counter += 1 unless RateConversion.compare_values(member_export[:rate], rate.amount, "Rate #{rate.id}'s amount is off")
|
68
|
+
end
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
# Deliverables
|
73
|
+
File.open(RateConversion::DeliverableDataFile) do |file|
|
74
|
+
YAML::load_documents(file) { |deliverable_export|
|
75
|
+
id = deliverable_export[:id]
|
76
|
+
spent = deliverable_export[:spent]
|
77
|
+
deliverable = Deliverable.find(id)
|
78
|
+
|
79
|
+
counter += 1 unless RateConversion.compare_values(spent, deliverable.spent, "Deliverable #{id}'s spent is off")
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
if counter > 0
|
84
|
+
puts "#{counter} errors found."
|
85
|
+
else
|
86
|
+
puts "No Budget conversation errors found, congrats."
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
namespace :billing do
|
92
|
+
desc "Export the values of the Billing plugin to a file before installing the rate plugin"
|
93
|
+
task :pre_install_export => :environment do
|
94
|
+
|
95
|
+
unless Redmine::Plugin.registered_plugins[:redmine_billing].version == "0.0.1"
|
96
|
+
puts "ERROR: This task is only needed when upgrading Billing from version 0.0.1 to version 0.3.0"
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
|
100
|
+
invoices = ''
|
101
|
+
|
102
|
+
FixedVendorInvoice.find(:all).each do |invoice|
|
103
|
+
invoices << {
|
104
|
+
:id => invoice.id,
|
105
|
+
:number => invoice.number,
|
106
|
+
:amount => invoice.amount,
|
107
|
+
:project_id => invoice.project_id,
|
108
|
+
:type => 'FixedVendorInvoice'
|
109
|
+
}.to_yaml
|
110
|
+
end
|
111
|
+
|
112
|
+
HourlyVendorInvoice.find(:all).each do |invoice|
|
113
|
+
invoices << {
|
114
|
+
:id => invoice.id,
|
115
|
+
:number => invoice.number,
|
116
|
+
:amount => invoice.amount_for_user,
|
117
|
+
:project_id => invoice.project_id,
|
118
|
+
:type => 'HourlyVendorInvoice'
|
119
|
+
}.to_yaml
|
120
|
+
end
|
121
|
+
|
122
|
+
File.open(RateConversion::VendorInvoiceDataFile, 'w') do |file|
|
123
|
+
file.puts invoices
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
desc "Check the values of the export"
|
128
|
+
task :post_install_check => :environment do
|
129
|
+
|
130
|
+
unless Redmine::Plugin.registered_plugins[:redmine_billing].version == "0.3.0"
|
131
|
+
puts "ERROR: Please upgrade the billing_plugin to 0.3.0 now"
|
132
|
+
return false
|
133
|
+
end
|
134
|
+
|
135
|
+
counter = 0
|
136
|
+
|
137
|
+
File.open(RateConversion::VendorInvoiceDataFile) do |file|
|
138
|
+
YAML::load_documents(file) { |invoice_export|
|
139
|
+
invoice = VendorInvoice.find_by_id(invoice_export[:id])
|
140
|
+
|
141
|
+
if invoice.nil?
|
142
|
+
puts "ERROR: No VendorInvoice found with the ID of #{invoice_export[:id]}"
|
143
|
+
counter += 1
|
144
|
+
else
|
145
|
+
if invoice.type.to_s == "FixedVendorInvoice"
|
146
|
+
counter += 1 unless RateConversion.compare_values(invoice_export[:amount], invoice.amount, "VendorInvoice #{invoice.id}'s amount is off")
|
147
|
+
else
|
148
|
+
counter += 1 unless RateConversion.compare_values(invoice_export[:amount], invoice.amount_for_user, "VendorInvoice #{invoice.id}'s amount is off")
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
}
|
153
|
+
end
|
154
|
+
|
155
|
+
if counter > 0
|
156
|
+
puts "#{counter} errors found."
|
157
|
+
else
|
158
|
+
puts "No Billing conversation errors found, congrats."
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../init"
|