Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8413b26a15 | ||
|
|
f7733011df | ||
|
|
a45253329d | ||
|
|
085dc1deb4 | ||
|
|
425047f305 | ||
|
|
ffb20347ce | ||
|
|
ee2e2dae8e | ||
|
|
3b26d08f0d | ||
|
|
833c2877af | ||
|
|
d88fce97f8 | ||
|
|
931c1ef6ed | ||
|
|
2018561e6a | ||
|
|
83d5af4a4a | ||
|
|
eb60581d1a | ||
|
|
e218218ffb | ||
|
|
7ed5c20a93 | ||
|
|
72f2f77a60 | ||
|
|
fd891e7f51 | ||
|
|
71ab0c6b54 | ||
|
|
328cd3fc3f | ||
|
|
4205fb95a3 | ||
|
|
3f00f12107 | ||
|
|
3720c486b4 | ||
|
|
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 |
10
CREDITS.txt
10
CREDITS.txt
@@ -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
|
||||
|
||||
@@ -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
111
Rakefile
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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?
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) %>
|
||||
|
||||
@@ -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> </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>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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| -%>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
16
app/views/settings/_budget_settings.rhtml
Normal file
16
app/views/settings/_budget_settings.rhtml
Normal 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
135
assets/javascripts/budget.js
Normal file
135
assets/javascripts/budget.js
Normal 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();
|
||||
});
|
||||
}
|
||||
@@ -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
47
config/locales/ca.yml
Normal 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
49
config/locales/en.yml
Normal 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
46
config/locales/es.yml
Normal 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
15
config/locales/hu.yml
Normal 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
15
config/locales/lt.yml
Normal 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)
|
||||
@@ -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
|
||||
33
init.rb
33
init.rb
@@ -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
46
lang/ca.yml
Normal 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: "
|
||||
34
lang/en.yml
34
lang/en.yml
@@ -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
45
lang/es.yml
Normal 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: "
|
||||
14
lang/hu.yml
Normal file
14
lang/hu.yml
Normal 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
14
lang/lt.yml
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -50,7 +50,4 @@ module QueryPatch
|
||||
end
|
||||
end
|
||||
|
||||
# Add module to Query
|
||||
Query.send(:include, QueryPatch)
|
||||
|
||||
|
||||
|
||||
@@ -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