├── 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 | <%= pagination_links_full @entry_pages, @entry_count %> 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 | 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 |
4 | 5 | 6 | 7 | 13 | <% if !@est_flg -%> 14 | <%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %> 15 | <%= sort_header_tag('user', :caption => l(:label_member)) %> 16 | <%= sort_header_tag('activity', :caption => l(:label_activity)) %> 17 | <%= sort_header_tag('project', :caption => l(:label_project)) %> 18 | <%= sort_header_tag('issue', :caption => l(:label_issue), :default_order => 'desc') %> 19 | 20 | <%= sort_header_tag('estimated_hours', :caption => l(:field_estimated_hours)) %> 21 | <%= sort_header_tag('hours', :caption => l(:field_hours)) %> 22 | 23 | <% else -%> 24 | <%= sort_header_tag('start_date', :caption => l(:label_date), :default_order => 'desc') %> 25 | <%= sort_header_tag('author', :caption => l(:label_member)) %> 26 | 27 | <%= sort_header_tag('project', :caption => l(:label_project)) %> 28 | <%= sort_header_tag('issue', :caption => l(:label_issue), :default_order => 'desc') %> 29 | 30 | <%= sort_header_tag('estimated_hours', :caption => l(:field_estimated_hours)) %> 31 | 32 | <% end -%> 33 | 34 | 35 | 36 | <% if !@est_flg -%> 37 | <% entries.each do |entry| -%> 38 | "> 39 | 40 | 41 | 42 | 43 | 44 | 49 | 50 | 51 | 52 | 62 | 63 | <% end -%> 64 | <% elsif @est_flg -%> 65 | <% entries.each do |entry| -%> 66 | <%= entry.css_classes %>"> 67 | 68 | 69 | 70 | 71 | 72 | 75 | 76 | 77 | 78 | 79 | <% end -%> 80 | <% end -%> 81 | 82 |
<%= l(:field_comments) %><%= l(:label_activity) %><%= l(:field_comments) %><%= l(:field_hours) %>
<%= format_date(entry.spent_on) %><%= link_to_user(entry.user) %><%=h entry.activity %><%= link_to_project(entry.project) %> 45 | <% if entry.issue -%> 46 | <%= entry.issue.visible? ? link_to_issue(entry.issue, :truncate => 50) : "##{entry.issue.id}" -%> 47 | <% end -%> 48 | <%=h entry.comments %><%= html_hours("%.2f" % entry.issue.estimated_hours) if entry.issue && entry.issue.estimated_hours %><%= html_hours("%.2f" % entry.hours) %> 53 | <% if entry.editable_by?(User.current) -%> 54 | <%= link_to image_tag('edit.png'), {:controller => 'estimate_timelog', :action => 'edit', :id => entry, :project_id => nil}, 55 | :title => l(:button_edit) %> 56 | <%= link_to image_tag('delete.png'), {:controller => 'estimate_timelog', :action => 'destroy', :id => entry, :project_id => nil}, 57 | :confirm => l(:text_are_you_sure), 58 | :method => :post, 59 | :title => l(:button_delete) %> 60 | <% end -%> 61 |
<%= format_date(entry.start_date) %> 〜 <%= format_date(entry.due_date) %> <%= link_to_user(entry.assigned_to) %>-<%= link_to_project(entry.project) %> 73 | <%= entry.visible? ? link_to_issue(entry, :truncate => 50) : "##{entry.id}" -%> 74 | -<%= html_hours("%.2f" % entry.estimated_hours) if entry.estimated_hours %>-
83 |
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 | : <%= 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 |
59 | 60 | 61 | 62 | <% @criterias.each do |criteria| %> 63 | 64 | <% end %> 65 | 66 | 67 | <% if @issue_cols -%> 68 | <% @issue_cols.each do |col| -%> 69 | 70 | <% end -%> 71 | <% end -%> 72 | 73 | 74 | 75 | <%= render :partial => 'report_criteria', :locals => {:criterias => @criterias, :hours => @hours, :level => 0} %> 76 | 77 | 78 | <%= ('' * (@criterias.size - 1)).html_safe %> 79 | <% total_est = 0 -%> 80 | <% total = 0 -%> 81 | <% sum_est = sum_hours_est(select_hours(@hours, @columns, '')); total_est += sum_est -%> 82 | <% sum = sum_hours(select_hours(@hours, @columns, '')); total += sum -%> 83 | 84 | 85 | 86 | 87 |
<%= l_or_humanize(@available_criterias[criteria][:label]) %><%= l(:et_label_estimated_hours) %><%= l(:et_label_hours) %><%= l(@available_criterias[col][:label]) %>
<%= l(:label_total) %><%= html_hours("%.2f" % total_est) if total_est > 0 %><%= html_hours("%.2f" % total) if total > 0 %>
88 |
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 | --------------------------------------------------------------------------------