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

View File

@@ -31,7 +31,7 @@ Budget is a plugin to manage the set of deliverables for each project, automatic
## 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

View File

@@ -1,24 +1,23 @@
#!/usr/bin/env ruby
require "fileutils"
require 'rubygems'
gem 'rspec'
gem 'rspec-rails'
Dir[File.expand_path(File.dirname(__FILE__)) + "/lib/tasks/**/*.rake"].sort.each { |ext| load ext }
# Modifided from the RSpec on Rails plugins
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.
# Check to see if the rspec plugin is installed first and require
# it if it is. If not, use the gem version.
rspec_base = File.expand_path(File.dirname(__FILE__) + '/../rspec/lib')
$LOAD_PATH.unshift(rspec_base) if File.exist?(rspec_base)
# Allows loading of an environment config based on the environment
REDMINE_ROOT = ENV["REDMINE_ROOT"] || File.dirname(__FILE__) + "/../../.."
REDMINE_APP = File.expand_path(REDMINE_ROOT + '/app')
REDMINE_LIB = File.expand_path(REDMINE_ROOT + '/lib')
require 'rake'
require 'rake/clean'
require 'rake/rdoctask'
require 'spec/rake/spectask'
require 'spec/translator'
PROJECT_NAME = 'budget_plugin'
ZIP_FILE = PROJECT_NAME + ".zip"
@@ -68,12 +67,14 @@ namespace :spec do
end
end
desc 'Generate documentation for the Budget plugin.'
desc 'Generate documentation for the plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'doc'
rdoc.title = 'Budget'
rdoc.title = PROJECT_NAME
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('app/**/*.rb')
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`
end
desc "Zip of the folder for release"
task :zip => [:clean, :rdoc] do
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
desc "Create release archives"
task :release => [:clean, :rdoc, 'release:zip', 'release:tarball']
# open or create the zip file
Zip::ZipFile.open(ZIP_FILE, Zip::ZipFile::CREATE) do |zipfile|
zipfile.mkdir(PROJECT_NAME)
files = Dir['**/*.*']
files.each do |file|
print "Adding #{file} ...."
zipfile.add(PROJECT_NAME + '/' + file, file)
puts ". done"
end
namespace :release do
desc "Create a zip archive"
task :zip => [:clean] do
sh "git archive --format=zip --prefix=#{PLUGIN_NAME}/ HEAD > #{PLUGIN_NAME}.zip"
end
# set read permissions on the file
File.chmod(0644, ZIP_FILE)
desc "Create a tarball archive"
task :tarball => [:clean] do
sh "git archive --format=tar --prefix=#{PLUGIN_NAME}/ HEAD | gzip > #{PLUGIN_NAME}.tar.gz"
end
end

View File

@@ -9,7 +9,7 @@ class DeliverablesController < ApplicationController
# Main deliverable list
def index
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_pages = Paginator.new self, @deliverable_count, per_page_option, params['page']

View File

@@ -109,38 +109,23 @@ class Budget
# Dollar amount of time that has been logged to the project itself
def amount_missing_on_issues
time_logs = TimeEntry.find_all_by_project_id_and_issue_id(self.project, nil)
total = 0
# 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
time_logs = TimeEntry.find_all_by_project_id_and_issue_id(self.project.id, nil)
return time_logs.collect(&:cost).sum
end
# Dollar amount of time that has been logged to issues that are not assigned to deliverables
def amount_missing_on_deliverables
total = 0
# Bisect the issues because NOT IN isn't reliable
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)])
return 0 if all_issues.empty?
missing_issues = all_issues - deliverable_issues
time_logs = missing_issues.collect(&:time_entries).flatten
# 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
return time_logs.collect(&:cost).sum
end
end

View File

@@ -49,7 +49,7 @@ class Deliverable < ActiveRecord::Base
def progress
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
balance = 0.0
@@ -123,10 +123,21 @@ class Deliverable < ActiveRecord::Base
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
def budget_remaining
return self.budget - self.spent
end
alias :left :budget_remaining
# Number of hours used.
def hours_used
@@ -144,11 +155,6 @@ class Deliverable < ActiveRecord::Base
return self.members_spent.collect(&:spent).sum
end
# Amount of the budget remaining
def left
return self.budget - self.spent
end
# Amount spent over the total budget
def overruns
if self.left >= 0

View File

@@ -17,13 +17,7 @@ class FixedDeliverable < Deliverable
# Get all timelogs assigned
time_logs = self.issues.collect(&:time_entries).flatten
# 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
return total + time_logs.collect(&:cost).sum
end

View File

@@ -9,14 +9,8 @@ class HourlyDeliverable < Deliverable
# Get all timelogs assigned
time_logs = self.issues.collect(&:time_entries).flatten
# 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
return time_logs.collect(&:cost).sum
end
def profit # :nodoc:

View File

@@ -24,14 +24,10 @@ class MemberSpent
project.members.each do |member|
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({
:user => member.user,
:hours => hours,

View File

@@ -127,7 +127,6 @@ Object.extend(BudgetModule.prototype, {
},
updateAmounts: function() {
console.log('updateAmounts() called');
if ($('deliverable_type').checked) {
// Fixed cost
var cost = Budget.toAmount($('deliverable_fixed_cost').value);

View File

@@ -4,6 +4,6 @@ class AddProjectIdToDeliverables < ActiveRecord::Migration
end
def self.down
remove_column :deliverables, :project_idx
remove_column :deliverables, :project_id
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -6,7 +6,6 @@ require_dependency 'query_patch'
# Hooks
require_dependency 'budget_issue_hook'
require_dependency 'budget_project_hook'
RAILS_DEFAULT_LOGGER.info 'Starting Budget plugin for RedMine'
@@ -14,13 +13,13 @@ Redmine::Plugin.register :budget_plugin do
name 'Budget'
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.'
version '0.1.0'
version '0.2.0'
settings :default => {
'budget_nonbillable_overhead' => '',
'budget_materials' => '',
'budget_profit' => ''
}, :partial => 'settings/settings'
}, :partial => 'settings/budget_settings'
project_module :budget_module do

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

View File

@@ -18,28 +18,6 @@ module IssuePatch
end
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

View File

@@ -325,6 +325,19 @@ describe Budget, '.left' do
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
it 'should be 0 if there is still unspent budget' do
@project = mock_model(Project)
@@ -424,3 +437,58 @@ describe Budget, '.profit' do
@budget.profit.should eql(0.0)
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

View File

@@ -120,6 +120,15 @@ describe Deliverable, '.budget_ratio' do
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
it 'should be calculated by the progress and the budget usage' do
@deliverable = Deliverable.new({ :subject => 'test' })
@@ -249,3 +258,81 @@ describe Deliverable, '.spent' do
@deliverable.spent.should eql(0)
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

View File

@@ -25,12 +25,10 @@ describe FixedDeliverable, '.spent' do
@user = mock_model(User)
@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])
@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' })
@issues = [@issue1]
@deliverable.stub!(:fixed_cost).and_return(5000.0)
@@ -51,3 +49,10 @@ describe FixedDeliverable, '.profit as a %' do
@deliverable.profit.should eql(1000.0)
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

View File

@@ -13,12 +13,10 @@ describe HourlyDeliverable, '.spent' do
@user = mock_model(User)
@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])
@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' })
@issues = [@issue1]
@deliverable.should_receive(:issues).twice.and_return(@issues)
@@ -38,3 +36,10 @@ describe HourlyDeliverable, '.profit as a %' do
@deliverable.profit.should eql(1000.0)
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

View File

@@ -1,7 +1,11 @@
# This file is copied to ~/spec when you run 'ruby script/generate rspec'
# from the project root directory.
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/rails'
@@ -37,3 +41,12 @@ Spec::Runner.configure do |config|
# config.mock_with :flexmock
# config.mock_with :rr
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