37 Commits
Author SHA1 Message Date
Eric Davis 5076b1c88b Use the first TimeEntry as the date_in_effect in case a user backdates time. #1924 2009-01-26 12:05:19 -08:00
Eric Davis fe11c40381 Bumping version to 0.2.0 for Rate plugin compatability 2009-01-23 14:25:06 -08:00
Eric Davis ad67e1634f Found some more calculation methods that were not tested. #1924 2009-01-21 16:27:39 -08:00
Eric Davis 049885c170 Refactored the class and added more tests on calculations. #1924 2009-01-21 16:20:18 -08:00
Eric Davis 42325fadf4 Added tests for Budget#amount_missing_on_deliverables. #1924 2009-01-21 16:06:03 -08:00
Eric Davis 559b52112d Added tests to check Budget#amount_missing_on_issues. #1924 2009-01-21 15:52:56 -08:00
Eric Davis 11b8a0a462 Added a test to cover Budget#labor_budget_left. #1924 2009-01-21 15:47:31 -08:00
Eric Davis 1706412bb8 Added test to get full coverage on HourlyDeliverable. #1924 2009-01-21 15:45:38 -08:00
Eric Davis eeec71f857 Added test to get full coverage on FixedDeliverable. #1924 2009-01-21 15:44:12 -08:00
Eric Davis 5ae939d82a One too many directories down 2009-01-21 15:40:42 -08:00
Eric Davis aed49c68da Make things simple and they are easier to understand. #1924 2009-01-21 15:29:00 -08:00
Eric Davis 075310da6b Swapped inject with a simple sum method. #1924 2009-01-21 15:26:46 -08:00
Eric Davis 3d8c1ecc5c Removed all the duplicate methods that were getting the rate for each TimeEntry
and replaced them with a simple collector that uses the new TimeEntry#cost
method (that wraps the Rate API).

  #1924
2009-01-21 15:20:21 -08:00
Eric Davis 7fa4f33748 Ported more items to the new Rate API. #1924 2009-01-21 14:46:58 -08:00
Eric Davis 045f50e0dd Fixed format of the date request to Rate.amount_for. #1924 2009-01-21 14:34:19 -08:00
Eric Davis 4f18c28bba Ported FixedDeliverable.spent to use Rate. #1924 2009-01-21 14:31:06 -08:00
Eric Davis 50a466d7af Ported HourlyDeliverable.spent to use Rate. #1924 2009-01-21 14:27:12 -08:00
Eric Davis 5510753a4b Added migration to fully remove rate from Members. #1924 2009-01-21 14:18:43 -08:00
Eric Davis c6b72683b4 Completed migration 9, creating new rates for each Member.rate
#1924
2009-01-21 14:16:39 -08:00
Eric Davis 8a08e654e0 Added the start of a new migration to use the Rate plugin
* Migration 9 will abort with a message if the Rate plugin isn't installed

  #1924
2009-01-21 14:09:34 -08:00
Eric Davis 0f7c630f9c Removed the project panel hook. #1924 2009-01-21 13:51:15 -08:00
Eric Davis 88026a1049 Renamed the settings view so it will not conflict with others. #1700 2009-01-02 21:19:31 -08:00
Eric Davis fb24bbf546 Merge branch 'master' of git@dev.littlestreamsoftware.com:budget_plugin 2009-01-02 19:22:53 -08:00
Eric Davis 18a2f1aefa Added docs to make sure the user knows what to name the plugin. #1930 2009-01-02 18:58:36 -08:00
Eric Davis 47ed9275df Merge branch 'master' of git@dev.littlestreamsoftware.com:budget_plugin 2009-01-02 18:37:20 -08:00
Eric Davis 8e31e8fb8a Added a required parameter to the sorting field. #1910 2009-01-02 18:35:28 -08:00
Eric Davis 7ef0b094de Added release tasks 2008-11-30 14:41:33 -08:00
Eric Davis b70728efca Refactored the rdoc task to include more files and use the PROJECT_NAME 2008-11-30 14:40:34 -08:00
Eric Davis 01d8159e0e Require all plugin code so rcov works and reports on everything 2008-11-30 01:59:20 -08:00
Eric Davis f7c2816e13 Allow rake tasks to use the REDMINE_ROOT environment also 2008-11-30 01:58:52 -08:00
Eric Davis 6f42cda5ca Allow the loading of a external Redmine environment
* If the REDMINE_ROOT environment variable is set, it will be used to load
  the Rails environment.  This will allow the plugin to be tested outside of
  a Redmine's vendor/plugins directory
2008-11-30 01:12:12 -08:00
Eric Davis 4897e6c228 Merge branch 'master' of git@dev.littlestreamsoftware.com:budget_plugin 2008-11-30 01:07:25 -08:00
Eric Davis e61b15b9d5 Removed the requirement of having rspec and rspec-rails plugins installed
in favor of using the gems.
2008-11-30 01:06:54 -08:00
Eric Davis 83b9458443 Fixed a typo in the down migration. Reported by Roman Tataurov. #1839 2008-11-26 09:54:32 -08:00
Eric Davis 36d9ab3ed8 Removed the monkey patching with Issue.find. It was required but is no
longer needed and keep breaking search.
2008-10-24 22:18:20 -07:00
Eric Davis 52e80b4f56 Wrapped Deliverable.budget getter so it will return 0 if the budget is nil. #1708 2008-10-15 09:43:22 -07:00
Eric Davis 71060831fc Removed console logging statement. #1703 2008-10-13 11:19:22 -07:00
21 changed files with 313 additions and 171 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ Budget is a plugin to manage the set of deliverables for each project, automatic
## Getting the plugin ## Getting the plugin
A copy of the plugin can be found in the [downloads](https://projects.littlestreamsoftware.com/projects/list_files/redmine-budget) at Little Stream Software and also on [GitHub](http://github.com/edavis10/redmine-budget-plugin/tree/master). A copy of the plugin can be found in the [downloads](https://projects.littlestreamsoftware.com/projects/list_files/redmine-budget) at Little Stream Software and also on [GitHub](http://github.com/edavis10/redmine-budget-plugin/tree/master). Make sure you install the plugin to @vendor/plugins/budget_plugin@.
## Install ## Install
+25 -33
View File
@@ -1,24 +1,23 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
require "fileutils" require "fileutils"
require 'rubygems'
gem 'rspec'
gem 'rspec-rails'
Dir[File.expand_path(File.dirname(__FILE__)) + "/lib/tasks/**/*.rake"].sort.each { |ext| load ext } Dir[File.expand_path(File.dirname(__FILE__)) + "/lib/tasks/**/*.rake"].sort.each { |ext| load ext }
# Modifided from the RSpec on Rails plugins # Modifided from the RSpec on Rails plugins
PLUGIN_ROOT = File.expand_path(File.dirname(__FILE__)) PLUGIN_ROOT = File.expand_path(File.dirname(__FILE__))
REDMINE_APP = File.expand_path(File.dirname(__FILE__) + '/../../../app')
REDMINE_LIB = File.expand_path(File.dirname(__FILE__) + '/../../../lib')
# In rails 1.2, plugins aren't available in the path until they're loaded. # Allows loading of an environment config based on the environment
# Check to see if the rspec plugin is installed first and require REDMINE_ROOT = ENV["REDMINE_ROOT"] || File.dirname(__FILE__) + "/../../.."
# it if it is. If not, use the gem version. REDMINE_APP = File.expand_path(REDMINE_ROOT + '/app')
rspec_base = File.expand_path(File.dirname(__FILE__) + '/../rspec/lib') REDMINE_LIB = File.expand_path(REDMINE_ROOT + '/lib')
$LOAD_PATH.unshift(rspec_base) if File.exist?(rspec_base)
require 'rake' require 'rake'
require 'rake/clean' require 'rake/clean'
require 'rake/rdoctask' require 'rake/rdoctask'
require 'spec/rake/spectask' require 'spec/rake/spectask'
require 'spec/translator'
PROJECT_NAME = 'budget_plugin' PROJECT_NAME = 'budget_plugin'
ZIP_FILE = PROJECT_NAME + ".zip" ZIP_FILE = PROJECT_NAME + ".zip"
@@ -68,12 +67,14 @@ namespace :spec do
end end
end end
desc 'Generate documentation for the Budget plugin.' desc 'Generate documentation for the plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc| Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'doc' rdoc.rdoc_dir = 'doc'
rdoc.title = 'Budget' rdoc.title = PROJECT_NAME
rdoc.options << '--line-numbers' << '--inline-source' rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README.markdown') rdoc.rdoc_files.include('*.markdown')
rdoc.rdoc_files.include('*.rdoc')
rdoc.rdoc_files.include('*.txt')
rdoc.rdoc_files.include('lib/**/*.rb') rdoc.rdoc_files.include('lib/**/*.rb')
rdoc.rdoc_files.include('app/**/*.rb') rdoc.rdoc_files.include('app/**/*.rb')
end end
@@ -85,28 +86,19 @@ task :upload_doc => ['spec:rcov', :doc, 'spec:htmldoc'] do |t|
`scp -r coverage/ dev.littlestreamsoftware.com:/home/websites/projects.littlestreamsoftware.com/shared/embedded_docs/redmine-budget/coverage` `scp -r coverage/ dev.littlestreamsoftware.com:/home/websites/projects.littlestreamsoftware.com/shared/embedded_docs/redmine-budget/coverage`
end end
desc "Zip of the folder for release" desc "Create release archives"
task :zip => [:clean, :rdoc] do task :release => [:clean, :rdoc, 'release:zip', 'release:tarball']
require 'zip/zip'
require 'zip/zipfilesystem'
# check to see if the file exists already, and if it does, delete it.
if File.file?(ZIP_FILE)
File.delete(ZIP_FILE)
end
# open or create the zip file namespace :release do
Zip::ZipFile.open(ZIP_FILE, Zip::ZipFile::CREATE) do |zipfile| desc "Create a zip archive"
zipfile.mkdir(PROJECT_NAME) task :zip => [:clean] do
files = Dir['**/*.*'] sh "git archive --format=zip --prefix=#{PLUGIN_NAME}/ HEAD > #{PLUGIN_NAME}.zip"
files.each do |file|
print "Adding #{file} ...."
zipfile.add(PROJECT_NAME + '/' + file, file)
puts ". done"
end
end end
# set read permissions on the file desc "Create a tarball archive"
File.chmod(0644, ZIP_FILE) task :tarball => [:clean] do
sh "git archive --format=tar --prefix=#{PLUGIN_NAME}/ HEAD | gzip > #{PLUGIN_NAME}.tar.gz"
end
end end
+1 -1
View File
@@ -9,7 +9,7 @@ class DeliverablesController < ApplicationController
# Main deliverable list # Main deliverable list
def index def index
sort_init "#{Deliverable.table_name}.id", "desc" sort_init "#{Deliverable.table_name}.id", "desc"
sort_update sort_update 'id' => "#{Deliverable.table_name}.id"
@deliverable_count = Deliverable.count(:conditions => { :project_id => @project.id}) @deliverable_count = Deliverable.count(:conditions => { :project_id => @project.id})
@deliverable_pages = Paginator.new self, @deliverable_count, per_page_option, params['page'] @deliverable_pages = Paginator.new self, @deliverable_count, per_page_option, params['page']
+6 -21
View File
@@ -109,38 +109,23 @@ class Budget
# Dollar amount of time that has been logged to the project itself # Dollar amount of time that has been logged to the project itself
def amount_missing_on_issues def amount_missing_on_issues
time_logs = TimeEntry.find_all_by_project_id_and_issue_id(self.project, nil) time_logs = TimeEntry.find_all_by_project_id_and_issue_id(self.project.id, nil)
total = 0
return time_logs.collect(&:cost).sum
# Find each Member for their rate
time_logs.each do |time_log|
member = Member.find_by_user_id_and_project_id(time_log.user_id, time_log.project_id)
total += (member.rate * time_log.hours) unless member.nil? || member.rate.nil?
end
return total
end end
# Dollar amount of time that has been logged to issues that are not assigned to deliverables # Dollar amount of time that has been logged to issues that are not assigned to deliverables
def amount_missing_on_deliverables def amount_missing_on_deliverables
total = 0
# Bisect the issues because NOT IN isn't reliable # Bisect the issues because NOT IN isn't reliable
all_issues = self.project.issues.find(:all) all_issues = self.project.issues.find(:all)
return 0 if all_issues.empty?
deliverable_issues = self.project.issues.find(:all, :conditions => ["deliverable_id IN (?)", self.deliverables.collect(&:id)]) deliverable_issues = self.project.issues.find(:all, :conditions => ["deliverable_id IN (?)", self.deliverables.collect(&:id)])
return 0 if all_issues.empty?
missing_issues = all_issues - deliverable_issues missing_issues = all_issues - deliverable_issues
time_logs = missing_issues.collect(&:time_entries).flatten time_logs = missing_issues.collect(&:time_entries).flatten
# Find each Member for their rate return time_logs.collect(&:cost).sum
time_logs.each do |time_log|
member = Member.find_by_user_id_and_project_id(time_log.user_id, time_log.project_id)
total += (member.rate * time_log.hours) unless member.nil? || member.rate.nil?
end
return total
end end
end end
+12 -6
View File
@@ -49,7 +49,7 @@ class Deliverable < ActiveRecord::Base
def progress def progress
return 0 unless self.issues.size > 0 return 0 unless self.issues.size > 0
total ||= self.issues.collect(&:estimated_hours).delete_if {|e| e.nil? }.inject {|sum, n| sum + n} || 0 total ||= self.issues.collect(&:estimated_hours).compact.sum || 0
return 0 unless total > 0 return 0 unless total > 0
balance = 0.0 balance = 0.0
@@ -123,10 +123,21 @@ class Deliverable < ActiveRecord::Base
end end
end end
# Wrap the budget getter so it returns 0 if budget is nil
def budget
raw_budget = read_attribute(:budget)
unless raw_budget.nil?
return raw_budget
else
return 0
end
end
# Amount of the budget remaining to be spent # Amount of the budget remaining to be spent
def budget_remaining def budget_remaining
return self.budget - self.spent return self.budget - self.spent
end end
alias :left :budget_remaining
# Number of hours used. # Number of hours used.
def hours_used def hours_used
@@ -144,11 +155,6 @@ class Deliverable < ActiveRecord::Base
return self.members_spent.collect(&:spent).sum return self.members_spent.collect(&:spent).sum
end end
# Amount of the budget remaining
def left
return self.budget - self.spent
end
# Amount spent over the total budget # Amount spent over the total budget
def overruns def overruns
if self.left >= 0 if self.left >= 0
+1 -7
View File
@@ -17,13 +17,7 @@ class FixedDeliverable < Deliverable
# Get all timelogs assigned # Get all timelogs assigned
time_logs = self.issues.collect(&:time_entries).flatten time_logs = self.issues.collect(&:time_entries).flatten
# Find each Member for their rate return total + time_logs.collect(&:cost).sum
time_logs.each do |time_log|
member = Member.find_by_user_id_and_project_id(time_log.user_id, time_log.project_id)
total += (member.rate * time_log.hours) unless member.nil? || member.rate.nil?
end
return total
end end
+2 -8
View File
@@ -9,14 +9,8 @@ class HourlyDeliverable < Deliverable
# Get all timelogs assigned # Get all timelogs assigned
time_logs = self.issues.collect(&:time_entries).flatten time_logs = self.issues.collect(&:time_entries).flatten
# Find each Member for their rate return time_logs.collect(&:cost).sum
time_logs.each do |time_log|
member = Member.find_by_user_id_and_project_id(time_log.user_id, time_log.project_id)
total += (member.rate * time_log.hours) unless member.nil? || member.rate.nil?
end
return total
end end
def profit # :nodoc: def profit # :nodoc:
+3 -7
View File
@@ -24,14 +24,10 @@ class MemberSpent
project.members.each do |member| project.members.each do |member|
member_time_entries = time_entries.select { |tl| tl.user_id == member.user.id} member_time_entries = time_entries.select { |tl| tl.user_id == member.user.id}
hours = member_time_entries.collect(&:hours).sum || 0.0
unless member.rate.nil?
spent = hours.to_f * member.rate
else
spent = 0.0
end
spent = member_time_entries.collect(&:cost).sum
hours = member_time_entries.collect(&:hours).sum
membership << MemberSpent.new({ membership << MemberSpent.new({
:user => member.user, :user => member.user,
:hours => hours, :hours => hours,
-1
View File
@@ -127,7 +127,6 @@ Object.extend(BudgetModule.prototype, {
}, },
updateAmounts: function() { updateAmounts: function() {
console.log('updateAmounts() called');
if ($('deliverable_type').checked) { if ($('deliverable_type').checked) {
// Fixed cost // Fixed cost
var cost = Budget.toAmount($('deliverable_fixed_cost').value); var cost = Budget.toAmount($('deliverable_fixed_cost').value);
@@ -4,6 +4,6 @@ class AddProjectIdToDeliverables < ActiveRecord::Migration
end end
def self.down def self.down
remove_column :deliverables, :project_idx remove_column :deliverables, :project_id
end end
end end
@@ -0,0 +1,46 @@
RateMigrationErrorMessage = "ERROR: The Rate plugin is not installed. Please install the Rate plugin or downgrade to version 0.1.0 of the Budget plugin."
begin
require_dependency 'rate'
rescue LoadError
raise Exception.new(RateMigrationErrorMessage)
end
require_dependency 'user'
require_dependency 'member'
class ConvertMemberRateToFullRates < ActiveRecord::Migration
def self.up
self.check_that_rate_plugin_is_installed
# Add a new Rate object for each Member
Member.find(:all, :conditions => ['rate IS NOT NULL']).each do |member|
say_with_time "Converting rate for #{member.user.to_s} - #{member.project.to_s}" do
# Need to find the first date for any TimeEntries #1924
first_time_entry = TimeEntry.find(:first,
:conditions => ['project_id = (?) AND user_id = (?)', member.project_id, member.user_id],
:order => 'spent_on ASC')
date_in_effect = first_time_entry.spent_on if first_time_entry
date_in_effect ||= member.created_on
rate = Rate.new({
:user => member.user,
:amount => member.rate,
:project => member.project,
:date_in_effect => date_in_effect
})
rate.save!
end
end
end
def self.down
self.check_that_rate_plugin_is_installed
raise ActiveRecord::IrreversibleMigration, "Can't move rates back onto the Members"
end
def self.check_that_rate_plugin_is_installed
raise Exception.new(RateMigrationErrorMessage) unless Object.const_defined?("Rate")
end
end
@@ -0,0 +1,26 @@
RateMigrationErrorMessage = "ERROR: The Rate plugin is not installed. Please install the Rate plugin or downgrade to version 0.1.0 of the Budget plugin."
begin
require_dependency 'rate'
rescue LoadError
raise Exception.new(RateMigrationErrorMessage)
end
require_dependency 'user'
require_dependency 'member'
class RemoveRateFromMembers < ActiveRecord::Migration
def self.up
self.check_that_rate_plugin_is_installed
remove_column :members, :rate
end
def self.down
self.check_that_rate_plugin_is_installed
add_column :members, :rate, :decimal, :precision => 15, :scale => 2
end
def self.check_that_rate_plugin_is_installed
raise Exception.new(RateMigrationErrorMessage) unless Object.const_defined?("Rate")
end
end
+2 -3
View File
@@ -6,7 +6,6 @@ require_dependency 'query_patch'
# Hooks # Hooks
require_dependency 'budget_issue_hook' require_dependency 'budget_issue_hook'
require_dependency 'budget_project_hook'
RAILS_DEFAULT_LOGGER.info 'Starting Budget plugin for RedMine' RAILS_DEFAULT_LOGGER.info 'Starting Budget plugin for RedMine'
@@ -14,13 +13,13 @@ Redmine::Plugin.register :budget_plugin do
name 'Budget' name 'Budget'
author 'Eric Davis <edavis@littlestreamsoftware.com>' author 'Eric Davis <edavis@littlestreamsoftware.com>'
description 'Budget is a plugin to manage the set of deliverables for each project, automatically calculating key performance indicators.' description 'Budget is a plugin to manage the set of deliverables for each project, automatically calculating key performance indicators.'
version '0.1.0' version '0.2.0'
settings :default => { settings :default => {
'budget_nonbillable_overhead' => '', 'budget_nonbillable_overhead' => '',
'budget_materials' => '', 'budget_materials' => '',
'budget_profit' => '' 'budget_profit' => ''
}, :partial => 'settings/settings' }, :partial => 'settings/budget_settings'
project_module :budget_module do project_module :budget_module do
-51
View File
@@ -1,51 +0,0 @@
# Hooks to attach to the Redmine Projects.
class BudgetProjectHook < Redmine::Hook::ViewListener
def protect_against_forgery?
false
end
# Renders an additional table header to the membership setting
#
# Context:
# * :project => Current project
#
def view_projects_settings_members_table_header(context ={ })
if context[:project].module_enabled?('budget_module')
return "<th>#{GLoc.l(:label_member_rate) }</th>"
else
return ''
end
end
# Renders an AJAX from to update the member's billing rate
#
# Context:
# * :project => Current project
# * :member => Current Member record
#
def view_projects_settings_members_table_row(context = { })
if context[:project].module_enabled?('budget_module')
# Build a form_remote_tag by hand since this isn't in the scope of a controller
form = form_tag({:controller => 'members', :action => 'edit', :id => context[:member].id, :protocol => Setting.protocol, :host => Setting.host_name},
:onsubmit => remote_function(:url => {
:controller => 'members',
:action => 'edit',
:id => context[:member].id,
:protocol => Setting.protocol,
:host => Setting.host_name
},
:host => Setting.host_name,
:protocol => Setting.protocol,
:form => true,
:method => 'post',
:return => 'false' )+ '; return false;') +
text_field_tag('member[rate]', number_with_precision(context[:member].rate, 0), :class => "small") +
submit_tag(GLoc.l(:button_change), :class => "small") + "</form>"
return content_tag(:td, form, :align => 'center' )
else
return ''
end
end
end
-22
View File
@@ -18,28 +18,6 @@ module IssuePatch
end end
module ClassMethods module ClassMethods
# Muck with the find arguements to append an include for deliverables
def find(*args)
# Options defined
if args[1].is_a?(Hash)
# Abort the special case of issue.search. LIKE is used by search
if args[1].has_key?(:conditions) && args[1][:conditions].is_a?(Array) && args[1][:conditions][0].match(/LIKE \?/)
# skip
elsif args[1].has_key?(:include) && !args[1][:include].nil? # include used?
# Add our include
if args[1][:include].is_a?(Array)
args[1][:include] << :deliverable
else
args[1][:include] = [args[1][:include], :deliverable] # Rewrite a the include as an array
end
else
# Add an include
args[1][:include] = [:deliverable]
end
end
super
end
end end
+68
View File
@@ -325,6 +325,19 @@ describe Budget, '.left' do
end end
end end
describe Budget, '.labor_budget_left' do
it 'should be calculated by the labor budget and total spent of the deliverables' do
@project = mock_model(Project)
Project.stub!(:find).with(@project.id).and_return(@project)
@budget = Budget.new(@project.id)
@budget.should_receive(:labor_budget).and_return(6000.0)
@budget.should_receive(:spent).and_return(4500.0)
@budget.labor_budget_left.should eql(1500.0)
end
end
describe Budget, '.overruns' do describe Budget, '.overruns' do
it 'should be 0 if there is still unspent budget' do it 'should be 0 if there is still unspent budget' do
@project = mock_model(Project) @project = mock_model(Project)
@@ -424,3 +437,58 @@ describe Budget, '.profit' do
@budget.profit.should eql(0.0) @budget.profit.should eql(0.0)
end end
end end
describe Budget, 'amount_missing_on_issues' do
it 'should caclulate the cost of the time logged to the project itself' do
project = mock_model(Project)
Project.stub!(:find).with(project.id).and_return(project)
budget = Budget.new(project.id)
time_entry_one = mock_model(TimeEntry, :cost => 300.0)
time_entry_two = mock_model(TimeEntry, :cost => 500.0)
TimeEntry.should_receive(:find_all_by_project_id_and_issue_id).with(project.id, nil).and_return([time_entry_one, time_entry_two])
budget.amount_missing_on_issues.should eql(time_entry_one.cost + time_entry_two.cost)
end
end
describe Budget, 'amount_missing_on_deliverables' do
before(:each) do
@project = mock_model(Project)
@project.stub!(:issues).and_return(Issue)
Project.stub!(:find).with(@project.id).and_return(@project)
@deliverable = mock_model(Deliverable, :project => @project)
@budget = Budget.new(@project.id)
@budget.stub!(:deliverables).and_return([@deliverable])
end
it 'should caclulate the cost of the time logged to the issues that are not on a Deliverable' do
time_entry_one = mock_model(TimeEntry, :cost => 300.0)
time_entry_two = mock_model(TimeEntry, :cost => 500.0)
issue_one = mock_model(Issue, :project => @project, :time_entries => [time_entry_one])
issue_two = mock_model(Issue, :project => @project, :time_entries => [time_entry_two])
issues = [issue_one, issue_two]
Issue.should_receive(:find).with(:all).and_return(issues)
Issue.should_receive(:find).with(:all, { :conditions => ["deliverable_id IN (?)", [@deliverable.id]]}).and_return([])
@budget.amount_missing_on_deliverables.should eql(time_entry_one.cost + time_entry_two.cost)
end
it 'should return 0 if there are no issues on the project' do
Issue.should_receive(:find).with(:all).and_return([])
@budget.amount_missing_on_deliverables.should eql(0)
end
it 'should return 0 if all issues are on a Deliverable' do
issue_one = mock_model(Issue, :project => @project)
issue_two = mock_model(Issue, :project => @project)
issues = [issue_one, issue_two]
Issue.should_receive(:find).with(:all).and_return(issues)
Issue.should_receive(:find).with(:all, { :conditions => ["deliverable_id IN (?)", [@deliverable.id]]}).and_return(issues)
@budget.amount_missing_on_deliverables.should eql(0)
end
end
+87
View File
@@ -120,6 +120,15 @@ describe Deliverable, '.budget_ratio' do
end end
end end
describe Deliverable, '.budget' do
it 'should return 0 if the budget is nil' do
@deliverable = Deliverable.new({ :subject => 'test' })
@deliverable.budget.should eql(0)
@deliverable.read_attribute(:budget).should eql(nil)
end
end
describe Deliverable, '.score' do describe Deliverable, '.score' do
it 'should be calculated by the progress and the budget usage' do it 'should be calculated by the progress and the budget usage' do
@deliverable = Deliverable.new({ :subject => 'test' }) @deliverable = Deliverable.new({ :subject => 'test' })
@@ -249,3 +258,81 @@ describe Deliverable, '.spent' do
@deliverable.spent.should eql(0) @deliverable.spent.should eql(0)
end end
end end
describe Deliverable, 'fixed?' do
it 'should be true for FixedDeliverables' do
FixedDeliverable.new.fixed?.should be_true
end
it 'should be false for HourlyDeliverables' do
HourlyDeliverable.new.fixed?.should be_false
end
it 'should be false for generic Deliverables' do
Deliverable.new.fixed?.should be_false
end
end
describe Deliverable, 'hourly?' do
it 'should be false for FixedDeliverables' do
FixedDeliverable.new.hourly?.should be_false
end
it 'should be true for HourlyDeliverables' do
HourlyDeliverable.new.hourly?.should be_true
end
it 'should be false for generic Deliverables' do
Deliverable.new.hourly?.should be_false
end
end
describe Deliverable, 'labor_budget' do
it 'should be 0 because the specific Deliverables will have their own logic' do
Deliverable.new.labor_budget.should eql(0)
end
end
describe Deliverable, 'budget_remaining' do
it 'should calculated by the budget minus the amount spent' do
deliverable = Deliverable.new({ :budget => 3000.00})
deliverable.should_receive(:spent).and_return(1000.0)
deliverable.budget_remaining.should eql(2000.0)
end
it 'should be the same as Budget#left' do
deliverable = Deliverable.new({ :budget => 3000.00})
deliverable.should_receive(:spent).twice.and_return(1000.0)
deliverable.budget_remaining.should eql(deliverable.left)
end
end
describe Deliverable, 'hours_used' do
it 'should return 0 if there are no issues on the Deliverable' do
deliverable = Deliverable.new
deliverable.hours_used.should eql(0)
end
it 'should total up the hours on the issues assigned to the Deliverable' do
deliverable = Deliverable.new
time_entries = [mock_model(TimeEntry, :hours => 100)]
issues = [mock_model(Issue, :time_entries => time_entries)]
deliverable.should_receive(:issues).at_least(:once).and_return(issues)
deliverable.hours_used.should eql(100)
end
end
describe Deliverable, 'overruns' do
it 'should be 0 if there still is budget left' do
deliverable = Deliverable.new
deliverable.should_receive(:left).and_return(100)
deliverable.overruns.should eql(0)
end
it 'should be the invoice of left if left is below 0' do
deliverable = Deliverable.new
deliverable.should_receive(:left).at_least(:once).and_return(-100)
deliverable.overruns.should eql(100)
end
end
+9 -4
View File
@@ -25,12 +25,10 @@ describe FixedDeliverable, '.spent' do
@user = mock_model(User) @user = mock_model(User)
@issue1 = mock_model(Issue) @issue1 = mock_model(Issue)
@issue_1_time_entry = mock_model(TimeEntry, :issue_id => @issue1.id, :user_id => @user.id, :project_id => @project.id, :hours => 1.0) @issue_1_time_entry = mock_model(TimeEntry, :issue_id => @issue1.id, :user => @user, :project => @project, :hours => 1.0, :spent_on => Date.today)
@issue_1_time_entry.should_receive(:cost).and_return(60.0)
@issue1.stub!(:time_entries).and_return([@issue_1_time_entry]) @issue1.stub!(:time_entries).and_return([@issue_1_time_entry])
@member = mock_model(Member, :user => @user, :project => @project, :rate => 60.0)
Member.should_receive(:find_by_user_id_and_project_id).with(@user.id, @project.id).and_return(@member)
@deliverable = FixedDeliverable.new({ :subject => 'test' }) @deliverable = FixedDeliverable.new({ :subject => 'test' })
@issues = [@issue1] @issues = [@issue1]
@deliverable.stub!(:fixed_cost).and_return(5000.0) @deliverable.stub!(:fixed_cost).and_return(5000.0)
@@ -51,3 +49,10 @@ describe FixedDeliverable, '.profit as a %' do
@deliverable.profit.should eql(1000.0) @deliverable.profit.should eql(1000.0)
end end
end end
describe FixedDeliverable, '.profit as an dollar amount' do
it 'should return the amount' do
deliverable = FixedDeliverable.new({ :subject => 'test', :profit => "$100.00", :fixed_cost => 1000.0, :overhead => "1000.00", :overhead_percent => nil })
deliverable.profit.should eql(100.0)
end
end
+9 -4
View File
@@ -13,12 +13,10 @@ describe HourlyDeliverable, '.spent' do
@user = mock_model(User) @user = mock_model(User)
@issue1 = mock_model(Issue) @issue1 = mock_model(Issue)
@issue_1_time_entry = mock_model(TimeEntry, :issue_id => @issue1.id, :user_id => @user.id, :project_id => @project.id, :hours => 1.0) @issue_1_time_entry = mock_model(TimeEntry, :issue_id => @issue1.id, :user => @user, :project => @project, :hours => 1.0, :spent_on => Date.today)
@issue_1_time_entry.should_receive(:cost).and_return(60.0)
@issue1.stub!(:time_entries).and_return([@issue_1_time_entry]) @issue1.stub!(:time_entries).and_return([@issue_1_time_entry])
@member = mock_model(Member, :user => @user, :project => @project, :rate => 60.0)
Member.should_receive(:find_by_user_id_and_project_id).with(@user.id, @project.id).and_return(@member)
@deliverable = HourlyDeliverable.new({ :subject => 'test' }) @deliverable = HourlyDeliverable.new({ :subject => 'test' })
@issues = [@issue1] @issues = [@issue1]
@deliverable.should_receive(:issues).twice.and_return(@issues) @deliverable.should_receive(:issues).twice.and_return(@issues)
@@ -38,3 +36,10 @@ describe HourlyDeliverable, '.profit as a %' do
@deliverable.profit.should eql(1000.0) @deliverable.profit.should eql(1000.0)
end end
end end
describe HourlyDeliverable, '.profit as an dollar amount' do
it 'should return the amount' do
deliverable = HourlyDeliverable.new({ :subject => 'test', :profit => "$100.00", :cost_per_hour => 100.0, :total_hours => 10, :overhead => "1000.00", :overhead_percent => nil })
deliverable.profit.should eql(100.0)
end
end
+14 -1
View File
@@ -1,7 +1,11 @@
# This file is copied to ~/spec when you run 'ruby script/generate rspec' # This file is copied to ~/spec when you run 'ruby script/generate rspec'
# from the project root directory. # from the project root directory.
ENV["RAILS_ENV"] = "test" ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment")
# Allows loading of an environment config based on the environment
redmine_root = ENV["REDMINE_ROOT"] || File.dirname(__FILE__) + "/../../../.."
require File.expand_path(redmine_root + "/config/environment")
require 'spec' require 'spec'
require 'spec/rails' require 'spec/rails'
@@ -37,3 +41,12 @@ Spec::Runner.configure do |config|
# config.mock_with :flexmock # config.mock_with :flexmock
# config.mock_with :rr # config.mock_with :rr
end end
# require the entire app if we're running under coverage testing,
# so we measure 0% covered files in the report
#
# http://www.pervasivecode.com/blog/2008/05/16/making-rcov-measure-your-whole-rails-app-even-if-tests-miss-entire-source-files/
if defined?(Rcov)
all_app_files = Dir.glob('{app,lib}/**/*.rb')
all_app_files.each{|rb| require rb}
end