Compare commits
37 Commits
0.1.0
...
rate_plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5076b1c88b | ||
|
|
fe11c40381 | ||
|
|
ad67e1634f | ||
|
|
049885c170 | ||
|
|
42325fadf4 | ||
|
|
559b52112d | ||
|
|
11b8a0a462 | ||
|
|
1706412bb8 | ||
|
|
eeec71f857 | ||
|
|
5ae939d82a | ||
|
|
aed49c68da | ||
|
|
075310da6b | ||
|
|
3d8c1ecc5c | ||
|
|
7fa4f33748 | ||
|
|
045f50e0dd | ||
|
|
4f18c28bba | ||
|
|
50a466d7af | ||
|
|
5510753a4b | ||
|
|
c6b72683b4 | ||
|
|
8a08e654e0 | ||
|
|
0f7c630f9c | ||
|
|
88026a1049 | ||
|
|
fb24bbf546 | ||
|
|
18a2f1aefa | ||
|
|
47ed9275df | ||
|
|
8e31e8fb8a | ||
|
|
7ef0b094de | ||
|
|
b70728efca | ||
|
|
01d8159e0e | ||
|
|
f7c2816e13 | ||
|
|
6f42cda5ca | ||
|
|
4897e6c228 | ||
|
|
e61b15b9d5 | ||
|
|
83b9458443 | ||
|
|
36d9ab3ed8 | ||
|
|
52e80b4f56 | ||
|
|
71060831fc |
@@ -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
|
||||
|
||||
|
||||
58
Rakefile
58
Rakefile
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4,6 +4,6 @@ class AddProjectIdToDeliverables < ActiveRecord::Migration
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :deliverables, :project_idx
|
||||
remove_column :deliverables, :project_id
|
||||
end
|
||||
end
|
||||
|
||||
46
db/migrate/009_convert_member_rate_to_full_rates.rb
Normal file
46
db/migrate/009_convert_member_rate_to_full_rates.rb
Normal 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
|
||||
26
db/migrate/010_remove_rate_from_members.rb
Normal file
26
db/migrate/010_remove_rate_from_members.rb
Normal 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
|
||||
5
init.rb
5
init.rb
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user