├── app
├── views
│ └── estimate_timelog
│ │ ├── index.html.erb
│ │ ├── edit.html.erb
│ │ ├── details.html.erb
│ │ ├── _report_criteria.html.erb
│ │ ├── _date_range.html.erb
│ │ ├── _list.html.erb
│ │ └── report.html.erb
├── helpers
│ └── estimate_timelog_helper.rb
└── controllers
│ └── estimate_timelog_controller.rb
├── config
├── locales
│ ├── en.yml
│ ├── zh.yml
│ └── ja.yml
└── routes.rb
├── test
└── test_helper.rb
├── init.rb
├── lib
└── ar_condition.rb
└── README.rdoc
/app/views/estimate_timelog/index.html.erb:
--------------------------------------------------------------------------------
1 |
Scheduleresults#index
2 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # English strings go here for Rails i18n
2 | en:
3 | my_label: "My label"
4 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # Load the normal Rails helper
2 | require File.expand_path(File.dirname(__FILE__) + '/../../../../test/test_helper')
3 |
4 | # Ensure that we are using the temporary fixture path
5 | Engines::Testing.set_fixture_path
6 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | if Rails::VERSION::MAJOR >= 3
2 | RedmineApp::Application.routes.draw do
3 | match '/estimate_timelog/:action', :controller => 'estimate_timelog', :via => [:get, :post]
4 | #match '/estimate_timelog/:action', :to => 'estimate_timelog#report'
5 | end
6 | else
7 | ActionController::Routing::Routes.draw do |map|
8 | map.connect ':controller/:action'
9 | end
10 | end
11 |
12 |
--------------------------------------------------------------------------------
/init.rb:
--------------------------------------------------------------------------------
1 | require 'redmine'
2 |
3 | Redmine::Plugin.register :redmine_estimate_timelog do
4 | name 'Redmine Estimate Timelog plugin'
5 | author 'toritori0318'
6 | description 'This is a plugin for Redmine'
7 | version '0.7.0'
8 | requires_redmine :version_or_higher => '4.0.0'
9 | menu :top_menu, :redmine_estimate_timelog, {:controller => 'estimate_timelog', :action => 'report'}, :caption => :et_label_menu
10 | end
11 |
--------------------------------------------------------------------------------
/config/locales/zh.yml:
--------------------------------------------------------------------------------
1 | zh:
2 | et_label_menu: 工时报告
3 | et_label_estimated_hours: 计划工时
4 | et_label_hours: 实际工时
5 | et_label_estimated_total: 计划工时总计
6 | et_label_hours_total: 实际工时总计
7 | et_label_radio_estimated: 计划工时
8 | et_label_radio_hours: 实用工时
9 | et_label_base_summary: 统计基准
10 | et_label_options: 选项
11 | et_label_mine: 仅显示我的报告
12 | et_label_without_closed: 不显示已关闭问题的工时统计
13 | et_label_redminedefault: Redmine默认设置
14 | et_label_tmpl: 报告模板
15 | et_label_tmpl_1: 成员ー>活动>问题
16 | et_label_tmpl_2: 成员ー>问题
17 | et_label_tmpl_3: 项目ー>成员ー>问题
18 | et_label_tmpl_4: 项目ー>版本ー>成员
19 |
20 | et_label_start_date: 起始日期
21 | et_label_due_date: 截止日期
22 | et_label_done_ratio: 完成率
23 |
24 | et_label_all_time: 整个时期
25 |
--------------------------------------------------------------------------------
/config/locales/ja.yml:
--------------------------------------------------------------------------------
1 | ja:
2 | et_label_menu: 予定/実績レポート
3 | et_label_estimated_hours: 予定工数
4 | et_label_hours: 実績工数
5 | et_label_estimated_total: 予定合計
6 | et_label_hours_total: 実績合計
7 | et_label_radio_estimated: 予定ベース
8 | et_label_radio_hours: 実績ベース
9 | et_label_base_summary: 集計ベース
10 | et_label_options: オプション
11 | et_label_mine: 自分のレポートのみ表示
12 | et_label_without_closed: 終了チケットを表示しない
13 | et_label_redminedefault: Redmineデフォルトの実績管理へ移動する
14 | et_label_tmpl: レポートテンプレート
15 | et_label_tmpl_1: メンバー>活動>チケット
16 | et_label_tmpl_2: メンバー>チケット
17 | et_label_tmpl_3: プロジェクト>メンバー>チケット
18 | et_label_tmpl_4: プロジェクト>バージョン>メンバー
19 |
20 | et_label_start_date: 開始日
21 | et_label_due_date: 期限日
22 | et_label_done_ratio: 進捗
23 |
24 | et_label_all_time: 全期間
25 |
--------------------------------------------------------------------------------
/app/views/estimate_timelog/edit.html.erb:
--------------------------------------------------------------------------------
1 | <%= l(:label_spent_time) %>
2 |
3 | <% labelled_tabular_form_for :time_entry, @time_entry, :url => {:action => 'edit', :id => @time_entry, :project_id => @time_entry.project} do |f| %>
4 | <%= error_messages_for 'time_entry' %>
5 | <%= back_url_hidden_field_tag %>
6 |
7 |
8 |
<%= f.text_field :issue_id, :size => 6 %> <%= h("#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}") if @time_entry.issue %>
9 |
<%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %>
10 |
<%= f.text_field :hours, :size => 6, :required => true %>
11 |
<%= f.text_field :comments, :size => 100 %>
12 |
<%= f.select :activity_id, activity_collection_for_select_options(@time_entry), :required => true %>
13 | <% @time_entry.custom_field_values.each do |value| %>
14 |
<%= custom_field_tag_with_label :time_entry, value %>
15 | <% end %>
16 | <%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %>
17 |
18 |
19 | <%= submit_tag l(:button_save) %>
20 |
21 | <% end %>
22 |
--------------------------------------------------------------------------------
/lib/ar_condition.rb:
--------------------------------------------------------------------------------
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 | class ARCondition
19 | attr_reader :conditions
20 |
21 | def initialize(condition=nil)
22 | @conditions = ['1=1']
23 | add(condition) if condition
24 | end
25 |
26 | def add(condition)
27 | if condition.is_a?(Array)
28 | @conditions.first << " AND (#{condition.first})"
29 | @conditions += condition[1..-1]
30 | elsif condition.is_a?(String)
31 | @conditions.first << " AND (#{condition})"
32 | else
33 | raise "Unsupported #{condition.class} condition: #{condition}"
34 | end
35 | self
36 | end
37 |
38 | def <<(condition)
39 | add(condition)
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/app/views/estimate_timelog/details.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= link_to_if_authorized l(:button_log_time), {:controller => 'estimate_timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
3 |
4 |
5 | <%= render_estimate_timelog_breadcrumb %>
6 |
7 | <%= l(:label_spent_time) %>
8 |
9 | <%# form_remote_tag( :url => {}, :html => {:method => :get, :id => 'query_form'}, :method => :get, :update => 'content', :remote=>true ) do %>
10 | <%= form_tag({:controller => 'estimate_timelog', :action => 'details', :project_id => @project, :issue_id => @issue}, :method => :get, :id => 'query_form') do %>
11 | <%# TOOD: remove the project_id and issue_id hidden fields, that information is
12 | already in the URI %>
13 | <%= hidden_field_tag('project_id', params[:project_id]) if @project %>
14 | <%= hidden_field_tag 'issue_id', params[:issue_id] if @issue %>
15 | <%= render :partial => 'date_range' %>
16 | <% end %>
17 |
18 |
19 | <% if !@est_flg -%>
20 |
<%= l(:et_label_hours_total) %>: <%= html_hours(l_hours(@total_hours)) %>
21 | <% elsif @est_flg -%>
22 |
<%= l(:et_label_estimated_total) %>: <%= html_hours(l_hours(@total_hours)) %>
23 | <% end -%>
24 |
25 |
26 | <% unless @entries.empty? %>
27 | <%= render :partial => 'list', :locals => { :entries => @entries }%>
28 |
29 |
30 | <% other_formats_links do |f| %>
31 | <%= f.link_to 'Atom', :url => params.merge({:issue_id => @issue, :key => User.current.rss_key}).permit! %>
32 | <%= f.link_to 'CSV', :url => params.permit! %>
33 | <% end %>
34 |
35 | <% end %>
36 |
37 | <% html_title l(:label_spent_time), l(:label_details) %>
38 |
39 | <% content_for :header_tags do %>
40 | <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
41 | <% end %>
42 |
--------------------------------------------------------------------------------
/app/views/estimate_timelog/_report_criteria.html.erb:
--------------------------------------------------------------------------------
1 | <% @hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value| %>
2 | <% hours_for_value = select_hours(hours, criterias[level], value) -%>
3 | <% next if hours_for_value.empty? -%>
4 | <% obj = abstract_obj_from_criterias(criterias[level], value) %>
5 |
6 | <%= (' ' * level).html_safe %>
7 | <% tmp_value = format_criteria_value(criterias[level], value, obj) %>
8 | <% check_value = tmp_value.to_s.scan(/#([0-9]*):/) %>
9 | <% if check_value.length > 0 %>
10 | <%= link_to h(format_criteria_value(criterias[level], value, obj)), :controller => 'issues', :action => 'show', :id => check_value[0][0] %>
11 | <% else %>
12 | <%= h(format_criteria_value(criterias[level], value)) %>
13 | <% end %>
14 | <%= (' ' * (criterias.length - level - 1)).html_safe -%>
15 | <% wkrow = {} -%>
16 | <% total = 0 -%>
17 | <% total_est = 0 -%>
18 | <% total_all = 0 -%>
19 | <% total_est_all = 0 -%>
20 | <%
21 | wkrow = select_hours(hours_for_value, @columns, '')
22 | if (criterias.length - 1 == level)
23 | total_est += sum_hours_est(wkrow)
24 | else
25 | total_est_all += sum_hours_est(wkrow)
26 | end
27 | -%>
28 | <%= html_hours("%.2f" % total_est) if total_est > 0 %><%= raw '(' + html_hours("%.2f" % total_est_all) + ')' if total_est_all > 0 %>
29 | <%
30 | if (criterias.length - 1 == level)
31 | total += sum_hours(wkrow, true)
32 | end
33 | -%>
34 | <%= html_hours("%.2f" % total) if total > 0 %>
35 |
36 | <% if criterias.length <= level+1 && @issue_cols -%>
37 | <% @issue_cols.each do |col| -%>
38 | <%= get_issuescol(hours_for_value, @columns, col) %>
39 | <% end -%>
40 | <% end -%>
41 |
42 | <% if criterias.length > level+1 -%>
43 | <%= render(:partial => 'report_criteria', :locals => {:criterias => criterias, :hours => hours_for_value, :level => (level + 1)}) %>
44 | <% end -%>
45 |
46 | <% end %>
47 |
--------------------------------------------------------------------------------
/README.rdoc:
--------------------------------------------------------------------------------
1 | = estimate_timelog
2 |
3 | 予定/実績工数比較できるレポーティング機能を提供します。
4 |
5 | = 動作確認バージョン(estimate_timelog-0.7.0)
6 |
7 | Redmine version 4.1.1.stable
8 | Ruby version 2.6.6-p146 (2020-03-31) [x86_64-linux]
9 | Rails version 5.2.4.2
10 |
11 | Redmine-4.x 系をお使いの方は
12 |
13 | 0.7.x をご利用ください。
14 |
15 | Redmine-3.x 系をお使いの方は
16 |
17 | 0.6.x をご利用ください。
18 |
19 | Redmine-2.x 系をお使いの方は
20 |
21 | 0.5.x をご利用ください。
22 |
23 | Redmine-1.3.x以下のバージョンをお使いの方は
24 |
25 | 0.3.x をご利用ください。
26 |
27 | Redmine-1.2.x以下のバージョンをお使いの方は
28 |
29 | 0.2.1 をご利用ください。
30 |
31 | = インストール
32 |
33 | redmine_estimate_timelogフォルダをまるごと
34 |
35 | /path-to-redmine-root/plugin
36 |
37 | に解凍し、Redmineを再起動するだけでOKです。
38 |
39 | dbのmigretionは必要ありません。
40 |
41 | = 必須プラグイン(※v0.7.0以降は必要ありません)
42 |
43 | v0.5.0 - v0.6.0 では[verification](https://github.com/sikachu/verification) が必要です。
44 |
45 | Gemfileに以下を追加してください。
46 |
47 | gem "verification", :git => "https://github.com/sikachu/verification.git"
48 |
49 |
50 |
51 | = 使い方
52 |
53 | トップレベルのメニュー(一番左上)に「予定/実績レポート」のリンクが追加されます。
54 |
55 | 基本的な使い方は「経過時間」レポートと同様です。
56 |
57 | = 仕様に関する注意点など
58 |
59 | 以下のリンクを参照ください。
60 |
61 | - http://d.zeromemory.info/2013/02/13/redmine-estimate_timelog-v0-5-0.html
62 | - http://d.hatena.ne.jp/toritori0318/20100320/1269108560
63 | - http://d.hatena.ne.jp/toritori0318/20091128/1259429374
64 |
65 | = 既知の不具合
66 | - 「予定ベース」の場合、csv 出力できない
67 |
68 | = バージョン
69 |
70 | == version 0.7.0
71 | - Redmine 4.x系(Rails5)に対応
72 | - 日付範囲選択が微妙にダウングレード(redmineの内部メソッドから削除された?)
73 |
74 | == version 0.6.1
75 | - iconv依存を削除
76 | - サブディレクトリ運用時のリンク不具合対応
77 | - 中国語対応
78 |
79 | == version 0.6.0
80 | - Redmine 3.x系(Rails4)に対応
81 |
82 | == version 0.5.2
83 | - 「終了チケットを表示しない」オプション追加
84 | - チケット出力時 redmine の css 適用
85 |
86 | == version 0.5.0
87 | - Redmine 2.x系(Rails3) に対応。
88 | - レポートの「予定ベース」の出力を実質「予定/実績ベース」に変更。
89 | - 子チケットが存在する予定工数は工数として集計しない。(Redmineにより集計されて二重カウントになるため)
90 | - 「全期間」を指定した場合、日付の範囲指定をしないようにした。(日付未設定も表示するため)
91 |
92 | == version 0.4.0
93 |
94 | - Redmine 1.4 対応
95 |
96 | == version 0.3.0
97 |
98 | - 1.3.0以降のバージョンでエラーが発生していたので対応
99 | - Redmineデフォルトの実績管理へのリンクを追加
100 | - デザインを最新のバージョンに合わせた
101 |
102 | == version 0.2.1
103 |
104 | - 予定ベースの詳細レポートでエラーが発生した不具合を修正しました
105 |
106 | == version 0.2.0
107 |
108 | - 0.9.x用にリビルドしました
109 |
110 | == version 0.1.2
111 |
112 | - 月をまたぐ期間を指定した場合に、列が複数に分解していたのを1列にまとめるように変更
113 | - レポート集計単位をチケットにした場合、「進捗」「開始日」「終了日」を項目に追加
114 | - 予定ベースの検索で、sql文を期間範囲検索に変更
115 | - レポートテンプレートをプルダウンリストに変更
116 |
117 | == version 0.1.1
118 |
119 | - IEで「レポートテンプレート」が効いていなかったバグの修正
120 |
121 | == version 0.1.0
122 |
123 | - redmine_estimate_timelogフォルダを最上位に作成(インストールしやすいようにしただけ)
124 |
125 | - 表示される集計データを「予定ベース」と「実績ベース」を選択できるように変更
126 | - 予定ベースの場合「チケットの担当者」がメンバーになる。
127 | また絞り込み日付は「チケットの開始日」になる。
128 | - 実績ベースの場合「時間トラッキングの担当者」がメンバーになる
129 | また絞り込み日付は「時間トラッキングの記録日」になる。
130 |
131 | - 条件によっては、予定が重複して集計されていたバグの対応
132 |
133 | - 検索条件に「自分のレポート」絞り込み条件を追加
134 | - レポートに、集計の階層構造を表示するように対応
135 | - レポートで「チケット階層」表示時に、チケットへのリンクが出来るように対応
136 | - csvのヘッダー表示バグ対応
137 | - csvのヘッダー年月表示を、htmlと同じ表示に変更
138 |
139 | == version 0.0.3
140 |
141 | - レポートのnilエラー対策
142 | - 詳細タブの合計ラベルを「実績合計」に変更
143 |
144 | == version 0.0.2
145 |
146 | - 予定も実績も0のレコードは除外するように変更
147 |
148 | == version 0.0.1
149 |
150 | - first commit
151 |
152 |
153 |
--------------------------------------------------------------------------------
/app/views/estimate_timelog/_date_range.html.erb:
--------------------------------------------------------------------------------
1 | <%= raw link_to(l(:et_label_redminedefault), (config.relative_url_root || '') + '/time_entries' + if controller.action_name == 'report'; ('/' + controller.action_name) else '' end) if User.current.allowed_to?(:view_time_entries, nil, :global => true) %>
2 |
7 |
8 | <%= l(:label_date_range) %>
9 |
10 |
11 | <%= label_tag "period_type_list", l(:description_date_range_list), :class => "hidden-for-sighted" %>
12 | <%= radio_button_tag 'period_type', '1', !@free_period, :onclick => '$("#from,#to").attr("disabled", true);$("#period").removeAttr("disabled");', :id => "period_type_list"%>
13 | <%= select_tag 'period', options_for_period_select(params[:period]),
14 | :onchange => 'this.form.submit();',
15 | :onfocus => '$("#period_type_list").attr("checked", true);',
16 | :disabled => @free_period %>
17 |
18 |
19 | <%= label_tag "period_type_interval", l(:description_date_range_interval), :class => "hidden-for-sighted" %>
20 | <%= radio_button_tag 'period_type', '2', @free_period, :onclick => '$("#from,#to").removeAttr("disabled");$("#period").attr("disabled", true);', :id => "period_type_interval" %>
21 |
22 | <%= l(:label_date_from_to,
23 | :start => ((label_tag "from", l(:description_date_from), :class => "hidden-for-sighted") +
24 | text_field_tag('from', @from, :size => 10, :disabled => !@free_period) + calendar_for('from')),
25 | :end => ((label_tag "to", l(:description_date_to), :class => "hidden-for-sighted") +
26 | text_field_tag('to', @to, :size => 10, :disabled => !@free_period) + calendar_for('to'))).html_safe %>
27 |
28 |
29 |
30 |
31 |
32 |
33 | <%= l(:et_label_base_summary) %>
34 |
35 |
36 | <%= radio_button_tag 'est_type', '1', @est_flg %>
37 | <%= l(:et_label_radio_estimated) %>
38 |
39 |
40 | <%= radio_button_tag 'est_type', '2', !@est_flg %>
41 | <%= l(:et_label_radio_hours) %>
42 |
43 |
44 |
45 |
46 |
47 | <%= l(:et_label_options) %>
48 |
49 |
50 | <%= check_box_tag 'my_type', User.current.id, @mine_flg %> <%= l(:et_label_mine) %>
51 | <%= check_box_tag 'without_closed', '1', @without_closed_flg %> <%= l(:et_label_without_closed) %>
52 |
53 |
54 |
55 |
56 |
57 | <%= link_to_function l(:button_apply), '$("#query_form").submit()', :class => 'icon icon-checked' %>
58 | <%= link_to l(:button_clear), {:controller => controller_name, :action => controller.action_name, :project_id => @project, :issue_id => @issue}, :class => 'icon icon-reload' %>
59 |
60 |
61 |
62 | <% url_params = @free_period ? { :from => @from, :to => @to } : { :period => params[:period] } %>
63 | <% url_params['est_flg'] = @est_flg if !@est_flg %>
64 | <% url_params['mine_flg'] = @mine_flg if @mine_flg %>
65 | <% url_params['without_closed'] = @without_closed_flg if !@without_closed_flg %>
66 |
67 | <%= link_to(l(:label_details), url_params.merge({:controller => 'estimate_timelog', :action => 'details', :project_id => @project }),
68 | :class => (controller.action_name == 'details' ? 'selected' : nil)) %>
69 | <%= link_to(l(:label_report), url_params.merge({:controller => 'estimate_timelog', :action => 'report', :project_id => @project}),
70 | :class => (controller.action_name == 'report' ? 'selected' : nil)) %>
71 |
72 |
73 |
74 | <%= javascript_tag do %>
75 | $('#from, #to').change(function(){
76 | $('#period_type_interval').attr('checked', true); $('#from,#to').removeAttr('disabled'); $('#period').attr('disabled', true);
77 | });
78 | <% end %>
79 |
--------------------------------------------------------------------------------
/app/views/estimate_timelog/_list.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_tag({}) do -%>
2 | <%= hidden_field_tag 'back_url', url_for(params.permit!) %>
3 |
84 | <% end -%>
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/app/views/estimate_timelog/report.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= link_to_if_authorized l(:button_log_time), {:controller => 'estimate_timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
3 |
4 |
5 | <%= render_estimate_timelog_breadcrumb %>
6 |
7 | <%= l(:label_spent_time) %>
8 |
9 | <%= form_tag({:controller => 'estimate_timelog', :action => 'report',
10 | :project_id => @project, :issue_id => @issue},
11 | :method => :get, :id => 'query_form') do %>
12 | <% @criterias.each do |criteria| %>
13 | <%= hidden_field_tag 'criterias[]', criteria, :id => nil %>
14 | <% end %>
15 | <%# TODO: get rid of the project_id field, that should already be in the URL %>
16 | <%= hidden_field_tag('project_id', params[:project_id]) if @project %>
17 | <%= hidden_field_tag('issue_id', params[:issue_id]) if @issue %>
18 | <%= render :partial => 'date_range' %>
19 |
20 |
21 | <%= l(:button_add) %> : <%= select_tag('criterias[]', options_for_select([[]] + (@combobox_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}),
22 | :onchange => "this.form.submit();",
23 | :style => 'width: 200px',
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, :est_type => params[:est_type], :my_type => params[:my_type], :columns => @columns}, :class => 'icon icon-reload' %>
27 | <%= l(:et_label_tmpl) %>:
28 | <%= select_tag(
29 | 'tmpl_cond',
30 | options_for_select(
31 | [ ['',''],
32 | [l(:et_label_tmpl_1), '1'],
33 | [l(:et_label_tmpl_2), '2'],
34 | [l(:et_label_tmpl_3), '3'],
35 | [l(:et_label_tmpl_4), '4']
36 | ]
37 | ),
38 | :onchange => "this.form.submit();"
39 | ) %>
40 | <% end %>
41 |
42 | <% unless @criterias.empty? %>
43 |
44 |
<%= l(:et_label_estimated_total) %>: <%= html_hours(l_hours(@total_hours_est)) %>
45 |
<%= l(:et_label_hours_total) %>: <%= html_hours(l_hours(@total_hours)) %>
46 | <% disp_criterias = [] %>
47 | <% @criterias.each do |criteria| %>
48 | <% disp_criterias.push(l(@available_criterias[criteria][:label])) %>
49 | <% end %>
50 | <% if disp_criterias.length > 0 -%>
51 |
<%= disp_criterias.join(' > ') %>
52 | <% end %>
53 |
54 |
55 | <% @cache_est = {} %>
56 |
57 | <% unless @hours.empty? %>
58 |
89 |
90 | <% other_formats_links do |f| %>
91 | <%= f.link_to 'CSV', :url => params.permit! %>
92 | <% end %>
93 | <% end %>
94 | <% end %>
95 |
96 | <% html_title l(:label_spent_time), l(:label_report) %>
97 |
98 |
--------------------------------------------------------------------------------
/app/helpers/estimate_timelog_helper.rb:
--------------------------------------------------------------------------------
1 | # redMine - project management software
2 | # Copyright (C) 2006 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 EstimateTimelogHelper
19 | include ApplicationHelper
20 |
21 | def render_estimate_timelog_breadcrumb
22 | links = []
23 | links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
24 | links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
25 | if @issue
26 | if @issue.visible?
27 | links << link_to_issue(@issue, :subject => false)
28 | else
29 | links << "##{@issue.id}"
30 | end
31 | end
32 | breadcrumb links
33 | end
34 |
35 | # is active.
36 | def activity_collection_for_select_options(time_entry=nil, project=nil)
37 | project ||= @project
38 | if project.nil?
39 | activities = TimeEntryActivity.shared.active
40 | else
41 | activities = project.activities
42 | end
43 |
44 | collection = []
45 | if time_entry && time_entry.activity && !time_entry.activity.active?
46 | collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
47 | else
48 | collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
49 | end
50 | activities.each { |a| collection << [a.name, a.id] }
51 | collection
52 | end
53 |
54 | def select_hours(data, criteria, value)
55 | if value.to_s.empty?
56 | data.select {|row| row[criteria].blank?}
57 | else
58 | data.select {|row| row[criteria].to_s == value}
59 | end
60 | end
61 |
62 | def sum_hours(data, is_child_only = true)
63 | sum = 0
64 | data.each do |row|
65 | if (is_child_only)
66 | sum += row['hours'].to_f
67 | else
68 | sum += row['hours_all'].to_f
69 | end
70 | end
71 | sum
72 | end
73 |
74 | def sum_hours_est(data, is_child_only = true)
75 | sum = 0
76 | data.each do |row|
77 | if (is_child_only)
78 | sum += row['hours_est'].to_f
79 | else
80 | sum += row['hours_est_all'].to_f
81 | end
82 | end
83 | sum
84 | end
85 |
86 | def get_issuescol(data, criteria, col)
87 | ret = ""
88 | data.each do |row|
89 | ret = row[col].to_s
90 | end
91 | ret
92 | end
93 |
94 | def options_for_period_select(value)
95 | options_for_select([[l(:et_label_all_time), 'all'],
96 | [l(:label_today), 'today'],
97 | [l(:label_yesterday), 'yesterday'],
98 | [l(:label_this_week), 'current_week'],
99 | [l(:label_last_week), 'last_week'],
100 | [l(:label_last_n_days, 7), '7_days'],
101 | [l(:label_this_month), 'current_month'],
102 | [l(:label_last_month), 'last_month'],
103 | [l(:label_last_n_days, 30), '30_days'],
104 | [l(:label_this_year), 'current_year']],
105 | value)
106 | end
107 |
108 | def entries_to_csv(entries)
109 | decimal_separator = l(:general_csv_decimal_separator)
110 | custom_fields = TimeEntryCustomField.find(:all)
111 | export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
112 | # csv header fields
113 | headers = [l(:field_spent_on),
114 | l(:field_user),
115 | l(:field_activity),
116 | l(:field_project),
117 | l(:field_issue),
118 | l(:field_tracker),
119 | l(:field_subject),
120 | l(:et_label_estimated_hours),
121 | l(:et_label_hours),
122 | l(:field_comments)
123 | ]
124 | # Export custom fields
125 | headers += custom_fields.collect(&:name)
126 |
127 | csv << headers.collect {|c| begin; c.to_s.encode(l(:general_csv_encoding)); rescue; c.to_s; end }
128 | # csv lines
129 | entries.each do |entry|
130 | if entry.is_a?(TimeEntry)
131 | fields = [format_date(entry.spent_on),
132 | entry.user,
133 | entry.activity,
134 | entry.project,
135 | entry.issue.id,
136 | entry.issue.tracker,
137 | entry.issue.subject,
138 | entry.issue.estimated_hours.to_s.gsub('.', decimal_separator),
139 | entry.hours.to_s.gsub('.', decimal_separator),
140 | entry.comments
141 | ]
142 | else
143 | # todo: bugs!
144 | fields = [format_date(entry.start_date),
145 | entry.assigned_to_id,
146 | nil,
147 | entry.project,
148 | entry.id,
149 | entry.tracker,
150 | entry.subject,
151 | entry.estimated_hours.to_s.gsub('.', decimal_separator),
152 | nil,
153 | nil
154 | ]
155 | end
156 | fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) }
157 |
158 | csv << fields.collect {|c| begin; c.to_s.encode(l(:general_csv_encoding)); rescue; c.to_s; end }
159 | end
160 | end
161 | export
162 | end
163 |
164 | # yet issue only
165 | def abstract_obj_from_criterias(criteria, value)
166 | if !value.blank? && k = @available_criterias[criteria][:klass]
167 | obj = k.find_by_id(value.to_i)
168 | if obj.is_a?(Issue)
169 | obj
170 | end
171 | end
172 | end
173 |
174 | def format_criteria_value(criteria, value, obj = nil)
175 | if value.blank?
176 | l(:label_none)
177 | elsif obj.is_a?(Issue)
178 | obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
179 | elsif k = @available_criterias[criteria][:klass]
180 | obj = k.find_by_id(value.to_i)
181 | if obj.is_a?(Issue)
182 | obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
183 | else
184 | obj
185 | end
186 | else
187 | format_value(value, @available_criterias[criteria][:format])
188 | end
189 | end
190 |
191 | def report_to_csv_est(criterias, issue_cols, hours)
192 | export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
193 | # Column headers
194 | headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
195 | headers << l(:et_label_estimated_hours)
196 | headers << l(:et_label_hours)
197 | issue_cols.each do |col|
198 | headers << l(@available_criterias[col][:label])
199 | end
200 | csv << headers.collect {|c| to_utf8(c) }
201 | # Content
202 | report_criteria_to_csv(csv, criterias, issue_cols, hours)
203 | # Total row
204 | row = [ l(:label_total) ] + [''] * (criterias.size - 1)
205 | total_est = 0
206 | total = 0
207 | sum_est = sum_hours_est(select_hours(hours, @columns, ''))
208 | total_est += sum_est
209 | sum = sum_hours(select_hours(hours, @columns, ''))
210 | total += sum
211 | row << "%.2f" %total_est
212 | row << "%.2f" %total
213 | csv << row.collect {|c| to_utf8(c) }
214 | #csv << row
215 | end
216 | export
217 | end
218 |
219 | def report_criteria_to_csv(csv, criterias, issue_cols, hours, level=0)
220 | hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value|
221 | hours_for_value = select_hours(hours, criterias[level], value)
222 | next if hours_for_value.empty?
223 | row = [''] * level
224 | row << to_utf8(format_criteria_value(criterias[level], value))
225 | row += [''] * (criterias.length - level - 1)
226 | total_est = 0
227 | total = 0
228 | sum_est = sum_hours_est(select_hours(hours_for_value, @columns, ''))
229 | total_est += sum_est
230 | sum = sum_hours(select_hours(hours_for_value, @columns, ''))
231 | total += sum
232 | row << "%.2f" %total_est
233 | row << "%.2f" %total
234 | if (criterias.length <= (level+1)) && issue_cols
235 | issue_cols.each do |col|
236 | row << get_issuescol(hours_for_value, @columns, col)
237 | end
238 | end
239 | csv << row
240 |
241 | if criterias.length > level + 1
242 | report_criteria_to_csv(csv, criterias, issue_cols, hours_for_value, level + 1)
243 | end
244 | end
245 | end
246 |
247 | def to_utf8(s)
248 | begin; s.to_s.encode(l(:general_csv_encoding)); rescue; s.to_s; end
249 | end
250 | end
251 |
--------------------------------------------------------------------------------
/app/controllers/estimate_timelog_controller.rb:
--------------------------------------------------------------------------------
1 | # redMine - project management software
2 | # Copyright (C) 2006-2007 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 | class EstimateTimelogController < ApplicationController
19 | unloadable
20 |
21 | menu_item :issues
22 | before_action :find_project, :authorize, :only => [:edit, :destroy]
23 | before_action :find_optional_project, :only => [:report, :details]
24 |
25 | helper :sort
26 | include SortHelper
27 | helper :issues
28 | include TimelogHelper
29 | helper :custom_fields
30 | include CustomFieldsHelper
31 | include EstimateTimelogHelper
32 | helper :issue_relations
33 | include IssueRelationsHelper
34 |
35 | def report
36 | @combobox_criterias = { 'project' => "project",
37 | 'version' => "version",
38 | 'category' => "category",
39 | 'member' => "member",
40 | 'tracker' => "tracker",
41 | 'activity' => "activity",
42 | 'issue' => "issue",
43 | }
44 |
45 | @available_criterias = { 'project' => {:sql => "project",
46 | :klass => Project,
47 | :label => :label_project},
48 | 'version' => {:sql => "version",
49 | :klass => Version,
50 | :label => :label_version},
51 | 'category' => {:sql => "category",
52 | :klass => IssueCategory,
53 | :label => :field_category},
54 | 'member' => {:sql => "member",
55 | :klass => User,
56 | :label => :label_member},
57 | 'tracker' => {:sql => "tracker",
58 | :klass => Tracker,
59 | :label => :label_tracker},
60 | 'activity' => {:sql => "activity",
61 | :klass => TimeEntryActivity,
62 | :label => :label_activity},
63 | 'issue' => {:sql => "issue",
64 | :klass => Issue,
65 | :label => :label_issue},
66 | 'start_date' => {:sql => "start_date",
67 | :klass => '',
68 | :label => :et_label_start_date},
69 | 'due_date' => {:sql => "due_date",
70 | :klass => '',
71 | :label => :et_label_due_date},
72 | 'done_ratio' => {:sql => "done_ratio",
73 | :klass => '',
74 | :label => :et_label_done_ratio}
75 | }
76 |
77 | @available_criterias_yotei = { 'project' => {:sql => "issues.project_id",
78 | :klass => Project,
79 | :label => :label_project},
80 | 'version' => {:sql => "issues.fixed_version_id",
81 | :klass => Version,
82 | :label => :label_version},
83 | 'category' => {:sql => "issues.category_id",
84 | :klass => IssueCategory,
85 | :label => :field_category},
86 | 'member' => {:sql => "issues.assigned_to_id",
87 | :klass => User,
88 | :label => :label_member},
89 | 'tracker' => {:sql => "issues.tracker_id",
90 | :klass => Tracker,
91 | :label => :label_tracker},
92 | 'activity' => {:sql => "''",
93 | :klass => TimeEntryActivity,
94 | :label => :label_activity},
95 | 'issue' => {:sql => "issues.id",
96 | :klass => Issue,
97 | :label => :label_issue},
98 | 'start_date' => {:sql => "issues.start_date",
99 | :klass => '',
100 | :label => :et_label_start_date},
101 | 'due_date' => {:sql => "issues.due_date",
102 | :klass => '',
103 | :label => :et_label_due_date},
104 | 'done_ratio' => {:sql => "issues.done_ratio",
105 | :klass => '',
106 | :label => :et_label_done_ratio}
107 | }
108 |
109 | @available_criterias_jisseki = { 'project' => {:sql => "time_entries.project_id",
110 | :klass => Project,
111 | :label => :label_project},
112 | 'version' => {:sql => "issues.fixed_version_id",
113 | :klass => Version,
114 | :label => :label_version},
115 | 'category' => {:sql => "issues.category_id",
116 | :klass => IssueCategory,
117 | :label => :field_category},
118 | 'member' => {:sql => "time_entries.user_id",
119 | :klass => User,
120 | :label => :label_member},
121 | 'tracker' => {:sql => "issues.tracker_id",
122 | :klass => Tracker,
123 | :label => :label_tracker},
124 | 'activity' => {:sql => "time_entries.activity_id",
125 | :klass => TimeEntryActivity,
126 | :label => :label_activity},
127 | 'issue' => {:sql => "time_entries.issue_id",
128 | :klass => Issue,
129 | :label => :label_issue},
130 | 'start_date' => {:sql => "issues.start_date",
131 | :klass => '',
132 | :label => :et_label_start_date},
133 | 'due_date' => {:sql => "issues.due_date",
134 | :klass => '',
135 | :label => :et_label_due_date},
136 | 'done_ratio' => {:sql => "issues.done_ratio",
137 | :klass => '',
138 | :label => :et_label_done_ratio}
139 | }
140 |
141 | # Add list and boolean custom fields as available criterias
142 | custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
143 | custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
144 | @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)",
145 | :format => cf.field_format,
146 | :label => cf.name}
147 | end if @project
148 |
149 | # Add list and boolean time entry custom fields
150 | TimeEntryCustomField.all().select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
151 | @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)",
152 | :format => cf.field_format,
153 | :label => cf.name}
154 | end
155 |
156 | # Add list and boolean time entry activity custom fields
157 | TimeEntryActivityCustomField.all().select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
158 | @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)",
159 | :format => cf.field_format,
160 | :label => cf.name}
161 | end
162 |
163 | if params[:tmpl_cond] == '1'
164 | @criterias = ["member", "activity", "issue"]
165 | elsif params[:tmpl_cond] == '2'
166 | @criterias = ["member", "issue"]
167 | elsif params[:tmpl_cond] == '3'
168 | @criterias = ["project", "member", "issue"]
169 | elsif params[:tmpl_cond] == '4'
170 | @criterias = ["project", "version", "member"]
171 | else
172 | @criterias = params[:criterias] || []
173 | end
174 | @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
175 | @criterias.uniq!
176 | @criterias = @criterias[0,3]
177 |
178 | @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
179 |
180 | retrieve_date_range
181 |
182 | @issue_cols = []
183 |
184 | unless @criterias.empty?
185 | @column_tbl = ''
186 | if params[:est_type] == '1'
187 | @column_tbl = 'yotei'
188 | elsif params[:est_type] == '2'
189 | @column_tbl = 'jisseki'
190 | end
191 | sql_select_all = @criterias.collect{|criteria| @column_tbl +'.'+ @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
192 | sql_group_by_all = @criterias.collect{|criteria| @column_tbl + '.' + @available_criterias[criteria][:sql]}.join(', ')
193 |
194 | sql_select_yotei = @criterias.collect{|criteria| @available_criterias_yotei[criteria][:sql] + " AS " + criteria}.join(', ')
195 | sql_group_by_yotei = @criterias.collect{|criteria| @available_criterias_yotei[criteria][:sql]}.join(', ')
196 |
197 | sql_select_jisseki = @criterias.collect{|criteria| @available_criterias_jisseki[criteria][:sql] + " AS " + criteria}.join(', ')
198 | sql_group_by_jisseki = @criterias.collect{|criteria| @available_criterias_jisseki[criteria][:sql]}.join(', ')
199 |
200 | if @criterias.index("issue")
201 | @issue_cols = ["start_date","due_date","done_ratio"]
202 | sql_select_all << ', ' + @issue_cols.collect{|col| @column_tbl +'.'+ @available_criterias[col][:sql] + " AS " + col}.join(', ')
203 | sql_group_by_all << ', ' + @issue_cols.collect{|col| @column_tbl +'.'+ @available_criterias[col][:sql]}.join(', ')
204 | sql_select_yotei << ', ' + @issue_cols.collect{|col| @available_criterias_yotei[col][:sql] + " AS " + col}.join(', ')
205 | sql_group_by_yotei << ', ' + @issue_cols.collect{|col| @available_criterias_yotei[col][:sql]}.join(', ')
206 | sql_select_jisseki << ', ' + @issue_cols.collect{|col| @available_criterias_jisseki[col][:sql] + " AS " + col}.join(', ')
207 | sql_group_by_jisseki << ', ' + @issue_cols.collect{|col| @available_criterias_jisseki[col][:sql]}.join(', ')
208 | end
209 |
210 | sql = "SELECT "
211 | if params[:est_type] == '1'
212 | sql << "#{sql_select_all}, sum(if(child_cnt = 0, yotei.hours_est, 0)) hours_est,"
213 | sql << "sum(if(child_cnt != 0, yotei.hours_est, 0)) hours_est_all, "
214 | sql << "sum(if(child_cnt = 0, yotei.hours, 0)) hours, "
215 | sql << "sum(if(child_cnt != 0, yotei.hours, 0)) hours_all "
216 | elsif params[:est_type] == '2'
217 | sql << "#{sql_select_all}, yotei.hours_est hours_est, jisseki.hours "
218 | end
219 | sql << "FROM "
220 | sql << "(SELECT #{sql_select_yotei}, issues.id as issue_id, "
221 | sql << " '' as spent_on, SUM(estimated_hours) AS hours_est, 0 AS hours, "
222 | sql << " (select count(*) from issues wk where wk.parent_id = issues.id) child_cnt "
223 | sql << " FROM issues "
224 | sql << " INNER JOIN #{IssueStatus.table_name} isst ON issues.status_id = isst.id "
225 | sql << " LEFT JOIN projects ON issues.project_id = projects.id "
226 | sql << " WHERE 1=1"
227 | sql << " AND (%s) " % @project.project_condition(Setting.display_subprojects_issues?) if @project
228 | sql << " AND (%s) " % Project.allowed_to_condition(User.current, :view_time_entries)
229 | if params[:est_type] == '1'
230 | if (!@period_all)
231 | sql << " AND (start_date <= '%s')" % [ActiveRecord::Base.connection.quoted_date(@to.to_time)]
232 | sql << " AND (due_date >= '%s')" % [ActiveRecord::Base.connection.quoted_date(@from.to_time)]
233 | end
234 | sql << " AND (issues.assigned_to_id = '%s')" % [User.current.id] if params[:my_type]
235 | sql << " AND isst.is_closed = 0 " if @without_closed_flg
236 | end
237 | sql << " GROUP BY #{sql_group_by_yotei}, issues.id"
238 | if params[:est_type] == '1'
239 | sql << " UNION ALL "
240 | elsif params[:est_type] == '2'
241 | sql << ") yotei "
242 | sql << "RIGHT JOIN ("
243 | end
244 | sql << "SELECT #{sql_select_jisseki}, issues.id as issue_id, '' as spent_on, '' as hours_est, SUM(hours) AS hours, 0 child_cnt "
245 | sql << " FROM time_entries LEFT JOIN issues ON time_entries.issue_id = issues.id "
246 | sql << " INNER JOIN #{IssueStatus.table_name} isst ON issues.status_id = isst.id "
247 | sql << " LEFT JOIN projects ON issues.project_id = projects.id "
248 | sql << " WHERE 1=1"
249 | sql << " AND (%s) " % @project.project_condition(Setting.display_subprojects_issues?) if @project
250 | sql << " AND (%s) " % Project.allowed_to_condition(User.current, :view_time_entries)
251 | if params[:est_type] == '2'
252 | if (!@period_all)
253 | sql << " AND (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
254 | end
255 | sql << " AND (time_entries.user_id = '%s')" % [User.current.id] if params[:my_type]
256 | sql << " AND isst.is_closed = 0 " if @without_closed_flg
257 | end
258 | sql << " group by #{sql_group_by_jisseki}, issues.id "
259 | if params[:est_type] == '1'
260 | sql << " ) yotei "
261 | sql << "WHERE yotei.hours_est > 0 OR yotei.hours > 0 "
262 | sql << "group by #{sql_group_by_all}"
263 | elsif params[:est_type] == '2'
264 | sql << " ) jisseki "
265 | sql << "ON (yotei.issue_id=jisseki.issue_id) "
266 | sql << "WHERE yotei.hours_est > 0 OR jisseki.hours > 0 "
267 | end
268 |
269 | @hours = ActiveRecord::Base.connection.select_all(sql)
270 | @hours = @hours.map{|i| i['done_ratio'] = i['done_ratio'].to_s+'%' if i.has_key? 'done_ratio'; i}
271 | @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
272 | @total_hours_est = @hours.inject(0) {|s,k| s = s + k['hours_est'].to_f}
273 |
274 | end
275 |
276 | respond_to do |format|
277 | format.html { render :layout => !request.xhr? }
278 | format.csv { send_data(report_to_csv_est(@criterias, @issue_cols, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
279 | end
280 | end
281 |
282 | def details
283 |
284 | retrieve_date_range
285 |
286 | if !@est_flg
287 | sort_init 'spent_on', 'desc'
288 | sort_update 'spent_on' => 'spent_on',
289 | 'user' => 'user_id',
290 | 'activity' => "#{TimeEntry.table_name}.activity_id",
291 | 'project' => "#{Project.table_name}.name",
292 | 'issue' => 'issue_id',
293 | 'hours' => 'hours'
294 | cond = ARCondition.new
295 | if @project.nil?
296 | cond << Project.allowed_to_condition(User.current, :view_time_entries)
297 | elsif @issue.nil?
298 | cond << @project.project_condition(Setting.display_subprojects_issues?)
299 | else
300 | cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]
301 | end
302 |
303 | if params[:my_type]
304 | cond << ["#{TimeEntry.table_name}.user_id = ?", User.current.id]
305 | end
306 |
307 | if @without_closed_flg
308 | cond << ["#{IssueStatus.table_name}.is_closed = 0"]
309 | end
310 | cond << ['spent_on BETWEEN ? AND ?', @from, @to]
311 |
312 | respond_to do |format|
313 | format.html {
314 | # Paginate results
315 | @entry_count = TimeEntry.includes({:issue => :status}, :project)
316 | .where(cond.conditions)
317 | .references(:status, :project)
318 | .count()
319 | @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
320 | @entries = TimeEntry.visible
321 | .includes({:issue => :status}, :project, :activity, :user, {:issue => :tracker})
322 | .where(cond.conditions)
323 | .references(:status, :project, :user)
324 | .order(sort_clause)
325 | .limit(@entry_pages.items_per_page)
326 | .offset(@entry_pages.current.offset)
327 | #@total_hours = TimeEntry.sum(:hours, :include => [{:issue => :status}, :project], :conditions => cond.conditions).to_f
328 | @total_hours = TimeEntry.includes({:issue => :status}, :project)
329 | .where(cond.conditions)
330 | .references(:status, :project)
331 | .sum(:hours).to_f
332 | render :layout => !request.xhr?
333 | }
334 | format.atom {
335 | entries = TimeEntry.visible
336 | .includes({:issue => [:status, :tracker]}, :project, :activity, :user)
337 | .where(cond.conditions)
338 | .references(:status, :project, :user)
339 | .order("#{TimeEntry.table_name}.created_on DESC")
340 | .limit(Setting.feeds_limit.to_i)
341 | }
342 | format.csv {
343 | # Export all entries
344 | @entries = TimeEntry.visible
345 | .includes({:issue => [:status, :tracker, :assigned_to, :priority]}, :project, :activity, :user)
346 | .where(cond.conditions)
347 | .references(:status, :project, :user)
348 | .order(sort_clause)
349 | }
350 | end
351 | elsif @est_flg
352 | sort_init 'start_date', 'desc'
353 | sort_update 'start_date' => 'start_date',
354 | 'author' => 'assigned_to_id',
355 | # 'activity' => "#{TimeEntry.table_name}.activity_id",
356 | 'project' => "#{Project.table_name}.name",
357 | 'issue' => "#{Issue.table_name}.id",
358 | 'estimated_hours' => 'estimated_hours'
359 | cond = ARCondition.new
360 | if @project.nil?
361 | cond << Project.allowed_to_condition(User.current, :view_time_entries)
362 | elsif @issue.nil?
363 | cond << @project.project_condition(Setting.display_subprojects_issues?)
364 | else
365 | cond << ["#{Issue.table_name}.id = ?", @issue.id]
366 | end
367 |
368 | if params[:my_type]
369 | cond << ["#{Issue.table_name}.assigned_to_id = ?", User.current.id]
370 | end
371 |
372 | if @without_closed_flg
373 | cond << ["#{IssueStatus.table_name}.is_closed = 0"]
374 | end
375 |
376 | if (!@period_all)
377 | cond << ['(start_date <= ?) AND (due_date >= ?) ', @to, @from]
378 | end
379 |
380 | respond_to do |format|
381 | format.html {
382 | # Paginate results
383 | @entry_count = Issue.includes(:status, :project)
384 | .references(:status, :project)
385 | .where(cond.conditions)
386 | .count()
387 | @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
388 | @entries = Issue.includes(:status, :project, :assigned_to)
389 | .where(cond.conditions)
390 | .references(:status, :project, :assigned_to)
391 | .order(sort_clause)
392 | .limit(@entry_pages.items_per_page)
393 | .offset(@entry_pages.current.offset)
394 | #@total_hours = Issue.sum(:estimated_hours, :include => [:status, :project], :conditions => cond.conditions).to_f
395 | @total_hours = Issue
396 | .includes(:status, :project)
397 | .where(cond.conditions)
398 | .sum(:estimated_hours).to_f
399 | render :layout => !request.xhr?
400 | }
401 | format.atom {
402 | entries = Issue.includes(:status, :project, :assigned_to)
403 | .where(cond.conditions)
404 | .references(:status, :project, :assigned_to)
405 | .order("#{Issue.table_name}.created_on DESC")
406 | .limit(Setting.feeds_limit.to_i)
407 | render_feed(entries, :title => l(:label_spent_time))
408 | }
409 | format.csv {
410 | # Export all entries
411 | @entries = Issue.includes(:status, :project, :assigned_to)
412 | .where(cond.conditions)
413 | .references(:status, :project, :assigned_to)
414 | .order(sort_clause)
415 | send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog_est.csv')
416 | }
417 | end
418 | end
419 | end
420 |
421 | def edit
422 | (render_403; return) if @time_entry && !@time_entry.editable_by?(User.current)
423 | @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
424 | @time_entry.attributes = params[:time_entry]
425 |
426 | call_hook(:controller_estimate_timelog_edit_before_save, {:params => params, :time_entry => @time_entry })
427 |
428 | if request.post? and @time_entry.save
429 | flash[:notice] = l(:notice_successful_update)
430 | redirect_back_or_default :action => 'details', :project_id => @time_entry.project
431 | return
432 | end
433 | end
434 |
435 | def destroy
436 | (render_404; return) unless @time_entry
437 | (render_403; return) unless @time_entry.editable_by?(User.current)
438 | @time_entry.destroy
439 | flash[:notice] = l(:notice_successful_delete)
440 | redirect_to :back
441 | rescue ::ActionController::RedirectBackError
442 | redirect_to :action => 'details', :project_id => @time_entry.project
443 | end
444 |
445 | private
446 | def find_project
447 | if params[:id]
448 | @time_entry = TimeEntry.find(params[:id])
449 | @project = @time_entry.project
450 | elsif params[:issue_id]
451 | @issue = Issue.find(params[:issue_id])
452 | @project = @issue.project
453 | elsif params[:project_id]
454 | @project = Project.find(params[:project_id])
455 | else
456 | render_404
457 | return false
458 | end
459 | rescue ActiveRecord::RecordNotFound
460 | render_404
461 | end
462 |
463 | def find_optional_project
464 | if !params[:issue_id].blank?
465 | @issue = Issue.find(params[:issue_id])
466 | @project = @issue.project
467 | elsif !params[:project_id].blank?
468 | @project = Project.find(params[:project_id])
469 | end
470 | deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
471 | end
472 |
473 | # Retrieves the date range based on predefined ranges or specific from/to param dates
474 | def retrieve_date_range
475 | @free_period = false
476 | @from, @to = nil, nil
477 | @period_all = false
478 | @without_closed_flg = false
479 |
480 | if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
481 | case params[:period].to_s
482 | when 'today'
483 | @from = @to = Date.today
484 | when 'yesterday'
485 | @from = @to = Date.today - 1
486 | when 'current_week'
487 | @from = Date.today - (Date.today.cwday - 1)%7
488 | @to = @from + 6
489 | when 'last_week'
490 | @from = Date.today - 7 - (Date.today.cwday - 1)%7
491 | @to = @from + 6
492 | when '7_days'
493 | @from = Date.today - 7
494 | @to = Date.today
495 | when 'current_month'
496 | @from = Date.civil(Date.today.year, Date.today.month, 1)
497 | @to = (@from >> 1) - 1
498 | when 'last_month'
499 | @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
500 | @to = (@from >> 1) - 1
501 | when '30_days'
502 | @from = Date.today - 30
503 | @to = Date.today
504 | when 'current_year'
505 | @from = Date.civil(Date.today.year, 1, 1)
506 | @to = Date.civil(Date.today.year, 12, 31)
507 | when 'all'
508 | @period_all = true
509 | end
510 | elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
511 | begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
512 | begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
513 | @free_period = true
514 | else
515 | # default
516 | end
517 |
518 | @from, @to = @to, @from if @from && @to && @from > @to
519 | @from ||= (Issue.minimum(:start_date) || Date.today) - 1
520 | @to ||= (Issue.maximum(:due_date) || Date.today)
521 |
522 | if params[:est_type] == '1' || params[:est_flg] == "true"
523 | @est_flg = true
524 | elsif params[:est_type] == '2' || params[:est_flg] == "false"
525 | @est_flg = false
526 | else
527 | @est_flg = true
528 | end
529 | @mine_flg = params[:my_type] || params[:mine_flg]
530 | @without_closed_flg = params[:without_closed]
531 | end
532 |
533 | end
534 |
--------------------------------------------------------------------------------