merged remote branch 'reporting_merge'

pull/6827/head
jwollert 14 years ago
commit 853e95cc37
  1. 362
      app/controllers/cost_reports_controller.rb
  2. 94
      app/controllers/costlog_controller.rb
  3. 1
      app/models/cost_object.rb
  4. 710
      app/models/cost_query.rb
  5. 48
      app/views/cost_reports/_filter.rhtml
  6. 170
      app/views/cost_reports/_filters.rhtml
  7. 18
      app/views/cost_reports/_group_by.rhtml
  8. 9
      app/views/cost_reports/_list.rhtml
  9. 132
      app/views/cost_reports/_list_group_by.rhtml
  10. 5
      app/views/cost_reports/_options.rhtml
  11. 0
      app/views/cost_reports/filter_types/_boolean.rhtml
  12. 1
      app/views/cost_reports/filter_types/_date.rhtml
  13. 8
      app/views/cost_reports/filter_types/_date_exact.rhtml
  14. 1
      app/views/cost_reports/filter_types/_date_past.rhtml
  15. 1
      app/views/cost_reports/filter_types/_integer.rhtml
  16. 1
      app/views/cost_reports/filter_types/_integer_zero.rhtml
  17. 2
      app/views/cost_reports/filter_types/_list.rhtml
  18. 1
      app/views/cost_reports/filter_types/_list_optional.rhtml
  19. 1
      app/views/cost_reports/filter_types/_list_status.rhtml
  20. 1
      app/views/cost_reports/filter_types/_list_subprojects.rhtml
  21. 1
      app/views/cost_reports/filter_types/_string.rhtml
  22. 1
      app/views/cost_reports/filter_types/_text.rhtml
  23. 84
      app/views/cost_reports/index.rhtml
  24. 2
      config/routes.rb
  25. 50
      doc/refactoring.rb
  26. 6
      features/cost_reports.feature
  27. 71
      features/step_definitions/cost_steps.rb
  28. 12
      features/view_own_rates.feature
  29. 20
      init.rb
  30. 1
      lang/de.yml
  31. 1
      lang/en.yml
  32. 25
      lib/costs_timelog_controller_patch.rb
  33. 8
      lib/costs_user_patch.rb
  34. 28
      test/exemplars/cost_entry_exemplar.rb
  35. 2
      test/exemplars/cost_object_exemplar.rb
  36. 12
      test/exemplars/cost_rate_exemplar.rb
  37. 8
      test/exemplars/rate_exemplar.rb

@ -1,362 +0,0 @@
class CostReportsController < ApplicationController
unloadable
before_filter :find_optional_project, :only => [:index, :get_filter]
before_filter :retrieve_query
before_filter :authorize
helper :sort
include SortHelper
include ActionView::Helpers::NumberHelper
include ActionView::Helpers::TextHelper
def get_filter
scope = params[:scope].to_sym if params[:scope]
column_name = params[:column_name] if params[:column_name]
unless scope || column_name
render_404
return
end
@line_index = params[:line_index] || "---INDEX---"
filter = @query.create_filter(scope, column_name)
render :partial => "filter", :object => filter, :layout => !request.xhr?
end
def index
sort_init(@query.sort_criteria.empty? ? [['entry__spent_on', 'desc']] : @query.sort_criteria)
sortable_columns = {
"issue__issue_id" => "issue_id",
"entry__spent_on" => "spent_on",
"entry__user_id" => "user_id",
"entry__cost_type_id" => "cost_type_id",
"entry__activity_id" => "activity_id",
"entry__costs" => "real_costs"
}
sort_update(sortable_columns)
if @query.valid?
limit = case params[:format]
when 'html', nil
per_page_option
when 'atom'
Setting.feeds_limit.to_i
else
Setting.issues_export_limit.to_i
end
unless @query.group_by_fields.empty?
get_aggregation
respond_to do |format|
format.html { render :layout => !request.xhr? }
# TODO: ATOM and CSV
end
else
get_entries(limit)
respond_to do |format|
format.html { render :layout => !request.xhr? }
format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
format.csv { send_data(entries_to_csv(@entries, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
end
end
else
render :layout => !request.xhr?
end
rescue Exception => e
logger.error "#{e.class.name}: #{e.message}" if logger
$@.each {|line| logger.error line} if logger
session.delete :cost_query
# Give it a name, required to be valid
@query = CostQuery.new(:name => "_")
@query.project = @project
get_entries(limit)
respond_to do |format|
format.html do
@custom_error = l(:error_generic)
render :layout => !request.xhr?
end
format.atom {render_500(l(:error_generic))}
format.csv {render_500(l(:error_generic))}
end
end
def new
# This action saves a new query for later reference
end
private
def find_optional_project
@project = Project.find(params[:project_id]) unless params[:project_id].blank?
allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
allowed ? true : deny_access
rescue ActiveRecord::RecordNotFound
render_404
end
def retrieve_query
# tries to find a active query in the session or loads the default one
unless params[:query_id].blank?
# The user provided an explicit query_id
cond = "project_id IS NULL"
cond << " OR project_id = #{@project.id}" if @project
@query = CostQuery.find(params[:query_id], :conditions => cond)
@query.project = @project
session[:cost_query] = {:id => @query.id, :project_id => @query.project_id}
sort_clear
else
if params[:set_filter] || session[:cost_query].nil? || session[:cost_query][:project_id] != (@project ? @project.id : nil)
# We have no current query or the query was reseted explicitly
# So generate a new query
# Give it a name, required to be valid
@query = CostQuery.new(:name => "_")
@query.project = @project
if params[:filters].blank?
@query.filters = []
else
@query.filters = params[:filters].collect {|f| f[1]}.select{|f| f[:enabled] != "0"}
end
@query.group_by = params[:group_by] || {}
if params[:cost_query]
@query.display_cost_entries = params[:cost_query][:display_cost_entries]
@query.display_time_entries = params[:cost_query][:display_time_entries]
end
session[:cost_query] = {:project_id => @query.project_id,
:filters => @query.filters,
:group_by => @query.group_by,
:display_cost_entries => @query.display_cost_entries,
:display_time_entries => @query.display_time_entries}
else
@query = CostQuery.find_by_id(session[:cost_query][:id]) if session[:cost_query][:id]
@query ||= CostQuery.new(:name => "_",
:project => @project,
:filters => session[:cost_query][:filters],
:group_by => session[:cost_query][:group_by],
:display_cost_entries => session[:cost_query][:display_cost_entries],
:display_time_entries => session[:cost_query][:display_time_entries])
@query.project = @project
end
end
end
def get_aggregation
fields = @query.group_by_fields.join(", ")
scopes = []
scopes << :cost_entries if @query.display_cost_entries
scopes << :time_entries if @query.display_time_entries
return @grouped_entries = [] if scopes.blank?
subselect = scopes.map do |type|
model, select_statement, rate_permission_statement, from, where_statement, group_by_statement = @query.sql_data_for type
table = model.table_name
<<-EOS
SELECT
#{select_statement},
SUM(
CASE WHEN #{rate_permission_statement} THEN
CASE WHEN #{table}.overridden_costs IS NULL THEN #{table}.costs
ELSE #{table}.overridden_costs END
ELSE
0.0000
END
) AS sum,
SUM(#{table == "time_entries" ? "hours" : "units"}) as unit_sum,
COUNT(*) AS count
FROM #{from}
WHERE #{where_statement}
#{group_by_statement}
EOS
end.join(" UNION ")
if scopes.length == 2
sql = "SELECT #{fields}, SUM(sum) as sum, SUM(unit_sum) as unit_sum, SUM(count) AS count FROM (#{subselect}) AS entries GROUP BY #{fields}"
else
sql = subselect
end
@grouped_entries = ActiveRecord::Base.connection.select_all(sql)
@entry_sum, @entry_count = @grouped_entries.inject([0, 0.0]) do |r,i|
r[0] += i["sum"].to_f
r[1] += i["count"].to_i
r
end
end
def get_entries(limit)
cost_where = @query.statement(:cost_entries)
time_where = @query.statement(:time_entries)
aggregate_select, display_costs = [TimeEntry, CostEntry].inject([{}, {}]) do |r,table|
table_name = table.table_name
rate_permission_statement = @query.rate_permission_statement(table_name.to_sym)
r[0][table_name] = <<-EOS
COUNT(#{table_name}.id) as count,
SUM(
CASE WHEN #{rate_permission_statement} THEN
CASE WHEN #{table_name}.overridden_costs IS NULL THEN #{table_name}.costs
ELSE #{table_name}.overridden_costs END
ELSE
0.0000
END
) AS sum
EOS
r[1][table_name] = table.column_names.collect{|n| "#{table_name}.#{n}"}.join(", ") + <<-EOS
, CASE WHEN #{rate_permission_statement} THEN
1
ELSE
NULL
END AS display_costs
EOS
r
end
# at first get the entry ids to match the current query
unless sort_clause.nil?
(sort_column, sort_order) = sort_clause.split(" ")
sort_column.gsub!(/\./, "__")
case sort_column
when "real_costs"
cost_sort_column = sort_column
cost_sort_column_sql = "costs, overridden_costs,"
time_sort_column = sort_column
time_sort_column_sql = "costs, overridden_costs,"
sort_clause = "overridden_costs #{sort_order}, costs #{sort_order}"
else
cost_sort_column = (CostEntry.new.respond_to? sort_column) ? sort_column : nil
cost_sort_column_sql = cost_sort_column || "NULL as #{sort_column}"
cost_sort_column_sql += ","
time_sort_column = (TimeEntry.new.respond_to? sort_column) ? sort_column : nil
time_sort_column_sql = time_sort_column || "NULL as #{sort_column}"
time_sort_column_sql += ","
sort_clause = self.sort_clause
end
end
if @query.display_time_entries
time_entry_sum, time_entry_count = TimeEntry.all(
:select => aggregate_select[TimeEntry.table_name],
:conditions => time_where,
:from => @query.from_statement(:time_entries)
).map {|i| [i.sum.to_f, i.count.to_i] }[0]
end
if @query.display_cost_entries
cost_entry_sum, cost_entry_count = CostEntry.all(
:select => aggregate_select[CostEntry.table_name],
:conditions => cost_where,
:from => @query.from_statement(:cost_entries)
).map {|i| [i.sum.to_f, i.count.to_i] }[0]
end
if @query.display_time_entries && !@query.display_cost_entries
@entry_sum, @entry_count = [time_entry_sum, time_entry_count]
@entry_pages = Paginator.new self, @entry_count, limit, params['page']
@entries = TimeEntry.all({ :select => display_costs[TimeEntry.table_name],
:order => (sort_clause if time_sort_column),
:from => @query.from_statement(:time_entries),
:conditions => time_where,
:limit => limit,
:offset => @entry_pages.current.offset})
return
elsif @query.display_cost_entries && !@query.display_time_entries
@entry_sum, @entry_count = [cost_entry_sum, cost_entry_count]
@entry_pages = Paginator.new self, @entry_count, limit, params['page']
@entries = CostEntry.all({ :select => display_costs[CostEntry.table_name],
:order => (sort_clause if cost_sort_column),
:from => @query.from_statement(:cost_entries),
:conditions => cost_where,
:limit => limit,
:offset => @entry_pages.current.offset})
return
elsif !@query.display_time_entries && !@query.display_time_entries
@entry_sum, @entry_count = [0 , 0]
@entry_pages = Paginator.new self, @entry_count, limit, params['page']
@entries = []
return
end
@entry_count = time_entry_count + cost_entry_count
@entry_sum = time_entry_sum + cost_entry_sum
@entry_pages = Paginator.new self, @entry_count, limit, params['page']
cost_from = @query.from_statement(:cost_entries)
time_from = @query.from_statement(:time_entries)
# TAKE extra care for SQL injection here!!!
sql = " SELECT #{CostEntry.table_name}.id AS id, #{cost_sort_column_sql} 'cost_entry' AS entry_type"
sql << " FROM #{cost_from}"
sql << " WHERE #{cost_where}"
sql << " UNION"
sql << " SELECT #{TimeEntry.table_name}.id AS id, #{time_sort_column_sql} 'time_entry' as entry_type"
sql << " FROM #{time_from}"
sql << " WHERE #{time_where}"
sql << " ORDER BY #{sort_clause}" if sort_clause
sql << " LIMIT #{limit} OFFSET #{@entry_pages.current.offset}"
raw_ids = ActiveRecord::Base.connection.select_all(sql)
cost_entry_ids = []
time_entry_ids = []
raw_ids.each do |row|
case row["entry_type"]
when "cost_entry"
cost_entry_ids << row["id"]
when "time_entry"
time_entry_ids << row["id"]
else
raise "Unknown entry type in SQL. Should never happen."
end
end
cost_entries = CostEntry.all({:select => display_costs[CostEntry.table_name],
:order => (sort_clause if cost_sort_column),
:from => @query.from_statement(:cost_entries),
:conditions => {:id => cost_entry_ids}})
time_entries = TimeEntry.all({:select => display_costs[TimeEntry.table_name],
:order => (sort_clause if time_sort_column),
:from => @query.from_statement(:time_entries),
:conditions => {:id => time_entry_ids}})
# now we merge the both entry types
if cost_sort_column && time_sort_column
@entries = cost_entries + time_entries
@entries.sort!{|a,b| a.send(sort_column) <=> b.send(sort_column)}
@entries.reverse! if sort_order && sort_order == "DESC"
elsif cost_sort_column
@entries = cost_entries + time_entries
else
@entries = time_entries + cost_entries
end
end
end

@ -1,12 +1,12 @@
class CostlogController < ApplicationController
unloadable
menu_item :issues
before_filter :find_project, :authorize, :only => [:edit, :destroy]
before_filter :find_optional_project, :only => [:report, :details]
verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
helper :sort
include SortHelper
helper :issues
@ -14,44 +14,31 @@ class CostlogController < ApplicationController
def details
unless @project.nil?
filters = []
filters = {:operators => {}, :values => {}}
if @issue
if @issue.respond_to?("lft")
issue_ids = Issue.all(:select => :id, :conditions => ["root_id = ? AND lft >= ? AND rgt <= ?", @issue.root_id, @issue.lft, @issue.rgt]).collect{|i| i.id.to_s}
issue_ids = Issue.all(:select => :id, :conditions => ["root_id = ? AND lft >= ? AND rgt <= ?", @issue.root_id, @issue.lft, @issue.rgt]).collect{|i| i.id}
else
issue_ids = [@issue.id.to_s]
issue_ids = [@issue.id]
end
filters << {
:column_name => "issue_id",
:enabled => "1",
:operator => "=",
:scope => "costs",
:values => issue_ids
}
filters[:operators][:issue_id] = "="
filters[:values][:issue_id] = issue_ids
end
if @cost_type
filters << {
:column_name => "cost_type_id",
:enabled => "1",
:operator => "=",
:scope => "costs",
:values => @cost_type.id.to_s
}
end
filters[:operators][:project_id] = "="
filters[:values][:project_id] = [@project.id.to_s]
respond_to do |format|
format.html {
session[:cost_query] = {:project_id => @project.id,
:filters => filters || {},
:group_by => {},
:display_cost_entries => "1",
:display_time_entries => "0"
}
redirect_to :controller => "cost_reports", :action => "index", :project_id => @project
session[:cost_query] = { :filters => filters, :groups => {:rows => [], :columns => []} }
if @cost_type
redirect_to :controller => "cost_reports", :action => "index", :project_id => @project, :unit => @cost_type.id
else
redirect_to :controller => "cost_reports", :action => "index", :project_id => @project
end
return
}
end
@ -65,7 +52,7 @@ class CostlogController < ApplicationController
'cost_type' => 'cost_type_id',
'units' => 'units',
'costs' => 'costs'
cond = ARCondition.new
if @project.nil?
@ -75,13 +62,13 @@ class CostlogController < ApplicationController
else
cond << "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
end
cond << User.current.allowed_for(:view_cost_entries, @project)
if @cost_type
cond << ["#{CostEntry.table_name}.cost_type_id = ?", @cost_type.id ]
end
retrieve_date_range
cond << ['spent_on BETWEEN ? AND ?', @from, @to]
@ -91,13 +78,13 @@ class CostlogController < ApplicationController
# Paginate results
@entry_count = CostEntry.count(:include => [:project, :user], :conditions => cond.conditions)
@entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
@entries = CostEntry.find(:all,
@entries = CostEntry.find(:all,
:include => [:project, :cost_type, :user, {:issue => :tracker}],
:conditions => cond.conditions,
:order => sort_clause,
:limit => @entry_pages.items_per_page,
:offset => @entry_pages.current.offset)
render :layout => !request.xhr?
}
format.atom {
@ -110,7 +97,7 @@ class CostlogController < ApplicationController
}
format.csv {
# Export all entries
@entries = CostEntry.find(:all,
@entries = CostEntry.find(:all,
:include => [:project, :cost_type, :user, {:issue => [:tracker, :assigned_to, :priority]}],
:conditions => cond.conditions,
:order => sort_clause)
@ -119,7 +106,7 @@ class CostlogController < ApplicationController
end
end
end
def edit
render_403 and return if @cost_entry && !@cost_entry.editable_by?(User.current)
if !@cost_entry
@ -128,27 +115,30 @@ class CostlogController < ApplicationController
# we have a new CostEntry in our request
new_user = User.find_by_id(params[:cost_entry][:user_id]) rescue nil
new_user ||= User.current
unless User.current.allowed_to?(:log_own_costs, @project, :for => new_user)
render_403
return
end
end
new_user ||= User.current
@cost_entry = CostEntry.new(:project => @project, :issue => @issue, :user => new_user, :spent_on => Date.today)
end
if params[:cost_entry].is_a?(Hash)
params[:cost_entry]["overridden_costs"] = CostRate.clean_currency(params[:cost_entry]["overridden_costs"])
end
@cost_entry.attributes = params[:cost_entry]
@cost_entry.cost_type ||= CostType.default
if request.post? and @cost_entry.save
flash[:notice] = l(:notice_successful_update)
redirect_back_or_default :action => 'details', :project_id => @cost_entry.project
return
end
end
def destroy
render_404 and return unless @cost_entry
render_403 and return unless @cost_entry.editable_by?(User.current)
@ -161,12 +151,12 @@ class CostlogController < ApplicationController
def get_cost_type_unit_plural
@cost_type = CostType.find(params[:cost_type_id]) unless params[:cost_type_id].empty?
if request.xhr?
render :partial => "cost_type_unit_plural", :layout => false
end
end
private
def find_project
# copied from timelog_controller.rb
@ -185,7 +175,7 @@ private
rescue ActiveRecord::RecordNotFound
render_404
end
def find_optional_project
if !params[:issue_id].blank?
@issue = Issue.find(params[:issue_id])
@ -193,12 +183,12 @@ private
elsif !params[:project_id].blank?
@project = Project.find(params[:project_id])
end
if !params[:cost_type_id].blank?
@cost_type = CostType.find(params[:cost_type_id])
end
end
def retrieve_date_range
# Mostly copied from timelog_controller.rb
@free_period = false
@ -239,10 +229,10 @@ private
else
# default
end
@from, @to = @to, @from if @from && @to && @from > @to
@from ||= (CostEntry.minimum(:spent_on, :include => [:project, :user], :conditions => User.current.allowed_for(:view_cost_entries)) || Date.today) - 1
@to ||= (CostEntry.maximum(:spent_on, :include => [:project, :user], :conditions => User.current.allowed_for(:view_cost_entries)) || Date.today)
end
end
end

@ -23,6 +23,7 @@ class CostObject < ActiveRecord::Base
validates_presence_of :subject, :project, :author, :kind
validates_length_of :subject, :maximum => 255
validates_length_of :subject, :minimum => 1
def before_validation

@ -1,710 +0,0 @@
require_dependency 'query'
class CostQueryColumn < QueryColumn
attr_reader :scope
def initialize(name, options={})
self.scope = (optione.delete(:scope) || :issues)
super
end
end
class CostQueryCustomFieldColumn < QueryCustomFieldColumn
attr_accessor :scope
def initialize(custom_field)
self.reader = :issues
super
end
end
class Filter
include GLoc
def initialize(scope, column_name, column)
@scope = scope
@column_name = column_name
@column = column
@enabled = true
default_operator = CostQuery.filter_types[@column[:type]][:default]
@operator = default_operator if default_operator
end
attr_reader :scope, :column_name, :column
attr_reader :values, :sql_values
def values=(v)
values = v.is_a?(Array) ? v : [v]
sql_values = values.dup
if column[:flags].include? :user
sql_values.push(User.current.logged? ? User.current.id.to_s : "0") if sql_values.delete("me")
end
if available_values
available_value_keys = available_values.collect {|o| o[1].to_s }
sql_values.each do |value|
unless (available_value_keys.include? value.to_s) or (value.to_s == "")
raise ArgumentError.new("Forbidden value (#{value.inspect} not in #{available_value_keys.inspect})")
end
end
end
@values = values
@sql_values = sql_values
end
attr_reader :operator
def operator=(o)
raise ArgumentError.new("Forbidden operator #{o}") unless available_operators.include? o
@operator = o
end
attr_accessor :enabled
def type_name
@column[:type]
end
def filter_type
CostQuery.filter_types[type_name]
end
def label
@column[:name] || l(("field_"+@column_name.gsub(/\_id$/, "")).to_sym)
end
def available_operators
filter_type[:operators]
end
def available_values
@column[:values]
end
def new_record?
return true
end
end
class CostQuery < ActiveRecord::Base
include GLoc
belongs_to :user
belongs_to :project
serialize :filters
serialize :group_by
attr_protected :user_id, :project_id, :created_at, :updated_at
def after_initialize
self.display_time_entries = true if display_time_entries.nil?
self.display_cost_entries = true if display_cost_entries.nil?
self.group_by ||= {}
end
def self.operators
# These are the operators used by filter types.
operators = {}
issue_operators = Query.operators
issue_operators.each_pair do |op, label|
simple = (["!*", "*", "t", "w", "o", "c"].include? op)
operators[op] = {:label => label, :simple => simple}
end
operators.merge(
{
"=n" => {:label => :label_equals, :simple => false},
"0" => {:label => :label_none, :simple => true},
"y" => {:label => :label_yes, :simple => true},
"n" => {:label => :label_no, :simple => true},
"<d" => {:label => :label_less_or_equal, :simple => false},
">d" => {:label => :label_greater_or_equal, :simple => false},
"<>d" => {:label => :label_between, :simple => false},
"=d" => {:label => :label_date_on, :simple => false}
}
)
end
def self.filter_types
return @filter_types if @filter_types
filter_types = Query.operators_by_filter_type.inject({}) do |r, f|
multiple = !([:list, :list_status, :list_optional, :list_subproject].include? f[0])
r[f[0]] = {:operators => f[1], :multiple => multiple}
r
end
@filter_types = filter_types.merge(
{
:integer_zero => {:operators => [ "=n", ">=", "<=", "0", "*" ], :multiple => true},
:boolean => {:operators => [ "y", "n" ], :multiple => false},
:date_exact => {:operators => [ "<d", ">d", "<>d", "=d", "t", "w"], :multiple => true, :default => "w"}
}
)
end
def available_filters
# This available_filters is different from the Redmine one
# available_filters[:issues]
# --> filters on issue fields. These are the one from redmine itself
# available_filters[:costs]
# --> filters on cost and time entries
return @available_filters if @available_filters
@available_filters = {
:costs => {
"cost_type_id" => { :type => :list_optional, :order => 2, :applies => [:cost_entries], :flags => [], :db_table => CostType.table_name, :db_field => "id", :values => CostType.find(:all, :order => 'name').collect{|s| [s.name, s.id.to_s] }},
"activity_id" => { :type => :list_optional, :order => 3, :applies => [:time_entries], :flags => [], :db_table => TimeEntryActivity.table_name, :db_field => "id", :values => TimeEntryActivity.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] }},
"created_on" => { :type => :date_exact, :applies => [:time_entries, :cost_entries], :flags => [], :order => 4 },
"updated_on" => { :type => :date_exact, :applies => [:time_entries, :cost_entries], :flags => [], :order => 5 },
"spent_on" => { :type => :date_exact, :applies => [:time_entries, :cost_entries], :flags => [], :order => 6},
"overridden_costs" => { :type => :boolean, :applies => [:time_entries, :cost_entries], :flags => [], :order => 7 },
# FIXME: Issues are not selected properly according to project selection
"issue_id" => { :type => :list_optional, :order => 8, :applies => [:cost_entries, :time_entries], :flags => [], :db_table => Issue.table_name, :db_field => "id", :values => Issue.find(:all, :order => :id, :include => :tracker).collect{|s| ["#{s.tracker} ##{s.id}: #{s.subject}", s.id.to_s] }},
}
}
tmp_query = Query.new(:project => project, :name => "_")
@available_filters[:issues] = tmp_query.available_filters
# flag columns that contain filters for user columns
@available_filters[:issues].each_pair do |k,v|
v[:flags] = []
v[:flags] << :user if %w(assigned_to_id author_id watcher_id).include?(k)
if k =~ /^cf_(\d+)$/
# custom field
v[:db_table] = CustomValue.table_name
v[:db_field] = 'value'
v[:db_field_id] = $1 # this is the numeric part in the regex above
v[:flags] << :custom_field
elsif k == "watcher_id"
v[:db_table] = Watcher.table_name
v[:db_field] = 'user_id'
v[:flags] << :watcher
else
if ["labor_costs", "material_costs", "overall_costs"].include? k
v[:type] = :integer_zero
end
if [:date_past, :date].include? v[:type]
v[:type] = :date_exact
end
v[:db_table] = Issue.table_name
v[:db_field] = k
end
end
if @available_filters[:issues]["author_id"]
# add a filter on cost entries for user_id if it is available
user_values = @available_filters[:issues]["author_id"][:values]
@available_filters[:costs]["user_id"] = {:type => :list_optional, :order => 1, :applies => [:time_entries, :cost_entries], :values => user_values, :flags => [:user]}
end
@available_filters
end
def create_filter(scope, column_name)
column = available_filters[scope][column_name]
column ? Filter.new(scope, column_name, column) : nil
end
def create_filter_from_hash(filter_hash = {})
scope = filter_hash[:scope].to_sym
column_name = filter_hash[:column_name]
column = available_filters[scope][column_name]
f = Filter.new(scope, column_name, column)
f.enabled = filter_hash[:enabled] unless filter_hash[:enabled].nil?
f.operator = filter_hash[:operator] unless filter_hash[:operator].nil?
f.values = filter_hash[:values] unless filter_hash[:values].nil?
f
end
def has_filter?(scope, column_name)
# returns the first matching filter or nil
return nil unless filters
match = filters.select {|f| f[:scope] == scope.to_s && f[:column_name] == column_name.to_s}
return match.blank? ? nil : match[0]
end
MAGIC_GROUP_KEYS = [:block, :time, :display, :db_field, :other_group]
def self.grouping_column(*names, &block)
options = names.extract_options!
names.each do |name|
group_by_columns[name] = options.with_indifferent_access.merge(
:block => block,
:scope => grouping_scope
)
group_by_columns[name][:db_field] ||= name
group_by_columns[name][:display] ||= Proc.new { |e| e }
group_by_columns[name][:other_group] ||= "<em>#{l :group_by_others}</em>"
end
end
def self.grouping_scope(type = nil)
@grouping_scope = type || @grouping_scope
yield if block_given?
@grouping_scope
end
def self.group_by_columns
@group_by_columns ||= {}.with_indifferent_access
end
def self.get_name(key, value)
return group_by_columns[key][:other_group] unless value
group_by_columns[key][:display].call value
end
def self.from_field(klass, field)
Proc.new do |id|
a = klass.find_by_id(id)
(a ? a.send(field) : id).to_s
end
end
grouping_scope(:issues) do
grouping_column :tracker_id, :display => from_field(Tracker, :name)
grouping_column :fixed_version_id, :display => from_field(Version, :name)
grouping_column :cost_object_id, :display => from_field(CostObject, :subject)
grouping_column :subproject_id, :display => from_field(Project, :name), :db_field => :project_id
end
grouping_scope(:costs) do
grouping_column :user_id, :display => from_field(User, :name)
grouping_column :issue_id, :display => from_field(Issue, :subject), :other_group => "<em>#{l(:caption_booked_on_project)}</em>"
grouping_column :cost_type_id, :display => from_field(CostType, :name), :other_group => l(:caption_labor_costs)
grouping_column :activity_id, :display => from_field(TimeEntryActivity, :name)
grouping_column(:spent_on, :tyear, :tmonth, :tweek, :time => true) do |column, fields|
values = []
if fields["spent_on"]
values = [fields["spent_on"].to_date] * 2
elsif fields["tyear"]
if fields["tmonth"]
start_of_month = Date.civil(fields["tyear"].to_i, fields["tmonth"].to_i , 1)
values = [start_of_month.to_s, start_of_month.end_of_month.to_s]
elsif fields["tweek"]
start_of_week = Date.commercial(fields["tyear"].to_i, fields["tweek"].to_i, 1)
values = [start_of_week.to_s, start_of_week.end_of_week.to_s]
else
start_of_year = Date.civil(fields["tyear"].to_i, 1, 1)
values = [start_of_year.to_s, start_of_year.end_of_year.to_s]
end
end
raise "Invalid group by values" if values.blank?
{
:operator => "<>d",
:values => values,
:column_name => :spent_on
}
end
end
def filter_from_group_by(fields)
column_name = group_by[:name].to_sym
data = self.class.group_by_columns[column_name].dup
options = {}
MAGIC_GROUP_KEYS.each do |key|
options[key] = data.delete key
end
block = options[:block] || Proc.new { {} }
equals_hash = {
:enabled => 1,
:operator => "=",
:column_name => column_name,
:values => fields[column_name.to_s],
}
# TODO: not all filters have this filter operator. We have to always select the correct operator
none_hash = {
:enabled => 1,
:operator => "!*",
:column_name => column_name,
:values => nil,
}
hash = fields[column_name.to_s].nil? ? none_hash : equals_hash
hash.merge(data).merge(block.call(column_name, fields))
end
def group_by_columns_for_select
self.class.group_by_columns.inject([["", ""]]) do |list, (column_name, values)|
filter = create_filter(values[:scope], column_name.to_s)
list << [filter.label, column_name] if filter
list
end
end
def time_groups
# returns an array of group_by names where time == true
self.class.group_by_columns.inject([]) do |list, (column_name, values)|
list << column_name if values[:time]
list
end
end
def projects
return @projects unless @projects.blank?
projects = [project]
if project && !project.children.active.empty?
if subprojects = has_filter?(:issues, "subproject_id")
subprojects = create_filter_from_hash(subprojects)
case subprojects.operator
when "="
# include the selected subprojects
projects += Project.find_by_id(subprojects.values.each(&:to_i))
when "!*"
# main project only
else
# all subprojects
projects += project.descendants
end
elsif Setting.display_subprojects_issues?
projects += project.descendants
end
elsif project
# show only the current project
else
projects = []
end
@projects = projects
end
def project_statement
"#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
end
def group_by_fields()
# returns the group_by of the current group by query
# These fields are one of the keys of group_by_columns
return [] unless !group_by[:name].blank? && (data = group_by_columns[group_by[:name].to_sym])
if data[:time]
# We have a group_by_time
return case group_by[:granularity]
when "year" then ["tyear"]
when "month" then ["tyear", "tmonth"]
when "week" then ["tyear", "tweek"]
else ["spent_on"]
end
else
[group_by[:name]]
end
end
def group_by_columns
self.class.group_by_columns
end
def sql_data_for(entry_scope)
case entry_scope
when :cost_entries
model = CostEntry
when :time_entries
model = TimeEntry
from_include_issue = false
end
my_fields, nil_fields, grouping_fields = [], [], []
group_by_fields.each do |field|
group_by_column = group_by_columns[field.to_sym]
klass = group_by_column[:scope] == :issues ? Issue : model
db_field = group_by_column[:db_field]
if klass.column_names.include? db_field.to_s
grouping_fields << "#{klass.table_name}.#{db_field}"
my_fields << "#{klass.table_name}.#{db_field} as #{field}"
else
nil_fields << "NULL as #{field}"
end
end
group_by = "GROUP BY #{grouping_fields.join(", ")}" unless grouping_fields.blank?
[model, (my_fields + nil_fields).join(", "), rate_permission_statement(entry_scope), from_statement(entry_scope), statement(entry_scope), group_by]
end
def rate_permission_statement(entry_scope)
case entry_scope
when :cost_entries
statement = User.current.allowed_for(:view_cost_rates, projects)
when :time_entries
statement = User.current.allowed_for(:view_hourly_rates, projects)
end
end
def from_statement(entry_scope, include_issue = false)
case entry_scope
when :cost_entries
from = <<-EOS
#{CostEntry.table_name}
LEFT OUTER JOIN #{CostType.table_name} ON #{CostType.table_name}.id = #{CostEntry.table_name}.cost_type_id
LEFT OUTER JOIN #{User.table_name} ON #{User.table_name}.id = #{CostEntry.table_name}.user_id
LEFT OUTER JOIN #{Issue.table_name} ON #{Issue.table_name}.id = #{CostEntry.table_name}.issue_id
LEFT OUTER JOIN #{Project.table_name} ON #{Project.table_name}.id = #{CostEntry.table_name}.project_id
EOS
when :time_entries
from = <<-EOS
#{TimeEntry.table_name}
LEFT OUTER JOIN #{TimeEntryActivity.table_name} ON #{TimeEntryActivity.table_name}.id = #{TimeEntry.table_name}.activity_id
LEFT OUTER JOIN #{User.table_name} ON #{User.table_name}.id = #{TimeEntry.table_name}.user_id
LEFT OUTER JOIN #{Issue.table_name} ON #{Issue.table_name}.id = #{TimeEntry.table_name}.issue_id
LEFT OUTER JOIN #{Project.table_name} ON #{Project.table_name}.id = #{TimeEntry.table_name}.project_id
EOS
end
end
def statement(entry_scope)
# entry_scope can currently be one of :cost_entries, :time_entries
# To not mix this with the scope (aka :issues vs. :costs)
issue_filter_clauses = []
entry_filter_clauses = []
# allow blank issue_ids if true
issue_nil_filter = true
if filters and valid?
filters.each do |filter|
filter = create_filter_from_hash(filter)
next if filter.column_name == "subproject_id"
sql = ''
case filter.scope
when :issues
if filter.column[:flags].include? :custom_field
sql << "#{Issue.table_name}.id IN ("
sql << " SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{filter.column[:db_table]} ON #{filter.column[:db_table]}.customized_type='Issue' AND #{filter.column[:db_table]}.customized_id=#{Issue.table_name}.id AND #{filter.column[:db_table]}.custom_field_id=#{filter.column[:db_field_id]} WHERE "
sql << sql_for_filter(filter, nil, true)
sql << ")"
elsif filter.column[:flags].include? :watcher
sql << "#{Issue.table_name}.id #{ field.operator == '=' ? 'IN' : 'NOT IN' } ("
sql << " SELECT #{filter.column[:db_table]}.watchable_id FROM #{filter.column[:db_table]} WHERE #{filter.column[:db_table]}.watchable_type='Issue' AND "
sql << sql_for_filter(filter)
sql << ")"
else
sql << '(' + sql_for_filter(filter) + ')'
end
issue_filter_clauses << sql
when :costs
issue_nil_filter = false if filter.column_name == "issue_id"
sql << '(' + sql_for_filter(filter, entry_scope) + ')'
entry_filter_clauses << sql
end
end
end
issue_filter_clauses = ["1=1"] if issue_filter_clauses.blank?
# FIXME: This is a hack calling a private ActiveRecord methods
# from http://pivotallabs.com/users/jsusser/blog/articles/567-hacking-a-subselect-in-activerecord
from = "#{Issue.table_name}"
from << " LEFT OUTER JOIN #{User.table_name} ON #{User.table_name}.id = #{Issue.table_name}.assigned_to_id"
from << " LEFT OUTER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id"
from << " LEFT OUTER JOIN #{Tracker.table_name} ON #{Tracker.table_name}.id = #{Issue.table_name}.tracker_id"
from << " LEFT OUTER JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Issue.table_name}.project_id"
from << " LEFT OUTER JOIN #{IssuePriority.table_name} ON #{IssuePriority.table_name}.id = #{Issue.table_name}.priority_id"
from << " LEFT OUTER JOIN #{IssueCategory.table_name} ON #{IssueCategory.table_name}.id = #{Issue.table_name}.category_id"
from << " LEFT OUTER JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Issue.table_name}.fixed_version_id"
issue_ids = Issue.send(
:construct_finder_sql,
:select => "#{Issue.table_name}.id",
#:include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
:from => from,
:conditions => (issue_filter_clauses << project_statement).join(' AND '))
case entry_scope
when :cost_entries
clause = ["#{CostEntry.table_name}.issue_id IN (#{issue_ids})"]
clause << "#{CostEntry.table_name}.issue_id IS NULL" if issue_nil_filter
entry_filter_clauses << "(#{clause.join(" OR ")})"
when :time_entries
clause = ["#{TimeEntry.table_name}.issue_id IN (#{issue_ids})"]
clause << "#{TimeEntry.table_name}.issue_id IS NULL" if issue_nil_filter
entry_filter_clauses << "(#{clause.join(" OR ")})"
end
entry_filter_clauses << User.current.allowed_for("view_#{entry_scope}".to_sym, projects)
entry_filter_clauses.join(' AND ')
end
def sort_criteria=(arg)
c = []
if arg.is_a?(Hash)
arg = arg.keys.sort.collect {|k| arg[k]}
end
c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
write_attribute(:sort_criteria, c)
end
def sort_criteria
read_attribute(:sort_criteria) || []
end
def sort_criteria_key(arg)
sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
end
def sort_criteria_order(arg)
sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
end
private
def sql_for_filter(filter, entry_scope = nil, string_as_null = false)
db_table = filter.column[:db_table]
if filter.scope == :costs && !db_table
case entry_scope
when :cost_entries
db_table = CostEntry.table_name
when :time_entries
db_table = TimeEntry.table_name
else
raise "Need a valid entry scope. Got #{entry_scope.inspect}" unless entry_scope
end
end
if filter.scope == :costs && (!filter.column[:applies].include? entry_scope)
# the current filter does not match the entry_scope so we just ignore it
return "1=1"
end
db_field = filter.column[:db_field] || filter.column_name
# Does not work for Redmine 0.8
#@sql_for_filter_query = Query.new(:name => "_") unless @sql_for_filter_query
#sql = @sql_for_filter_query.send(
# :sql_for_field,
# filter.column_name, filter.operator, filter.sql_values, db_table, db_field, string_as_null)
sql = sql_for_field(filter.column_name, filter.operator, filter.sql_values, db_table, db_field, string_as_null)
return sql unless sql == "1=1"
# We have an operator that was added by us. So we provide the logic here
case filter.operator
when "0"
sql = "#{db_table}.#{db_field} = 0"
when "y"
sql = "#{db_table}.#{db_field} IS NOT NULL"
when "n"
sql = "#{db_table}.#{db_field} IS NULL"
when "=n"
sql = "#{db_table}.#{db_field} = #{CostRate.clean_currency(filter.sql_values).to_f.to_s}"
when "<>d"
begin
date1 = filter.sql_values.first.to_date
date2 = filter.sql_values.last.to_date
sql = "#{db_table}.#{db_field} BETWEEN '#{connection.quoted_date(date1)}' AND '#{connection.quoted_date(date2)}'"
rescue
end
when ">d"
begin
date = filter.sql_values.first.to_date
sql = "#{db_table}.#{db_field} >= '#{connection.quoted_date(date)}'"
rescue
end
when "<d"
begin
date = filter.sql_values.first.to_date
sql = "#{db_table}.#{db_field} <= '#{connection.quoted_date(date)}'"
rescue
end
when "=d"
begin
date = filter.sql_values.first.to_date
sql = "#{db_table}.#{db_field} = '#{connection.quoted_date(date)}'"
rescue
end
end
return sql
end
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
# FIXME: This methods comes from redmine trunk. Delete this one and call the one from redmine instead!
def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
sql = ''
case operator
when "="
sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" unless value.blank?
when "!"
sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
when "!*"
sql = "#{db_table}.#{db_field} IS NULL"
sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
when "*"
sql = "#{db_table}.#{db_field} IS NOT NULL"
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
when ">="
sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
when "<="
sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
when "o"
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
when "c"
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
when ">t-"
sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
when "<t-"
sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
when "t-"
sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
when ">t+"
sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
when "<t+"
sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
when "t+"
sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
when "t"
sql = date_range_clause(db_table, db_field, 0, 0)
when "w"
from = l(:general_first_day_of_week) == '7' ?
# week starts on sunday
((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
# week starts on monday (Rails default)
Time.now.at_beginning_of_week
sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
when "~"
sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
when "!~"
sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
end
return sql.blank? ? "1=1" : sql
end
# Returns a SQL clause for a date or datetime field.
def date_range_clause(table, field, from, to)
s = []
if from
s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
end
if to
s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
end
s.join(' AND ')
end
end

@ -1,48 +0,0 @@
<%#
This partial requires the following locals:
filter The filter object to create a row for
The following locals are optional
index The id of the filter field
%>
<%
@line_index ||= "---INDEX---"
prefix = "filters[]"
id_prefix = "filters_#{@line_index}"
name_prefix = "filters[#{@line_index}]"
%>
<% fields_for prefix, filter do |f| %>
<tr class="filter" id="<%= id_prefix %>">
<td style="width: 200px">
<%= f.hidden_field :scope, :index => @line_index %>
<%= f.hidden_field :column_name, :index => @line_index %>
<%= f.check_box :enabled, :index => @line_index, :checked => true, :onclick => "toggle_filter($('#{id_prefix}'));" %>
<%= f.label :enabled, filter.label, :index => @line_index, :class => scope_icon_class(filter) %>
</td>
<td style="width: 150px;">
<%= f.select :operator, operators_for_select(filter.type_name), {}, :index => @line_index, :onchange => "toggle_operator($('#{id_prefix}'), '#{filter.type_name}')", :class => "select-small filter_operator", :style => "vertical-align: top;" %>
</td>
<td>
<% simple_operator = CostQuery.operators[filter.operator] ? CostQuery.operators[filter.operator][:simple] : false -%>
<div class="filter_values" <%= 'style="display: none"' if simple_operator %>>
<%= render :partial => "cost_reports/filter_types/#{filter.type_name}", :locals => {:f => f, :filter => filter, :id_prefix => id_prefix, :name_prefix => name_prefix} %>
</div>
</td>
</tr>
<script type="text/javascript">
//<![CDATA[
toggle_filter($('<%= id_prefix %>'));
<% if !filter.filter_type[:multiple] %>
disable_select('<%= filter.column_name %>', '<%= (filter.scope == :issues) ? "add_issue_filter_select" : "add_cost_filter_select" %>');
<% end %>
//]]>
</script>
<% end %>

@ -1,170 +0,0 @@
<%
add_issue_filter_select = [["", ""]]
add_cost_filter_select = [["", ""]]
%>
<script type="text/javascript">
//<![CDATA[
var Filterform = Class.create({
lineIndex: 1,
parentElement: "",
initialize: function(filters, lineIndex, parentElement) {
this.filters = filters;
this.lineIndex = lineIndex;
this.parentElement = parentElement;
},
add_html: function(e, scope, key, insertion) {
new Ajax.Updater({ success: e.identify() }, '<%= url_for(:action => :get_filter) %>', {
parameters: {
scope: scope,
column_name: key,
line_index: this.lineIndex++
},
insertion: insertion,
evalScripts: true
});
},
add: function(scope, key) {
var e = $(this.parentElement);
this.add_html(e, scope, key, "bottom")
return this.lineIndex-1;
}
});
multiFilters = new Hash();
<% @query.available_filters.each_pair do |scope, available_filters| -%>
multiFilters.set('<%= scope %>', [<%= available_filters.select{|k,v| CostQuery.filter_types[v[:type]][:multiple]}.collect{|k,v| "'#{k}'"}.join(", ") %>]);
<%- end %>
function reset_select(select) {
select.selectedIndex = 0;
}
function add_filter(select, scope) {
var column_name = select.value;
if (!column_name) {
return reset_select(select);
}
var filter_id = filterform.add(scope, column_name);
select.selectedIndex = 0;
}
function toggle_filter(filter) {
var check_box = filter.down("input[type=checkbox]")
if (check_box.checked) {
filter.down("select.filter_operator").show();
toggle_operator(filter);
} else {
filter.down("select.filter_operator").hide();
filter.down("div.filter_values").hide();
}
}
function toggle_operator(filter, filter_type) {
var operator = filter.down("select.filter_operator");
var simple_operators = [<%= CostQuery.operators.select{|k,v| v[:simple]}.collect{|k,v| "'#{k}'"}.join(", ") %>]
var values_field = filter.down("div.filter_values");
if (simple_operators.include(operator.value)) {
values_field.hide();
} else {
values_field.show();
}
if (filter_type == 'date_exact') {
var between_tags = filter.down("span.between_tags");
if (operator.value == '<>d') {
between_tags.show();
} else {
between_tags.hide();
}
}
}
function toggle_multi_select(filter) {
var select = filter.down("div.filter_values").down("select");
if (select.readAttribute("multiple")) {
select.writeAttribute("multiple", null);
} else {
select.writeAttribute("multiple", "multiple");
}
}
finished_loading = false;
deferred_disable_select = new Array();
function disable_select(filter_name, select_id) {
if (finished_loading) {
select = $(select_id)
// the current filter can only be applied once
for (i=0; i<select.options.length; i++) {
var option = select.options[i];
if (option.value == filter_name) {
option.disabled = true;
}
}
} else {
deferred_disable_select.push([filter_name, select_id]);
}
}
Event.observe(window, 'load', function() {
finished_loading = true;
deferred_disable_select.each(function(item) {
disable_select(item[0], item[1]);
});
});
IssueFilterTypes = new Array();
<%
@query.available_filters[:issues].sort_by{|c| c[1][:order]}.each do |e|
column_name = e[0]
filter = @query.create_filter(:issues, column_name)
add_issue_filter_select << [filter.label, column_name]
-%>
IssueFilterTypes.push('<%= escape_javascript(column_name) %>');
<% end %>
CostEntryFilterTypes = new Array()
<%
@query.available_filters[:costs].sort_by{|c| c[1][:order]}.each do |e|
column_name = e[0]
filter = @query.create_filter(:costs, column_name)
add_cost_filter_select << [filter.label, column_name]
%>
CostEntryFilterTypes.push('<%= escape_javascript(column_name) %>');
<% end %>
filterform = new Filterform($H({issues: IssueFilterTypes, costs: CostEntryFilterTypes}), <%= @query.filters ? @query.filters.length : 0 %>, 'filter_table');
//]]>
</script>
<table width="100%">
<tbody><tr>
<td><table id="filter_table">
<% @query.filters.each_with_index do |filter, index| %>
<% @line_index = index %>
<%= render(:partial => "filter", :object => @query.create_filter_from_hash(filter)) %>
<% end if @query.filters %>
</table></td>
<td class="add-filter">
<%= l(:label_cost_filter_add) %>: <%= select_tag 'add_cost_filter_select', options_for_select(add_cost_filter_select),
:onchange => "add_filter(this, 'costs');",
:class => "select-small",
:name => nil %><br />
<%= l(:label_issue_filter_add) %>: <%= select_tag 'add_issue_filter_select', options_for_select(add_issue_filter_select),
:onchange => "add_filter(this, 'issues');",
:class => "select-small",
:name => nil %>
</td>
</tr></tbody>
</table>
<% include_calendar_headers_tags %>

@ -1,18 +0,0 @@
<script type="text/javascript">
//<![CDATA[
function group_by_changed()
{
$('group_by_granularity').setStyle({
'display': <%= @query.time_groups.inject([]){|r,t| r<<t.to_s}.inspect %>.include($("group_by_name").value) ? 'inline' : 'none'});
}
//]]>
</script>
<%=
select_tag "group_by[name]", options_for_select(@query.group_by_columns_for_select, (@query.group_by[:name].to_s unless @query.group_by[:name].blank?)),
:class => "select-small", :onchange => "group_by_changed();"
%>
<%=
select_tag("group_by[granularity]", options_for_select([[l(:label_year), "year"], [l(:label_month), "month"], [l(:label_week), "week"], [l(:label_day_plural), "day"]], @query.group_by[:granularity]),
:class => "select-small", :onload => "group_by_changed();", :style => ("display: none" if @query.group_by["name"].blank? || !@query.time_groups.include?(@query.group_by["name"].to_s)))
%>

@ -1,9 +0,0 @@
<% if @grouped_entries %>
<%= render :partial => "list_group_by" %>
<% else %>
<%= render :partial => "list_items" %>
<% end %>
<% other_formats_links do |f| %>
<%= call_hook :view_cost_report_other_formats, :f => f %>
<% end %>

@ -1,132 +0,0 @@
<%
def group_by_column
CostQuery.group_by_columns[@query.group_by[:name]]
end
def display_costs
CostEntry.column_names.include?(group_by_column[:db_field].to_s) && @query.display_cost_entries
end
def display_time
TimeEntry.column_names.include?(group_by_column[:db_field].to_s) && @query.display_time_entries
end
def display_js(invert=false)
return "'' + Form.serialize('filter-options')" unless group_by_column[:scope] == :costs
if invert
display_costs = !display_costs
display_time = !display_time
end
if display_costs && !display_time
"'cost_query[display_cost_entries]=1&cost_query[display_time_entries]=0'"
elsif !display_costs && display_time
"'cost_query[display_cost_entries]=0&cost_query[display_time_entries]=1'"
else
"Form.serialize('filter-options')"
end
end
def filter_js(filter_hash)
if group_by_column[:scope] == :costs && filter_hash[:values].nil?
return ""
end
{:filters => {(@query.filters ? @query.filters.length : 0) => filter_hash}}.to_query
end
%>
<% if @grouped_entries.blank? %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% else %>
<%= element_hidden_warning %>
<table class="list">
<thead>
<th>Group By</th>
<th class="units">Count</th>
<% if (@query.group_by["name"] == "cost_type_id") || (!display_costs) %><th><%= l(:caption_cost_unit_plural) %></th><% end %>
<th class="currency">Sum</th>
<th>Drill Down</th>
</thead>
<tbody>
<%
unit_sum = Hash.new(0)
@grouped_entries.each do |entry|
entry &&= entry.with_indifferent_access
filter = @query.filter_from_group_by(entry)
if filter[:values].nil?
display_js = display_js(true)
else
display_js = display_js(false)
end
filter_js = filter_js(filter)
group_by = {:group_by=>{:name=>"", :granularity=>"year"}}
fields = entry.keys - %w[count sum unit_sum]
if fields.include? "tmonth"
name = "#{entry[:tyear]}, #{month_name(entry["tmonth"].to_i)}"
elsif fields.include? "tweek"
name = "#{entry[:tyear]}, #{l(:week)} \##{entry["tweek"]}"
else
name = fields.map { |k| CostQuery.get_name(k, entry[k]) }.join " "
end
name.strip!
%>
<tr class="<%= cycle('odd', 'even') %>">
<td>
<%= name %>
</td>
<td class="units"><%=l :x_entries, :count => entry["count"].to_i %></td>
<% if (@query.group_by["name"] == "cost_type_id") || (!display_costs)
cost_type = CostType.find_by_id(entry["cost_type_id"])
-%>
<td class="units">
<% if cost_type %>
<%- unit_sum[cost_type] += entry["unit_sum"].to_f -%>
<%= pluralize(entry["unit_sum"], cost_type.unit, cost_type.unit_plural) %>
<%- elsif display_costs || entry[group_by_column[:db_field]] -%>
<%- unit_sum[cost_type] += entry["unit_sum"].to_f -%>
<%= l_hours(entry["unit_sum"] || "0") %>
<%- end %>
</td>
<%- end %>
<td class="currency"><%= number_to_currency(entry["sum"]) %></td>
<td>
<%= link_to_remote "Drill Down", {
:url => { :set_filter => 1 },
:update => "content",
:with => "original_filters + '&#{filter_js}&#{group_by.to_query}&' + #{display_js}"
} %>
</td>
</tr>
<% end %>
<tr>
<td>&nbsp;</td>
<td class="units"><strong><%=l :x_entries, :count => @entry_count.to_i %></strong></td>
<% if (@query.group_by["name"] == "cost_type_id") || (!display_costs) %><td class="units">
<% if unit_sum.count == 1
cost_type = unit_sum.keys[0]
sum = unit_sum[cost_type]
-%>
<%- if cost_type -%>
<%= pluralize(sum, cost_type.unit, cost_type.unit_plural) %>
<%- else -%>
<%= l_hours(sum || "0") %>
<%- end %>
<%- else -%>
&nbsp;
<%- end %>
</td><% end %>
<td class="currency"><strong><%= number_to_currency @entry_sum %></strong></td>
<td>&nbsp;</td>
</tr>
</tbody>
</table>
<% end %>

@ -1,5 +0,0 @@
<p>
<%= l(:label_display_types) %>:
<%= query_form.check_box :display_cost_entries %> <%= query_form.label :display_cost_entries, l(:field_material_costs) %>
<%= query_form.check_box :display_time_entries %> <%= query_form.label :display_time_entries, l(:field_labor_costs) %>
</p>

@ -1 +0,0 @@
<%= f.text_field :values, :size => 3, :index => @line_index, :class => "select-small" %> <%= l(:label_day_plural) %>

@ -1,8 +0,0 @@
<%= text_field_tag "#{name_prefix}[values][]", (filter.values.first if filter.values), :size => 10, :class => "select-small", :id => "#{id_prefix}_date1"%> <%= calendar_for("#{id_prefix}_date1") %>
<span id="<%= id_prefix %>_between_tags" class="between_tags" <%= 'style="display:none"' unless filter.operator == "<>d" %>>
<%=
l(:label_until)
text_field_tag("#{name_prefix}[values][]", (filter.values.last if filter.values), :size => 10, :class => "select-small", :id => "#{id_prefix}_date2") + " " +
calendar_for("#{id_prefix}_date2")
%>
</span>

@ -1 +0,0 @@
<%= render :partial => "cost_reports/filter_types/date", :locals => {:f => f, :filter => filter, :index => @line_index, :id_prefix => id_prefix, :name_prefix => name_prefix} %>

@ -1 +0,0 @@
<%= f.text_field :values, :size => 3, :index => @line_index, :class => "select-small" %>

@ -1 +0,0 @@
<%= render :partial => "cost_reports/filter_types/integer", :locals => {:f => f, :filter => filter, :id_prefix => id_prefix, :name_prefix => name_prefix} %>

@ -1,2 +0,0 @@
<%= f.select :values, filter.available_values, {}, {:multiple => (filter.values && filter.values.length > 1), :index => "#{@line_index}[]", :name => "#{name_prefix}[values][]" } %>
<%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select($('#{id_prefix}'));", :style => "vertical-align: bottom;" %>

@ -1 +0,0 @@
<%= render :partial => "cost_reports/filter_types/list", :locals => {:f => f, :filter => filter, :id_prefix => id_prefix, :name_prefix => name_prefix} %>

@ -1 +0,0 @@
<%= render :partial => "cost_reports/filter_types/list", :locals => {:f => f, :filter => filter, :id_prefix => id_prefix, :name_prefix => name_prefix} %>

@ -1 +0,0 @@
<%= render :partial => "cost_reports/filter_types/list", :locals => {:f => f, :filter => filter, :id_prefix => id_prefix, :name_prefix => name_prefix} %>

@ -1 +0,0 @@
<%= f.text_field :values, :size => 30, :index => @line_index, :class => "select-small" %>

@ -1 +0,0 @@
<%= render :partial => "cost_reports/filter_types/string", :locals => {:f => f, :filter => filter, :id_prefix => id_prefix, :name_prefix => name_prefix} %>

@ -1,84 +0,0 @@
<% if @custom_error %>
<div class="flash error"><%= @custom_error %></div>
<% end %>
<div class="contextual">
<% if !@query.new_record? && @query.editable_by?(User.current) %>
<%= link_to l(:button_edit), {:controller => 'cost_report', :action => 'edit', :id => @query}, :class => 'icon icon-edit' %>
<%= link_to l(:button_delete), {:controller => 'cost_report', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
<% end %>
</div>
<h2><%= @query.new_record? ? l(:label_cost_report) : h(@query.name) %></h2>
<% html_title( @query.new_record? ? l(:label_cost_report) : @query.name ) %>
<script type="text/javascript">
//<![CDATA[
function toggle_options(select, options, state) {
for (i=0; i<select.options.length; i++) {
var option = select.options[i];
if (options.indexOf(option.value) >= 0) {
if (state == "disable") {
option.writeAttribute("disabled", "disabled");
} else {
option.writeAttribute("disabled", null);
}
}
}
}
//]]>
</script>
<% form_for @query, :url => {:controller => 'cost_report', :action => 'new' }, :html => {:id => 'query_form', :method => :post} do |query_form| %>
<div id="query_from_content">
<fieldset id="filters" class="collapsible <%= "collapsed" unless @query.new_record? %>">
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
<div <%= 'style="display:none;"' unless @query.new_record? %>><%= render :partial => 'filters', :locals => {:query_form => query_form} %></div>
</fieldset>
<fieldset id="group-by" class="collapsible">
<legend onclick="toggleFieldset(this);"><%= l(:label_group_by) %></legend>
<div><%= render :partial => 'group_by', :locals => {:query_form => query_form} %></div>
</fieldset>
<fieldset id="filter-options" class="collapsible">
<legend onclick="toggleFieldset(this);"><%= l(:label_option_plural) %></legend>
<div><%= render :partial => 'options', :locals => {:query_form => query_form} %></div>
</fieldset>
<p class="buttons">
<%= link_to_remote l(:button_apply),
{ :url => { :set_filter => 1 },
:update => "content",
:with => "Form.serialize('query_form')"
}, :class => 'icon icon-checked' %>
<%= link_to_remote l(:button_clear),
{ :url => { :set_filter => 1 },
:update => "content",
}, :class => 'icon icon-reload' %>
<% if User.current.allowed_to?(:save_queries, @project, :global => true) %>
<%
#link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save'
%>
<% end %>
</p>
</div>
<% end %>
<%= javascript_tag "original_filters = Form.serialize('filters');" %>
<%= error_messages_for 'query' %>
<% if @query.valid? %>
<%= render :partial => 'list', :locals => {:entries => @entries, :query => @query} %>
<% end %>
<% content_for :header_tags do %>
<%= stylesheet_link_tag 'scm' %>
<%= stylesheet_link_tag 'costs', :plugin => 'redmine_costs' %>
<% end %>

@ -59,8 +59,6 @@ ActionController::Routing::Routes.draw do |map|
end
end
map.connect 'projects/:project_id/cost_reports.:format', :controller => 'cost_reports', :project_id => /.+/, :action => 'index'
map.connect 'projects/:project_id/cost_reports/:action/:id', :controller => 'cost_reports', :project_id => /.+/
map.connect 'projects/:project_id/costlog/:action/:id', :controller => 'costlog', :project_id => /.+/
# map.connect 'projects/:project_id/hourly_rates/:action/:id', :controller => 'hourly_rates', :project_id => /.+/

@ -45,13 +45,13 @@ class Filter
def self.operators(*operators)
# Does it make sense to just store the names here and perform Operator.find
# if needed?
operators.each do |o|
o = Operator.find(o) unless o.is_a? Operator
@operators << o
end
end
# Store the default operator
# NOTE: this should be implemented explictly
cattr_accessor :default_operator
@ -63,54 +63,54 @@ class Filter
# ...
end
def to_hash
# serialize self to a hash suitable for later deserialization with
# Filter.from_hash
# This can be used to save the filter to a database or to create a part of
# the query string in the view
# ...
end
attr_accessor :operator
def sql_select
# returns the default select part of a query
# This might be overwritten in child classes
# NOTE: this might be changed to an item of the :include array of an ActiveRecord::Base.find
"#{model.table_name}.#{db_field} as #{self.class.name.underscore}"
end
def sql_where()
# returns the default where part of a query
# This might be overwritten in child classes to provide special logic besides
# standard operators.
#
# NOTE: This should be suitable to be used in :conditions in an ActiveRecord::Base.find
Operator.find(operator).sql_where()
end
# self.model
# self.db_field
# available_values(project)
# available_values(user)
def sql_joins(otiginal_table)
# returns an array of all needed joins
# original_table is thw name of the original table of the join (e.g. time_entries or cost_entries)
# NOTE: this might be used to generate :include items
["JOIN issues ON #{table}.issue_id = issues.id", "JOIN users on #{table}.user_id = user.id"]
end
end
class FooFilter < FilterColumn
# This is an example definition of a folter column class
operators :=, :!=, :<=, :>=, :<&>
column :foo
model :issues
@ -147,20 +147,20 @@ class ReportGroupOfGroups < Array
def sum
@sum ||= inject(0) { |e| e.sum }
end
def count
@count ||= inject(0) { |e| e.cont }
end
def has_children?
true
end
def drill_down_filter
# this uses the parent pointer of the GroupBy instance
# ...
end
def recursive_each(level = 0, &block)
block.call(level, self)
each { |child| child.recursive_each(level + 1, &block) }
@ -184,7 +184,7 @@ end
class GroupBy
# This provides the stared functionality of a group-by columnh
module BasicGroupBy
# Module to be used, if this instance is the group-by with the finest
# granularity (or the single one)
@ -205,9 +205,9 @@ class GroupBy
[filter_for_group] << @based_on.filters
end
end
attr_accessor :parent
def initialize(based_on)
# NOTE: based_on should actually be an array of filters
if based_on.is_a? Filter
@ -218,17 +218,17 @@ class GroupBy
based_on.parent = self
end
end
def filter_for_group
# create filter from group by from drill down
# NOTE: this does not make sense here (???)
# ...
end
end
class GroupByName < GroupBy
def results(columns)
columns.delete :dont_like
super

@ -1,4 +1,4 @@
Feature: Cost Reports
Feature: Cost Control
Scenario: Anonymous user sees no costs
Given I am not logged in
@ -77,10 +77,10 @@ Feature: Cost Reports
And the project "CostProject" has 1 cost entry with the following:
| units | 1234.0 |
And I am admin
When I am on the Cost Reports page for the project called "CostProject"
And I am on the overall Cost Reports page without filters or groups
When I click on "Edit"
When I fill in "4321.0" for "cost_entry_units"
When I click on "Save"
Then I should see "4321.0"
And I should not see "1234.0"

@ -25,11 +25,38 @@ Given /^there is 1 cost type with the following:$/ do |table|
end})
end
Given /^the [Uu]ser "([^\"]*)" has (\d+) [Cc]ost(?: )?[Ee]ntr(?:ies|y)$/ do |user, count|
Given /^there (?:is|are) (\d+) (default )?hourly rate[s]? with the following:$/ do |num, is_default, table|
if is_default
hr = DefaultHourlyRate.spawn
else
hr = HourlyRate.spawn
end
send_table_to_object(hr, table, {
:user => Proc.new do |rate, value|
# I am sorry, but it didn't seem to work with any less saving!
rate.save!
rate.reload.save!
unless rate.project.nil? || User.find_by_login(value).projects.include?(rate.project)
rate.save!
rate.update_attribute :project_id, User.find_by_login(value).projects.last.id
rate.reload.save!
end
rate.update_attribute :user_id, User.find_by_login(value).id
rate.reload.save!
end,
:valid_from => Proc.new do |rate, value|
# This works for definitions like "2 years ago"
number, time_unit, tempus = value.split
time = number.to_i.send(time_unit.to_sym).send(tempus.to_sym)
rate.update_attribute :valid_from, time
end })
end
Given /^the [Uu]ser "([^\"]*)" has (\d+) [Cc]ost(?: )?[Ee]ntr(?:ies|y)$/ do |user, count|
u = User.find_by_login user
p = u.projects.last
i = Issue.generate_for_project!(p)
as_admin count do
as_admin count do
ce = CostEntry.spawn
ce.user = u
ce.project = p
@ -74,8 +101,14 @@ Given /^there is a standard cost control project named "([^\"]*)"$/ do |name|
Given there is 1 project with the following:
| Name | #{name} |
And the project "#{name}" has 1 subproject
And the project "#{name}" has 1 issue with:
| subject | #{name}issue |
And the role "Manager" may have the following rights:
| View own cost entries |
| view_own_hourly_rate |
| view_issues |
| view_own_time_entries |
| view_own_cost_entries |
| view_cost_rates |
And there is a role "Controller"
And the role "Controller" may have the following rights:
| View own cost entries |
@ -101,3 +134,35 @@ Given /^there is a standard cost control project named "([^\"]*)"$/ do |name|
And the user "reporter" is a "Reporter" in the project "#{name}"
}
end
Given /^users have times and the cost type "([^\"]*)" logged on the issue "([^\"]*)" with:$/ do |cost_type, issue, table|
i = Issue.find(:last, :conditions => ["subject = '#{issue}'"])
raise "No such issue: #{issue}" unless i
table.rows_hash.collect do |k,v|
user = k.split.first
if k.end_with? "hours"
steps %Q{
And the issue "#{issue}" has 1 time entry with the following:
| hours | #{v} |
| user | #{user} |
}
elsif k.end_with? "units"
steps %Q{
And the issue "#{issue}" has 1 cost entry with the following:
| units | #{v} |
| user | #{user} |
| cost type | #{cost_type} |
}
elsif k.end_with? "rate"
steps %Q{
And the user "#{user}" has:
| default rate | #{v} |
}
else
"Don't know what to do with #{k} => #{v}. Use | <username> (hours|rate|units) | <x> | as."
next
end
end
end

@ -44,14 +44,16 @@ Feature: Permission View Own hourly and cost rates
And I should not see "35.00 EUR" # material costs only of Manager
And I should not see "43.00 EUR" # labour costs of me and Manager
And I should not see "49.00 EUR" # material costs of me and Manager
And I am on the issues page for the project called "Standard Project"
And I select to see column "overall costs"
And I select to see column "labour costs"
And I select to see column "material costs"
And I am on the issues page for the project called "Standard Project"
And I select to see columns
| Overall costs |
| Labor costs |
| Material costs |
Then I should see "24.00 EUR"
And I should see "10.00 EUR"
And I should see "14.00 EUR"
And I should not see "33.00 EUR" # labour costs only of Manager
And I should not see "35.00 EUR" # material costs only of Manager
And I should not see "43.00 EUR" # labour costs of me and Manager
And I should not see "49.00 EUR" # material costs of me and Manager
And I should not see "49.00 EUR" # material costs of me and Manager

@ -98,9 +98,6 @@ Redmine::Plugin.register :redmine_costs do
permission :edit_cost_entries, {:costlog => [:edit, :destroy]},
:require => :member,
:inherits => :view_cost_entries
permission :view_cost_entries, {:costlog => [:details], :cost_reports => [:index, :get_filter]}
permission :view_own_cost_entries, {:costlog => [:details], :cost_reports => [:index, :get_filter]},
:granular_for => :view_cost_entries
permission :block_tickets, {}, :require => :member
permission :view_cost_objects, {:cost_objects => [:index, :show]},
@ -108,13 +105,12 @@ Redmine::Plugin.register :redmine_costs do
permission :edit_cost_objects, {:cost_objects => [:index, :show, :edit, :destroy, :new]},
:inherits => :view_cost_objects
end
# register additional permissions for the time log
project_module :time_tracking do
permission :view_own_time_entries, {:timelog => [:details, :report], :cost_reports => [:index, :get_filter]},
:granular_for => :view_time_entries
permission :view_own_time_entries, {:timelog => [:details, :report]}, :granular_for => :view_time_entries
end
view_time_entries = Redmine::AccessControl.permission(:view_time_entries)
view_time_entries.instance_variable_set("@inherits", [:view_own_time_entries])
view_time_entries.actions << "cost_reports/index"
@ -129,20 +125,10 @@ Redmine::Plugin.register :redmine_costs do
# Menu extensions
menu :top_menu, :cost_types, {:controller => 'cost_types', :action => 'index'},
:caption => :cost_types_title, :if => Proc.new { User.current.admin? }
# menu :top_menu, :cost_reports, {:controller => 'cost_reports', :action => 'index'},
# :caption => :cost_reports_title,
# :if => Proc.new {
# ( User.current.allowed_to?(:view_cost_objects, nil, :global => true) ||
# User.current.allowed_to?(:edit_cost_objects, nil, :global => true)
# )
# }
menu :project_menu, :cost_objects, {:controller => 'cost_objects', :action => 'index'},
:param => :project_id, :after => :new_issue, :caption => :cost_objects_title
menu :project_menu, :cost_reports, {:controller => 'cost_reports', :action => 'index'},
:param => :project_id, :after => :cost_objects, :caption => :cost_reports_title
# Activities
activity_provider :cost_objects
end

@ -3,7 +3,6 @@ group_by_others: "In keiner der Gruppen"
project_module_costs_module: "Controlling"
cost_types_title: "Kostenarten"
cost_objects_title: "Controlling"
cost_reports_title: "Reports"
currency_delimiter: "."
currency_separator: ","

@ -3,7 +3,6 @@ group_by_others: "not in any group"
project_module_costs_module: "Cost Control"
cost_types_title: "Cost Types"
cost_objects_title: "Cost Control"
cost_reports_title: "Cost Reports"
currency_delimiter: ","
currency_separator: "."

@ -15,6 +15,7 @@ module CostsTimelogControllerPatch
def details_with_reports_view
# we handle single project reporting currently
return details_without_reports_view if @project.nil?
filters = {:operators => {}, :values => {}}
if @issue
if @issue.respond_to?("lft")
@ -23,25 +24,17 @@ module CostsTimelogControllerPatch
issue_ids = [@issue.id.to_s]
end
filters = [{
:column_name => "issue_id",
:enabled => "1",
:operator => "=",
:scope => "costs",
:values => issue_ids
}]
filters[:operators][:issue_id] = "="
filters[:values][:issue_id] = [issue_ids]
end
filters[:operators][:project_id] = "="
filters[:values][:project_id] = [@project.id.to_s]
respond_to do |format|
format.html {
session[:cost_query] = {:project_id => @project.id,
:filters => (filters || {}),
:group_by => {},
:display_cost_entries => "0",
:display_time_entries => "1"
}
redirect_to :controller => "cost_reports", :action => "index", :project_id => @project
session[:cost_query] = { :filters => filters, :groups => {:rows => [], :columns => []} }
redirect_to :controller => "cost_reports", :action => "index", :project_id => @project, :unit => -1
}
format.all {
details_without_report_view

@ -120,12 +120,15 @@ module CostsUserPatch
end
def allowed_for(permission, projects = nil)
unless projects.blank?
unless projects.nil? or projects.blank?
projects = [projects] unless projects.is_a? Array
projects, ids = projects.partition{|p| p.is_a?(Project)}
projects += Project.find_all_by_id(ids)
else
projects = Project.find(:all, :conditions => Project.visible_by(self), :include => [:enabled_modules])
vis_projects = Project.find(:all, :conditions => Project.visible_by(self), :include => [:enabled_modules])
projects = vis_projects + (projects.nil? ? [] : projects)
# In case there is no Project, we assume that an admin still has all the permissions
return (self.admin? ? "(1=1)" : "(1=0)") if projects.blank?
end
return "(#{Project.table_name}.id in (#{projects.collect(&:id).join(", ")}))" if self.admin?
@ -164,7 +167,6 @@ module CostsUserPatch
"(#{cond.join " OR "})"
end
def current_rate(project = nil, include_default = true)
rate_at(Date.today, project, include_default)
end

@ -4,34 +4,34 @@ CostEntry.class_eval do
generator_for :cost_type, :method => :next_cost_type
generator_for :rate, :method => :next_cost_rate
generator_for :units, rand(1000)
generator_for :spent_on, 1.year.ago
generator_for :spent_on, 1.day.ago
generator_for :comments, "Some comment"
generator_for :issue, :method => :next_issue
generator_for :created_on, 1.year.ago
generator_for :created_on, 1.day.ago
generator_for :updated_on, Date.today
generator_for :blocked, false
generator_for :costs, 20
generator_for :tyear, 2010
generator_for :tmonth, 3
generator_for :tweek, 10
generator_for :tyear, 1.day.ago.year
generator_for :tmonth, 1.day.ago.month
generator_for :tweek, 1.day.ago.to_date.cweek
def self.next_project
Project.last or Project.generate!
Project.last || Project.generate!
end
def self.next_cost_rate
CostRate.last or CostRate.generate!
CostRate.last || CostRate.generate!
end
def self.next_user
User.find_by_login("admin")
User.generate_with_protected!
end
def self.next_cost_type
CostType.last or CostType.generate!
CostType.last || CostType.generate!
end
def self.next_issue
self.next_project.issues.last or Issue.generate_for_project!(next_project)
self.next_project.issues.last || Issue.generate_for_project!(next_project)
end
end

@ -10,6 +10,6 @@ CostObject.class_eval do
end
def self.next_author
User.find_by_name("admin")
User.first
end
end

@ -1,13 +1,7 @@
CostRate.class_eval do
generator_for :valid_from, :method => :next_valid_from
generator_for :rate, 10
generator_for :cost_type, :method => :next_cost_type
def self.next_cost_type
CostType.last or CostType.generate!
CostType.last || CostType.generate!
end
def self.next_valid_from
1.year.ago + Rate.count
end
end
end

@ -0,0 +1,8 @@
Rate.class_eval do
generator_for :valid_from, :method => :next_valid_from
generator_for :rate, 10
def self.next_valid_from
1.year.ago + Rate.count
end
end
Loading…
Cancel
Save