diff --git a/app/controllers/timesheet_controller.rb b/app/controllers/timesheet_controller.rb index 0e94679..d2a9c8c 100644 --- a/app/controllers/timesheet_controller.rb +++ b/app/controllers/timesheet_controller.rb @@ -17,12 +17,11 @@ class TimesheetController < ApplicationController verify :method => :delete, :only => :reset, :render => {:nothing => true, :status => :method_not_allowed } def index - load_filters_from_session + #load_filters_from_session unless @timesheet @timesheet ||= Timesheet.new end @timesheet.allowed_projects = allowed_projects - if @timesheet.allowed_projects.empty? render :action => 'no_projects' return @@ -36,7 +35,7 @@ def report redirect_to :action => 'index' return end - + @timesheet.allowed_projects = allowed_projects if @timesheet.allowed_projects.empty? @@ -51,27 +50,31 @@ def report else @timesheet.projects = @timesheet.allowed_projects end - + call_hook(:plugin_timesheet_controller_report_pre_fetch_time_entries, { :timesheet => @timesheet, :params => params }) - save_filters_to_session(@timesheet) - - @timesheet.fetch_time_entries + #save_filters_to_session(@timesheet) + @timesheet.fetch_time_entries if @timesheet.detailed == "yes" + @timesheet.fetch_time_entries_summary unless @timesheet.detailed == "yes" # Sums @total = { } + @total_non_billable_hours = { } unless @timesheet.sort == :issue @timesheet.time_entries.each do |project,logs| @total[project] = 0 + @total_non_billable_hours[project] = 0 if logs[:logs] logs[:logs].each do |log| @total[project] += log.hours + @total_non_billable_hours[project] += log.non_billable_hours.to_f unless log.non_billable_hours.blank? end end end else @timesheet.time_entries.each do |project, project_data| @total[project] = 0 + @total_non_billable_hours[project] = 0 if project_data[:issues] project_data[:issues].each do |issue, issue_data| @total[project] += issue_data.collect(&:hours).sum @@ -79,12 +82,13 @@ def report end end end - @grand_total = @total.collect{|k,v| v}.inject{|sum,n| sum + n} - + @grand_total_non_billable_hours = @total_non_billable_hours.collect{|k,v| v}.inject{|sum,n| sum + n} + + respond_to do |format| format.html { render :action => 'details', :layout => false if request.xhr? } - format.csv { send_data @timesheet.to_csv, :filename => 'timesheet.csv', :type => "text/csv" } + format.csv { send_data @timesheet.to_csv, :filename => 'timesheet.csv', :type => "text/csv" } end end @@ -97,6 +101,44 @@ def reset clear_filters_from_session redirect_to :action => 'index' end + + def getprojects + custom_field_id = CustomField.find_by_name("Project Type").id + project_type = params[:project_type] + project_status = params[:project_status] + cond = ARCondition.new + cond << ["status =?",project_status] unless project_status == "Both" + if User.current.admin? + if project_type == "Both" + projects = Project.timesheet_order_by_name.find(:all,:conditions => cond.conditions,:order => "name ASC") + else + cond << ["custom_values.custom_field_id=? && custom_values.value=?",custom_field_id,project_type] + projects = Project.timesheet_order_by_name.find(:all,:joins => :custom_values,:conditions => cond.conditions,:order => "name ASC") + end + elsif Setting.plugin_timesheet_plugin['project_status'] == 'all' + if project_type == "Both" + projects = Project.timesheet_order_by_name.timesheet_with_membership(User.current).find(:all,:conditions => cond.conditions,:order => "name ASC") + else + cond << ["custom_values.custom_field_id=? && custom_values.value=?",custom_field_id,project_type] + projects = Project.timesheet_order_by_name.timesheet_with_membership(User.current).find(:all,:joins => :custom_values,:conditions => cond.conditions,:order => "name ASC") + end + else + cond << Project.visible_condition(User.current) + if project_type == "Both" + projects = Project.timesheet_order_by_name.find(:all,:conditions => cond.conditions,:order => "name ASC") + else + cond << ["custom_values.custom_field_id=? && custom_values.value=?",custom_field_id,project_type] + projects = Project.timesheet_order_by_name.find(:all,:joins => :custom_values,:conditions => cond.conditions,:order => "name ASC") + end + end + projStr ="" + projects.each do |project| + projStr << project.id.to_s() + ',' + project.name + "\n" + end + respond_to do |format| + format.text { render :text => projStr } + end + end private def get_list_size @@ -115,7 +157,7 @@ def get_precision end def get_activities - @activities = TimeEntryActivity.all(:conditions => 'parent_id IS NULL') + @activities = TimeEntryActivity.all(:conditions => 'parent_id IS NULL',:order => "name ASC") end def allowed_projects @@ -124,7 +166,7 @@ def allowed_projects elsif Setting.plugin_timesheet_plugin['project_status'] == 'all' Project.timesheet_order_by_name.timesheet_with_membership(User.current) else - Project.timesheet_order_by_name.all(:conditions => Project.visible_by(User.current)) + Project.timesheet_order_by_name.all(:conditions => Project.visible_condition(User.current)) end end diff --git a/app/helpers/timesheet_helper.rb b/app/helpers/timesheet_helper.rb index e108092..105a002 100644 --- a/app/helpers/timesheet_helper.rb +++ b/app/helpers/timesheet_helper.rb @@ -16,7 +16,7 @@ def link_to_csv_export(timesheet) :controller => 'timesheet', :action => 'report', :format => 'csv', - :timesheet => timesheet.to_param + :timesheet => timesheet.to_param, }, :method => 'post', :class => 'icon icon-timesheet') @@ -46,7 +46,8 @@ def displayed_time_entries_for_issue(time_entries) end def project_options(timesheet) - available_projects = timesheet.allowed_projects + #Onload of the page the project type is billable + available_projects = timesheet.filtered_projects(timesheet.project_type.blank? ? "Billable" : timesheet.project_type,timesheet.project_status.blank? ? Project::STATUS_ACTIVE : timesheet.project_status) selected_projects = timesheet.projects.collect(&:id) selected_projects = available_projects.collect(&:id) if selected_projects.blank? diff --git a/app/models/timesheet.rb b/app/models/timesheet.rb index 505cbc6..b53dc53 100644 --- a/app/models/timesheet.rb +++ b/app/models/timesheet.rb @@ -1,5 +1,5 @@ class Timesheet - attr_accessor :date_from, :date_to, :projects, :activities, :users, :allowed_projects, :period, :period_type + attr_accessor :date_from, :date_to, :projects, :activities, :users, :allowed_projects, :period, :period_type,:project_type,:detailed,:project_status # Time entries on the Timesheet in the form of: # project.name => {:logs => [time entries], :users => [users shown in logs] } @@ -12,10 +12,16 @@ class Timesheet # Sort time entries by this field attr_accessor :sort + ValidSortOptions = { :project => 'Project', - :user => 'User', - :issue => 'Issue' + :user => 'User'#, + #:issue => 'Issue' + } + ValidProjectTypes = { + :Billable => 'Billable', + :NonBillable => 'Non Billable', + :Both => 'Both' } ValidPeriodType = { @@ -23,6 +29,13 @@ class Timesheet :default => 1 } + ValidProjectStatuses = { + :Active => Project::STATUS_ACTIVE, + :Archived => Project::STATUS_ARCHIVED, + :Both => "Both" + } + CUSTOM_FIELD = CustomField.find_by_name("Non Billable Hours").id + def initialize(options = { }) self.projects = [ ] self.time_entries = options[:time_entries] || { } @@ -56,6 +69,12 @@ def initialize(options = { }) self.date_from = options[:date_from] || Date.today.to_s self.date_to = options[:date_to] || Date.today.to_s + + self.project_type = options[:project_type] || ValidProjectTypes[:Billable] + + self.detailed = options[:detailed] + + self.project_status = options[:project_status] unless options[:project_status].nil? if options[:period_type] && ValidPeriodType.values.include?(options[:period_type].to_i) self.period_type = options[:period_type].to_i @@ -79,6 +98,20 @@ def fetch_time_entries fetch_time_entries_by_project end end + + def fetch_time_entries_summary + self.time_entries = { } + case self.sort + when :project + fetch_time_entries_by_project_summary + when :user + fetch_time_entries_by_user_summary + when :issue + fetch_time_entries_by_issue + else + fetch_time_entries_by_project_summary + end + end def period=(period) return if self.period_type == Timesheet::ValidPeriodType[:free_period] @@ -122,7 +155,8 @@ def to_param :date_to => date_to, :activities => activities, :users => users, - :sort => sort + :sort => sort, + :detailed => detailed } end @@ -164,10 +198,42 @@ def self.viewable_users } end + def filtered_projects(project_type,project_status) + custom_field_id = CustomField.find_by_name("Project Type").id + cond = ARCondition.new + cond << ["status =?",project_status] unless project_status == "Both" + if User.current.admin? + if project_type == "Both" + projects = Project.timesheet_order_by_name.find(:all,:conditions => cond.conditions,:order => "name ASC") + else + cond << ["custom_values.custom_field_id=? && custom_values.value=?",custom_field_id,project_type] + projects = Project.timesheet_order_by_name.find(:all,:joins => :custom_values,:conditions => cond.conditions,:order => "name ASC") + end + elsif Setting.plugin_timesheet_plugin['project_status'] == 'all' + if project_type == "Both" + projects = Project.timesheet_order_by_name.timesheet_with_membership(User.current).find(:all,:conditions => cond.conditions,:order => "name ASC") + else + cond << ["custom_values.custom_field_id=? && custom_values.value=?",custom_field_id,project_type] + projects = Project.timesheet_order_by_name.timesheet_with_membership(User.current).find(:all,:joins => :custom_values,:conditions => cond.conditions,:order => "name ASC") + end + else + cond << Project.visible_condition(User.current) + if project_type == "Both" + projects = Project.timesheet_order_by_name.find(:all,:conditions => cond.conditions,:order => "name ASC") + else + cond << ["custom_values.custom_field_id=? && custom_values.value=?",custom_field_id,project_type] + projects = Project.timesheet_order_by_name.find(:all,:joins => :custom_values,:conditions => cond.conditions,:order => "name ASC") + end + end + + return projects + end + protected def csv_header - csv_data = [ + if self.detailed == "yes" + csv_data = [ '#', l(:label_date), l(:label_member), @@ -176,14 +242,24 @@ def csv_header l(:label_issue), "#{l(:label_issue)} #{l(:field_subject)}", l(:field_comments), - l(:field_hours) + l(:field_hours), + l(:field_non_billable_hours) ] + else + csv_data = [ + l(:label_member), + l(:label_project), + l(:field_hours), + l(:field_non_billable_hours) + ] + end Redmine::Hook.call_hook(:plugin_timesheet_model_timesheet_csv_header, { :timesheet => self, :csv_data => csv_data}) return csv_data end def time_entry_to_csv(time_entry) - csv_data = [ + if self.detailed == "yes" + csv_data = [ time_entry.id, time_entry.spent_on, time_entry.user.name, @@ -192,8 +268,17 @@ def time_entry_to_csv(time_entry) ("#{time_entry.issue.tracker.name} ##{time_entry.issue.id}" if time_entry.issue), (time_entry.issue.subject if time_entry.issue), time_entry.comments, - time_entry.hours + sprintf('%.2f', time_entry.hours), + time_entry.non_billable_hours.blank? ? nil : sprintf('%.2f', time_entry.non_billable_hours) ] + else + csv_data = [ + time_entry.first_name+" "+time_entry.last_name, + time_entry.project_name, + sprintf('%.2f', time_entry.hours), + time_entry.non_billable_hours.blank? ? nil : sprintf('%.2f', time_entry.non_billable_hours) + ] + end Redmine::Hook.call_hook(:plugin_timesheet_model_timesheet_time_entry_to_csv, { :timesheet => self, :time_entry => time_entry, :csv_data => csv_data}) return csv_data end @@ -240,25 +325,48 @@ def includes Redmine::Hook.call_hook(:plugin_timesheet_model_timesheet_includes, { :timesheet => self, :includes => includes}) return includes end - - private + + private def time_entries_for_all_users(project) return project.time_entries.find(:all, - :conditions => self.conditions(self.users), + :conditions => self.conditions(self.users,"custom_values.custom_field_id=#{CUSTOM_FIELD}"), :include => self.includes, + :joins => :custom_values, + :select => "time_entries.*,custom_values.value as non_billable_hours", :order => "spent_on ASC") end + def hours_summary_for_all_users(project) + return project.time_entries.find(:all, + :conditions => self.conditions(self.users,"custom_values.custom_field_id=#{CUSTOM_FIELD}"), + :joins => [:custom_values,:user,:project], + :select => "sum(time_entries.hours) as hours,time_entries.user_id,sum(custom_values.value) as non_billable_hours,users.firstname as first_name,projects.name as project_name,users.lastname as last_name", + :group => "user_id", + :order => "first_name ASC" + ) + end + def time_entries_for_current_user(project) return project.time_entries.find(:all, - :conditions => self.conditions(User.current.id), + :conditions => self.conditions(User.current.id,"custom_values.custom_field_id=#{CUSTOM_FIELD}"), :include => self.includes, :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], + :joins => :custom_values, + :select => "time_entries.*,custom_values.value as non_billable_hours", :order => "spent_on ASC") end + def hours_summary_for_current_user(project) + return project.time_entries.find(:all, + :conditions => self.conditions(User.current.id,"custom_values.custom_field_id=#{CUSTOM_FIELD}"), + :joins => [:custom_values,:user,:project], + :select => "sum(time_entries.hours) as hours,time_entries.user_id,sum(custom_values.value) as non_billable_hours,users.firstname as first_name,projects.name as project_name,users.lastname as last_name", + :group => "user_id", + :order => "first_name ASC") + end + def issue_time_entries_for_all_users(issue) return issue.time_entries.find(:all, :conditions => self.conditions(self.users), @@ -276,15 +384,29 @@ def issue_time_entries_for_current_user(issue) end def time_entries_for_user(user, options={}) - extra_conditions = options.delete(:conditions) + extra_conditions = "custom_values.custom_field_id=#{CUSTOM_FIELD}" return TimeEntry.find(:all, :conditions => self.conditions([user], extra_conditions), :include => self.includes, + :joins => :custom_values, + :select => "time_entries.*,custom_values.value as non_billable_hours", :order => "spent_on ASC" ) end + def hours_summary_for_user(user, options={}) + extra_conditions = "custom_values.custom_field_id=#{CUSTOM_FIELD}" + + return TimeEntry.find(:all, + :conditions => self.conditions([user], extra_conditions), + :joins => [:custom_values,:user,:project], + :select => "sum(time_entries.hours) as hours,time_entries.user_id,time_entries.project_id,sum(custom_values.value) as non_billable_hours,users.firstname as first_name,projects.name as project_name,users.lastname as last_name", + :group => "project_id", + :order => "project_name ASC" + ) + end + def fetch_time_entries_by_project self.projects.each do |project| logs = [] @@ -297,7 +419,7 @@ def fetch_time_entries_by_project # Users with the Role and correct permission can see all time entries logs = time_entries_for_all_users(project) users = logs.collect(&:user).uniq.sort - elsif User.current.allowed_to_on_single_potentially_archived_project?(:view_time_entries, project) + elsif User.current.allowed_to_on_single_potentially_archived_project?(:view_time_entries, project) && self.users.include?(User.current.id) # Users with permission to see their time entries logs = time_entries_for_current_user(project) users = logs.collect(&:user).uniq.sort @@ -305,15 +427,34 @@ def fetch_time_entries_by_project # Rest can see nothing end - # Append the parent project name - if project.parent.nil? - unless logs.empty? + unless logs.empty? self.time_entries[project.name] = { :logs => logs, :users => users } - end + end + end + end + + def fetch_time_entries_by_project_summary + self.projects.each do |project| + logs = [] + users = [] + if User.current.admin? + # Administrators can see all time entries + logs = hours_summary_for_all_users(project) + users = logs.collect(&:user).uniq.sort + elsif User.current.allowed_to_on_single_potentially_archived_project?(:see_project_timesheets, project) + # Users with the Role and correct permission can see all time entries + logs = hours_summary_for_all_users(project) + users = logs.collect(&:user).uniq.sort + elsif User.current.allowed_to_on_single_potentially_archived_project?(:view_time_entries, project) && self.users.include?(User.current.id) + # Users with permission to see their time entries + logs = hours_summary_for_current_user(project) + users = logs.collect(&:user).uniq.sort else - unless logs.empty? - self.time_entries[project.parent.name + ' / ' + project.name] = { :logs => logs, :users => users } - end + # Rest can see nothing + end + + unless logs.empty? + self.time_entries[project.name] = { :logs => logs, :users => users } end end end @@ -342,6 +483,30 @@ def fetch_time_entries_by_user end end + def fetch_time_entries_by_user_summary + self.users.each do |user_id| + logs = [] + if User.current.admin? + # Administrators can see all time entries + logs = hours_summary_for_user(user_id) + elsif User.current.id == user_id + # Users can see their own their time entries + logs = hours_summary_for_user(user_id) + elsif User.current.allowed_to_on_single_potentially_archived_project?(:see_project_timesheets, nil, :global => true) + # User can see project timesheets in at least once place, so + # fetch the user timelogs for those projects + logs = hours_summary_for_user(user_id, :conditions => Project.allowed_to_condition(User.current, :see_project_timesheets)) + else + # Rest can see nothing + end + + unless logs.empty? + user = User.find_by_id(user_id) + self.time_entries[user.name] = { :logs => logs } unless user.nil? + end + end + end + # project => { :users => [users shown in logs], # :issues => # { issue => {:logs => [time entries], diff --git a/app/views/timesheet/_form.rhtml b/app/views/timesheet/_form.rhtml index 923e458..bf42a8f 100644 --- a/app/views/timesheet/_form.rhtml +++ b/app/views/timesheet/_form.rhtml @@ -1,3 +1,33 @@ + +