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.
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"