60 Commits
0.1.0 ... 0.2.0

Author SHA1 Message Date
Eric Davis
8413b26a15 [#2431] Use RedminePluginSupport for rake tasks. 2009-04-27 15:40:35 -07:00
Eric Davis
f7733011df [#2431] Reformatted the CREDITS file 2009-04-27 15:35:46 -07:00
Eric Davis
a45253329d [#2431] Updated the Readme and install instructions. 2009-04-27 15:34:49 -07:00
Eric Davis
085dc1deb4 Added an explicit requirement on the Rate plugin. 2009-04-27 15:31:29 -07:00
Eric Davis
425047f305 [#2431] Updated the Readme to rdoc 2009-04-27 15:27:36 -07:00
Eric Davis
ffb20347ce Renamed README 2009-04-27 15:21:45 -07:00
Eric Davis
ee2e2dae8e [#2431] Requiring Redmine 0.8.x now 2009-04-27 15:21:12 -07:00
Eric Davis
3b26d08f0d [#2405] Make the Total Budget in the header bold 2009-04-27 15:13:11 -07:00
Eric Davis
833c2877af [#2403] [#2404] Added a Total Budget column to the Deliverable row. 2009-04-27 15:07:30 -07:00
Eric Davis
d88fce97f8 [#1498] Added the Deliverable description to the expanded row. 2009-04-27 14:22:20 -07:00
Eric Davis
931c1ef6ed [#1999] Added a link to expand/collapse all Deliverable rows to the sidebar 2009-04-27 14:13:51 -07:00
Eric Davis
2018561e6a [#1919] Moved the JavaScript for calculating the Budget to an assets file. 2009-04-27 13:57:52 -07:00
Eric Davis
83d5af4a4a [#2157] Added Lithuanian translation by Sergej Jegorov. 2009-04-27 13:44:02 -07:00
Eric Davis
eb60581d1a [#1840] Added Spanish and Catalan translations from Pau Garcia i Quiles. 2009-04-27 13:33:32 -07:00
Eric Davis
e218218ffb [#1840] Completed the i18n string support.
Thanks to Pau Garcia i Quiles for the patch
2009-04-27 13:31:47 -07:00
Eric Davis
7ed5c20a93 [#1959] Fixed a typo in the css class name. 2009-04-27 13:24:44 -07:00
Eric Davis
72f2f77a60 [#2040] Added Hungarian translation by Gergő Jónás. 2009-04-27 13:20:11 -07:00
Eric Davis
fd891e7f51 Changed the Redmine core patches to use the Rails dispatcher.
This will let the core patches work in development mode when the classes
are reloaded.
2009-04-10 08:49:12 -07:00
Eric Davis
71ab0c6b54 [#2301] Check that the session has a sort before trying to access it. 2009-04-01 08:43:37 -07:00
Eric Davis
328cd3fc3f Removed GLoc calls, should stay 0.8 compatibile 2009-02-26 17:09:52 -08:00
Eric Davis
4205fb95a3 Updated to the Rails 2.2.x i18n format 2009-02-26 17:09:12 -08:00
Eric Davis
3f00f12107 Added a hook into Project#copy so Deliverables are copied to the new project. 2009-02-25 10:26:43 -08:00
Eric Davis
3720c486b4 Added a Deliverable#to_s so the Deliverable subject can be printed. 2009-02-18 14:44:06 -08:00
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
45 changed files with 905 additions and 470 deletions

View File

@@ -1,10 +1,6 @@
Thanks go to the following people for patches and contributions:
Eric Davis of Little Stream Software
- Project Maintainer
* Eric Davis of Little Stream Software - Project Maintainer
* Peter Chester of Shane and Peter, Inc - Project sponsorship
* Shane Pearlman of Shane and Peter, Inc - Project sponsorship
Peter Chester of Shane and Peter, Inc
- Project sponsership
Shane Pearlman of Shane and Peter, Inc
- Project sponsership

View File

@@ -1,8 +1,8 @@
## Budget Plugin
== Budget Plugin
Budget is a plugin to manage the set of deliverables for each project, automatically calculating key performance indicators.
## Features
== Features
* Add new deliverable to a project - Fixed bid or Time Based
* New issues appended using JavaScript to the top of the deliverables list
@@ -29,44 +29,46 @@ Budget is a plugin to manage the set of deliverables for each project, automatic
* Ability to add the Deliverable column to the Main Issues list, including sorting and filtering by Deliverable name
* Adding the billable rate to project members on a per project basis
## 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/redmine-budget/files] at Little Stream Software and also on {GitHub}[http://github.com/edavis10/redmine-budget-plugin/tree/master].
## Install
== Install
0. Before installing, make sure you are running Redmine after r1796.
1. Follow the Redmine plugin installation steps at: http://www.redmine.org/wiki/redmine/Plugins
2. Login to your Redmine install as an Administrator
3. Enable the permissions for your Roles
4. Setup your companies defaults in the Plugins' configuration panel
5. Add the "Budget module" to the enabled modules for the projects you want to manage
6. The link to the plugin should appear on that project's navigation
1. Follow the Redmine plugin installation steps at: http://www.redmine.org/wiki/redmine/Plugins Make sure the plugin is installed to Make sure you install the plugin to +vendor/plugins/budget_plugin+.
2. The Rate plugin is required also, install it from {Little Stream Software}[https://projects.littlestreamsoftware.com/projects/redmine-rate/files]
3. Run the plugin migrations +rake db:migrate_plugins+
4. Restart your Redmine web servers (e.g. mongrel, thin, mod_rails)
5. Login to your Redmine install as an Administrator
6. Enable the permissions for your Roles
7. Setup your companies defaults in the Plugins' configuration panel
8. Add the "Budget module" to the enabled modules for the projects you want to manage
9. The link to the plugin should appear on that project's navigation
## Usage
== Usage
### Adding deliverables
=== Adding deliverables
1. Use the "New Deliverable" link on sidebar of the Budget page to add a new deliverable
2. The deliverable can be a Fixed Bid or an Hourly deliverable. The type can be changed later.
3. Saving the deliverable will add it to the Deliverable list.
### Viewing deliverables
=== Viewing deliverables
1. Clicking the pencil icon will drop down the deliverable details, with summary and calculations about the deliverable.
2. Links on the left can be used to edit or delete a deliverable and well as view the issue list for the deliverable.
### Assignment of an issue to a deliverable
=== Assignment of an issue to a deliverable
Edit an issue and select the deliverable from the dropdown
### Bulk assignment of issues to a deliverable
=== Bulk assignment of issues to a deliverable
1. Select multiple issues on the issue list
2. Right click and select the edit option
3. Select the deliverable from the dropdown and save
### Bulk Assignment of issues by Version
=== Bulk Assignment of issues by Version
Issues can be bulk assigned to a deliverable based on a Version.
@@ -75,16 +77,12 @@ Issues can be bulk assigned to a deliverable based on a Version.
3. Click the Bulk Assign button
4. All issues for that version will now be assigned to that Deliverable
## License
== License
This plugin is licensed under the GNU GPL v2. See LICENSE.txt and GPL.txt for details.
## Project help
== Project help
If you need help you can contact the maintainer at his email address (See CREDITS.txt) or create an issue in the Bug Tracker.
### Bug tracker
If you would like to report a bug or request a new feature the bug tracker is located at [https://projects.littlestreamsoftware.com/projects/show/redmine-budget](https://projects.littlestreamsoftware.com/projects/show/redmine-budget)
If you need help you can contact the maintainer on the bug tracker at https://projects.littlestreamsoftware.com/projects/redmine-budget

111
Rakefile
View File

@@ -1,112 +1,9 @@
#!/usr/bin/env ruby
require "fileutils"
require 'redmine_plugin_support'
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)
require 'rake'
require 'rake/clean'
require 'rake/rdoctask'
require 'spec/rake/spectask'
require 'spec/translator'
PROJECT_NAME = 'budget_plugin'
ZIP_FILE = PROJECT_NAME + ".zip"
CLEAN.include('**/semantic.cache', ZIP_FILE)
# No Database needed
spec_prereq = :noop
task :noop do
end
task :default => :spec
task :stats => "spec:statsetup"
desc "Run all specs in spec directory (excluding plugin specs)"
Spec::Rake::SpecTask.new(:spec => spec_prereq) do |t|
t.spec_opts = ['--options', "\"#{PLUGIN_ROOT}/spec/spec.opts\""]
t.spec_files = FileList['spec/**/*_spec.rb']
end
namespace :spec do
desc "Run all specs in spec directory with RCov (excluding plugin specs)"
Spec::Rake::SpecTask.new(:rcov) do |t|
t.spec_opts = ['--options', "\"#{PLUGIN_ROOT}/spec/spec.opts\""]
t.spec_files = FileList['spec/**/*_spec.rb']
t.rcov = true
t.rcov_opts << ["--rails", "--sort=coverage", "--exclude '/var/lib/gems,spec,#{REDMINE_APP},#{REDMINE_LIB}'"]
end
desc "Print Specdoc for all specs (excluding plugin specs)"
Spec::Rake::SpecTask.new(:doc) do |t|
t.spec_opts = ["--format", "specdoc", "--dry-run"]
t.spec_files = FileList['spec/**/*_spec.rb']
end
desc "Print Specdoc for all specs as HTML (excluding plugin specs)"
Spec::Rake::SpecTask.new(:htmldoc) do |t|
t.spec_opts = ["--format", "html:doc/rspec_report.html", "--loadby", "mtime"]
t.spec_files = FileList['spec/**/*_spec.rb']
end
[:models, :controllers, :views, :helpers, :lib].each do |sub|
desc "Run the specs under spec/#{sub}"
Spec::Rake::SpecTask.new(sub => spec_prereq) do |t|
t.spec_opts = ['--options', "\"#{PLUGIN_ROOT}/spec/spec.opts\""]
t.spec_files = FileList["spec/#{sub}/**/*_spec.rb"]
end
end
end
desc 'Generate documentation for the Budget plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'doc'
rdoc.title = 'Budget'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README.markdown')
rdoc.rdoc_files.include('lib/**/*.rb')
rdoc.rdoc_files.include('app/**/*.rb')
end
desc 'Uploads project documentation'
task :upload_doc => ['spec:rcov', :doc, 'spec:htmldoc'] do |t|
# TODO: Get rdoc working without frames
`scp -r doc/ dev.littlestreamsoftware.com:/home/websites/projects.littlestreamsoftware.com/shared/embedded_docs/redmine-budget/doc`
`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
# 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
end
# set read permissions on the file
File.chmod(0644, ZIP_FILE)
RedminePluginSupport::Base.setup do |plugin|
plugin.project_name = 'budget_plugin'
plugin.default_task = [:spec]
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']
@@ -138,7 +138,7 @@ class DeliverablesController < ApplicationController
# Sorting orders
def sort_order
if %w(score spent progress labor_budget).include?(session[@sort_name][:key])
if session[@sort_name] && %w(score spent progress labor_budget).include?(session[@sort_name][:key])
return { }
else
return { :order => sort_clause }
@@ -147,7 +147,7 @@ class DeliverablesController < ApplicationController
# Sort +deliverables+ manually using the virtual fields
def sort_if_needed(deliverables)
if %w(score spent progress labor_budget).include?(session[@sort_name][:key])
if session[@sort_name] && %w(score spent progress labor_budget).include?(session[@sort_name][:key])
case session[@sort_name][:key]
when "score":
sorted = deliverables.sort {|a,b| a.score <=> b.score}

View File

@@ -73,8 +73,8 @@ module DeliverablesHelper
end
def toggle_arrows(deliverable_id)
open_js = "$('deliverable-details-#{deliverable_id}').show(); $$('.toggle_#{deliverable_id}').each(function(e) {e.toggle();})"
close_js = "$('deliverable-details-#{deliverable_id}').hide(); $$('.toggle_#{deliverable_id}').each(function(e) {e.toggle();})"
open_js = "expandRow(#{deliverable_id})"
close_js = "collapseRow(#{deliverable_id})"
return toggle_arrow(deliverable_id, "toggle-arrow-closed.gif", open_js, false) +
toggle_arrow(deliverable_id, "toggle-arrow-open.gif", close_js, true)
@@ -86,7 +86,7 @@ module DeliverablesHelper
content_tag(:span,
link_to_function(image_tag(image, :plugin => "budget_plugin"), js),
:class => "toggle_" + deliverable_id.to_s,
:class => "toggle toggle_" + deliverable_id.to_s,
:style => style
)

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
@@ -189,6 +195,11 @@ class Deliverable < ActiveRecord::Base
def hourly?
return self.class == HourlyDeliverable
end
def to_s
self.subject
end
private
def use_issue_status_for_done_ratios?

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

@@ -1,10 +1,10 @@
<h2 class="title">Budget</h2>
<h2 class="title"><%= l(:budget_title) %></h2>
<table>
<% if allowed_management? %>
<tr>
<tr class="total">
<td>
Total Budget:
<%= l(:field_budget) %>
</td>
<td class="calculation-column">
<%= h number_to_currency(budget.budget, :precision => 0) %>
@@ -15,7 +15,7 @@
<% if allowed_management? %>
<tr>
<td>
Labor Budget:
<%= l(:label_labor_budget) %>
</td>
<td class="calculation-column">
<%= h number_to_currency(budget.labor_budget, :precision => 0) %>
@@ -26,7 +26,7 @@
<% if allowed_management? %>
<tr>
<td>
Labor Budget Spent:
<%= l(:label_labor_budget_spent) %>
</td>
<td class="calculation-column">
<%= h number_to_currency(budget.spent, :precision => 0) %>
@@ -37,7 +37,7 @@
<% if allowed_management? %>
<tr>
<td>
Labor Budget Remaining:
<%= l(:label_labor_budget_remaining) %>
</td>
<td class="calculation-column">
<%= h number_to_currency(budget.labor_budget_left, :precision => 0) %>
@@ -47,7 +47,7 @@
<tr>
<td>
Progress:
<%= l(:label_progress) %>
</td>
<td class="calculation-column">
<%= h number_to_percentage(budget.progress, :precision => 0) %>
@@ -57,7 +57,7 @@
<% if allowed_management? %>
<tr>
<td>
Budget Score:
<%= l(:label_budget_score) %>
</td>
<td class="calculation-column">
<%= h budget.score %>
@@ -68,7 +68,7 @@
<% if allowed_management? %>
<tr>
<td>
Overruns:
<%= l(:label_overruns) %>
</td>
<td class="calculation-column">
<%= h number_to_currency(budget.overruns, :precision => 0) %>
@@ -79,7 +79,7 @@
<% if allowed_management? && budget.amount_missing_on_deliverables > 0%>
<tr>
<td>
Missing on <%= link_to('Deliverables:', :action => 'issues', :id => @project.id, :deliverable_id => :none) %>
<%= l(:label_missing_on) %> <%= link_to('Deliverables:', :action => 'issues', :id => @project.id, :deliverable_id => :none) %>
</td>
<td class="calculation-column">
<%= h number_to_currency(budget.amount_missing_on_deliverables, :precision => 0) %>
@@ -90,7 +90,7 @@
<% if allowed_management? && budget.amount_missing_on_issues > 0 %>
<tr>
<td>
Missing on <%= link_to('Issues:',
<%= l(:label_missing_on) %> <%= link_to('Issues:',
:controller => 'timelog',
:action => 'details',
:project_id => @project.id,
@@ -106,7 +106,7 @@
<tr>
<td>
Next Due Date:
<%= l(:label_next_due_date) %>
</td>
<td class="calculation-column">
<%= h distance_of_time_in_words_to_now(budget.next_due_date) if budget.next_due_date %>
@@ -114,7 +114,7 @@
</tr>
<tr>
<td>
Completion:
<%= l(:label_completion) %>
</td>
<td class="calculation-column">
<%= h distance_of_time_in_words_to_now(budget.final_due_date) if budget.final_due_date %>
@@ -124,7 +124,7 @@
<% if allowed_management? %>
<tr>
<td>
Potential Profit:
<%= l(:label_potential_profit) %>
</td>
<td class="calculation-column">
<%= h number_to_currency(budget.profit, :precision => 0) %>

View File

@@ -7,22 +7,23 @@
<%= content_tag(:td, deliverable.id) %>
<%= content_tag(:td, h(deliverable.score.to_i), :class => 'score') if allowed_management? %>
<%= content_tag(:td, h(deliverable.subject), :class => 'subject') %>
<%= content_tag(:td, number_to_currency(deliverable.budget || 0.0, :precision => 0), :class => 'budget') if allowed_management? %>
<%= content_tag(:td, number_to_currency(deliverable.labor_budget || 0.0, :precision => 0), :class => 'budget') if allowed_management? %>
<%= content_tag(:td, number_to_currency(deliverable.spent, :precision => 0), :class => 'spent') if allowed_management? %>
<%= content_tag(:td, format_date(deliverable.due), :class => 'due_date') %>
<%= content_tag(:td, progress_bar(deliverable.progress, :width => '100%', :class => 'dont_ratio')) %>
<%= content_tag(:td, progress_bar(deliverable.progress, :width => '100%', :class => 'done_ratio')) %>
</tr>
<tr id="deliverable-details-<%= deliverable.id %>" class="deliverable deliverable-details <%= css %>" style="display:none;">
<td class="deliverable-actions">
<%= content_tag(:p,link_to("Edit", :action => 'edit', :id => @project.id, :deliverable_id => deliverable.id)) if allowed_management? -%>
<%= content_tag(:p,link_to("Delete", { :action => 'destroy', :id => @project.id, :deliverable_id => deliverable.id}, :confirm => l(:text_are_you_sure))) if allowed_management? %>
<%= content_tag(:p,link_to("Issues", :action => 'issues', :id => @project.id, :deliverable_id => deliverable.id)) -%>
<%= content_tag(:p,link_to(l(:button_edit), :action => 'edit', :id => @project.id, :deliverable_id => deliverable.id)) if allowed_management? -%>
<%= content_tag(:p,link_to(l(:button_delete), { :action => 'destroy', :id => @project.id, :deliverable_id => deliverable.id}, :confirm => l(:text_are_you_sure))) if allowed_management? %>
<%= content_tag(:p,link_to(l(:label_issue_plural), :action => 'issues', :id => @project.id, :deliverable_id => deliverable.id)) -%>
<% if allowed_management? && @project.versions.size > 0 %>
<div>
<% form_for :deliverable, deliverable, :url => { :action => "bulk_assign_issues", :id => @project.id, :deliverable_id => deliverable.id} do |f| %>
<%= select("version", "id", @project.versions.sort.collect {|v| [v.name, v.id ] }, { :prompt => '-- Version --' }) %><br />
<%= submit_tag("Bulk Assign") %>
<%= submit_tag(l(:label_bulk_assign)) %>
<% end %>
</div>
<% end %>
@@ -30,18 +31,18 @@
<td colspan="2">
<table class="progress-table">
<%= row_with_double_data "Progress: ", number_to_percentage(deliverable.progress, :precision => 0), '' %>
<%= row_with_double_data l(:label_progress), number_to_percentage(deliverable.progress, :precision => 0), '' %>
<% if allowed_management? %>
<% if deliverable.hourly? %>
<%= row_with_double_data "Hours Estimated: ", number_with_precision(deliverable.total_hours, 0), '' %>
<%= row_with_double_data l(:label_hours_estimated), number_with_precision(deliverable.total_hours, 0), '' %>
<% end %>
<% if deliverable.fixed? %>
<%= row_with_double_data "Fixed Amount: ", '', number_to_currency(deliverable.fixed_cost, :precision => 0) %>
<%= row_with_double_data l(:label_fixed_amount), '', number_to_currency(deliverable.fixed_cost, :precision => 0) %>
<% end %>
<% if deliverable.hours_used > 0 %>
<%= row_with_double_data "Hours Used: ", number_with_precision(deliverable.hours_used,0), number_to_currency(deliverable.spent_by_members, :precision => 0) %>
<%= row_with_double_data l(:label_hours_used), number_with_precision(deliverable.hours_used,0), number_to_currency(deliverable.spent_by_members, :precision => 0) %>
<tr><td>&nbsp;</td></tr>
@@ -55,18 +56,18 @@
<td colspan="3">
<table class="financials">
<%= row_with_data("Total Budget: ", number_to_currency(deliverable.budget, :precision => 0)) -%>
<%= row_with_data(l(:field_budget), number_to_currency(deliverable.budget, :precision => 0)) -%>
<% if allowed_management? %>
<% base_price_label = deliverable.hourly? ? "Labor: " : "Fixed Amount: " %>
<% base_price_label = deliverable.hourly? ? l(:label_labor) : l(:label_fixed_amount) %>
<%= row_with_data(base_price_label, number_to_currency(deliverable.labor_budget || 0.0, :precision => 0)) -%>
<%= row_with_data("Overhead: ", number_or_percent(deliverable.overhead, deliverable.overhead_percent)) -%>
<%= row_with_data("Materials: ", number_or_percent(deliverable.materials, deliverable.materials_percent)) -%>
<%= row_with_data("Potential Profit: ", number_or_percent(deliverable.profit, deliverable.profit_percent)) -%>
<%= row_with_data(l(:label_overhead), number_or_percent(deliverable.overhead, deliverable.overhead_percent)) -%>
<%= row_with_data(l(:label_materials), number_or_percent(deliverable.materials, deliverable.materials_percent)) -%>
<%= row_with_data(l(:label_potential_profit), number_or_percent(deliverable.profit, deliverable.profit_percent)) -%>
<% end %>
</table>
</td>
<td colspan="2">
<td colspan="3">
<table class="issue-totals">
<%= row_with_data(l(:label_issue_plural), deliverable.issues.size, 'issue-totals') -%>
@@ -76,3 +77,8 @@
</table>
</td>
</tr>
<tr id="deliverable-description-<%= deliverable.id %>" class="deliverable deliverable-details <%= css %>" style="display:none;">
<td colspan="9" style="text-align: left;">
<%= l(:field_description) %>: <%= textilizable deliverable.description %>
</td>
</tr>

View File

@@ -19,7 +19,7 @@
<tr>
<td>
<label for="deliverable_type">Fixed Cost</label>
<label for="deliverable_type"><%= l(:label_fixed_cost) %></label>
</td>
<td>
<%= check_box(:deliverable, :type, {}, FixedDeliverable.name, HourlyDeliverable.name) %>
@@ -31,7 +31,7 @@
<tr class="budget-hourly">
<td>
<label for="deliverable_cost_per_hour">Cost per hour</label>
<label for="deliverable_cost_per_hour"><%= l(:field_cost_per_hour)%></label>
</td>
<td>
<%= text_field :deliverable, :cost_per_hour, :size => 7 %>
@@ -42,7 +42,7 @@
<tr class="budget-hourly">
<td>
<label for="deliverable_total_hours">Total Hours</label>
<label for="deliverable_total_hours"><%= l(:field_total_hours) %></label>
</td>
<td>
<%= text_field :deliverable, :total_hours, :size => 7 %>
@@ -54,7 +54,7 @@
<tr class="budget-fixed" style="display:none;">
<td>
<label for="deliverable_fixed_cost">Fixed Bid</label>
<label for="deliverable_fixed_cost"><%= l(:field_fixed_cost) %></label>
</td>
<td>
<%= text_field :deliverable, :fixed_cost, :size => 7 %>
@@ -69,7 +69,7 @@
<%= field_with_budget_observer_and_totals(f, @deliverable, :profit, :profit_percent, @settings[:budget_profit]) %>
<tr class="total-budget">
<td><label>Total Budget:</label></td>
<td><label><%= l(:field_budget) %></label></td>
<td></td>
<td class="calculation-column">
<%= content_tag(:span, 0, :id => 'total-budget-calculation', :class => "budget-calculation") %>
@@ -109,121 +109,3 @@
<div id="preview" class="wiki"></div>
</fieldset>
<% content_for :header_tags do %>
<script type="text/javascript">
var BudgetModule = Class.create();
Object.extend(BudgetModule.prototype, {
initialize: function () {},
toAmount: function(value) {
var amount = value.replace(/[^1234567890.]/ig,'');
if (amount) {
return parseFloat(amount);
} else {
return 0;
}
},
updateAmounts: function() {
console.log('updateAmounts() called');
if ($('deliverable_type').checked) {
// Fixed cost
var cost = Budget.toAmount($('deliverable_fixed_cost').value);
Budget.updateAmount($('fixedCost'), cost);
} else {
// Variable cost
var perHour = Budget.toAmount($('deliverable_cost_per_hour').value);
var hours = Budget.toAmount($('deliverable_total_hours').value);
var cost = perHour * hours;
Budget.updateAmount($('variableCost'), cost);
}
if ($('deliverable_overhead').value.match('%')) {
var overhead_subtotal = (Budget.toAmount($('deliverable_overhead').value) / 100) * cost;
} else {
var overhead_subtotal = Budget.toAmount($('deliverable_overhead').value);
}
if ($('deliverable_materials').value.match('%')) {
var materials_subtotal = (Budget.toAmount($('deliverable_materials').value) / 100) * cost;
} else {
var materials_subtotal = Budget.toAmount($('deliverable_materials').value);
}
// Profit uses labor cost and overhead
if ($('deliverable_profit').value.match('%')) {
var profit_subtotal = (Budget.toAmount($('deliverable_profit').value) / 100) * (cost + overhead_subtotal);
} else {
var profit_subtotal = Budget.toAmount($('deliverable_profit').value);
}
// Amounts
Budget.updateAmount($('overhead_subtotal'), overhead_subtotal);
Budget.updateAmount($('materials_subtotal'), materials_subtotal);
Budget.updateAmount($('profit_subtotal'), profit_subtotal);
var total = cost + overhead_subtotal + materials_subtotal + profit_subtotal;
$('deliverable_budget').value = total;
$('total-budget-calculation').innerHTML = Budget.number_to_currency(total);
},
updateAmount: function(element, value) {
if (element) {
element.innerHTML = Budget.number_to_currency(value);
}
},
changeType: function() {
if ($('deliverable_type').checked) {
// Fixed
$$('.budget-hourly').each(function(ele) { ele.hide(); });
$$('.budget-fixed').each(function(ele) { ele.show(); });
} else {
// Variable
$$('.budget-hourly').each(function(ele) { ele.show(); });
$$('.budget-fixed').each(function(ele) { ele.hide(); });
}
Budget.updateAmounts();
},
// Rails-like number_to_currency currency formatting
// http://snippets.dzone.com/posts/show/4646
number_to_currency: function (number, options) {
try {
var options = options || {};
var precision = options["precision"] || 2;
var unit = options["unit"] || "$";
var separator = precision > 0 ? options["separator"] || "." : "";
var delimiter = options["delimiter"] || ",";
var parts = parseFloat(number).toFixed(precision).split('.');
return unit + Budget.number_with_delimiter(parts[0], delimiter) + separator + parts[1].toString();
} catch(e) {
return number
}
},
number_with_delimiter: function (number, delimiter, separator) {
try {
var delimiter = delimiter || ",";
var separator = separator || ".";
var parts = number.toString().split('.');
parts[0] = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1" + delimiter);
return parts.join(separator);
} catch(e) {
return number
}
}
});
Budget = new BudgetModule();
</script>
<% end %>

View File

@@ -2,12 +2,13 @@
<thead><tr>
<%= content_tag('th', " ") %>
<%= sort_header_tag("#{Deliverable.table_name}.id", :caption => '#', :default_order => 'desc') %>
<%= sort_header_tag("score", :caption => 'Score', :default_order => 'desc') if allowed_management? %>
<%= sort_header_tag("#{Deliverable.table_name}.subject", :caption => 'Subject') %>
<%= sort_header_tag("labor_budget", :caption => 'Budget') if allowed_management? %>
<%= sort_header_tag("spent", :caption => 'Spent') if allowed_management? %>
<%= sort_header_tag("#{Deliverable.table_name}.due", :caption => 'Due') %>
<%= sort_header_tag("progress", :caption => 'Progress') %>
<%= sort_header_tag("score", :caption => l(:caption_score), :default_order => 'desc') if allowed_management? %>
<%= sort_header_tag("#{Deliverable.table_name}.subject", :caption => l(:caption_subject)) %>
<%= sort_header_tag("total_budget", :caption => l(:caption_budget)) if allowed_management? %>
<%= sort_header_tag("labor_budget", :caption => l(:caption_labor_budget)) if allowed_management? %>
<%= sort_header_tag("spent", :caption => l(:caption_spent)) if allowed_management? %>
<%= sort_header_tag("#{Deliverable.table_name}.due", :caption => l(:caption_due)) %>
<%= sort_header_tag("progress", :caption => l(:caption_progress)) %>
</tr></thead>
<tbody>
<% deliverables.each do |deliverable| -%>

View File

@@ -1,3 +1,4 @@
<h3>Budget</h3>
<h3><%= l(:budget_title) %></h3>
<%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %><br />
<%= link_to_function "New Deliverable", "$('new-deliverable').toggle();" if allowed_management? %><br />
<%= link_to_function l(:label_new_deliverable), "$('new-deliverable').toggle();" if allowed_management? %><br />
<%= link_to_function l(:label_toggle_all), "toggleAll();" if allowed_management? %><br />

View File

@@ -1,4 +1,4 @@
<h2>Update Deliverable</h2>
<h2><%= l(:label_update_deliverable) %></h2>
<% form_for :deliverable, @deliverable, :url => {:controller => 'deliverables', :action => 'update', :id => @project, :deliverable_id => @deliverable.id },
:method => :post, :builder => TabularFormBuilder, :lang => current_language,
:html => {:multipart => true, :id => 'deliverable-form', :class => 'tabular'} do |f| %>
@@ -8,4 +8,5 @@
<% content_for :header_tags do %>
<%= stylesheet_link_tag "budget.css", :plugin => "budget_plugin", :media => "screen" %>
<%= javascript_include_tag('budget', :plugin => 'budget_plugin') %>
<% end %>

View File

@@ -4,7 +4,7 @@
<% if allowed_management? %>
<div id="new-deliverable" style="display:none;">
<h2>New Deliverable</h2>
<h2><%= l(:label_new_deliverable) %></h2>
<% remote_form_for :deliverable, @deliverable, :url => {:controller => 'deliverables', :action => 'create', :id => @project },
:method => :post, :builder => TabularFormBuilder, :lang => current_language,
:html => {:multipart => true, :id => 'deliverable-form', :class => 'tabular'} do |f| %>
@@ -28,4 +28,5 @@
<% content_for :header_tags do %>
<%= stylesheet_link_tag "budget.css", :plugin => "budget_plugin", :media => "screen" %>
<%= javascript_include_tag('budget', :plugin => 'budget_plugin') %>
<% end %>

View File

@@ -0,0 +1,16 @@
<p>
<%= l(:message_budget_settings) %>
</p>
<p>
<label><%= l(:label_non_billable_overhead) %></label><%= text_field_tag 'settings[budget_non_billable_overhead]', @settings['budget_non_billable_overhead'], :size => 15 %>
</p>
<p>
<label><%= l(:label_materials) %></label><%= text_field_tag 'settings[budget_materials]', @settings['budget_materials'], :size => 15 %>
</p>
<p>
<label><%= l(:label_profit) %></label><%= text_field_tag 'settings[budget_profit]', @settings['budget_profit'], :size => 15 %>
</p>

View File

@@ -1,16 +0,0 @@
<p>
Enter a dollar amount or percentage into each field to set your default amount. Use <strong>%</strong> in the field for percentages.
</p>
<p>
<label>Non billable overhead</label><%= text_field_tag 'settings[budget_non_billable_overhead]', @settings['budget_non_billable_overhead'], :size => 15 %>
</p>
<p>
<label>Materials</label><%= text_field_tag 'settings[budget_materials]', @settings['budget_materials'], :size => 15 %>
</p>
<p>
<label>Profit</label><%= text_field_tag 'settings[budget_profit]', @settings['budget_profit'], :size => 15 %>
</p>

View File

@@ -0,0 +1,135 @@
/* Used to calculate the Budget */
var BudgetModule = Class.create();
Object.extend(BudgetModule.prototype, {
initialize: function () {},
toAmount: function(value) {
var amount = value.replace(/[^1234567890.]/ig,'');
if (amount) {
return parseFloat(amount);
} else {
return 0;
}
},
updateAmounts: function() {
if ($('deliverable_type').checked) {
// Fixed cost
var cost = Budget.toAmount($('deliverable_fixed_cost').value);
Budget.updateAmount($('fixedCost'), cost);
} else {
// Variable cost
var perHour = Budget.toAmount($('deliverable_cost_per_hour').value);
var hours = Budget.toAmount($('deliverable_total_hours').value);
var cost = perHour * hours;
Budget.updateAmount($('variableCost'), cost);
}
if ($('deliverable_overhead').value.match('%')) {
var overhead_subtotal = (Budget.toAmount($('deliverable_overhead').value) / 100) * cost;
} else {
var overhead_subtotal = Budget.toAmount($('deliverable_overhead').value);
}
if ($('deliverable_materials').value.match('%')) {
var materials_subtotal = (Budget.toAmount($('deliverable_materials').value) / 100) * cost;
} else {
var materials_subtotal = Budget.toAmount($('deliverable_materials').value);
}
// Profit uses labor cost and overhead
if ($('deliverable_profit').value.match('%')) {
var profit_subtotal = (Budget.toAmount($('deliverable_profit').value) / 100) * (cost + overhead_subtotal);
} else {
var profit_subtotal = Budget.toAmount($('deliverable_profit').value);
}
// Amounts
Budget.updateAmount($('overhead_subtotal'), overhead_subtotal);
Budget.updateAmount($('materials_subtotal'), materials_subtotal);
Budget.updateAmount($('profit_subtotal'), profit_subtotal);
var total = cost + overhead_subtotal + materials_subtotal + profit_subtotal;
$('deliverable_budget').value = total;
$('total-budget-calculation').innerHTML = Budget.number_to_currency(total);
},
updateAmount: function(element, value) {
if (element) {
element.innerHTML = Budget.number_to_currency(value);
}
},
changeType: function() {
if ($('deliverable_type').checked) {
// Fixed
$$('.budget-hourly').each(function(ele) { ele.hide(); });
$$('.budget-fixed').each(function(ele) { ele.show(); });
} else {
// Variable
$$('.budget-hourly').each(function(ele) { ele.show(); });
$$('.budget-fixed').each(function(ele) { ele.hide(); });
}
Budget.updateAmounts();
},
// Rails-like number_to_currency currency formatting
// http://snippets.dzone.com/posts/show/4646
number_to_currency: function (number, options) {
try {
var options = options || {};
var precision = options["precision"] || 2;
var unit = options["unit"] || "$";
var separator = precision > 0 ? options["separator"] || "." : "";
var delimiter = options["delimiter"] || ",";
var parts = parseFloat(number).toFixed(precision).split('.');
return unit + Budget.number_with_delimiter(parts[0], delimiter) + separator + parts[1].toString();
} catch(e) {
return number;
}
},
number_with_delimiter: function (number, delimiter, separator) {
try {
var delimiter = delimiter || ",";
var separator = separator || ".";
var parts = number.toString().split('.');
parts[0] = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1" + delimiter);
return parts.join(separator);
} catch(e) {
return number
}
}
});
Budget = new BudgetModule();
function toggleAll() {
$$('.deliverable-details').each(function(ele) {
ele.toggle();
});
$$('.toggle').each(function(e) {
e.toggle();
});
}
function expandRow(deliverable_id) {
$('deliverable-details-'+ deliverable_id).show();
$('deliverable-description-'+ deliverable_id).show();
$$('.toggle_' + deliverable_id).each(function(e) {
e.toggle();
});
}
function collapseRow(deliverable_id) {
$('deliverable-details-'+ deliverable_id).hide();
$('deliverable-description-'+ deliverable_id).hide();
$$('.toggle_' + deliverable_id).each(function(e) {
e.toggle();
});
}

View File

@@ -37,7 +37,7 @@ tr.deliverable-details table tr td.data { text-align:right; }
td.calculation-column { text-align:right; width: auto; }
.budget-calculation { color:#999999; font-weight:bold; display:block; text-align:right; width: 100%;}
p.total-budget { font-weight: bold; }
p.total-budget, .total { font-weight: bold; }
p.total-budget #total-budget-calculation { }

47
config/locales/ca.yml Normal file
View File

@@ -0,0 +1,47 @@
ca:
budget_title: Pressupost
field_cost_per_hour: Cost per hora
field_total_hours: Total hores
field_overhead: Imprevists
label_overhead: "Imprevists: "
field_materials: Cost dels materials
label_materials: "Materials: "
field_profit: Benefici
field_budget: Pressupost total
label_budget: "Pressupost total: "
field_fixed_cost: Preu tancat
field_project_manager_signoff: Vistiplau cap projecte
field_client_signoff: Vistiplau client
field_deliverable: Entregable
field_deliverable_subject: Entregable
field_due: Data finalització
label_member_rate: Tarifa (EUR)
message_updated_issues: Actualitzats %d assumptes
message_settings: Introduïu una quantitat en euros o un percentatge en cada camp per a establir un valor per defecte. Utilitzeu <strong>%%</strong> per indicar percentatges.
label_non_billable_overhead: Imprevists no facturables
label_materials: Materials
label_profit: Benefici
label_new_deliverable: Nou entregable
label_fixed_cost: Cost fix
caption_due: Entrega
caption_progress: Progrés
caption_subject: Assumpte
caption_score: Puntuació
caption_budget: Pressupost
caption_spent: Gastat
label_update_deliverable: Actualitzar entregable
label_labor_budget: "Pressupost mà d'obra: "
label_labor_budget_spent: "Pressupost gastat mà d'obra: "
label_labor_budget_remaining: "Pressupost romanent mà d'obra: "
label_progress: "Progrés: "
label_budget_score: "Puntuació pressupost: "
label_overruns: "Excessos: "
label_missing_on: Falta en
label_next_due_date: "Següent data d'entrega: "
label_completion: "Finalització: "
label_potential_profit: "Benefici potencial: "
label_bulk_assign: Assignar tots
label_labor: "Mà d'obra: "
label_fixed_amount: "Quantitat fixa: "
label_hours_estimated: "Hores estimades: "
label_hours_used: "Hores gastades: "

49
config/locales/en.yml Normal file
View File

@@ -0,0 +1,49 @@
en:
budget_title: Budget
field_cost_per_hour: Cost per hour
field_total_hours: Total hours
field_overhead: Overhead
label_overhead: "Overhead: "
field_materials: Material costs
label_materials: "Materials: "
field_profit: Profit
field_budget: Total Budget
label_budget: "Total Budget: "
field_fixed_cost: Fixed Bid
field_project_manager_signoff: Project Manager Signoff
field_client_signoff: Client Signoff
field_deliverable: Deliverable
field_deliverable_subject: Deliverable
field_due: Due Date
label_member_rate: Rate ($)
message_updated_issues: Updated %d issues
message_budget_settings: Enter a dollar amount or percentage into each field to set your default amount. Use <strong>%%</strong> in the field for percentages.
label_non_billable_overhead: Non billable overhead
label_materials: Materials
label_profit: Profit
label_new_deliverable: New deliverable
label_fixed_cost: Fixed cost
caption_due: Due
caption_progress: Progress
caption_subject: Subject
caption_score: Score
caption_budget: Budget
caption_labor_budget: Labor Budget
caption_spent: Spent
label_update_deliverable: Update Deliverable
label_labor_budget: "Labor Budget: "
label_labor_budget_spent: "Labor Budget Spent: "
label_labor_budget_remaining: "Labor Budget Remaining: "
label_progress: "Progress: "
label_budget_score: "Budget Score: "
label_overruns: "Overruns: "
label_missing_on: Missing on
label_next_due_date: "Next Due Date: "
label_completion: "Completion: "
label_potential_profit: "Potential Profit: "
label_bulk_assign: Bulk Assign
label_labor: "Labor: "
label_fixed_amount: "Fixed Amount: "
label_hours_estimated: "Hours Estimated: "
label_hours_used: "Hours Used: "
label_toggle_all: "Expand all deliverable rows"

46
config/locales/es.yml Normal file
View File

@@ -0,0 +1,46 @@
es:
budget_title: Presupuestos
field_cost_per_hour: Coste por hora
field_total_hours: Total horas
field_overhead: Imprevistos
label_overhead: "Imprevistos: "
field_materials: Coste de los materiales
label_materials: "Materiales: "
field_profit: Beneficio
field_budget: Presupuesto total
label_budget: "Presupuesto total: "
field_fixed_cost: Precio cerrado
field_project_manager_signoff: Visto bueno jefe proyecto
field_client_signoff: Visto bueno cliente
field_deliverable: Entregable
field_deliverable_subject: Entregable
field_due: Fecha entrega
label_member_rate: Tarifa (EUR)
message_updated_issues: Actualizadas %d peticiones
message_settings: Introduce cantidades en euros o porcentajes en cada campo para establecer los valores por defecto. Usa <strong>%%</strong> para indicar porcentajes.
label_non_billable_overhead: Imprevistos no facturables
label_profit: Beneficio
label_new_deliverable: Nuevo entregable
label_fixed_cost: Coste fijo
caption_due: Entrega
caption_progress: Progreso
caption_subject: Asunto
caption_score: Importancia
caption_budget: Presupuesto
caption_spent: Gastado
label_update_deliverable: Actualizar entregable
label_labor_budget: "Presupuesto mano de obra: "
label_labor_budget_spent: "Presupuesto gastado mano de obra: "
label_labor_budget_remaining: "Presupuesto remanente mano de obra: "
label_progress: "Progreso: "
label_budget_score: "Puntuación presupuesto: "
label_overruns: "Excesos: "
label_missing_on: Falta en
label_next_due_date: "Siguiente entrega: "
label_completion: "Finalización: "
label_potential_profit: "Beneficio potencial: "
label_bulk_assign: Asignar todos
label_labor: "Mano de obra: "
label_fixed_amount: "Cantidad fija: "
label_hours_estimated: "Horas estimadas: "
label_hours_used: "Horas usadas: "

15
config/locales/hu.yml Normal file
View File

@@ -0,0 +1,15 @@
hu:
field_cost_per_hour: Óradíj
field_total_hours: Összes órák
field_overhead: Felfüggesztett
field_materials: Anyag költség
field_profit: Profit
field_budget: Teljes költség
field_fixed_cost: Rögzített árajánlat
field_project_manager_signoff: Projekt felelős által aláíratlan
field_client_signoff: Ügyfél által aláíratlan
field_deliverable: Teljesítés
field_deliverable_subject: Teljesítés
field_due: Esedékesség dátuma
label_member_rate: Részesedés ($)
message_updated_issues: Frissítve % feladat

15
config/locales/lt.yml Normal file
View File

@@ -0,0 +1,15 @@
lt:
field_cost_per_hour: Valandinis užmokestis
field_total_hours: Iš viso valandų
field_overhead: Viršyjimas
field_materials: Material costs
field_profit: Pelnas
field_budget: Visas biudžetas
field_fixed_cost: Patovus pasiūlymas
field_project_manager_signoff: Pasirašo projekto vadovas
field_client_signoff: Pasirašo klientas
field_deliverable: Pateiktis
field_deliverable_subject: Pateiktis
field_due: Data iki
label_member_rate: Užmokestis (LTL)
message_updated_issues: Atnaujinta(i) %d darbų(ai)

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

33
init.rb
View File

@@ -1,26 +1,41 @@
require 'redmine'
# Patches to the Redmine core. Will not work in development mode
require_dependency 'issue_patch'
require_dependency 'query_patch'
# Budget requires the Rate plugin
begin
require 'rate' unless Object.const_defined?('Rate')
rescue LoadError
# rate_plugin is not installed
raise Exception.new("ERROR: The Rate plugin is not installed. Please install the Rate plugin from https://projects.littlestreamsoftware.com/projects/redmine-rate")
end
# Patches to the Redmine core.
require 'dispatcher'
require 'issue_patch'
require 'query_patch'
Dispatcher.to_prepare do
Issue.send(:include, IssuePatch)
Query.send(:include, QueryPatch)
end
# Hooks
require_dependency 'budget_issue_hook'
require_dependency 'budget_project_hook'
RAILS_DEFAULT_LOGGER.info 'Starting Budget plugin for RedMine'
Redmine::Plugin.register :budget_plugin do
name 'Budget'
author 'Eric Davis <edavis@littlestreamsoftware.com>'
author 'Eric Davis'
description 'Budget is a plugin to manage the set of deliverables for each project, automatically calculating key performance indicators.'
version '0.1.0'
url 'https://projects.littlestreamsoftware.com/projects/redmine-budget'
author_url 'http://www.littlestreamsoftware.com'
version '0.2.0'
requires_redmine :version_or_higher => '0.8.0'
settings :default => {
'budget_nonbillable_overhead' => '',
'budget_materials' => '',
'budget_profit' => ''
}, :partial => 'settings/settings'
}, :partial => 'settings/budget_settings'
project_module :budget_module do
@@ -28,5 +43,5 @@ Redmine::Plugin.register :budget_plugin do
permission :manage_budget, { :deliverables => [:new, :edit, :create, :update, :destroy, :preview, :bulk_assign_issues]}
end
menu :project_menu, :budget, :controller => "deliverables", :action => 'index'
menu :project_menu, :budget, {:controller => "deliverables", :action => 'index'}, :caption => :budget_title
end

46
lang/ca.yml Normal file
View File

@@ -0,0 +1,46 @@
budget_title: Pressupost
field_cost_per_hour: Cost per hora
field_total_hours: Total hores
field_overhead: Imprevists
label_overhead: "Imprevists: "
field_materials: Cost dels materials
label_materials: "Materials: "
field_profit: Benefici
field_budget: Pressupost total
label_budget: "Pressupost total: "
field_fixed_cost: Preu tancat
field_project_manager_signoff: Vistiplau cap projecte
field_client_signoff: Vistiplau client
field_deliverable: Entregable
field_deliverable_subject: Entregable
field_due: Data finalització
label_member_rate: Tarifa (EUR)
message_updated_issues: Actualitzats %d assumptes
message_settings: Introduïu una quantitat en euros o un percentatge en cada camp per a establir un valor per defecte. Utilitzeu <strong>%%</strong> per indicar percentatges.
label_non_billable_overhead: Imprevists no facturables
label_materials: Materials
label_profit: Benefici
label_new_deliverable: Nou entregable
label_fixed_cost: Cost fix
caption_due: Entrega
caption_progress: Progrés
caption_subject: Assumpte
caption_score: Puntuació
caption_budget: Pressupost
caption_spent: Gastat
label_update_deliverable: Actualitzar entregable
label_labor_budget: "Pressupost mà d'obra: "
label_labor_budget_spent: "Pressupost gastat mà d'obra: "
label_labor_budget_remaining: "Pressupost romanent mà d'obra: "
label_progress: "Progrés: "
label_budget_score: "Puntuació pressupost: "
label_overruns: "Excessos: "
label_missing_on: Falta en
label_next_due_date: "Següent data d'entrega: "
label_completion: "Finalització: "
label_potential_profit: "Benefici potencial: "
label_bulk_assign: Assignar tots
label_labor: "Mà d'obra: "
label_fixed_amount: "Quantitat fixa: "
label_hours_estimated: "Hores estimades: "
label_hours_used: "Hores gastades: "

View File

@@ -1,9 +1,13 @@
budget_title: Budget
field_cost_per_hour: Cost per hour
field_total_hours: Total hours
field_overhead: Overhead
label_overhead: "Overhead: "
field_materials: Material costs
label_materials: "Materials: "
field_profit: Profit
field_budget: Total Budget
label_budget: "Total Budget: "
field_fixed_cost: Fixed Bid
field_project_manager_signoff: Project Manager Signoff
field_client_signoff: Client Signoff
@@ -12,3 +16,33 @@ field_deliverable_subject: Deliverable
field_due: Due Date
label_member_rate: Rate ($)
message_updated_issues: Updated %d issues
message_budget_settings: Enter a dollar amount or percentage into each field to set your default amount. Use <strong>%%</strong> in the field for percentages.
label_non_billable_overhead: Non billable overhead
label_materials: Materials
label_profit: Profit
label_new_deliverable: New deliverable
label_fixed_cost: Fixed cost
caption_due: Due
caption_progress: Progress
caption_subject: Subject
caption_score: Score
caption_budget: Budget
caption_labor_budget: Labor Budget
caption_spent: Spent
label_update_deliverable: Update Deliverable
label_labor_budget: "Labor Budget: "
label_labor_budget_spent: "Labor Budget Spent: "
label_labor_budget_remaining: "Labor Budget Remaining: "
label_progress: "Progress: "
label_budget_score: "Budget Score: "
label_overruns: "Overruns: "
label_missing_on: Missing on
label_next_due_date: "Next Due Date: "
label_completion: "Completion: "
label_potential_profit: "Potential Profit: "
label_bulk_assign: Bulk Assign
label_labor: "Labor: "
label_fixed_amount: "Fixed Amount: "
label_hours_estimated: "Hours Estimated: "
label_hours_used: "Hours Used: "
label_toggle_all: "Expand all deliverable rows"

45
lang/es.yml Normal file
View File

@@ -0,0 +1,45 @@
budget_title: Presupuestos
field_cost_per_hour: Coste por hora
field_total_hours: Total horas
field_overhead: Imprevistos
label_overhead: "Imprevistos: "
field_materials: Coste de los materiales
label_materials: "Materiales: "
field_profit: Beneficio
field_budget: Presupuesto total
label_budget: "Presupuesto total: "
field_fixed_cost: Precio cerrado
field_project_manager_signoff: Visto bueno jefe proyecto
field_client_signoff: Visto bueno cliente
field_deliverable: Entregable
field_deliverable_subject: Entregable
field_due: Fecha entrega
label_member_rate: Tarifa (EUR)
message_updated_issues: Actualizadas %d peticiones
message_settings: Introduce cantidades en euros o porcentajes en cada campo para establecer los valores por defecto. Usa <strong>%%</strong> para indicar porcentajes.
label_non_billable_overhead: Imprevistos no facturables
label_profit: Beneficio
label_new_deliverable: Nuevo entregable
label_fixed_cost: Coste fijo
caption_due: Entrega
caption_progress: Progreso
caption_subject: Asunto
caption_score: Importancia
caption_budget: Presupuesto
caption_spent: Gastado
label_update_deliverable: Actualizar entregable
label_labor_budget: "Presupuesto mano de obra: "
label_labor_budget_spent: "Presupuesto gastado mano de obra: "
label_labor_budget_remaining: "Presupuesto remanente mano de obra: "
label_progress: "Progreso: "
label_budget_score: "Puntuación presupuesto: "
label_overruns: "Excesos: "
label_missing_on: Falta en
label_next_due_date: "Siguiente entrega: "
label_completion: "Finalización: "
label_potential_profit: "Beneficio potencial: "
label_bulk_assign: Asignar todos
label_labor: "Mano de obra: "
label_fixed_amount: "Cantidad fija: "
label_hours_estimated: "Horas estimadas: "
label_hours_used: "Horas usadas: "

View File

14
lang/hu.yml Normal file
View File

@@ -0,0 +1,14 @@
field_cost_per_hour: Óradíj
field_total_hours: Összes órák
field_overhead: Felfüggesztett
field_materials: Anyag költség
field_profit: Profit
field_budget: Teljes költség
field_fixed_cost: Rögzített árajánlat
field_project_manager_signoff: Projekt felelős által aláíratlan
field_client_signoff: Ügyfél által aláíratlan
field_deliverable: Teljesítés
field_deliverable_subject: Teljesítés
field_due: Esedékesség dátuma
label_member_rate: Részesedés ($)
message_updated_issues: Frissítve % feladat

14
lang/lt.yml Normal file
View File

@@ -0,0 +1,14 @@
field_cost_per_hour: Valandinis užmokestis
field_total_hours: Iš viso valandų
field_overhead: Viršyjimas
field_materials: Material costs
field_profit: Pelnas
field_budget: Visas biudžetas
field_fixed_cost: Patovus pasiūlymas
field_project_manager_signoff: Pasirašo projekto vadovas
field_client_signoff: Pasirašo klientas
field_deliverable: Pateiktis
field_deliverable_subject: Pateiktis
field_due: Data iki
label_member_rate: Užmokestis (LTL)
message_updated_issues: Atnaujinta(i) %d darbų(ai)

View File

@@ -38,11 +38,11 @@ class BudgetIssueHook < Redmine::Hook::ViewListener
def view_issues_bulk_edit_details_bottom(context = { })
if context[:project].module_enabled?('budget_module')
select = select_tag('deliverable_id',
content_tag('option', GLoc.l(:label_no_change_option), :value => '') +
content_tag('option', GLoc.l(:label_none), :value => 'none') +
content_tag('option', l(:label_no_change_option), :value => '') +
content_tag('option', l(:label_none), :value => 'none') +
options_from_collection_for_select(Deliverable.find_all_by_project_id(context[:project].id, :order => 'subject ASC'), :id, :subject))
return content_tag(:p, "<label>#{GLoc.l(:field_deliverable)}: " + select + "</label>")
return content_tag(:p, "<label>#{l(:field_deliverable)}: " + select + "</label>")
else
return ''
end

View File

@@ -1,51 +1,26 @@
# Hooks to attach to the Redmine Projects.
class BudgetProjectHook < Redmine::Hook::ViewListener
class BudgetProjectHook < Redmine::Hook::ViewListener
def model_project_copy_before_save(context = {})
source = context[:source_project]
destination = context[:destination_project]
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 ''
if source.module_enabled?(:budget_module)
Deliverable.find(:all, :conditions => {:project_id => source.id}).each do |source_deliverable|
destination_deliverable = source_deliverable.class.new # STI classes
# Copy attribute except for the ones that have wrapped
# accessors, use read/write attribute for them
destination_deliverable.attributes = source_deliverable.attributes.except("project_id", "profit", "materials", "overhead")
destination_deliverable.write_attribute(:profit, source_deliverable.read_attribute(:profit))
destination_deliverable.write_attribute(:profit_percent, source_deliverable.read_attribute(:profit_percent))
destination_deliverable.write_attribute(:materials, source_deliverable.read_attribute(:materials))
destination_deliverable.write_attribute(:materials_percent, source_deliverable.read_attribute(:materials_percent))
destination_deliverable.write_attribute(:overhead, source_deliverable.read_attribute(:overhead))
destination_deliverable.write_attribute(:overhead_percent, source_deliverable.read_attribute(:overhead_percent))
destination_deliverable.project = destination
destination_deliverable.save # Need to save here because there is no relation on project to deliverable
end
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
@@ -54,7 +32,4 @@ module IssuePatch
end
end
# Add module to Issue
Issue.send(:include, IssuePatch)

View File

@@ -50,7 +50,4 @@ module QueryPatch
end
end
# Add module to Query
Query.send(:include, QueryPatch)

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