redmine_rate 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. data/COPYRIGHT.txt +18 -0
  2. data/CREDITS.txt +5 -0
  3. data/GPL.txt +339 -0
  4. data/README.rdoc +93 -0
  5. data/Rakefile +37 -0
  6. data/VERSION +1 -0
  7. data/app/controllers/rate_caches_controller.rb +35 -0
  8. data/app/controllers/rates_controller.rb +154 -0
  9. data/app/models/rate.rb +161 -0
  10. data/app/views/rate_caches/index.html.erb +21 -0
  11. data/app/views/rates/_form.html.erb +36 -0
  12. data/app/views/rates/_list.html.erb +42 -0
  13. data/app/views/rates/create.js.rjs +2 -0
  14. data/app/views/rates/create_error.js.rjs +5 -0
  15. data/app/views/rates/edit.html.erb +3 -0
  16. data/app/views/rates/index.html.erb +5 -0
  17. data/app/views/rates/new.html.erb +3 -0
  18. data/app/views/rates/show.html.erb +23 -0
  19. data/app/views/users/_membership_rate.html.erb +23 -0
  20. data/app/views/users/_rates.html.erb +17 -0
  21. data/assets/images/database_refresh.png +0 -0
  22. data/config/locales/de.yml +18 -0
  23. data/config/locales/en.yml +18 -0
  24. data/config/locales/fr.yml +20 -0
  25. data/config/locales/ru.yml +9 -0
  26. data/config/routes.rb +4 -0
  27. data/init.rb +49 -0
  28. data/lang/de.yml +9 -0
  29. data/lang/en.yml +9 -0
  30. data/lang/fr.yml +8 -0
  31. data/lib/rate_conversion.rb +16 -0
  32. data/lib/rate_memberships_hook.rb +15 -0
  33. data/lib/rate_project_hook.rb +106 -0
  34. data/lib/rate_sort_helper_patch.rb +102 -0
  35. data/lib/rate_time_entry_patch.rb +66 -0
  36. data/lib/rate_users_helper_patch.rb +37 -0
  37. data/lib/redmine_rate/hooks/plugin_timesheet_view_timesheets_report_header_tags_hook.rb +11 -0
  38. data/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_group_header_hook.rb +9 -0
  39. data/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_hook.rb +18 -0
  40. data/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_sum_hook.rb +17 -0
  41. data/lib/redmine_rate/hooks/plugin_timesheet_views_timesheets_time_entry_row_class_hook.rb +21 -0
  42. data/lib/redmine_rate/hooks/timesheet_hook_helper.rb +14 -0
  43. data/lib/redmine_rate/hooks/view_layouts_base_html_head_hook.rb +9 -0
  44. data/lib/tasks/cache.rake +13 -0
  45. data/lib/tasks/data.rake +163 -0
  46. data/rails/init.rb +1 -0
  47. data/test/functional/rates_controller_test.rb +401 -0
  48. data/test/integration/admin_panel_test.rb +81 -0
  49. data/test/integration/routing_test.rb +16 -0
  50. data/test/test_helper.rb +43 -0
  51. data/test/unit/lib/rate_time_entry_patch_test.rb +77 -0
  52. data/test/unit/lib/rate_users_helper_patch_test.rb +37 -0
  53. data/test/unit/lib/redmine_rate/hooks/plugin_timesheet_view_timesheets_report_header_tags_hook_test.rb +26 -0
  54. data/test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_group_header_hook_test.rb +26 -0
  55. data/test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_hook_test.rb +47 -0
  56. data/test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_sum_hook_test.rb +48 -0
  57. data/test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheets_time_entry_row_class_hook_test.rb +60 -0
  58. data/test/unit/rate_for_test.rb +74 -0
  59. data/test/unit/rate_test.rb +333 -0
  60. 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', '&#187; ' + 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,9 @@
1
+ module RedmineRate
2
+ module Hooks
3
+ class PluginTimesheetViewsTimesheetGroupHeaderHook < Redmine::Hook::ViewListener
4
+ def plugin_timesheet_views_timesheet_group_header(context={})
5
+ return content_tag(:th, l(:rate_cost), :width => '8%')
6
+ end
7
+ end
8
+ end
9
+ 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('&nbsp;')
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('&nbsp;')
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
@@ -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"