##// END OF EJS Templates
Extracted time report logic from the controller....
Jean-Philippe Lang -
r7906:caf898d7d13f
parent child
Show More
@@ -0,0 +1,164
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 module Redmine
19 module Helpers
20 class TimeReport
21 attr_reader :criteria, :columns, :from, :to, :hours, :total_hours, :periods
22
23 def initialize(project, issue, criteria, columns, from, to)
24 @project = project
25 @issue = issue
26
27 @criteria = criteria || []
28 @criteria = @criteria.select{|criteria| available_criteria.has_key? criteria}
29 @criteria.uniq!
30 @criteria = @criteria[0,3]
31
32 @columns = (columns && %w(year month week day).include?(columns)) ? columns : 'month'
33 @from = from
34 @to = to
35
36 run
37 end
38
39 def available_criteria
40 @available_criteria || load_available_criteria
41 end
42
43 private
44
45 def run
46 unless @criteria.empty?
47 sql_select = @criteria.collect{|criteria| @available_criteria[criteria][:sql] + " AS " + criteria}.join(', ')
48 sql_group_by = @criteria.collect{|criteria| @available_criteria[criteria][:sql]}.join(', ')
49 sql_condition = ''
50
51 if @project.nil?
52 sql_condition = Project.allowed_to_condition(User.current, :view_time_entries)
53 elsif @issue.nil?
54 sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
55 else
56 sql_condition = "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
57 end
58
59 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
60 sql << " FROM #{TimeEntry.table_name}"
61 sql << time_report_joins
62 sql << " WHERE"
63 sql << " (%s) AND" % sql_condition
64 sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
65 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
66
67 @hours = ActiveRecord::Base.connection.select_all(sql)
68
69 @hours.each do |row|
70 case @columns
71 when 'year'
72 row['year'] = row['tyear']
73 when 'month'
74 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
75 when 'week'
76 row['week'] = "#{row['tyear']}-#{row['tweek']}"
77 when 'day'
78 row['day'] = "#{row['spent_on']}"
79 end
80 end
81
82 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
83
84 @periods = []
85 # Date#at_beginning_of_ not supported in Rails 1.2.x
86 date_from = @from.to_time
87 # 100 columns max
88 while date_from <= @to.to_time && @periods.length < 100
89 case @columns
90 when 'year'
91 @periods << "#{date_from.year}"
92 date_from = (date_from + 1.year).at_beginning_of_year
93 when 'month'
94 @periods << "#{date_from.year}-#{date_from.month}"
95 date_from = (date_from + 1.month).at_beginning_of_month
96 when 'week'
97 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
98 date_from = (date_from + 7.day).at_beginning_of_week
99 when 'day'
100 @periods << "#{date_from.to_date}"
101 date_from = date_from + 1.day
102 end
103 end
104 end
105 end
106
107 def load_available_criteria
108 @available_criteria = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
109 :klass => Project,
110 :label => :label_project},
111 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
112 :klass => Version,
113 :label => :label_version},
114 'category' => {:sql => "#{Issue.table_name}.category_id",
115 :klass => IssueCategory,
116 :label => :field_category},
117 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
118 :klass => User,
119 :label => :label_member},
120 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
121 :klass => Tracker,
122 :label => :label_tracker},
123 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
124 :klass => TimeEntryActivity,
125 :label => :label_activity},
126 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
127 :klass => Issue,
128 :label => :label_issue}
129 }
130
131 # Add list and boolean custom fields as available criteria
132 custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
133 custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
134 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
135 :format => cf.field_format,
136 :label => cf.name}
137 end if @project
138
139 # Add list and boolean time entry custom fields
140 TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
141 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
142 :format => cf.field_format,
143 :label => cf.name}
144 end
145
146 # Add list and boolean time entry activity custom fields
147 TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
148 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)",
149 :format => cf.field_format,
150 :label => cf.name}
151 end
152
153 @available_criteria
154 end
155
156 def time_report_joins
157 sql = ''
158 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
159 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
160 sql
161 end
162 end
163 end
164 end
@@ -1,209 +1,82
1 1 class TimeEntryReportsController < ApplicationController
2 2 menu_item :issues
3 3 before_filter :find_optional_project
4 before_filter :load_available_criterias
5 4
6 5 helper :sort
7 6 include SortHelper
8 7 helper :issues
9 8 helper :timelog
10 9 include TimelogHelper
11 10 helper :custom_fields
12 11 include CustomFieldsHelper
13 12
14 13 def report
15 @criterias = params[:criterias] || []
16 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
17 @criterias.uniq!
18 @criterias = @criterias[0,3]
19
20 @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
21
22 14 retrieve_date_range
23
24 unless @criterias.empty?
25 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
26 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
27 sql_condition = ''
28
29 if @project.nil?
30 sql_condition = Project.allowed_to_condition(User.current, :view_time_entries)
31 elsif @issue.nil?
32 sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
33 else
34 sql_condition = "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
35 end
36
37 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
38 sql << " FROM #{TimeEntry.table_name}"
39 sql << time_report_joins
40 sql << " WHERE"
41 sql << " (%s) AND" % sql_condition
42 sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
43 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
44
45 @hours = ActiveRecord::Base.connection.select_all(sql)
46
47 @hours.each do |row|
48 case @columns
49 when 'year'
50 row['year'] = row['tyear']
51 when 'month'
52 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
53 when 'week'
54 row['week'] = "#{row['tyear']}-#{row['tweek']}"
55 when 'day'
56 row['day'] = "#{row['spent_on']}"
57 end
58 end
59
60 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
61
62 @periods = []
63 # Date#at_beginning_of_ not supported in Rails 1.2.x
64 date_from = @from.to_time
65 # 100 columns max
66 while date_from <= @to.to_time && @periods.length < 100
67 case @columns
68 when 'year'
69 @periods << "#{date_from.year}"
70 date_from = (date_from + 1.year).at_beginning_of_year
71 when 'month'
72 @periods << "#{date_from.year}-#{date_from.month}"
73 date_from = (date_from + 1.month).at_beginning_of_month
74 when 'week'
75 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
76 date_from = (date_from + 7.day).at_beginning_of_week
77 when 'day'
78 @periods << "#{date_from.to_date}"
79 date_from = date_from + 1.day
80 end
81 end
82 end
15 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], @from, @to)
83 16
84 17 respond_to do |format|
85 18 format.html { render :layout => !request.xhr? }
86 format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
19 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
87 20 end
88 21 end
89 22
90 23 private
91 24
92 25 # TODO: duplicated in TimelogController
93 26 def find_optional_project
94 27 if !params[:issue_id].blank?
95 28 @issue = Issue.find(params[:issue_id])
96 29 @project = @issue.project
97 30 elsif !params[:project_id].blank?
98 31 @project = Project.find(params[:project_id])
99 32 end
100 33 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
101 34 end
102 35
103 36 # Retrieves the date range based on predefined ranges or specific from/to param dates
104 37 # TODO: duplicated in TimelogController
105 38 def retrieve_date_range
106 39 @free_period = false
107 40 @from, @to = nil, nil
108 41
109 42 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
110 43 case params[:period].to_s
111 44 when 'today'
112 45 @from = @to = Date.today
113 46 when 'yesterday'
114 47 @from = @to = Date.today - 1
115 48 when 'current_week'
116 49 @from = Date.today - (Date.today.cwday - 1)%7
117 50 @to = @from + 6
118 51 when 'last_week'
119 52 @from = Date.today - 7 - (Date.today.cwday - 1)%7
120 53 @to = @from + 6
121 54 when '7_days'
122 55 @from = Date.today - 7
123 56 @to = Date.today
124 57 when 'current_month'
125 58 @from = Date.civil(Date.today.year, Date.today.month, 1)
126 59 @to = (@from >> 1) - 1
127 60 when 'last_month'
128 61 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
129 62 @to = (@from >> 1) - 1
130 63 when '30_days'
131 64 @from = Date.today - 30
132 65 @to = Date.today
133 66 when 'current_year'
134 67 @from = Date.civil(Date.today.year, 1, 1)
135 68 @to = Date.civil(Date.today.year, 12, 31)
136 69 end
137 70 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
138 71 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
139 72 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
140 73 @free_period = true
141 74 else
142 75 # default
143 76 end
144 77
145 78 @from, @to = @to, @from if @from && @to && @from > @to
146 79 @from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today)
147 80 @to ||= (TimeEntry.latest_date_for_project(@project) || Date.today)
148 81 end
149
150 def load_available_criterias
151 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
152 :klass => Project,
153 :label => :label_project},
154 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
155 :klass => Version,
156 :label => :label_version},
157 'category' => {:sql => "#{Issue.table_name}.category_id",
158 :klass => IssueCategory,
159 :label => :field_category},
160 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
161 :klass => User,
162 :label => :label_member},
163 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
164 :klass => Tracker,
165 :label => :label_tracker},
166 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
167 :klass => TimeEntryActivity,
168 :label => :label_activity},
169 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
170 :klass => Issue,
171 :label => :label_issue}
172 }
173
174 # Add list and boolean custom fields as available criterias
175 custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
176 custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
177 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
178 :format => cf.field_format,
179 :label => cf.name}
180 end if @project
181
182 # Add list and boolean time entry custom fields
183 TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
184 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
185 :format => cf.field_format,
186 :label => cf.name}
187 end
188
189 # Add list and boolean time entry activity custom fields
190 TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
191 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)",
192 :format => cf.field_format,
193 :label => cf.name}
194 end
195
196 call_hook(:controller_timelog_available_criterias, { :available_criterias => @available_criterias, :project => @project })
197 @available_criterias
198 end
199
200 def time_report_joins
201 sql = ''
202 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
203 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
204 # TODO: rename hook
205 call_hook(:controller_timelog_time_report_joins, {:sql => sql} )
206 sql
207 end
208
209 82 end
@@ -1,194 +1,194
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module TimelogHelper
19 19 include ApplicationHelper
20 20
21 21 def render_timelog_breadcrumb
22 22 links = []
23 23 links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
24 24 links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
25 25 if @issue
26 26 if @issue.visible?
27 27 links << link_to_issue(@issue, :subject => false)
28 28 else
29 29 links << "##{@issue.id}"
30 30 end
31 31 end
32 32 breadcrumb links
33 33 end
34 34
35 35 # Returns a collection of activities for a select field. time_entry
36 36 # is optional and will be used to check if the selected TimeEntryActivity
37 37 # is active.
38 38 def activity_collection_for_select_options(time_entry=nil, project=nil)
39 39 project ||= @project
40 40 if project.nil?
41 41 activities = TimeEntryActivity.shared.active
42 42 else
43 43 activities = project.activities
44 44 end
45 45
46 46 collection = []
47 47 if time_entry && time_entry.activity && !time_entry.activity.active?
48 48 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
49 49 else
50 50 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
51 51 end
52 52 activities.each { |a| collection << [a.name, a.id] }
53 53 collection
54 54 end
55 55
56 56 def select_hours(data, criteria, value)
57 57 if value.to_s.empty?
58 58 data.select {|row| row[criteria].blank? }
59 59 else
60 60 data.select {|row| row[criteria].to_s == value.to_s}
61 61 end
62 62 end
63 63
64 64 def sum_hours(data)
65 65 sum = 0
66 66 data.each do |row|
67 67 sum += row['hours'].to_f
68 68 end
69 69 sum
70 70 end
71 71
72 72 def options_for_period_select(value)
73 73 options_for_select([[l(:label_all_time), 'all'],
74 74 [l(:label_today), 'today'],
75 75 [l(:label_yesterday), 'yesterday'],
76 76 [l(:label_this_week), 'current_week'],
77 77 [l(:label_last_week), 'last_week'],
78 78 [l(:label_last_n_days, 7), '7_days'],
79 79 [l(:label_this_month), 'current_month'],
80 80 [l(:label_last_month), 'last_month'],
81 81 [l(:label_last_n_days, 30), '30_days'],
82 82 [l(:label_this_year), 'current_year']],
83 83 value)
84 84 end
85 85
86 86 def entries_to_csv(entries)
87 87 decimal_separator = l(:general_csv_decimal_separator)
88 88 custom_fields = TimeEntryCustomField.find(:all)
89 89 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
90 90 # csv header fields
91 91 headers = [l(:field_spent_on),
92 92 l(:field_user),
93 93 l(:field_activity),
94 94 l(:field_project),
95 95 l(:field_issue),
96 96 l(:field_tracker),
97 97 l(:field_subject),
98 98 l(:field_hours),
99 99 l(:field_comments)
100 100 ]
101 101 # Export custom fields
102 102 headers += custom_fields.collect(&:name)
103 103
104 104 csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
105 105 c.to_s,
106 106 l(:general_csv_encoding) ) }
107 107 # csv lines
108 108 entries.each do |entry|
109 109 fields = [format_date(entry.spent_on),
110 110 entry.user,
111 111 entry.activity,
112 112 entry.project,
113 113 (entry.issue ? entry.issue.id : nil),
114 114 (entry.issue ? entry.issue.tracker : nil),
115 115 (entry.issue ? entry.issue.subject : nil),
116 116 entry.hours.to_s.gsub('.', decimal_separator),
117 117 entry.comments
118 118 ]
119 119 fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) }
120 120
121 121 csv << fields.collect {|c| Redmine::CodesetUtil.from_utf8(
122 122 c.to_s,
123 123 l(:general_csv_encoding) ) }
124 124 end
125 125 end
126 126 export
127 127 end
128 128
129 def format_criteria_value(criteria, value)
129 def format_criteria_value(criteria_options, value)
130 130 if value.blank?
131 131 l(:label_none)
132 elsif k = @available_criterias[criteria][:klass]
132 elsif k = criteria_options[:klass]
133 133 obj = k.find_by_id(value.to_i)
134 134 if obj.is_a?(Issue)
135 135 obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
136 136 else
137 137 obj
138 138 end
139 139 else
140 format_value(value, @available_criterias[criteria][:format])
140 format_value(value, criteria_options[:format])
141 141 end
142 142 end
143 143
144 def report_to_csv(criterias, periods, hours)
144 def report_to_csv(report)
145 145 decimal_separator = l(:general_csv_decimal_separator)
146 146 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
147 147 # Column headers
148 headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
149 headers += periods
148 headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) }
149 headers += report.periods
150 150 headers << l(:label_total)
151 151 csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
152 152 c.to_s,
153 153 l(:general_csv_encoding) ) }
154 154 # Content
155 report_criteria_to_csv(csv, criterias, periods, hours)
155 report_criteria_to_csv(csv, report.available_criteria, report.columns, report.criteria, report.periods, report.hours)
156 156 # Total row
157 157 str_total = Redmine::CodesetUtil.from_utf8(l(:label_total), l(:general_csv_encoding))
158 row = [ str_total ] + [''] * (criterias.size - 1)
158 row = [ str_total ] + [''] * (report.criteria.size - 1)
159 159 total = 0
160 periods.each do |period|
161 sum = sum_hours(select_hours(hours, @columns, period.to_s))
160 report.periods.each do |period|
161 sum = sum_hours(select_hours(report.hours, report.columns, period.to_s))
162 162 total += sum
163 163 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
164 164 end
165 165 row << ("%.2f" % total).gsub('.',decimal_separator)
166 166 csv << row
167 167 end
168 168 export
169 169 end
170 170
171 def report_criteria_to_csv(csv, criterias, periods, hours, level=0)
171 def report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours, level=0)
172 172 decimal_separator = l(:general_csv_decimal_separator)
173 hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value|
174 hours_for_value = select_hours(hours, criterias[level], value)
173 hours.collect {|h| h[criteria[level]].to_s}.uniq.each do |value|
174 hours_for_value = select_hours(hours, criteria[level], value)
175 175 next if hours_for_value.empty?
176 176 row = [''] * level
177 177 row << Redmine::CodesetUtil.from_utf8(
178 format_criteria_value(criterias[level], value).to_s,
178 format_criteria_value(available_criteria[criteria[level]], value).to_s,
179 179 l(:general_csv_encoding) )
180 row += [''] * (criterias.length - level - 1)
180 row += [''] * (criteria.length - level - 1)
181 181 total = 0
182 182 periods.each do |period|
183 sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s))
183 sum = sum_hours(select_hours(hours_for_value, columns, period.to_s))
184 184 total += sum
185 185 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
186 186 end
187 187 row << ("%.2f" % total).gsub('.',decimal_separator)
188 188 csv << row
189 if criterias.length > level + 1
190 report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1)
189 if criteria.length > level + 1
190 report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours_for_value, level + 1)
191 191 end
192 192 end
193 193 end
194 194 end
@@ -1,19 +1,19
1 <% @hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value| %>
1 <% @report.hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value| %>
2 2 <% hours_for_value = select_hours(hours, criterias[level], value) -%>
3 3 <% next if hours_for_value.empty? -%>
4 4 <tr class="<%= cycle('odd', 'even') %> <%= 'last-level' unless criterias.length > level+1 %>">
5 5 <%= '<td></td>' * level %>
6 <td><%= h(format_criteria_value(criterias[level], value)) %></td>
6 <td><%= h(format_criteria_value(@report.available_criteria[criterias[level]], value)) %></td>
7 7 <%= '<td></td>' * (criterias.length - level - 1) -%>
8 8 <% total = 0 -%>
9 <% @periods.each do |period| -%>
10 <% sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)); total += sum -%>
9 <% @report.periods.each do |period| -%>
10 <% sum = sum_hours(select_hours(hours_for_value, @report.columns, period.to_s)); total += sum -%>
11 11 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
12 12 <% end -%>
13 13 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
14 14 </tr>
15 15 <% if criterias.length > level+1 -%>
16 16 <%= render(:partial => 'report_criteria', :locals => {:criterias => criterias, :hours => hours_for_value, :level => (level + 1)}) %>
17 17 <% end -%>
18 18
19 19 <% end %>
@@ -1,72 +1,72
1 1 <div class="contextual">
2 2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'new', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
3 3 </div>
4 4
5 5 <%= render_timelog_breadcrumb %>
6 6
7 7 <h2><%= l(:label_spent_time) %></h2>
8 8
9 9 <% form_tag({:controller => 'time_entry_reports', :action => 'report', :project_id => @project, :issue_id => @issue}, :method => :get, :id => 'query_form') do %>
10 <% @criterias.each do |criteria| %>
11 <%= hidden_field_tag 'criterias[]', criteria, :id => nil %>
10 <% @report.criteria.each do |criterion| %>
11 <%= hidden_field_tag 'criteria[]', criterion, :id => nil %>
12 12 <% end %>
13 13 <%= render :partial => 'timelog/date_range' %>
14 14
15 15 <p><label for='columns'><%= l(:label_details) %></label>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
16 16 [l(:label_month), 'month'],
17 17 [l(:label_week), 'week'],
18 [l(:label_day_plural).titleize, 'day']], @columns),
18 [l(:label_day_plural).titleize, 'day']], @report.columns),
19 19 :onchange => "this.form.onsubmit();" %>
20 20
21 <label for='criterias'><%= l(:button_add) %></label>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l_or_humanize(@available_criterias[k][:label]), k]}),
21 <label for='criterias'><%= l(:button_add) %></label>: <%= select_tag('criteria[]', options_for_select([[]] + (@report.available_criteria.keys - @report.criteria).collect{|k| [l_or_humanize(@report.available_criteria[k][:label]), k]}),
22 22 :onchange => "this.form.submit();",
23 23 :style => 'width: 200px',
24 24 :id => nil,
25 :disabled => (@criterias.length >= 3), :id => "criterias") %>
26 <%= link_to l(:button_clear), {:project_id => @project, :issue_id => @issue, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @columns}, :class => 'icon icon-reload' %></p>
25 :disabled => (@report.criteria.length >= 3), :id => "criterias") %>
26 <%= link_to l(:button_clear), {:project_id => @project, :issue_id => @issue, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @report.columns}, :class => 'icon icon-reload' %></p>
27 27 <% end %>
28 28
29 <% unless @criterias.empty? %>
29 <% unless @report.criteria.empty? %>
30 30 <div class="total-hours">
31 <p><%= l(:label_total) %>: <%= html_hours(l_hours(@total_hours)) %></p>
31 <p><%= l(:label_total) %>: <%= html_hours(l_hours(@report.total_hours)) %></p>
32 32 </div>
33 33
34 <% unless @hours.empty? %>
34 <% unless @report.hours.empty? %>
35 35 <div class="autoscroll">
36 36 <table class="list" id="time-report">
37 37 <thead>
38 38 <tr>
39 <% @criterias.each do |criteria| %>
40 <th><%= l_or_humanize(@available_criterias[criteria][:label]) %></th>
39 <% @report.criteria.each do |criteria| %>
40 <th><%= l_or_humanize(@report.available_criteria[criteria][:label]) %></th>
41 41 <% end %>
42 <% columns_width = (40 / (@periods.length+1)).to_i %>
43 <% @periods.each do |period| %>
42 <% columns_width = (40 / (@report.periods.length+1)).to_i %>
43 <% @report.periods.each do |period| %>
44 44 <th class="period" width="<%= columns_width %>%"><%= period %></th>
45 45 <% end %>
46 46 <th class="total" width="<%= columns_width %>%"><%= l(:label_total) %></th>
47 47 </tr>
48 48 </thead>
49 49 <tbody>
50 <%= render :partial => 'report_criteria', :locals => {:criterias => @criterias, :hours => @hours, :level => 0} %>
50 <%= render :partial => 'report_criteria', :locals => {:criterias => @report.criteria, :hours => @report.hours, :level => 0} %>
51 51 <tr class="total">
52 52 <td><%= l(:label_total) %></td>
53 <%= '<td></td>' * (@criterias.size - 1) %>
53 <%= '<td></td>' * (@report.criteria.size - 1) %>
54 54 <% total = 0 -%>
55 <% @periods.each do |period| -%>
56 <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)); total += sum -%>
55 <% @report.periods.each do |period| -%>
56 <% sum = sum_hours(select_hours(@report.hours, @report.columns, period.to_s)); total += sum -%>
57 57 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
58 58 <% end -%>
59 59 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
60 60 </tr>
61 61 </tbody>
62 62 </table>
63 63 </div>
64 64
65 65 <% other_formats_links do |f| %>
66 66 <%= f.link_to 'CSV', :url => params %>
67 67 <% end %>
68 68 <% end %>
69 69 <% end %>
70 70
71 71 <% html_title l(:label_spent_time), l(:label_report) %>
72 72
@@ -1,298 +1,297
1 1 # -*- coding: utf-8 -*-
2 2 require File.expand_path('../../test_helper', __FILE__)
3 3
4 4 class TimeEntryReportsControllerTest < ActionController::TestCase
5 5 fixtures :projects, :enabled_modules, :roles, :members, :member_roles,
6 6 :issues, :time_entries, :users, :trackers, :enumerations,
7 7 :issue_statuses, :custom_fields, :custom_values
8 8
9 9 include Redmine::I18n
10 10
11 11 def setup
12 12 Setting.default_language = "en"
13 13 end
14 14
15 15 def test_report_at_project_level
16 16 get :report, :project_id => 'ecookbook'
17 17 assert_response :success
18 18 assert_template 'report'
19 19 assert_tag :form,
20 20 :attributes => {:action => "/projects/ecookbook/time_entries/report", :id => 'query_form'}
21 21 end
22 22
23 23 def test_report_all_projects
24 24 get :report
25 25 assert_response :success
26 26 assert_template 'report'
27 27 assert_tag :form,
28 28 :attributes => {:action => "/time_entries/report", :id => 'query_form'}
29 29 end
30 30
31 31 def test_report_all_projects_denied
32 32 r = Role.anonymous
33 33 r.permissions.delete(:view_time_entries)
34 34 r.permissions_will_change!
35 35 r.save
36 36 get :report
37 37 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftime_entries%2Freport'
38 38 end
39 39
40 40 def test_report_all_projects_one_criteria
41 get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
41 get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project']
42 42 assert_response :success
43 43 assert_template 'report'
44 assert_not_nil assigns(:total_hours)
45 assert_equal "8.65", "%.2f" % assigns(:total_hours)
44 assert_not_nil assigns(:report)
45 assert_equal "8.65", "%.2f" % assigns(:report).total_hours
46 46 end
47 47
48 48 def test_report_all_time
49 get :report, :project_id => 1, :criterias => ['project', 'issue']
49 get :report, :project_id => 1, :criteria => ['project', 'issue']
50 50 assert_response :success
51 51 assert_template 'report'
52 assert_not_nil assigns(:total_hours)
53 assert_equal "162.90", "%.2f" % assigns(:total_hours)
52 assert_not_nil assigns(:report)
53 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
54 54 end
55 55
56 56 def test_report_all_time_by_day
57 get :report, :project_id => 1, :criterias => ['project', 'issue'], :columns => 'day'
57 get :report, :project_id => 1, :criteria => ['project', 'issue'], :columns => 'day'
58 58 assert_response :success
59 59 assert_template 'report'
60 assert_not_nil assigns(:total_hours)
61 assert_equal "162.90", "%.2f" % assigns(:total_hours)
60 assert_not_nil assigns(:report)
61 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
62 62 assert_tag :tag => 'th', :content => '2007-03-12'
63 63 end
64 64
65 65 def test_report_one_criteria
66 get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
66 get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project']
67 67 assert_response :success
68 68 assert_template 'report'
69 assert_not_nil assigns(:total_hours)
70 assert_equal "8.65", "%.2f" % assigns(:total_hours)
69 assert_not_nil assigns(:report)
70 assert_equal "8.65", "%.2f" % assigns(:report).total_hours
71 71 end
72 72
73 def test_report_two_criterias
74 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
73 def test_report_two_criteria
74 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["member", "activity"]
75 75 assert_response :success
76 76 assert_template 'report'
77 assert_not_nil assigns(:total_hours)
78 assert_equal "162.90", "%.2f" % assigns(:total_hours)
77 assert_not_nil assigns(:report)
78 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
79 79 end
80 80
81 81 def test_report_one_day
82 get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criterias => ["member", "activity"]
82 get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criteria => ["member", "activity"]
83 83 assert_response :success
84 84 assert_template 'report'
85 assert_not_nil assigns(:total_hours)
86 assert_equal "4.25", "%.2f" % assigns(:total_hours)
85 assert_not_nil assigns(:report)
86 assert_equal "4.25", "%.2f" % assigns(:report).total_hours
87 87 end
88 88
89 89 def test_report_at_issue_level
90 get :report, :project_id => 1, :issue_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
90 get :report, :project_id => 1, :issue_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["member", "activity"]
91 91 assert_response :success
92 92 assert_template 'report'
93 assert_not_nil assigns(:total_hours)
94 assert_equal "154.25", "%.2f" % assigns(:total_hours)
93 assert_not_nil assigns(:report)
94 assert_equal "154.25", "%.2f" % assigns(:report).total_hours
95 95 assert_tag :form,
96 96 :attributes => {:action => "/projects/ecookbook/issues/1/time_entries/report", :id => 'query_form'}
97 97 end
98 98
99 99 def test_report_custom_field_criteria
100 get :report, :project_id => 1, :criterias => ['project', 'cf_1', 'cf_7']
100 get :report, :project_id => 1, :criteria => ['project', 'cf_1', 'cf_7']
101 101 assert_response :success
102 102 assert_template 'report'
103 assert_not_nil assigns(:total_hours)
104 assert_not_nil assigns(:criterias)
105 assert_equal 3, assigns(:criterias).size
106 assert_equal "162.90", "%.2f" % assigns(:total_hours)
103 assert_not_nil assigns(:report)
104 assert_equal 3, assigns(:report).criteria.size
105 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
107 106 # Custom field column
108 107 assert_tag :tag => 'th', :content => 'Database'
109 108 # Custom field row
110 109 assert_tag :tag => 'td', :content => 'MySQL',
111 110 :sibling => { :tag => 'td', :attributes => { :class => 'hours' },
112 111 :child => { :tag => 'span', :attributes => { :class => 'hours hours-int' },
113 112 :content => '1' }}
114 113 # Second custom field column
115 114 assert_tag :tag => 'th', :content => 'Billable'
116 115 end
117 116
118 117 def test_report_one_criteria_no_result
119 get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project']
118 get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criteria => ['project']
120 119 assert_response :success
121 120 assert_template 'report'
122 assert_not_nil assigns(:total_hours)
123 assert_equal "0.00", "%.2f" % assigns(:total_hours)
121 assert_not_nil assigns(:report)
122 assert_equal "0.00", "%.2f" % assigns(:report).total_hours
124 123 end
125 124
126 125 def test_report_all_projects_csv_export
127 126 get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30",
128 :criterias => ["project", "member", "activity"], :format => "csv"
127 :criteria => ["project", "member", "activity"], :format => "csv"
129 128 assert_response :success
130 129 assert_equal 'text/csv', @response.content_type
131 130 lines = @response.body.chomp.split("\n")
132 131 # Headers
133 132 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total',
134 133 lines.first
135 134 # Total row
136 135 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
137 136 end
138 137
139 138 def test_report_csv_export
140 139 get :report, :project_id => 1, :columns => 'month',
141 140 :from => "2007-01-01", :to => "2007-06-30",
142 :criterias => ["project", "member", "activity"], :format => "csv"
141 :criteria => ["project", "member", "activity"], :format => "csv"
143 142 assert_response :success
144 143 assert_equal 'text/csv', @response.content_type
145 144 lines = @response.body.chomp.split("\n")
146 145 # Headers
147 146 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total',
148 147 lines.first
149 148 # Total row
150 149 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
151 150 end
152 151
153 152 def test_csv_big_5
154 153 Setting.default_language = "zh-TW"
155 154 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
156 155 str_big5 = "\xa4@\xa4\xeb"
157 156 if str_utf8.respond_to?(:force_encoding)
158 157 str_utf8.force_encoding('UTF-8')
159 158 str_big5.force_encoding('Big5')
160 159 end
161 160 user = User.find_by_id(3)
162 161 user.firstname = str_utf8
163 162 user.lastname = "test-lastname"
164 163 assert user.save
165 164 comments = "test_csv_big_5"
166 165 te1 = TimeEntry.create(:spent_on => '2011-11-11',
167 166 :hours => 7.3,
168 167 :project => Project.find(1),
169 168 :user => user,
170 169 :activity => TimeEntryActivity.find_by_name('Design'),
171 170 :comments => comments)
172 171
173 172 te2 = TimeEntry.find_by_comments(comments)
174 173 assert_not_nil te2
175 174 assert_equal 7.3, te2.hours
176 175 assert_equal 3, te2.user_id
177 176
178 177 get :report, :project_id => 1, :columns => 'day',
179 178 :from => "2011-11-11", :to => "2011-11-11",
180 :criterias => ["member"], :format => "csv"
179 :criteria => ["member"], :format => "csv"
181 180 assert_response :success
182 181 assert_equal 'text/csv', @response.content_type
183 182 lines = @response.body.chomp.split("\n")
184 183 # Headers
185 184 s1 = "\xa6\xa8\xad\xfb,2011-11-11,\xc1`\xadp"
186 185 s2 = "\xc1`\xadp"
187 186 if s1.respond_to?(:force_encoding)
188 187 s1.force_encoding('Big5')
189 188 s2.force_encoding('Big5')
190 189 end
191 190 assert_equal s1, lines.first
192 191 # Total row
193 192 assert_equal "#{str_big5} #{user.lastname},7.30,7.30", lines[1]
194 193 assert_equal "#{s2},7.30,7.30", lines[2]
195 194
196 195 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
197 196 if str_tw.respond_to?(:force_encoding)
198 197 str_tw.force_encoding('UTF-8')
199 198 end
200 199 assert_equal str_tw, l(:general_lang_name)
201 200 assert_equal 'Big5', l(:general_csv_encoding)
202 201 assert_equal ',', l(:general_csv_separator)
203 202 assert_equal '.', l(:general_csv_decimal_separator)
204 203 end
205 204
206 205 def test_csv_cannot_convert_should_be_replaced_big_5
207 206 Setting.default_language = "zh-TW"
208 207 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
209 208 if str_utf8.respond_to?(:force_encoding)
210 209 str_utf8.force_encoding('UTF-8')
211 210 end
212 211 user = User.find_by_id(3)
213 212 user.firstname = str_utf8
214 213 user.lastname = "test-lastname"
215 214 assert user.save
216 215 comments = "test_replaced"
217 216 te1 = TimeEntry.create(:spent_on => '2011-11-11',
218 217 :hours => 7.3,
219 218 :project => Project.find(1),
220 219 :user => user,
221 220 :activity => TimeEntryActivity.find_by_name('Design'),
222 221 :comments => comments)
223 222
224 223 te2 = TimeEntry.find_by_comments(comments)
225 224 assert_not_nil te2
226 225 assert_equal 7.3, te2.hours
227 226 assert_equal 3, te2.user_id
228 227
229 228 get :report, :project_id => 1, :columns => 'day',
230 229 :from => "2011-11-11", :to => "2011-11-11",
231 :criterias => ["member"], :format => "csv"
230 :criteria => ["member"], :format => "csv"
232 231 assert_response :success
233 232 assert_equal 'text/csv', @response.content_type
234 233 lines = @response.body.chomp.split("\n")
235 234 # Headers
236 235 s1 = "\xa6\xa8\xad\xfb,2011-11-11,\xc1`\xadp"
237 236 if s1.respond_to?(:force_encoding)
238 237 s1.force_encoding('Big5')
239 238 end
240 239 assert_equal s1, lines.first
241 240 # Total row
242 241 s2 = ""
243 242 if s2.respond_to?(:force_encoding)
244 243 s2 = "\xa5H?"
245 244 s2.force_encoding('Big5')
246 245 elsif RUBY_PLATFORM == 'java'
247 246 s2 = "??"
248 247 else
249 248 s2 = "\xa5H???"
250 249 end
251 250 assert_equal "#{s2} #{user.lastname},7.30,7.30", lines[1]
252 251 end
253 252
254 253 def test_csv_fr
255 254 with_settings :default_language => "fr" do
256 255 str1 = "test_csv_fr"
257 256 user = User.find_by_id(3)
258 257 te1 = TimeEntry.create(:spent_on => '2011-11-11',
259 258 :hours => 7.3,
260 259 :project => Project.find(1),
261 260 :user => user,
262 261 :activity => TimeEntryActivity.find_by_name('Design'),
263 262 :comments => str1)
264 263
265 264 te2 = TimeEntry.find_by_comments(str1)
266 265 assert_not_nil te2
267 266 assert_equal 7.3, te2.hours
268 267 assert_equal 3, te2.user_id
269 268
270 269 get :report, :project_id => 1, :columns => 'day',
271 270 :from => "2011-11-11", :to => "2011-11-11",
272 :criterias => ["member"], :format => "csv"
271 :criteria => ["member"], :format => "csv"
273 272 assert_response :success
274 273 assert_equal 'text/csv', @response.content_type
275 274 lines = @response.body.chomp.split("\n")
276 275 # Headers
277 276 s1 = "Membre;2011-11-11;Total"
278 277 s2 = "Total"
279 278 if s1.respond_to?(:force_encoding)
280 279 s1.force_encoding('ISO-8859-1')
281 280 s2.force_encoding('ISO-8859-1')
282 281 end
283 282 assert_equal s1, lines.first
284 283 # Total row
285 284 assert_equal "#{user.firstname} #{user.lastname};7,30;7,30", lines[1]
286 285 assert_equal "#{s2};7,30;7,30", lines[2]
287 286
288 287 str_fr = "Fran\xc3\xa7ais"
289 288 if str_fr.respond_to?(:force_encoding)
290 289 str_fr.force_encoding('UTF-8')
291 290 end
292 291 assert_equal str_fr, l(:general_lang_name)
293 292 assert_equal 'ISO-8859-1', l(:general_csv_encoding)
294 293 assert_equal ';', l(:general_csv_separator)
295 294 assert_equal ',', l(:general_csv_decimal_separator)
296 295 end
297 296 end
298 297 end
General Comments 0
You need to be logged in to leave comments. Login now