redmine_rate 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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"
|