Compare commits
132 Commits
4420-wip-m
...
6441-contr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e50eb7558 | ||
|
|
bf1339fc61 | ||
|
|
256325a39d | ||
|
|
e0ecae8220 | ||
|
|
b99f57f51c | ||
|
|
edc9e84719 | ||
|
|
2f77619fef | ||
|
|
6357fd83e4 | ||
|
|
1b4191dc15 | ||
|
|
e019a9435e | ||
|
|
f70ade5011 | ||
|
|
495407f127 | ||
|
|
be37e00f10 | ||
|
|
f8fe23a68d | ||
|
|
7c23725bbd | ||
|
|
2f79493cca | ||
|
|
62cdfd35de | ||
|
|
5c1e254181 | ||
|
|
0a916fea70 | ||
|
|
ac24ae08e6 | ||
|
|
51f62ca5a3 | ||
|
|
07c3c42ca8 | ||
|
|
953587ce46 | ||
|
|
5e2e309304 | ||
|
|
9453b52c10 | ||
|
|
fbd38b3678 | ||
|
|
4ade8de2c4 | ||
|
|
2153220675 | ||
|
|
496c6bada7 | ||
|
|
3384dd3dbd | ||
|
|
7d7d6da8b5 | ||
|
|
96fa509a1e | ||
|
|
a0b92851a7 | ||
|
|
d2aa87fdd2 | ||
|
|
f514b15361 | ||
|
|
889a825426 | ||
|
|
a92559c133 | ||
|
|
af0eecb6dc | ||
|
|
70fad4d8e2 | ||
|
|
ff363f08f3 | ||
|
|
40fc6e9751 | ||
|
|
75680e5a49 | ||
|
|
39073905c3 | ||
|
|
abeba76f40 | ||
|
|
726c7a206f | ||
|
|
6f1ce637c5 | ||
|
|
bef015f4ea | ||
|
|
b74ed932db | ||
|
|
2957d8e2cc | ||
|
|
a302c17b04 | ||
|
|
1fafe8ea92 | ||
|
|
2aea176df0 | ||
|
|
5a0cb948c9 | ||
|
|
71c3d114ed | ||
|
|
46a5aaeabe | ||
|
|
80d4c7bfa6 | ||
|
|
3b5c008a65 | ||
|
|
650296bd52 | ||
|
|
bf270623d2 | ||
|
|
691adf521e | ||
|
|
a5552845a5 | ||
|
|
ac2a9ddbf0 | ||
|
|
67469c9d94 | ||
|
|
ed9f828efa | ||
|
|
0f9308267c | ||
|
|
d3f9255eef | ||
|
|
5a277c723f | ||
|
|
b0b434dbfb | ||
|
|
d6aee43b4b | ||
|
|
39c04b9896 | ||
|
|
15d626b6d8 | ||
|
|
20f89adf39 | ||
|
|
632cff8f7f | ||
|
|
f37306aace | ||
|
|
38f2bcb17f | ||
|
|
7a5599b4f9 | ||
|
|
0fd801c709 | ||
|
|
f3fc82d9dc | ||
|
|
2a60d283a4 | ||
|
|
cd382fd784 | ||
|
|
1350a47593 | ||
|
|
56a38fbd93 | ||
|
|
83e3023d85 | ||
|
|
7bc31d6be0 | ||
|
|
d778201eff | ||
|
|
893ad7943d | ||
|
|
059b971e54 | ||
|
|
b8d7c15b39 | ||
|
|
ed9f73e8a0 | ||
|
|
0a80a54be3 | ||
|
|
8582ad0ecd | ||
|
|
c9861835e7 | ||
|
|
0ef87e7b9c | ||
|
|
c6d43d9149 | ||
|
|
7c6c2797c4 | ||
|
|
967ef6bc7a | ||
|
|
4a7484e1ea | ||
|
|
d4de581b3c | ||
|
|
f8270498f3 | ||
|
|
2c04ef4f45 | ||
|
|
d78e7b1dc3 | ||
|
|
67dd75a980 | ||
|
|
55c9f93c6c | ||
|
|
081df7874d | ||
|
|
ed7536897e | ||
|
|
86be29cecc | ||
|
|
294c88f11d | ||
|
|
ce523df7eb | ||
|
|
306e4cfc80 | ||
|
|
19ed07b617 | ||
|
|
74d707a1a4 | ||
|
|
ee282a42de | ||
|
|
30f90b7c8d | ||
|
|
e1a7297312 | ||
|
|
4ba5ac7f8f | ||
|
|
a7128fc15b | ||
|
|
c9e699e9fa | ||
|
|
893d83a03d | ||
|
|
9b49959bcf | ||
|
|
edc8db7fe2 | ||
|
|
feee5e8b43 | ||
|
|
07da5d98d8 | ||
|
|
7f7e5b3042 | ||
|
|
d315252565 | ||
|
|
6b38dfca91 | ||
|
|
75046cfdd1 | ||
|
|
12fffee314 | ||
|
|
362610dc77 | ||
|
|
1b08487a0d | ||
|
|
5ef7d0271d | ||
|
|
0a1bb9169d | ||
|
|
91d8c1818e |
6
Gemfile
Normal file
6
Gemfile
Normal file
@@ -0,0 +1,6 @@
|
||||
gem 'formtastic', "0.9.10"
|
||||
gem 'inherited_resources', '1.0.6'
|
||||
|
||||
group :test do
|
||||
gem 'webrat'
|
||||
end
|
||||
@@ -7,8 +7,12 @@ class ContractsController < InheritedResources::Base
|
||||
before_filter :authorize
|
||||
before_filter :require_admin, :only => :destroy
|
||||
|
||||
helper :contract_formatter
|
||||
|
||||
def create
|
||||
create! { contract_url(@project, resource) }
|
||||
create! do |success, failure|
|
||||
success.html { redirect_to contract_url(@project, resource) }
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
|
||||
@@ -7,6 +7,7 @@ class DeliverablesController < InheritedResources::Base
|
||||
before_filter :authorize
|
||||
|
||||
helper :contracts
|
||||
helper :contract_formatter
|
||||
|
||||
def index
|
||||
redirect_to contract_url(@project, @contract)
|
||||
@@ -17,18 +18,18 @@ class DeliverablesController < InheritedResources::Base
|
||||
if params[:deliverable] && params[:deliverable][:type] && Deliverable.valid_types.include?(params[:deliverable][:type])
|
||||
@deliverable.type = params[:deliverable][:type]
|
||||
end
|
||||
create! { contract_url(@project, @contract) }
|
||||
create!(:notice => l(:text_flash_deliverable_created, :name => @deliverable.title)) { contract_url(@project, @contract) }
|
||||
end
|
||||
|
||||
def update
|
||||
@deliverable = begin_of_association_chain.deliverables.find_by_id(params[:id])
|
||||
params[:deliverable] = params[:fixed_deliverable] || params[:hourly_deliverable] || params[:retainer_deliverable]
|
||||
update! { contract_url(@project, @contract) }
|
||||
update!(:notice => l(:text_flash_deliverable_updated, :name => @deliverable.title)) { contract_url(@project, @contract) }
|
||||
end
|
||||
|
||||
def show
|
||||
if show_partial?
|
||||
@period = params[:period]
|
||||
@period = extract_period(params[:period])
|
||||
render :partial => 'deliverables/details_row', :locals => {:contract => @contract, :deliverable => @contract.deliverables.find(params[:id]), :period => @period}
|
||||
else
|
||||
redirect_to contract_url(@project, @contract)
|
||||
@@ -36,7 +37,7 @@ class DeliverablesController < InheritedResources::Base
|
||||
end
|
||||
|
||||
def destroy
|
||||
destroy! { contract_url(@project, @contract) }
|
||||
destroy!(:notice => l(:text_flash_deliverable_deleted, :name => resource.title)) { contract_url(@project, @contract) }
|
||||
end
|
||||
|
||||
protected
|
||||
@@ -57,4 +58,13 @@ class DeliverablesController < InheritedResources::Base
|
||||
@project = @contract.project
|
||||
end
|
||||
|
||||
def extract_period(param)
|
||||
period = nil
|
||||
if param.present? && param.match(/\A\d{4}-\d{2}\z/) # "YYYY-MM"
|
||||
year, month = param.split('-')
|
||||
period = Date.new(year.to_i, month.to_i, 1)
|
||||
end
|
||||
period
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
57
app/helpers/contract_formatter_helper.rb
Normal file
57
app/helpers/contract_formatter_helper.rb
Normal file
@@ -0,0 +1,57 @@
|
||||
# Formatting helpers
|
||||
module ContractFormatterHelper
|
||||
def format_as_yes_or_no(value)
|
||||
if value
|
||||
l(:general_text_Yes)
|
||||
else
|
||||
l(:general_text_No)
|
||||
end
|
||||
end
|
||||
|
||||
def format_budget_for_deliverable(deliverable, spent, total, options={})
|
||||
extra_css_class = options[:class] || ''
|
||||
|
||||
if total > 0 || spent > 0
|
||||
spent_css_classes = 'spent-amount'
|
||||
spent_css_classes << " #{overage_class(spent, total)}"
|
||||
spent_css_classes << ' ' << extra_css_class
|
||||
total_css_classes = 'total-amount white'
|
||||
total_css_classes << ' ' << extra_css_class
|
||||
|
||||
content_tag(:td, h(format_value_field_for_contracts(spent)), :class => spent_css_classes) +
|
||||
content_tag(:td, h(format_value_field_for_contracts(total)), :class => total_css_classes)
|
||||
else
|
||||
content_tag(:td, '----', :colspan => '2', :class => 'no-value ' + extra_css_class)
|
||||
end
|
||||
end
|
||||
|
||||
def format_deliverable_value_fields(value)
|
||||
number_with_precision(value, :precision => Deliverable::ViewPrecision, :delimiter => '')
|
||||
end
|
||||
|
||||
def format_deliverable_value_fields_as_dollar_or_percent(value)
|
||||
case
|
||||
when value.blank? || value.to_s.delete('$%').blank?
|
||||
''
|
||||
when value.to_s.match('%')
|
||||
h(value)
|
||||
else # currency or straight amount
|
||||
number_to_currency(value.to_s.gsub('$',''), :precision => Deliverable::ViewPrecision, :delimiter => '', :unit => '$')
|
||||
end
|
||||
end
|
||||
|
||||
def format_hourly_rate(decimal)
|
||||
number_to_currency(decimal, :precision => 0) + "/hr" if decimal
|
||||
end
|
||||
|
||||
def format_payment_terms(value)
|
||||
return '' if value.blank?
|
||||
return h(value.name)
|
||||
end
|
||||
|
||||
def format_value_field_for_contracts(value, options={})
|
||||
opt = {:unit => '', :precision => Contract::ViewPrecision, :delimiter => ','}.merge(options)
|
||||
number_to_currency(value, opt)
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,22 +1,52 @@
|
||||
module ContractsHelper
|
||||
def setup_nested_deliverable_records(deliverable)
|
||||
returning(deliverable) do |d|
|
||||
d.labor_budgets.build if d.labor_budgets.empty?
|
||||
d.overhead_budgets.build if d.overhead_budgets.empty?
|
||||
deliverable.labor_budgets.build if deliverable.labor_budgets.empty?
|
||||
deliverable.overhead_budgets.build if deliverable.overhead_budgets.empty?
|
||||
deliverable.fixed_budgets.build if deliverable.fixed_budgets.empty?
|
||||
deliverable
|
||||
end
|
||||
|
||||
def group_contracts_by_status(contracts)
|
||||
grouped_contracts = contracts.inject({}) do |grouped, contract|
|
||||
grouped[contract.status] ||= []
|
||||
grouped[contract.status] << contract
|
||||
grouped
|
||||
end
|
||||
grouped_contracts["open"] ||= []
|
||||
grouped_contracts["locked"] ||= []
|
||||
grouped_contracts["closed"] ||= []
|
||||
grouped_contracts
|
||||
end
|
||||
|
||||
def grouped_deliverable_options_for_select(project, selected_key=nil)
|
||||
project.contracts.all(:include => :deliverables).inject("") do |html, contract|
|
||||
if contract.closed? && !contract.includes_deliverable_id?(selected_key)
|
||||
html
|
||||
else
|
||||
html << content_tag(:optgroup,
|
||||
deliverable_options_for_contract(contract, selected_key).join("\n"),
|
||||
:label => h(contract.name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def format_budget_for_deliverable(deliverable, spent, total, options={})
|
||||
extra_css_class = options[:class] || ''
|
||||
|
||||
if total > 0 || spent > 0
|
||||
content_tag(:td, h(format_value_field_for_contracts(spent)), :class => 'spent-amount ' + extra_css_class) +
|
||||
content_tag(:td, h(format_value_field_for_contracts(total)), :class => 'total-amount white ' + extra_css_class)
|
||||
else
|
||||
content_tag(:td, '----', :colspan => '2', :class => 'no-value ' + extra_css_class)
|
||||
def deliverable_options_for_contract(contract, selected_key)
|
||||
contract.deliverables.collect do |deliverable|
|
||||
deliverable_option(deliverable, selected_key)
|
||||
end
|
||||
end
|
||||
|
||||
def deliverable_option(deliverable, selected_key)
|
||||
option_attributes = {}
|
||||
option_attributes[:value] = h(deliverable.id)
|
||||
option_attributes[:selected] = "selected" if selected_key.to_i == deliverable.id
|
||||
option_attributes[:disabled] = "disabled" if (deliverable.locked? || deliverable.contract_locked?) && selected_key.to_i != deliverable.id
|
||||
|
||||
return "" if deliverable.closed? && option_attributes[:selected].blank? # Skip unselected, closed
|
||||
|
||||
content_tag(:option, h(deliverable.title), option_attributes)
|
||||
end
|
||||
|
||||
# Simple helper to show the values of a field on an object in a standard format
|
||||
#
|
||||
# <p>
|
||||
@@ -30,7 +60,8 @@ module ContractsHelper
|
||||
|
||||
formatter = options[:format]
|
||||
raw_content = options[:raw] || false
|
||||
wrap_in_td = options[:wrap_in_td] || true
|
||||
wrap_in_td = options[:wrap_in_td]
|
||||
wrap_in_td = true if wrap_in_td.nil?
|
||||
|
||||
content = ''
|
||||
|
||||
@@ -59,41 +90,95 @@ module ContractsHelper
|
||||
def show_budget_field(object, spent_field, total_field, options={})
|
||||
|
||||
formatter = options[:format] || :number_to_currency
|
||||
spent_content = send(formatter, object.send(spent_field))
|
||||
total_content = send(formatter, object.send(total_field))
|
||||
spent_value = object.send(spent_field)
|
||||
total_value = object.send(total_field)
|
||||
spent_content = send(formatter, spent_value)
|
||||
total_content = send(formatter, total_value)
|
||||
|
||||
# Show overages except for profit fields
|
||||
overage_css_class = overage_class(spent_value, total_value) unless spent_field.to_s.match(/profit/i)
|
||||
|
||||
show_field(object, spent_field, options.merge(:raw => true, :wrap_in_td => false)) do
|
||||
|
||||
content_tag(:td, h(spent_content), :class => 'spent') +
|
||||
content_tag(:td, h(spent_content), :class => "spent #{overage_css_class}") +
|
||||
content_tag(:td, h(total_content), :class => 'budget')
|
||||
end
|
||||
end
|
||||
|
||||
def format_hourly_rate(decimal)
|
||||
number_to_currency(decimal) + "/hr" if decimal
|
||||
end
|
||||
def retainer_period_options(deliverable, method_options={})
|
||||
selected = method_options[:selected]
|
||||
if selected && selected.is_a?(Date)
|
||||
selected = selected.strftime("%Y-%m")
|
||||
end
|
||||
|
||||
def format_payment_terms(value)
|
||||
return '' if value.blank?
|
||||
return h(value.name)
|
||||
end
|
||||
|
||||
def format_deliverable_value_fields(value)
|
||||
number_with_precision(value, :precision => Deliverable::ViewPrecision, :delimiter => '')
|
||||
end
|
||||
|
||||
def format_value_field_for_contracts(value)
|
||||
number_with_precision(value, :precision => Contract::ViewPrecision, :delimiter => ',')
|
||||
end
|
||||
|
||||
def retainer_period_options(deliverable)
|
||||
options = []
|
||||
options << content_tag(:option, l(:label_all).capitalize, :value => '')
|
||||
|
||||
deliverable.months.collect do |month|
|
||||
options << content_tag(:option, month.strftime("%B %Y"), :value => month.strftime("%Y-%m"))
|
||||
value = month.strftime("%Y-%m")
|
||||
options << content_tag(:option, month.strftime("%B %Y"), :value => value, :selected => (selected == value) ? 'selected' : nil)
|
||||
end
|
||||
|
||||
options
|
||||
end
|
||||
|
||||
# Given a deliverable and period, validate the period
|
||||
# TODO: could use a better name
|
||||
def validate_period(deliverable, period)
|
||||
if deliverable.current_date && deliverable.within_period_range?(period)
|
||||
return period
|
||||
end
|
||||
end
|
||||
|
||||
# Should the markup be display?
|
||||
#
|
||||
# On Contracts and Deliverables, markup is hidden if both the spent
|
||||
# and budget is 0.
|
||||
def show_markup_for?(object, date=nil)
|
||||
if object.is_a?(Contract)
|
||||
!(object.fixed_markup_spent == 0 && object.fixed_markup_budget == 0)
|
||||
elsif object.is_a?(Deliverable)
|
||||
!(object.fixed_markup_budget_total_spent(date) == 0 && object.fixed_markup_budget_total(date) == 0)
|
||||
else
|
||||
true
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def link_to_issue_list_with_filter(text, options={})
|
||||
deliverable_id = options[:deliverable_id] || '*'
|
||||
status_id = options[:status_id] || '*'
|
||||
|
||||
link_to(h(text), {
|
||||
:controller => 'issues',
|
||||
:action => 'index',
|
||||
:project_id => @project,
|
||||
:set_filter => 't',
|
||||
:status_id => status_id,
|
||||
:deliverable_id => deliverable_id
|
||||
})
|
||||
|
||||
end
|
||||
|
||||
def release(version=5, message='')
|
||||
return '' unless (1..5).include?(version)
|
||||
image_tag("todo#{version}.png", :plugin => 'redmine_contracts', :title => "Coming in release #{version}. #{message}")
|
||||
end
|
||||
|
||||
# Overage occurs when spent is negative or spent is greater than budget
|
||||
def overage?(spent, budget, options={})
|
||||
return false unless spent && budget
|
||||
return true if spent < 0
|
||||
|
||||
spent.to_f > budget.to_f
|
||||
end
|
||||
|
||||
def overage_class(spent, budget, options={})
|
||||
if overage?(spent, budget, options)
|
||||
'overage'
|
||||
else
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Contract < ActiveRecord::Base
|
||||
unloadable
|
||||
extend ActiveSupport::Memoizable
|
||||
|
||||
ViewPrecision = 0
|
||||
|
||||
@@ -16,7 +17,9 @@ class Contract < ActiveRecord::Base
|
||||
validates_presence_of :start_date
|
||||
validates_presence_of :end_date
|
||||
validates_inclusion_of :discount_type, :in => %w($ %), :allow_blank => true, :allow_nil => true
|
||||
validates_inclusion_of :status, :in => ["open","locked","closed"], :allow_blank => true, :allow_nil => true
|
||||
validate :start_and_end_date_are_valid
|
||||
validate_on_update :validate_status_changes
|
||||
|
||||
# Accessors
|
||||
attr_accessible :name
|
||||
@@ -32,95 +35,303 @@ class Contract < ActiveRecord::Base
|
||||
attr_accessible :po_number
|
||||
attr_accessible :client_point_of_contact
|
||||
attr_accessible :details
|
||||
attr_accessible :status
|
||||
|
||||
named_scope :by_name, {:order => "#{Contract.table_name}.name ASC"}
|
||||
|
||||
[:status, :contract_type,
|
||||
:fixed_spent, :fixed_budget,
|
||||
:markup_spent, :markup_budget,
|
||||
named_scope :with_status, lambda {|statuses|
|
||||
{
|
||||
:conditions => ["#{Contract.table_name}.status IN (?)", statuses]
|
||||
}
|
||||
}
|
||||
|
||||
[:contract_type,
|
||||
:discount_spent, :discount_budget
|
||||
].each do |mthd|
|
||||
define_method(mthd) { "TODO in later release" }
|
||||
end
|
||||
|
||||
# OPTIMIZE: N+1
|
||||
def labor_budget
|
||||
deliverables.inject(0) {|total, deliverable| total += deliverable.labor_budget_total }
|
||||
def status
|
||||
read_attribute(:status) || "open"
|
||||
end
|
||||
|
||||
def lock!
|
||||
update_attribute(:status, "locked")
|
||||
end
|
||||
|
||||
def close!
|
||||
update_attribute(:status, "closed")
|
||||
end
|
||||
|
||||
def open?
|
||||
self.status == "open"
|
||||
end
|
||||
|
||||
def locked?
|
||||
self.status == "locked"
|
||||
end
|
||||
|
||||
def closed?
|
||||
self.status == "closed"
|
||||
end
|
||||
|
||||
def includes_deliverable_id?(deliverable_id)
|
||||
deliverable_ids.include?(deliverable_id.to_i)
|
||||
end
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Labor Methods
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# Currency value that is budgeted for the contract for labor
|
||||
# ie. estimated billable amount
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
def labor_budget
|
||||
summarize_associated_values(deliverables, :labor_budget_total)
|
||||
end
|
||||
memoize :labor_budget
|
||||
|
||||
# Currency value that is spent for the contract on labor
|
||||
# ie. actual billable cost
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
# OPTIMIZE: also hits redmine_overhead which is known to be slow
|
||||
def labor_spent
|
||||
deliverables.inject(0) {|total, deliverable| total += deliverable.labor_budget_spent }
|
||||
summarize_associated_values(deliverables, :labor_budget_spent)
|
||||
end
|
||||
memoize :labor_spent
|
||||
|
||||
# Hours budgeted for the contract for labor
|
||||
# ie. estimated billable hours
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
def labor_hour_budget
|
||||
summarize_associated_values(deliverables, :labor_budget_hours)
|
||||
end
|
||||
memoize :labor_hour_budget
|
||||
|
||||
# Hours spent for the contract on labor
|
||||
# ie. actual billable time worked
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
# OPTIMIZE: also hits redmine_overhead which is known to be slow
|
||||
def labor_hour_spent
|
||||
summarize_associated_values(deliverables, :labor_hours_spent_total)
|
||||
end
|
||||
memoize :labor_hour_spent
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Overhead Methods
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# Currency value budgeted for the contract on overhead
|
||||
# ie. estimated non-billable amount
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
def overhead_budget
|
||||
deliverables.inject(0) {|total, deliverable| total += deliverable.overhead_budget_total }
|
||||
summarize_associated_values(deliverables, :overhead_budget_total)
|
||||
end
|
||||
memoize :overhead_budget
|
||||
|
||||
# Currency value spent for the contract on overhead work
|
||||
# ie. actual non-billable used
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
# OPTIMIZE: also hits redmine_overhead which is known to be slow
|
||||
def overhead_spent
|
||||
deliverables.inject(0) {|total, deliverable| total += deliverable.overhead_spent }
|
||||
summarize_associated_values(deliverables, :overhead_spent)
|
||||
end
|
||||
memoize :overhead_spent
|
||||
|
||||
# Hours budgeted for the contract for overhead
|
||||
# ie. estimated non-billable time
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
def overhead_hour_budget
|
||||
summarize_associated_values(deliverables, :overhead_budget_hours)
|
||||
end
|
||||
memoize :overhead_hour_budget
|
||||
|
||||
# Hours used for the contract on overhead
|
||||
# ie. actual time spent on non-billable work
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
# OPTIMIZE: also hits redmine_overhead which is known to be slow
|
||||
def overhead_hour_spent
|
||||
summarize_associated_values(deliverables, :overhead_hours_spent_total)
|
||||
end
|
||||
memoize :overhead_hour_spent
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Total Methods (labor + overhead)
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# Total hours budgeted for the contract
|
||||
# ie. total time estimated
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
def estimated_hour_budget
|
||||
deliverables.inject(0) {|total, deliverable| total += deliverable.estimated_hour_budget_total }
|
||||
summarize_associated_values(deliverables, :estimated_hour_budget_total)
|
||||
end
|
||||
memoize :estimated_hour_budget
|
||||
|
||||
# Total hours spent on the contract
|
||||
# ie. hours used
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
def estimated_hour_spent
|
||||
deliverables.inject(0) {|total, deliverable| total += deliverable.hours_spent_total }
|
||||
summarize_associated_values(deliverables, :hours_spent_total)
|
||||
end
|
||||
memoize :estimated_hour_spent
|
||||
|
||||
# Currency amount budgeted for the contract
|
||||
# ie. estimated budget
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
def total_budget
|
||||
deliverables.inject(0) {|total, deliverable| total += deliverable.total }
|
||||
summarize_associated_values(deliverables, :total)
|
||||
end
|
||||
memoize :total_budget
|
||||
|
||||
# Currency amount spent on the contract
|
||||
# ie. amount spent already
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
def total_spent
|
||||
deliverables.inject(0) {|total, deliverable| total += deliverable.total_spent }
|
||||
summarize_associated_values(deliverables, :total_spent)
|
||||
end
|
||||
memoize :total_spent
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Profit Methods
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# Estimated currency amount of profit
|
||||
# ie. profit estimate
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
def profit_budget
|
||||
deliverables.inject(0) {|total, deliverable| total += deliverable.profit_budget }
|
||||
summarize_associated_values(deliverables, :profit_budget)
|
||||
end
|
||||
memoize :profit_budget
|
||||
|
||||
# Amount of the profit that is left in the contract
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
def profit_left
|
||||
deliverables.inject(0) {|total, deliverable| total += deliverable.profit_left }
|
||||
summarize_associated_values(deliverables, :profit_left)
|
||||
end
|
||||
alias_method :profit_spent, :profit_left
|
||||
memoize :profit_left
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Fixed Budget Methods
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# Currency amount of estimated fixed expenses
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
def fixed_budget
|
||||
summarize_associated_values(deliverables, :fixed_budget_total)
|
||||
end
|
||||
memoize :fixed_budget
|
||||
|
||||
# Currency amount of spent fixed expenses
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
def fixed_spent
|
||||
summarize_associated_values(deliverables, :fixed_budget_total_spent)
|
||||
end
|
||||
memoize :fixed_spent
|
||||
|
||||
# Currency amount of estimated fixed expense markups
|
||||
# ie. estimated fixed expense markups
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
def fixed_markup_budget
|
||||
summarize_associated_values(deliverables, :fixed_markup_budget_total)
|
||||
end
|
||||
memoize :fixed_markup_budget
|
||||
|
||||
# Currency amount of fixed expenses spent
|
||||
#
|
||||
# OPTIMIZE: N+1
|
||||
def fixed_markup_spent
|
||||
summarize_associated_values(deliverables, :fixed_markup_budget_total_spent)
|
||||
end
|
||||
memoize :fixed_markup_spent
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def after_initialize
|
||||
self.executed = false unless self.executed.present?
|
||||
self.status = "open" unless self.status.present?
|
||||
end
|
||||
|
||||
# Are the start_date and end_date valid?
|
||||
def start_and_end_date_are_valid
|
||||
if start_date && end_date && end_date < start_date
|
||||
errors.add :end_date, :greater_than_start_date
|
||||
end
|
||||
end
|
||||
|
||||
def valid_status_change?
|
||||
change_to_status_only? || changing_to_the_open_status? || changing_from_the_open_status?
|
||||
end
|
||||
|
||||
def change_to_status_only?
|
||||
["status"] == changes.keys
|
||||
end
|
||||
|
||||
def changing_to_the_open_status?
|
||||
changes["status"].present? && "open" == changes["status"].second
|
||||
end
|
||||
|
||||
def changing_from_the_open_status?
|
||||
changes["status"].present? && "open" == changes["status"].first
|
||||
end
|
||||
|
||||
# TODO: duplicated on Deliverable, refactor after one more duplication
|
||||
def validate_status_changes
|
||||
return if valid_status_change?
|
||||
|
||||
errors.add_to_base(:cant_update_locked_contract) if locked?
|
||||
errors.add_to_base(:cant_update_closed_contract) if closed?
|
||||
end
|
||||
|
||||
# Currency amount of time that is logged to the project or to issues
|
||||
# that are not assigned to a Deliverable
|
||||
def orphaned_time
|
||||
@orphaned_time ||= project.time_entries.all(:include => [:issue => :deliverable],
|
||||
:conditions => "#{Issue.table_name}.deliverable_id IS NULL OR #{TimeEntry.table_name}.issue_id IS NULL").inject(0) do |total, time_entry|
|
||||
total += time_entry.cost
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
|
||||
if Rails.env.test?
|
||||
generator_for :name, :method => :next_name
|
||||
generator_for :name, :start => "Contract 0000"
|
||||
generator_for :executed => true
|
||||
generator_for(:start_date) { Date.yesterday }
|
||||
generator_for(:end_date) { Date.tomorrow }
|
||||
|
||||
def self.next_name
|
||||
@last_name ||= 'Contract 0000'
|
||||
@last_name.succ!
|
||||
end
|
||||
generator_for :discount, ''
|
||||
generator_for :details, ''
|
||||
generator_for :discount_note, ''
|
||||
generator_for :client_point_of_contact, ''
|
||||
generator_for :client_ap_contact_information, ''
|
||||
generator_for :po_number, ''
|
||||
generator_for :status, 'open'
|
||||
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# This is a potential N+1 method since value_method might be calculated
|
||||
def summarize_associated_values(records, value_method)
|
||||
records.inject(0) {|total, record| total += record.send(value_method)}
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -8,22 +8,38 @@ class Deliverable < ActiveRecord::Base
|
||||
belongs_to :manager, :class_name => 'User', :foreign_key => 'manager_id'
|
||||
has_many :labor_budgets
|
||||
has_many :overhead_budgets
|
||||
has_many :issues
|
||||
has_many :fixed_budgets
|
||||
has_many :issues, :dependent => :nullify
|
||||
|
||||
accepts_nested_attributes_for :labor_budgets
|
||||
accepts_nested_attributes_for :overhead_budgets
|
||||
accepts_nested_attributes_for :fixed_budgets
|
||||
|
||||
# Validations
|
||||
validates_presence_of :title
|
||||
validates_presence_of :type
|
||||
validates_presence_of :manager
|
||||
validates_inclusion_of :status, :in => ["open","locked","closed"], :allow_blank => true, :allow_nil => true
|
||||
validate_on_update :validate_status_changes
|
||||
validate :validate_contract_status
|
||||
|
||||
# Accessors
|
||||
include DollarizedAttribute
|
||||
dollarized_attribute :total
|
||||
|
||||
delegate :name, :to => :contract, :prefix => true, :allow_nil => true
|
||||
delegate "open?", :to => :contract, :prefix => true, :allow_nil => true
|
||||
delegate "closed?", :to => :contract, :prefix => true, :allow_nil => true
|
||||
delegate "locked?", :to => :contract, :prefix => true, :allow_nil => true
|
||||
|
||||
# Callbacks
|
||||
|
||||
before_destroy :block_on_locked_contracts
|
||||
before_destroy :block_on_closed_contracts
|
||||
|
||||
def after_initialize
|
||||
self.status = "open" unless self.status.present?
|
||||
end
|
||||
|
||||
# Register callbacks here, on new records the class isn't set so class-specific
|
||||
# callbacks don't fire.
|
||||
def after_save
|
||||
@@ -33,11 +49,107 @@ class Deliverable < ActiveRecord::Base
|
||||
end
|
||||
|
||||
named_scope :by_title, {:order => "#{Deliverable.table_name}.title ASC"}
|
||||
named_scope :with_status, lambda {|statuses|
|
||||
{
|
||||
:conditions => ["#{Deliverable.table_name}.status IN (?)", statuses]
|
||||
}
|
||||
}
|
||||
|
||||
def short_type
|
||||
''
|
||||
end
|
||||
|
||||
def humanize_type
|
||||
type.to_s.sub('Deliverable','')
|
||||
end
|
||||
|
||||
# Deliverable's aren't dated. Subclasses may override this for period behavior.
|
||||
def current_date
|
||||
nil
|
||||
end
|
||||
|
||||
def lock!
|
||||
update_attribute(:status, "locked")
|
||||
end
|
||||
|
||||
def close!
|
||||
update_attribute(:status, "closed")
|
||||
end
|
||||
|
||||
def open?
|
||||
self.status == "open"
|
||||
end
|
||||
|
||||
def locked?
|
||||
self.status == "locked"
|
||||
end
|
||||
|
||||
def closed?
|
||||
self.status == "closed"
|
||||
end
|
||||
|
||||
def editable?
|
||||
(new_record? || open?)
|
||||
end
|
||||
|
||||
def valid_status_change?
|
||||
change_to_status_only? || changing_to_the_open_status? || changing_from_the_open_status?
|
||||
end
|
||||
|
||||
def change_to_status_only?
|
||||
["status"] == changes.keys
|
||||
end
|
||||
|
||||
def changing_to_the_open_status?
|
||||
changes["status"].present? && "open" == changes["status"].second
|
||||
end
|
||||
|
||||
def changing_from_the_open_status?
|
||||
changes["status"].present? && "open" == changes["status"].first
|
||||
end
|
||||
|
||||
# TODO: duplicated on Contract, refactor after one more duplication
|
||||
def validate_status_changes
|
||||
return if valid_status_change?
|
||||
|
||||
errors.add_to_base(:cant_update_locked_deliverable) if locked?
|
||||
errors.add_to_base(:cant_update_closed_deliverable) if closed?
|
||||
end
|
||||
|
||||
def validate_contract_status
|
||||
return if contract_open?
|
||||
return if change_to_status_only?
|
||||
|
||||
if contract_locked?
|
||||
if new_record?
|
||||
errors.add_to_base(:cant_create_deliverable_on_locked_contract)
|
||||
else
|
||||
errors.add_to_base(:cant_update_locked_contract)
|
||||
end
|
||||
end
|
||||
|
||||
if contract_closed?
|
||||
if new_record?
|
||||
errors.add_to_base(:cant_create_deliverable_on_closed_contract)
|
||||
else
|
||||
errors.add_to_base(:cant_update_closed_contract)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# No operation method, useful to clean up logic with an optional message
|
||||
# for documentation
|
||||
def noop(message="")
|
||||
end
|
||||
|
||||
def block_on_locked_contracts
|
||||
!contract_locked?
|
||||
end
|
||||
|
||||
def block_on_closed_contracts
|
||||
!contract_closed?
|
||||
end
|
||||
|
||||
def to_s
|
||||
title
|
||||
end
|
||||
@@ -46,45 +158,113 @@ class Deliverable < ActiveRecord::Base
|
||||
self.class.to_s.underscore
|
||||
end
|
||||
|
||||
def total=(v)
|
||||
if v.is_a? String
|
||||
write_attribute(:total, v.gsub(/[$ ,]/, ''))
|
||||
else
|
||||
write_attribute(:total, v)
|
||||
def labor_budget_total(date=nil)
|
||||
memoize_by_date("@labor_budget_total", date) do
|
||||
labor_budgets.sum(:budget)
|
||||
end
|
||||
end
|
||||
|
||||
def labor_budget_total(date=nil)
|
||||
labor_budgets.sum(:budget)
|
||||
end
|
||||
|
||||
def overhead_budget_total(date=nil)
|
||||
overhead_budgets.sum(:budget)
|
||||
memoize_by_date("@overhead_budget_total", date) do
|
||||
overhead_budgets.sum(:budget)
|
||||
end
|
||||
end
|
||||
|
||||
# The amount of profit that is budgeted for this deliverable.
|
||||
# Profit = Total - ( Labor + Overhead + Fixed + Markup )
|
||||
def profit_budget(date=nil)
|
||||
nil
|
||||
memoize_by_date("@profit_budget", date) do
|
||||
budgets = labor_budget_total(date) + overhead_budget_total(date) + fixed_budget_total(date) + fixed_markup_budget_total(date)
|
||||
(total(date) || 0.0) - budgets
|
||||
end
|
||||
end
|
||||
|
||||
# The amount of money remaining after expenses have been taken out
|
||||
# Profit left = Total - Labor spent - Overhead spent - Fixed - Markup
|
||||
def profit_left(date=nil)
|
||||
memoize_by_date("@profit_left", date) do
|
||||
total_spent(date) - labor_budget_spent(date) - overhead_spent(date) - fixed_budget_total_spent(date) - fixed_markup_budget_total_spent(date)
|
||||
end
|
||||
end
|
||||
|
||||
def labor_budget_hours(date=nil)
|
||||
labor_budgets.sum(:hours)
|
||||
memoize_by_date("@labor_budget_hours", date) do
|
||||
labor_budgets.sum(:hours)
|
||||
end
|
||||
end
|
||||
|
||||
def overhead_budget_hours(date=nil)
|
||||
memoize_by_date("@overhead_budget_hours", date) do
|
||||
overhead_budgets.sum(:hours)
|
||||
end
|
||||
end
|
||||
|
||||
# Total number of hours estimated in the Deliverable's budgets
|
||||
def estimated_hour_budget_total
|
||||
(labor_budgets.sum(:hours) || 0.0) +
|
||||
(overhead_budgets.sum(:hours) || 0.0)
|
||||
def estimated_hour_budget_total(date=nil)
|
||||
memoize_by_date("@estimated_hour_budget_total", date) do
|
||||
labor_budget_hours(date) + overhead_budget_hours(date)
|
||||
end
|
||||
end
|
||||
|
||||
# OPTIMIZE: N+1
|
||||
def hours_spent_total
|
||||
issues.inject(0) {|total, issue| total += issue.spent_hours }
|
||||
def labor_hours_spent_total(date=nil)
|
||||
memoize_by_date("@labor_hours_spent_total", date) do
|
||||
issues.inject(0) {|total, issue| total += issue.billable_time_spent } # From redmine_overhead
|
||||
end
|
||||
end
|
||||
|
||||
# OPTIMIZE: N+1
|
||||
def overhead_hours_spent_total(date=nil)
|
||||
memoize_by_date("@overhead_hours_spent_total", date) do
|
||||
issues.inject(0) {|total, issue| total += issue.overhead_time_spent } # From redmine_overhead
|
||||
end
|
||||
end
|
||||
|
||||
def hours_spent_total(date=nil)
|
||||
return 0 if issues.empty?
|
||||
|
||||
# Don't count subissues
|
||||
TimeEntry.sum(:hours, :conditions => { :issue_id => issues.collect(&:id) })
|
||||
end
|
||||
|
||||
def fixed_budget_total(date=nil)
|
||||
memoize_by_date("@fixed_budget_total", date) do
|
||||
fixed_budgets.sum(:budget)
|
||||
end
|
||||
end
|
||||
|
||||
def fixed_budget_total_spent(date=nil)
|
||||
memoize_by_date("@fixed_budget_total_spent", date) do
|
||||
fixed_budgets.paid.sum(:budget)
|
||||
end
|
||||
end
|
||||
|
||||
# OPTIMIZE: N+1
|
||||
def fixed_markup_budget_total(date=nil)
|
||||
memoize_by_date("@fixed_markup_budget_total", date) do
|
||||
fixed_budgets.inject(0) {|total, fixed_budget| total += fixed_budget.markup_value }
|
||||
end
|
||||
end
|
||||
|
||||
# OPTIMIZE: N+1
|
||||
def fixed_markup_budget_total_spent(date=nil)
|
||||
memoize_by_date("@fixed_markup_budget_total_spent", date) do
|
||||
fixed_budgets.paid.inject(0) {|total, fixed_budget| total += fixed_budget.markup_value }
|
||||
end
|
||||
end
|
||||
|
||||
def filter_by_date(date=nil, &block)
|
||||
block.call
|
||||
end
|
||||
|
||||
def issues_by_status
|
||||
issues.inject({}) {|grouped, issue|
|
||||
grouped[issue.status] ||= []
|
||||
grouped[issue.status] << issue
|
||||
grouped
|
||||
}
|
||||
end
|
||||
|
||||
def retainer?
|
||||
type == "RetainerDeliverable"
|
||||
end
|
||||
@@ -133,12 +313,34 @@ class Deliverable < ActiveRecord::Base
|
||||
|
||||
if Rails.env.test?
|
||||
generator_for :title, :method => :next_title
|
||||
|
||||
generator_for :status, 'open'
|
||||
|
||||
def self.next_title
|
||||
@last_title ||= 'Deliverable 0000'
|
||||
@last_title.succ!
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def memoize_by_date(ivar, date, &block)
|
||||
cache_hash = instance_variable_get(ivar)
|
||||
cache_hash ||= {}
|
||||
|
||||
if date
|
||||
if date.is_a?(Date)
|
||||
cache_key = "#{date.year}-#{date.month}"
|
||||
else
|
||||
cache_key = :invalid
|
||||
end
|
||||
else
|
||||
cache_key = :all
|
||||
end
|
||||
|
||||
cache_hash[cache_key] ||= block.call
|
||||
instance_variable_set(ivar, cache_hash)
|
||||
|
||||
cache_hash[cache_key]
|
||||
end
|
||||
end
|
||||
|
||||
66
app/models/fixed_budget.rb
Normal file
66
app/models/fixed_budget.rb
Normal file
@@ -0,0 +1,66 @@
|
||||
class FixedBudget < ActiveRecord::Base
|
||||
unloadable
|
||||
|
||||
# Associations
|
||||
belongs_to :deliverable
|
||||
|
||||
# Validations
|
||||
|
||||
# Accessors
|
||||
include DollarizedAttribute
|
||||
dollarized_attribute :budget
|
||||
|
||||
named_scope :by_period, lambda {|date|
|
||||
if date
|
||||
{
|
||||
:conditions => {:year => date.year, :month => date.month}
|
||||
}
|
||||
end
|
||||
}
|
||||
|
||||
named_scope :paid, {:conditions => {:paid => true}}
|
||||
|
||||
def markup_value
|
||||
return 0 if budget.blank? || markup.blank?
|
||||
|
||||
case
|
||||
when percent_markup?
|
||||
percent = markup.gsub('%','').to_f
|
||||
return budget.to_f * (percent / 100)
|
||||
when dollar_markup?
|
||||
markup.gsub('$','').gsub(',','').to_f
|
||||
when straight_markup?
|
||||
markup.to_f
|
||||
else
|
||||
0 # Invalid markup
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def budget_spent
|
||||
if paid?
|
||||
budget
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
def percent_markup?
|
||||
markup && markup.to_s.match(/%/)
|
||||
end
|
||||
|
||||
def dollar_markup?
|
||||
markup && markup.to_s.match(/[$,]+/)
|
||||
end
|
||||
|
||||
def straight_markup?
|
||||
markup && markup.to_s.match(/[\d.]/)
|
||||
end
|
||||
|
||||
# Is this a blank budget item. Retainers will create blank ones when
|
||||
# they are copied. (RetainerDeliverable#create_budgets_for_periods)
|
||||
def blank_record?
|
||||
return true if new_record?
|
||||
return title.blank? && budget.blank? && markup.blank?
|
||||
end
|
||||
end
|
||||
@@ -16,23 +16,18 @@ class FixedDeliverable < Deliverable
|
||||
end
|
||||
|
||||
# Fixed deliverables are always 100% spent
|
||||
def total_spent
|
||||
def total_spent(date=nil)
|
||||
total
|
||||
end
|
||||
|
||||
# The amount of profit that is budgeted for this deliverable.
|
||||
# Profit = Total - ( Labor + Overhead + Fixed + Markup )
|
||||
def profit_budget(date=nil)
|
||||
budgets = labor_budget_total(date) + overhead_budget_total(date)
|
||||
(total(date) || 0.0) - budgets
|
||||
# Fixed deliverables are always 100% spent so they markup is captured
|
||||
# right away.
|
||||
def fixed_markup_budget_total_spent(date=nil)
|
||||
memoize_by_date("@fixed_markup_budget_total_spent", date) do
|
||||
fixed_markup_budget_total(date)
|
||||
end
|
||||
end
|
||||
|
||||
# The amount of money remaining after expenses have been taken out
|
||||
# Profit left = Total - Labor spent - Overhead spent
|
||||
def profit_left
|
||||
total_spent - labor_budget_spent - overhead_spent
|
||||
end
|
||||
|
||||
# Hardcoded value used as a wrapper for the old Budget plugin API.
|
||||
#
|
||||
# The Overhead plugin uses this in it's calcuations.
|
||||
|
||||
@@ -14,28 +14,32 @@ class HourlyDeliverable < Deliverable
|
||||
'H'
|
||||
end
|
||||
|
||||
# Total = ( Labor Hours * Billing Rate ) + ( Fixed + Markup )
|
||||
def total(date=nil)
|
||||
return 0 if contract.nil?
|
||||
return 0 if contract.billable_rate.blank?
|
||||
return 0 if labor_budgets.count == 0 && overhead_budgets.count == 0
|
||||
memoize_by_date("@total", date) do
|
||||
return 0 if contract.nil?
|
||||
return 0 if contract.billable_rate.blank?
|
||||
return 0 if labor_budgets.count == 0 && overhead_budgets.count == 0
|
||||
|
||||
return contract.billable_rate * labor_budget_hours(date)
|
||||
fixed_budget_amount = fixed_budget_total(date) + fixed_markup_budget_total(date)
|
||||
return (contract.billable_rate * labor_budget_hours(date)) + fixed_budget_amount
|
||||
end
|
||||
end
|
||||
|
||||
# Total amount to be billed on the deliverable, using the total time logged
|
||||
# and the contract rate
|
||||
def total_spent
|
||||
return 0 if contract.nil?
|
||||
return 0 if contract.billable_rate.blank?
|
||||
return 0 unless self.issues.count > 0
|
||||
def total_spent(date=nil)
|
||||
memoize_by_date("@total_spent", date) do
|
||||
return 0 if contract.nil?
|
||||
return 0 if contract.billable_rate.blank?
|
||||
return 0 unless self.issues.count > 0
|
||||
|
||||
time_logs = self.issues.collect(&:time_entries).flatten
|
||||
hours = time_logs.inject(0) {|total, time_entry|
|
||||
total += time_entry.hours if time_entry.billable?
|
||||
total
|
||||
}
|
||||
time_logs = self.issues.collect(&:time_entries).flatten
|
||||
hours = billable_hours_on_time_entries(time_logs)
|
||||
|
||||
return hours * contract.billable_rate
|
||||
fixed_budget_amount = fixed_budget_total_spent(date) + fixed_markup_budget_total_spent(date)
|
||||
return (hours * contract.billable_rate) + fixed_budget_amount
|
||||
end
|
||||
end
|
||||
|
||||
# Block setting the total on HourlyDeliverables
|
||||
@@ -47,16 +51,20 @@ class HourlyDeliverable < Deliverable
|
||||
write_attribute(:total, nil)
|
||||
end
|
||||
|
||||
# The amount of profit that is budgeted for this deliverable
|
||||
# Profit = Total - ( Labor + Overhead + Fixed + Markup )
|
||||
def profit_budget(date=nil)
|
||||
budgets = labor_budget_total(date) + overhead_budget_total(date)
|
||||
(total(date) || 0.0) - budgets
|
||||
protected
|
||||
|
||||
def billable_hours_on_time_entries(time_entries)
|
||||
hours_on_time_entries_with_billable_option(true, time_entries)
|
||||
end
|
||||
|
||||
# The amount of money remaining after expenses have been taken out
|
||||
# Profit left = Total - Labor spent - Overhead spent
|
||||
def profit_left
|
||||
total_spent - labor_budget_spent - overhead_spent
|
||||
def nonbillable_hours_on_time_entries(time_entries)
|
||||
hours_on_time_entries_with_billable_option(false, time_entries)
|
||||
end
|
||||
|
||||
def hours_on_time_entries_with_billable_option(billable, time_entries)
|
||||
time_entries.inject(0) {|total, time_entry|
|
||||
total += time_entry.hours if (time_entry.billable? == billable)
|
||||
total
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,12 +7,6 @@ class LaborBudget < ActiveRecord::Base
|
||||
# Validations
|
||||
|
||||
# Accessors
|
||||
|
||||
def budget=(v)
|
||||
if v.is_a? String
|
||||
write_attribute(:budget, v.gsub(/[$ ,]/, ''))
|
||||
else
|
||||
write_attribute(:budget, v)
|
||||
end
|
||||
end
|
||||
include DollarizedAttribute
|
||||
dollarized_attribute :budget
|
||||
end
|
||||
|
||||
@@ -7,12 +7,6 @@ class OverheadBudget < ActiveRecord::Base
|
||||
# Validations
|
||||
|
||||
# Accessors
|
||||
|
||||
def budget=(v)
|
||||
if v.is_a? String
|
||||
write_attribute(:budget, v.gsub(/[$ ,]/, ''))
|
||||
else
|
||||
write_attribute(:budget, v)
|
||||
end
|
||||
end
|
||||
include DollarizedAttribute
|
||||
dollarized_attribute :budget
|
||||
end
|
||||
|
||||
@@ -19,8 +19,12 @@ class RetainerDeliverable < HourlyDeliverable
|
||||
'R'
|
||||
end
|
||||
|
||||
def current_date
|
||||
Date.today
|
||||
end
|
||||
|
||||
def current_period
|
||||
Date.today.strftime("%B %Y")
|
||||
current_date.strftime("%B %Y")
|
||||
end
|
||||
|
||||
def beginning_date
|
||||
@@ -32,13 +36,28 @@ class RetainerDeliverable < HourlyDeliverable
|
||||
end
|
||||
|
||||
def date_range
|
||||
(beginning_date..ending_date)
|
||||
if beginning_date && ending_date && beginning_date <= ending_date
|
||||
(beginning_date..ending_date)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def within_date_range?(date)
|
||||
date_range.include?(date)
|
||||
end
|
||||
|
||||
# period in the format of "%Y-%m" or "%B %Y"
|
||||
def within_period_range?(period)
|
||||
begin
|
||||
# both valid formats work by adding a day to the end like -01
|
||||
date = Date.parse(period.to_s + "-01")
|
||||
within_date_range?(date)
|
||||
rescue ArgumentError
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def months
|
||||
month_acc = []
|
||||
|
||||
@@ -75,42 +94,251 @@ class RetainerDeliverable < HourlyDeliverable
|
||||
budgets
|
||||
end
|
||||
|
||||
def fixed_budgets_for_date(date)
|
||||
budgets = fixed_budgets.all(:conditions => {:year => date.year, :month => date.month})
|
||||
budgets = [fixed_budgets.build(:year => date.year, :month => date.month)] if budgets.empty?
|
||||
budgets
|
||||
end
|
||||
|
||||
def labor_budget_total(date=nil)
|
||||
if date
|
||||
if within_date_range?(date)
|
||||
case scope_date_status(date)
|
||||
when :in
|
||||
memoize_by_date("@labor_budget_total", date) do
|
||||
labor_budgets.sum(:budget, :conditions => {:year => date.year, :month => date.month})
|
||||
else
|
||||
0 # outside of range
|
||||
end
|
||||
when :out
|
||||
0
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def overhead_budget_total(date=nil)
|
||||
if date
|
||||
if within_date_range?(date)
|
||||
case scope_date_status(date)
|
||||
when :in
|
||||
memoize_by_date("@overhead_budget_total", date) do
|
||||
overhead_budgets.sum(:budget, :conditions => {:year => date.year, :month => date.month})
|
||||
else
|
||||
0 # outside of range
|
||||
end
|
||||
when :out
|
||||
0
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def labor_budget_hours(date=nil)
|
||||
if date
|
||||
if within_date_range?(date)
|
||||
case scope_date_status(date)
|
||||
when :in
|
||||
memoize_by_date("@labor_budget_hours", date) do
|
||||
labor_budgets.sum(:hours, :conditions => {:year => date.year, :month => date.month})
|
||||
else
|
||||
0 # outside of range
|
||||
end
|
||||
when :out
|
||||
0
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def labor_hours_spent_total(date=nil)
|
||||
case scope_date_status(date)
|
||||
when :in
|
||||
memoize_by_date("@labor_hours_spent_total", date) do
|
||||
time_entries = issues.collect {|issue| issue.time_entries.all(:conditions => {:tyear => date.year, :tmonth => date.month}) }.flatten
|
||||
|
||||
billable_hours_on_time_entries(time_entries)
|
||||
end
|
||||
when :out
|
||||
0
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def overhead_hours_spent_total(date=nil)
|
||||
case scope_date_status(date)
|
||||
when :in
|
||||
memoize_by_date("@overhead_hours_spent_total", date) do
|
||||
time_entries = issues.collect {|issue| issue.time_entries.all(:conditions => {:tyear => date.year, :tmonth => date.month}) }.flatten
|
||||
|
||||
nonbillable_hours_on_time_entries(time_entries)
|
||||
end
|
||||
when :out
|
||||
0
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def hours_spent_total(date=nil)
|
||||
case scope_date_status(date)
|
||||
when :in
|
||||
return 0 if issues.empty?
|
||||
|
||||
TimeEntry.sum(:hours, :conditions => {
|
||||
:issue_id => issues.collect(&:id),
|
||||
:tyear => date.year,
|
||||
:tmonth => date.month
|
||||
})
|
||||
when :out
|
||||
0
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def fixed_budget_total(date=nil)
|
||||
case scope_date_status(date)
|
||||
when :in
|
||||
memoize_by_date("@fixed_budget_total", date) do
|
||||
fixed_budgets.sum(:budget, :conditions => {:year => date.year, :month => date.month})
|
||||
end
|
||||
when :out
|
||||
0
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def fixed_budget_total_spent(date=nil)
|
||||
case scope_date_status(date)
|
||||
when :in
|
||||
memoize_by_date("@fixed_budget_total_spent", date) do
|
||||
fixed_budgets.paid.sum(:budget, :conditions => {:year => date.year, :month => date.month})
|
||||
end
|
||||
when :out
|
||||
0
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def fixed_markup_budget_total(date=nil)
|
||||
case scope_date_status(date)
|
||||
when :in
|
||||
memoize_by_date("@fixed_markup_budget_total", date) do
|
||||
fixed_budgets.
|
||||
all(:conditions => {:year => date.year, :month => date.month}).
|
||||
inject(0) {|total, fixed_budget| total += fixed_budget.markup_value }
|
||||
end
|
||||
when :out
|
||||
0
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def fixed_markup_budget_total_spent(date=nil)
|
||||
case scope_date_status(date)
|
||||
when :in
|
||||
memoize_by_date("@fixed_markup_budget_total_spent", date) do
|
||||
fixed_budgets.
|
||||
paid.
|
||||
all(:conditions => {:year => date.year, :month => date.month}).
|
||||
inject(0) {|total, fixed_budget| total += fixed_budget.markup_value }
|
||||
end
|
||||
when :out
|
||||
0
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def total_spent(date=nil)
|
||||
case scope_date_status(date)
|
||||
when :in
|
||||
# TODO: duplicated on HourlyDeliverable#total_spent
|
||||
memoize_by_date("@total_spent", date) do
|
||||
return 0 if contract.nil?
|
||||
return 0 if contract.billable_rate.blank?
|
||||
return 0 unless self.issues.count > 0
|
||||
|
||||
issue_ids = self.issues.collect(&:id)
|
||||
time_logs = time_entries_for_date_and_issue_ids(date, issue_ids)
|
||||
|
||||
hours = billable_hours_on_time_entries(time_logs)
|
||||
|
||||
fixed_budget_amount = fixed_budget_total_spent(date) + fixed_markup_budget_total_spent(date)
|
||||
|
||||
return (hours * contract.billable_rate) + fixed_budget_amount
|
||||
end
|
||||
when :out
|
||||
0
|
||||
else
|
||||
super
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# TODO: stolen directly from redmine_overhead but with a block option
|
||||
def labor_budget_spent_with_filter(&block)
|
||||
return 0.0 unless self.issues.size > 0
|
||||
total = 0.0
|
||||
|
||||
# Get all timelogs assigned
|
||||
if block_given?
|
||||
time_logs = block.call
|
||||
else
|
||||
time_logs = self.issues.collect(&:time_entries).flatten
|
||||
end
|
||||
|
||||
return time_logs.collect {|time_log|
|
||||
if time_log.billable?
|
||||
time_log.cost
|
||||
else
|
||||
0.0
|
||||
end
|
||||
}.sum
|
||||
end
|
||||
|
||||
# TODO: stolen directly from redmine_overhead but with a block option
|
||||
def overhead_spent_with_filter(&block)
|
||||
if block_given?
|
||||
time_logs = block.call
|
||||
else
|
||||
time_logs = issues.collect(&:time_entries).flatten
|
||||
end
|
||||
|
||||
return time_logs.collect {|time_entry|
|
||||
if time_entry.billable?
|
||||
0
|
||||
else
|
||||
time_entry.cost
|
||||
end
|
||||
}.sum
|
||||
end
|
||||
|
||||
def labor_budget_spent(date=nil)
|
||||
case scope_date_status(date)
|
||||
when :in
|
||||
memoize_by_date("@labor_budget_spent", date) do
|
||||
labor_budget_spent_with_filter do
|
||||
issue_ids = self.issues.collect(&:id)
|
||||
time_entries_for_date_and_issue_ids(date, issue_ids)
|
||||
end
|
||||
end
|
||||
when :out
|
||||
0
|
||||
else
|
||||
labor_budget_spent_with_filter
|
||||
end
|
||||
end
|
||||
|
||||
def overhead_spent(date=nil)
|
||||
case scope_date_status(date)
|
||||
when :in
|
||||
memoize_by_date("@overhead_spent", date) do
|
||||
overhead_spent_with_filter do
|
||||
issue_ids = self.issues.collect(&:id)
|
||||
time_entries_for_date_and_issue_ids(date, issue_ids)
|
||||
end
|
||||
end
|
||||
when :out
|
||||
0
|
||||
else
|
||||
overhead_spent_with_filter
|
||||
end
|
||||
end
|
||||
|
||||
def create_budgets_for_periods
|
||||
# For each month in the time span
|
||||
months.each do |month|
|
||||
@@ -124,10 +352,16 @@ class RetainerDeliverable < HourlyDeliverable
|
||||
undated_overhead_budgets.each do |template_budget|
|
||||
overhead_budgets.create(template_budget.attributes.merge(:year => month.year, :month => month.month))
|
||||
end
|
||||
|
||||
undated_fixed_budgets = fixed_budgets.all(:conditions => ["#{FixedBudget.table_name}.year IS NULL AND #{FixedBudget.table_name}.month IS NULL"])
|
||||
undated_fixed_budgets.each do |template_budget|
|
||||
fixed_budgets.create(template_budget.attributes.merge(:year => month.year, :month => month.month))
|
||||
end
|
||||
end
|
||||
# Destroy origional un-dated budgets
|
||||
labor_budgets.all(:conditions => ["#{LaborBudget.table_name}.year IS NULL AND #{LaborBudget.table_name}.month IS NULL"]).collect(&:destroy)
|
||||
overhead_budgets.all(:conditions => ["#{OverheadBudget.table_name}.year IS NULL AND #{OverheadBudget.table_name}.month IS NULL"]).collect(&:destroy)
|
||||
fixed_budgets.all(:conditions => ["#{FixedBudget.table_name}.year IS NULL AND #{FixedBudget.table_name}.month IS NULL"]).collect(&:destroy)
|
||||
end
|
||||
|
||||
def check_for_extended_period
|
||||
@@ -152,40 +386,36 @@ class RetainerDeliverable < HourlyDeliverable
|
||||
|
||||
def shrink_budgets_to_new_period
|
||||
return if beginning_date.nil? || ending_date.nil?
|
||||
labor_budgets.all.each do |labor_budget|
|
||||
# Purge un-dated budgets, should not be saved at all
|
||||
labor_budget.destroy unless labor_budget.year.present?
|
||||
labor_budget.destroy unless labor_budget.month.present?
|
||||
|
||||
# Purge budgets outside the new beginning/ending range
|
||||
unless (beginning_date..ending_date).to_a.include?(Date.new(labor_budget.year, labor_budget.month, 1))
|
||||
labor_budget.destroy
|
||||
end
|
||||
end
|
||||
|
||||
overhead_budgets.all.each do |overhead_budget|
|
||||
# Purge un-dated budgets, should not be saved at all
|
||||
overhead_budget.destroy unless overhead_budget.year.present?
|
||||
overhead_budget.destroy unless overhead_budget.month.present?
|
||||
|
||||
# Purge budgets outside the new beginning/ending range
|
||||
unless (beginning_date..ending_date).to_a.include?(Date.new(overhead_budget.year, overhead_budget.month, 1))
|
||||
overhead_budget.destroy
|
||||
end
|
||||
end
|
||||
shrink_budgets(labor_budgets.all)
|
||||
shrink_budgets(overhead_budgets.all)
|
||||
shrink_budgets(fixed_budgets.all)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def shrink_budgets(budget_items)
|
||||
budget_items.each do |budget_item|
|
||||
# Purge un-dated budgets, should not be saved at all
|
||||
budget_item.destroy unless budget_item.year.present?
|
||||
budget_item.destroy unless budget_item.month.present?
|
||||
|
||||
# Purge budgets outside the new beginning/ending range
|
||||
unless (beginning_date..ending_date).to_a.include?(Date.new(budget_item.year, budget_item.month, 1))
|
||||
budget_item.destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def extend_period_to_new_end_date
|
||||
return if end_date_change[0].nil? # No previous end date, so it will not have budgets
|
||||
|
||||
old_end_date = end_date_change[0]
|
||||
last_labor_budgets = labor_budgets.all(:conditions => {:year => old_end_date.year, :month => old_end_date.month})
|
||||
last_overhead_budgets = overhead_budgets.all(:conditions => {:year => old_end_date.year, :month => old_end_date.month})
|
||||
|
||||
last_fixed_budgets = fixed_budgets.all(:conditions => {:year => old_end_date.year, :month => old_end_date.month})
|
||||
|
||||
months_after_date(old_end_date.end_of_month.to_date).each do |new_period|
|
||||
create_budgets_for_new_period(new_period, last_labor_budgets, last_overhead_budgets)
|
||||
create_budgets_for_new_period(new_period, last_labor_budgets, last_overhead_budgets, last_fixed_budgets)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -195,14 +425,15 @@ class RetainerDeliverable < HourlyDeliverable
|
||||
old_start_date = start_date_change[0]
|
||||
first_labor_budgets = labor_budgets.all(:conditions => {:year => old_start_date.year, :month => old_start_date.month})
|
||||
first_overhead_budgets = overhead_budgets.all(:conditions => {:year => old_start_date.year, :month => old_start_date.month})
|
||||
first_fixed_budgets = fixed_budgets.all(:conditions => {:year => old_start_date.year, :month => old_start_date.month})
|
||||
|
||||
months_before_date(old_start_date.beginning_of_month.to_date).each do |new_period|
|
||||
create_budgets_for_new_period(new_period, first_labor_budgets, first_overhead_budgets)
|
||||
create_budgets_for_new_period(new_period, first_labor_budgets, first_overhead_budgets, first_fixed_budgets)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def create_budgets_for_new_period(new_period, labor_budgets_to_copy, overhead_budgets_to_copy)
|
||||
def create_budgets_for_new_period(new_period, labor_budgets_to_copy, overhead_budgets_to_copy, fixed_budgets_to_copy)
|
||||
labor_budgets_to_copy.each do |labor_budget_to_copy|
|
||||
create_new_labor_budget_based_on_existing_budget(labor_budget_to_copy, 'year' => new_period.year, 'month' => new_period.month)
|
||||
end
|
||||
@@ -210,6 +441,10 @@ class RetainerDeliverable < HourlyDeliverable
|
||||
overhead_budgets_to_copy.each do |overhead_budget_to_copy|
|
||||
create_new_overhead_budget_based_on_existing_budget(overhead_budget_to_copy, 'year' => new_period.year, 'month' => new_period.month)
|
||||
end
|
||||
|
||||
fixed_budgets_to_copy.each do |fixed_budget_to_copy|
|
||||
create_new_fixed_budget_based_on_existing_budget(fixed_budget_to_copy, 'year' => new_period.year, 'month' => new_period.month)
|
||||
end
|
||||
end
|
||||
|
||||
def create_new_labor_budget_based_on_existing_budget(existing_labor_budget, attributes={})
|
||||
@@ -219,4 +454,36 @@ class RetainerDeliverable < HourlyDeliverable
|
||||
def create_new_overhead_budget_based_on_existing_budget(existing_overhead_budget, attributes={})
|
||||
overhead_budgets.create(existing_overhead_budget.attributes.except('id').merge(attributes))
|
||||
end
|
||||
|
||||
def create_new_fixed_budget_based_on_existing_budget(existing_fixed_budget, attributes={})
|
||||
fixed_budgets.create(existing_fixed_budget.attributes.except('id').merge(attributes))
|
||||
end
|
||||
|
||||
def scope_date_status(date)
|
||||
if date
|
||||
if within_date_range?(date)
|
||||
status = :in
|
||||
else
|
||||
status = :out # outside of range
|
||||
end
|
||||
else
|
||||
status = :no_date
|
||||
end
|
||||
|
||||
status
|
||||
end
|
||||
|
||||
def time_entries_for_date_and_issue_ids(date, issue_ids)
|
||||
if issue_ids.present?
|
||||
TimeEntry.all(:conditions => ["#{Issue.table_name}.id IN (:issue_ids) AND tyear = (:year) AND tmonth = (:month)",
|
||||
{:issue_ids => issue_ids,
|
||||
:year => date.year,
|
||||
:month => date.month}
|
||||
],
|
||||
:include => :issue)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<% if resource.locked? || resource.closed? %>
|
||||
<div class="error_msg">
|
||||
<p><%= resource.locked? ? l(:text_contract_locked_warning) : l(:text_contract_closed_warning) %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="box tabular">
|
||||
<% form.inputs :name => l(:text_general_legend) do %>
|
||||
<%= form.input :name, :required => true %>
|
||||
<%= form.input :status, :required => true, :collection => [["Open","open"],["Locked","locked"],["Closed","closed"]] %>
|
||||
<%= form.input :account_executive, :required => true, :collection => @project.users.sort %>
|
||||
<li class="boolean optional">
|
||||
<%= label('contract', 'executed') %>
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<%= content_tag(:h2, h(contract.name)) %>
|
||||
|
||||
<div class="title-bar-actions">
|
||||
<span><a href="#TODO-release-4" class="">Log time (Release 4)</a></span>
|
||||
<span><a href="#TODO-release-4" class=""><%= release(4, "Log time") %></a></span>
|
||||
|
||||
<span class="meta-sep"> | </span>
|
||||
|
||||
<span id="watcher"><a class="icon icon-fav-off" href="#TODO-release-3" onclick="">Watch (Release 3)</a></span>
|
||||
<span id="watcher"><a class="icon icon-fav-off" href="#TODO-release-3" onclick=""><%= release(3, "Watch") %></a></span>
|
||||
|
||||
<span class="meta-sep"> | </span>
|
||||
|
||||
<span><a href="#TODO">Copy (Release ?)</a></span>
|
||||
<span><a href="#TODO"><%= release(5, "Copy") %></a></span>
|
||||
<span class="meta-sep"> | </span>
|
||||
|
||||
<div class="update button-large">
|
||||
@@ -18,8 +18,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%# TODO: release 3/4, contract messages
|
||||
<div class="error_msg">
|
||||
<p>There is $243 worth of time clocked to issues that are not assigned to any deliverables. <span>Please update the orphaned issues.</span> </p>
|
||||
</div>
|
||||
%>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
<%= content_tag(:h2, h(l(:text_edit_contract_name, :name => resource.name))) %>
|
||||
|
||||
<%= error_messages_for 'contract' %>
|
||||
|
||||
<% semantic_form_for resource, :url => contract_path(@project, resource), :html => {:class => 'tabular'} do |form| %>
|
||||
<%= render :partial => 'form', :object => form, :locals => {:cancel_path => contract_path(@project, resource)} %>
|
||||
<% end %>
|
||||
|
||||
@@ -12,24 +12,26 @@
|
||||
|
||||
</div>
|
||||
|
||||
<% if collection.empty? %>
|
||||
<% if group_contracts_by_status(collection)["open"].empty? %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% else %>
|
||||
<table class="list" cellspacing="0" border="0" cellpadding="0" id="contracts">
|
||||
<table class="list open" cellspacing="0" border="0" cellpadding="0" id="contracts">
|
||||
<thead>
|
||||
<th><%= l(:field_id) %></th>
|
||||
<th><%= l(:field_name) %></th>
|
||||
<%# TODO: Release 5, Status %>
|
||||
<%# TODO: Release 5, Type %>
|
||||
<th><%= l(:field_status) %></th>
|
||||
<th><%= l(:field_type) %></th>
|
||||
<th><%= l(:field_account_executive_short) %></th>
|
||||
<th><%= l(:field_total_budget) %></th>
|
||||
<th><%= l(:field_end_date) %></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% collection.each do |contract| %>
|
||||
<% group_contracts_by_status(collection)["open"].each do |contract| %>
|
||||
<% content_tag_for(:tr, contract, :class => cycle('','odd')) do %>
|
||||
<td class="id"><%= link_to(h(contract.id), contract_path(@project, contract)) %></td>
|
||||
<td class="name"><%= link_to(h(contract.name), contract_path(@project, contract)) %></td>
|
||||
<td class="status"><%= h(contract.status) %></td>
|
||||
<td><%= release(5, "Contract Type") %></td>
|
||||
<td class="account-executive"><%= h contract.account_executive.name %></td>
|
||||
<td class="total-budget"><%= h(format_value_field_for_contracts(contract.total_budget)) %></td>
|
||||
<td class="end-date"><%= h format_date(contract.end_date) %></td>
|
||||
@@ -40,12 +42,71 @@
|
||||
<% end %>
|
||||
|
||||
<div class="title-bar">
|
||||
<h2>Inactive Contracts</h2>
|
||||
<h2>Locked Contracts</h2>
|
||||
</div>
|
||||
|
||||
<%# TODO: split contracts by active and inactive %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<p>TODO: Release 5 (Contract Status)</p>
|
||||
<% if group_contracts_by_status(collection)["locked"].empty? %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% else %>
|
||||
<table class="list locked" cellspacing="0" border="0" cellpadding="0" id="contracts">
|
||||
<thead>
|
||||
<th><%= l(:field_id) %></th>
|
||||
<th><%= l(:field_name) %></th>
|
||||
<th><%= l(:field_status) %></th>
|
||||
<th><%= l(:field_type) %></th>
|
||||
<th><%= l(:field_account_executive_short) %></th>
|
||||
<th><%= l(:field_total_budget) %></th>
|
||||
<th><%= l(:field_end_date) %></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% group_contracts_by_status(collection)["locked"].each do |contract| %>
|
||||
<% content_tag_for(:tr, contract, :class => cycle('','odd')) do %>
|
||||
<td class="id"><%= link_to(h(contract.id), contract_path(@project, contract)) %></td>
|
||||
<td class="name"><%= link_to(h(contract.name), contract_path(@project, contract)) %></td>
|
||||
<td class="status"><%= h(contract.status) %></td>
|
||||
<td><%= release(5, "Contract Type") %></td>
|
||||
<td class="account-executive"><%= h contract.account_executive.name %></td>
|
||||
<td class="total-budget"><%= h(format_value_field_for_contracts(contract.total_budget)) %></td>
|
||||
<td class="end-date"><%= h format_date(contract.end_date) %></td>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% end %>
|
||||
|
||||
<div class="title-bar">
|
||||
<h2>Closed Contracts</h2>
|
||||
</div>
|
||||
|
||||
<% if group_contracts_by_status(collection)["closed"].empty? %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% else %>
|
||||
<table class="list closed" cellspacing="0" border="0" cellpadding="0" id="contracts">
|
||||
<thead>
|
||||
<th><%= l(:field_id) %></th>
|
||||
<th><%= l(:field_name) %></th>
|
||||
<th><%= l(:field_status) %></th>
|
||||
<th><%= l(:field_type) %></th>
|
||||
<th><%= l(:field_account_executive_short) %></th>
|
||||
<th><%= l(:field_total_budget) %></th>
|
||||
<th><%= l(:field_end_date) %></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% group_contracts_by_status(collection)["closed"].each do |contract| %>
|
||||
<% content_tag_for(:tr, contract, :class => cycle('','odd')) do %>
|
||||
<td class="id"><%= link_to(h(contract.id), contract_path(@project, contract)) %></td>
|
||||
<td class="name"><%= link_to(h(contract.name), contract_path(@project, contract)) %></td>
|
||||
<td class="status"><%= h(contract.status) %></td>
|
||||
<td><%= release(5, "Contract Type") %></td>
|
||||
<td class="account-executive"><%= h contract.account_executive.name %></td>
|
||||
<td class="total-budget"><%= h(format_value_field_for_contracts(contract.total_budget)) %></td>
|
||||
<td class="end-date"><%= h format_date(contract.end_date) %></td>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% end %>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<%= content_tag(:h2, l(:text_new_contract)) %>
|
||||
|
||||
<%= error_messages_for 'contract' %>
|
||||
|
||||
<% semantic_form_for resource, :html => {:class => 'tabular'} do |form| %>
|
||||
<%= render :partial => 'form', :object => form, :locals => {:cancel_path => contracts_path} %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
<%= render :partial => 'title', :locals => {:contract => resource} %>
|
||||
|
||||
<% if resource.orphaned_time && resource.orphaned_time > 0 %>
|
||||
<div class="error_msg">
|
||||
<p>
|
||||
<%= l(:text_error_message_orphaned_time, :amount => format_value_field_for_contracts(resource.orphaned_time, :unit => '$')) %>
|
||||
<%= link_to_issue_list_with_filter(l(:text_error_message_update_orphaned_time), :deliverable_id => '!*') %>
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% div_for(resource) do %>
|
||||
|
||||
<%= avatar(resource.account_executive, :size => 40) %>
|
||||
@@ -8,7 +17,11 @@
|
||||
<table class="left">
|
||||
<%= show_field(resource, :status, :html_options => {:class => 'contract-status'}) %>
|
||||
<%= show_field(resource, :account_executive, :html_options => {:class => 'contract-account-manager'}) %>
|
||||
<%= show_field(resource, :contract_type, :html_options => {:class => 'contract-type'}) %>
|
||||
<tr class="contract-type">
|
||||
<%# show_field(resource, :contract_type, :html_options => {:class => 'contract-type'}) %>
|
||||
<td colspan="2"><%= release(5, "Contract type") %></td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
<table class="middle">
|
||||
@@ -30,13 +43,13 @@
|
||||
|
||||
<div class="left">
|
||||
<table class="info">
|
||||
<%= show_field(resource, :client_point_of_contact, :format => :textilizable, :raw => true, :html_options => {:class => 'contract-client-point-of-contact padd'}, :label_html_options => {:width => '25%'}) %>
|
||||
<%= show_field(resource, :executed, :html_options => {:class => 'contract-executed'}) %>
|
||||
<%= show_field(resource, :discount_note, :format => :textilizable, :raw => true, :html_options => {:class => 'contract-discount-note'}) %>
|
||||
<%= show_field(resource, :client_point_of_contact, :format => :textilizable, :raw => true, :html_options => {:class => 'contract-client-point-of-contact padd wiki'}, :label_html_options => {:width => '25%'}) %>
|
||||
<%= show_field(resource, :executed, :format => :format_as_yes_or_no, :html_options => {:class => 'contract-executed'}) %>
|
||||
<%= show_field(resource, :discount_note, :format => :textilizable, :raw => true, :html_options => {:class => 'contract-discount-note wiki'}) %>
|
||||
<%= show_field(resource, :payment_term, :format => :format_payment_terms, :html_options => {:class => 'contract-payment-terms'}) %>
|
||||
<%= show_field(resource, :client_ap_contact_information, :format => :textilizable, :raw => true, :html_options => {:class => 'contract-client-ap-contact-information'}) %>
|
||||
<%= show_field(resource, :client_ap_contact_information, :format => :textilizable, :raw => true, :html_options => {:class => 'contract-client-ap-contact-information wiki'}) %>
|
||||
<%= show_field(resource, :po_number, :html_options => {:class => 'contract-po-number'}) %>
|
||||
<%= show_field(resource, :details, :format => :textilizable, :raw => true, :html_options => {:class => 'contract-details'}) %>
|
||||
<%= show_field(resource, :details, :format => :textilizable, :raw => true, :html_options => {:class => 'contract-details wiki'}) %>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -44,9 +57,14 @@
|
||||
<%= show_budget_field(resource, :labor_spent, :labor_budget, :format => :format_value_field_for_contracts, :html_options => {:class => 'contract-labor'}, :label_html_options => {:width => '46%'}) %>
|
||||
<%= show_budget_field(resource, :overhead_spent, :overhead_budget, :format => :format_value_field_for_contracts, :html_options => {:class => 'contract-overhead'}) %>
|
||||
<%= show_budget_field(resource, :fixed_spent, :fixed_budget, :format => :format_value_field_for_contracts, :html_options => {:class => 'contract-fixed'}) %>
|
||||
<%= show_budget_field(resource, :markup_spent, :markup_budget, :format => :format_value_field_for_contracts, :html_options => {:class => 'contract-markup'}) %>
|
||||
<% if show_markup_for?(resource) %>
|
||||
<%= show_budget_field(resource, :fixed_markup_spent, :fixed_markup_budget, :format => :format_value_field_for_contracts, :html_options => {:class => 'contract-markup'}) %>
|
||||
<% end %>
|
||||
<%= show_budget_field(resource, :profit_spent, :profit_budget, :format => :format_value_field_for_contracts, :html_options => {:class => 'contract-profit'}) %>
|
||||
<%= show_budget_field(resource, :discount_spent, :discount_budget, :format => :format_value_field_for_contracts, :html_options => {:class => 'contract-discount'}) %>
|
||||
<tr class="contract-discount">
|
||||
<%# show_budget_field(resource, :discount_spent, :discount_budget, :format => :format_value_field_for_contracts, :html_options => {:class => 'contract-discount'}) %>
|
||||
<td colspan="3"><%= release(4, "Discount") %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
@@ -55,10 +73,22 @@
|
||||
</tr>
|
||||
|
||||
<%= show_budget_field(resource, :total_spent, :total_budget, :format => :format_value_field_for_contracts, :html_options => {:class => 'contract-total'}) %>
|
||||
<%= show_field(resource, :billable_rate, :format => :format_hourly_rate, :html_options => {:class => 'contract-billable-rate total'}) %>
|
||||
<%= show_budget_field(resource, :billable_rate, 'nil?', :format => :format_hourly_rate, :html_options => {:class => 'contract-billable-rate total'}) %>
|
||||
|
||||
<%= show_budget_field(resource, :estimated_hour_spent, :estimated_hour_budget, :format => :l_hours, :html_options => {:class => 'contract-estimated-hour total'}) %>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<div class="hr"></div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<%= show_budget_field(resource, :labor_hour_spent, :labor_hour_budget, :format => :l_hours, :html_options => {:class => 'contract-labor-hour'}) %>
|
||||
<%= show_budget_field(resource, :overhead_hour_spent, :overhead_hour_budget, :format => :l_hours, :html_options => {:class => 'contract-overhead-hour'}) %>
|
||||
<%= show_budget_field(resource, :estimated_hour_spent, :estimated_hour_budget, :format => :l_hours, :html_options => {:class => 'contract-total-hour total'}) %>
|
||||
<tr>
|
||||
<td class="stretch" colspan="3">
|
||||
<!-- Stretch table height -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
@@ -70,9 +100,9 @@
|
||||
<%= content_tag(:h3, l(:field_deliverable_plural)) %>
|
||||
|
||||
<div class="actions">
|
||||
<a href="#TODO-release-2">CSV (Release 2)</a>
|
||||
<a href="#TODO-release?">View All (9) (Release ?)</a>
|
||||
<%= link_to(l(:button_add_new), new_contract_deliverable_path(@project, resource), :id => 'new-deliverable') %>
|
||||
<a href="#TODO-release-2"><%= release(2, "CSV") %></a>
|
||||
<a href="#TODO-release?"><%= release(5, "View All/Pagination") %></a>
|
||||
<%= link_to(l(:button_add_new), new_contract_deliverable_path(@project, resource), :id => 'new-deliverable') if resource.open? %>
|
||||
</div>
|
||||
|
||||
<div class="clear"></div>
|
||||
@@ -100,15 +130,15 @@
|
||||
<td width="10%" class="arrow end-date"><span><%= h format_date(deliverable.end_date) %></span></td>
|
||||
<td width="2%" class="type"><%= h deliverable.short_type %></td>
|
||||
<td width="25%" class="title"><%= h deliverable.title %></td>
|
||||
<td width="15%">TODO: Release 5</td>
|
||||
<td width="15%" class="status"><%= h deliverable.status %></td>
|
||||
<td width="15%" class="manager"><%= h deliverable.manager.try(:name) %></td>
|
||||
<%= format_budget_for_deliverable(deliverable, deliverable.labor_budget_spent, deliverable.labor_budget_total, :class => 'labor') %>
|
||||
<%= format_budget_for_deliverable(deliverable, deliverable.overhead_spent, deliverable.overhead_budget_total, :class => 'overhead') %>
|
||||
<%= format_budget_for_deliverable(deliverable, 0, 0) %><%# TODO: Release 2, Fixed Budgets %>
|
||||
<%= format_budget_for_deliverable(deliverable, deliverable.fixed_budget_total_spent, deliverable.fixed_budget_total, :class => 'fixed') %>
|
||||
<% end %>
|
||||
|
||||
<tr id="deliverable_details_<%= h(deliverable.id) %>" class="ign">
|
||||
<%= render :partial => 'deliverables/details_row', :locals => {:deliverable => deliverable, :contract => resource} %>
|
||||
<%= render :partial => 'deliverables/details_row', :locals => {:deliverable => deliverable, :contract => resource, :period => deliverable.current_date} %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
@@ -119,7 +149,7 @@
|
||||
</div>
|
||||
|
||||
<% end %> <%# div_for(resource) %>
|
||||
<p>TODO: Release 4, Milestones</p>
|
||||
<p><%= release(4, "Milestones") %></p>
|
||||
<!--
|
||||
<div id="milestones">
|
||||
|
||||
@@ -194,6 +224,6 @@
|
||||
</div>
|
||||
-->
|
||||
|
||||
<p>TODO: Release 2+, history</p>
|
||||
<p><%= release(3, "history") %></p>
|
||||
|
||||
<% html_title "#{l(:text_contracts)} - #{h(resource.name)}" %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<% period ||= '' %>
|
||||
<% validated_period = validate_period(deliverable, period) %>
|
||||
|
||||
<td colspan="11" class="deliverable_details_outer_wrapper_<%= h(deliverable.id) %>">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="info">
|
||||
<div class="title">
|
||||
<%= link_to(l(:button_edit), edit_contract_deliverable_path(@project, contract, deliverable), :class => 'icon icon-edit') %>
|
||||
<%= link_to(l(:button_delete), contract_deliverable_path(@project, contract, deliverable), :method => :delete, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %>
|
||||
<%= link_to(l(:button_delete), contract_deliverable_path(@project, contract, deliverable), :method => :delete, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') if contract.open? %>
|
||||
</div>
|
||||
|
||||
<%= textilizable(deliverable, :notes) %>
|
||||
@@ -16,7 +16,7 @@
|
||||
<form method="get" action="<%= contract_deliverable_path(@project, contract, deliverable, :format => 'js', :as => 'deliverable_details_row') %>">
|
||||
<fieldset>
|
||||
<select name="period" id="retainer_period_change_<%= h(deliverable.id) %>" class="retainer_period_change">
|
||||
<%= retainer_period_options(deliverable) %>
|
||||
<%= retainer_period_options(deliverable, :selected => validated_period) %>
|
||||
</select>
|
||||
</fieldset>
|
||||
</form>
|
||||
@@ -24,21 +24,22 @@
|
||||
|
||||
<table>
|
||||
<%= show_field(deliverable, :current_period, :html_options => {:class => 'deliverable-current-period'}) if deliverable.retainer? %>
|
||||
<tr><td colspan="2"><%= release(5, "Retainer lightbox") %></td></tr>
|
||||
<%= show_field(deliverable, :start_date, :format => :format_date, :html_options => {:class => 'deliverable-start-date'}) %>
|
||||
<%= show_field(deliverable, :end_date, :format => :format_date, :html_options => {:class => 'deliverable-end-date'}) %>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<%# TODO: Release 2, skinning port %>
|
||||
<div class="finance">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>Spent</th>
|
||||
<th>Budget</th>
|
||||
<th>Hours</th>
|
||||
<th><%= l(:field_spent) %></th>
|
||||
<th><%= l(:field_budget) %></th>
|
||||
<th><%= l(:field_hours) %></th>
|
||||
<th><%= l(:field_estimated) %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
@@ -47,34 +48,92 @@
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="l"><a href="#"><strong>Labor</strong></a></td>
|
||||
<td class="labor_budget_spent"><%= h(format_value_field_for_contracts(deliverable.labor_budget_spent)) %></td>
|
||||
<td class="labor_budget_total"><%= h(format_value_field_for_contracts(deliverable.labor_budget_total)) %></td>
|
||||
<td> TODO: Release 2 / TODO hrs </td>
|
||||
<td class="l"><a href="#"><strong><%= l(:field_labor) %></strong></a><%= release(3, "Deliverable lightbox") %></td>
|
||||
<td class="labor_budget_spent <%= overage_class(deliverable.labor_budget_spent(validated_period), deliverable.labor_budget_total(validated_period)) %>">
|
||||
<%= h(format_value_field_for_contracts(deliverable.labor_budget_spent(validated_period))) %>
|
||||
</td>
|
||||
<td class="labor_budget_total"><%= h(format_value_field_for_contracts(deliverable.labor_budget_total(validated_period))) %></td>
|
||||
<td class="labor_hours_spent">
|
||||
<span class="<%= overage_class(deliverable.labor_hours_spent_total(validated_period), deliverable.labor_budget_hours(validated_period)) %>"><%= h(format_value_field_for_contracts(deliverable.labor_hours_spent_total(validated_period))) %></span> <%= l(:text_short_hours) %>
|
||||
</td>
|
||||
<td class="labor_hours">
|
||||
<%= h(format_value_field_for_contracts(deliverable.labor_budget_hours(validated_period))) %> <%= l(:text_short_hours) %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="l"><a href="#"><strong>Overhead</strong></a></td>
|
||||
<td class="overhead_budget_spent"><%= h(format_value_field_for_contracts(deliverable.overhead_spent)) %></td>
|
||||
<td class="overhead_budget_total"><%= h(format_value_field_for_contracts(deliverable.overhead_budget_total)) %></td>
|
||||
<td> TODO: Release 2 / TODO hrs </td>
|
||||
<td class="l"><a href="#"><strong><%= l(:field_overhead) %></strong></a><%= release(3, "Deliverable lightbox") %></td>
|
||||
<td class="overhead_budget_spent <%= overage_class(deliverable.overhead_spent(validated_period), deliverable.overhead_budget_total(validated_period)) %>">
|
||||
<%= h(format_value_field_for_contracts(deliverable.overhead_spent(validated_period))) %>
|
||||
</td>
|
||||
<td class="overhead_budget_total"><%= h(format_value_field_for_contracts(deliverable.overhead_budget_total(validated_period))) %></td>
|
||||
<td class="overhead_hours_spent">
|
||||
<span class="<%= overage_class(deliverable.overhead_hours_spent_total(validated_period), deliverable.overhead_budget_hours(validated_period)) %>"><%= h(format_value_field_for_contracts(deliverable.overhead_hours_spent_total(validated_period))) %></span> <%= l(:text_short_hours) %>
|
||||
</td>
|
||||
<td class="overhead_hours">
|
||||
<%= h(format_value_field_for_contracts(deliverable.overhead_budget_hours(validated_period))) %> <%= l(:text_short_hours) %>
|
||||
</td>
|
||||
</tr>
|
||||
<%# TODO: Release 2, Fixed %>
|
||||
<%# TODO: Release 2, Markup %>
|
||||
<tr>
|
||||
<td class="l">Profit</td>
|
||||
<td><%= h(format_value_field_for_contracts(deliverable.profit_left)) %></td>
|
||||
<td><%= h(format_value_field_for_contracts(deliverable.profit_budget)) %></td>
|
||||
|
||||
<% deliverable.fixed_budgets.by_period(validated_period).each do |fixed_budget| %>
|
||||
<% next if fixed_budget.blank_record? %>
|
||||
<tr id="fixed_budget_<%= fixed_budget.id %>">
|
||||
<td class="l fixed_title" title="<%= h(fixed_budget.description) %>"><%= h(fixed_budget.title) %></td>
|
||||
<td class="fixed_budget_spent <%= overage_class(fixed_budget.budget_spent, fixed_budget.budget) %>">
|
||||
<%= h(format_value_field_for_contracts(fixed_budget.budget_spent)) %>
|
||||
</td>
|
||||
<td class="fixed_budget_total"><%= h(format_value_field_for_contracts(fixed_budget.budget)) %></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
|
||||
<% if show_markup_for?(deliverable, validated_period) %>
|
||||
<tr>
|
||||
<td class="l"><%= l(:field_markup) %></td>
|
||||
<td class="fixed_markup_budget_spent <%= overage_class(deliverable.fixed_markup_budget_total_spent(validated_period), deliverable.fixed_markup_budget_total(validated_period)) %>">
|
||||
<%= h(format_value_field_for_contracts(deliverable.fixed_markup_budget_total_spent(validated_period))) %>
|
||||
</td>
|
||||
<td class="fixed_markup_budget_total"><%= h(format_value_field_for_contracts(deliverable.fixed_markup_budget_total(validated_period))) %></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
|
||||
</tr>
|
||||
<% end %>
|
||||
|
||||
<tr>
|
||||
<td class="l"><%= l(:field_profit) %></td>
|
||||
<td class="profit_spent">
|
||||
<%= h(format_value_field_for_contracts(deliverable.profit_left(validated_period))) %>
|
||||
</td>
|
||||
<td class="profit_total"><%= h(format_value_field_for_contracts(deliverable.profit_budget(validated_period))) %></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
|
||||
</tr>
|
||||
<tr class="total">
|
||||
<td class="l"><strong>Total:</strong></td>
|
||||
<td class="total_spent"><strong><%= h(format_value_field_for_contracts(deliverable.total_spent)) %></strong></td>
|
||||
<td class="total"><strong><%= h(format_value_field_for_contracts(deliverable.total)) %></strong></td>
|
||||
<td><strong>TODO: Release 2</strong></td>
|
||||
<td class="l"><strong><%= l(:field_total) %>:</strong></td>
|
||||
<td class="total_spent <%= overage_class(deliverable.total_spent(validated_period), deliverable.total(validated_period)) %>">
|
||||
<strong>
|
||||
<%= h(format_value_field_for_contracts(deliverable.total_spent(validated_period))) %>
|
||||
</strong>
|
||||
</td>
|
||||
<td class="total"><strong><%= h(format_value_field_for_contracts(deliverable.total(validated_period))) %></strong></td>
|
||||
<td class="total_hours_spent">
|
||||
<strong>
|
||||
<span class="<%= overage_class(deliverable.hours_spent_total(validated_period), deliverable.estimated_hour_budget_total(validated_period)) %>">
|
||||
<%= h(format_value_field_for_contracts(deliverable.hours_spent_total(validated_period))) %></span> <%= l(:text_short_hours) %>
|
||||
</strong>
|
||||
</td>
|
||||
<td class="total_hours">
|
||||
<strong>
|
||||
<%= h(format_value_field_for_contracts(deliverable.estimated_hour_budget_total(validated_period))) %> <%= l(:text_short_hours) %>
|
||||
</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -88,24 +147,32 @@
|
||||
<th colspan="2"><%= l(:label_issue_status_plural) %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<%# TODO: Release 2 Issue status counters
|
||||
<tbody>
|
||||
<% deliverable.issues_by_status.each do |status, issues| %>
|
||||
<tr>
|
||||
<td>Proposed</td>
|
||||
<td class="number">1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>On Hold</td>
|
||||
<td class="number">1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Complete</td>
|
||||
<td class="number">14</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>All</strong></td>
|
||||
<td class="number">16</td>
|
||||
</tr>
|
||||
%>
|
||||
<td>
|
||||
<%= link_to_issue_list_with_filter(status.name,
|
||||
:deliverable_id => deliverable.id,
|
||||
:status_id => status.id) %>
|
||||
</td>
|
||||
<td class="number">
|
||||
<%= link_to_issue_list_with_filter(issues.length,
|
||||
:deliverable_id => deliverable.id,
|
||||
:status_id => status.id) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<tr>
|
||||
<td><strong>
|
||||
<%= link_to_issue_list_with_filter(l(:label_all).capitalize,
|
||||
:deliverable_id => deliverable.id) %>
|
||||
</strong></td>
|
||||
<td class="number">
|
||||
<%= link_to_issue_list_with_filter(deliverable.issues.count,
|
||||
:deliverable_id => deliverable.id) %>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<% form.inputs :name => label, :class => "deliverable-finances #{fieldset_class}" do %>
|
||||
|
||||
<li style="display:none;" id='retainer-finances-message'>
|
||||
<%= content_tag(:label, l(:text_retainer_monthly_message)) %>
|
||||
</li>
|
||||
|
||||
<li class="numeric optional">
|
||||
<%= content_tag(:label, l(:field_labor)) %>
|
||||
<table id="deliverable-labor" class="deliverable_finance_table">
|
||||
@@ -8,21 +12,19 @@
|
||||
<%= labor_budget.hidden_field(:year) %>
|
||||
<%= labor_budget.hidden_field(:month) %>
|
||||
<tr>
|
||||
<%# TODO: Select field for the Time Entry Activity in a td %>
|
||||
<td>
|
||||
<select><option>TODO: Release 3</option></select>
|
||||
<%= release(3, "Select field for the Time Entry Activity in a td") %>
|
||||
</td>
|
||||
<td>
|
||||
<p class="inline-hints"><%= labor_budget.label(:hours, l(:text_short_hours)) %></p>
|
||||
<%= labor_budget.text_field(:hours, :value => format_deliverable_value_fields(labor_budget.object.hours), :size => 10) %>
|
||||
<%= labor_budget.text_field(:hours, :value => format_deliverable_value_fields(labor_budget.object.hours), :class => 'financial') %>
|
||||
</td>
|
||||
<td>
|
||||
<p class="inline-hints"><%= labor_budget.label(:budget, l(:text_dollar_sign)) %></p>
|
||||
<%= labor_budget.text_field(:budget, :value => format_deliverable_value_fields(labor_budget.object.budget), :size => 10) %>
|
||||
<%= labor_budget.text_field(:budget, :value => format_deliverable_value_fields(labor_budget.object.budget), :class => 'financial') %>
|
||||
</td>
|
||||
<%# TODO: Green Add button for multiple records %>
|
||||
<td>
|
||||
Todo: Add button (Release 3)
|
||||
<%= release(3, "Green Add button for multiple records") %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
@@ -37,27 +39,63 @@
|
||||
<%= overhead_budget.hidden_field(:year) %>
|
||||
<%= overhead_budget.hidden_field(:month) %>
|
||||
<tr>
|
||||
<%# TODO: Select field for the Time Entry Activity in a td %>
|
||||
<td>
|
||||
<select><option>TODO: Release 3</option></select>
|
||||
<%= release(3, "Select field for the Time Entry Activity in a td") %>
|
||||
</td>
|
||||
<td>
|
||||
<p class="inline-hints"><%= overhead_budget.label(:hours, l(:text_short_hours)) %></p>
|
||||
<%= overhead_budget.text_field(:hours, :value => format_deliverable_value_fields(overhead_budget.object.hours),:size => 10) %>
|
||||
<%= overhead_budget.text_field(:hours, :value => format_deliverable_value_fields(overhead_budget.object.hours),:class => 'financial') %>
|
||||
</td>
|
||||
<td>
|
||||
<p class="inline-hints"><%= overhead_budget.label(:budget, l(:text_dollar_sign)) %></p>
|
||||
<%= overhead_budget.text_field(:budget, :value => format_deliverable_value_fields(overhead_budget.object.budget), :size => 10) %>
|
||||
<%= overhead_budget.text_field(:budget, :value => format_deliverable_value_fields(overhead_budget.object.budget), :class => 'financial') %>
|
||||
</td>
|
||||
<%# TODO: Green Add button for multiple records %>
|
||||
<td>
|
||||
Todo: Add button (Release 3)
|
||||
<%= release(3, "Green Add button for multiple records") %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
</li>
|
||||
|
||||
<li class="numeric optional">
|
||||
<div id="deliverable-fixed" class="fixed-item-form">
|
||||
<label for="contract_discount">Fixed</label>
|
||||
|
||||
<% form.fields_for :fixed_budgets, fixed_budgets do |fixed_budget| %>
|
||||
<%= fixed_budget.hidden_field(:year) %>
|
||||
<%= fixed_budget.hidden_field(:month) %>
|
||||
|
||||
<p class="inline-hints"><%= fixed_budget.label(:title, l(:field_title))%>
|
||||
<%= fixed_budget.text_field(:title) %>
|
||||
</p>
|
||||
|
||||
<p class="inline-hints">
|
||||
<%= fixed_budget.label(:budget, l(:field_budget))%> <%= l(:text_dollar_sign) %>
|
||||
<%= fixed_budget.text_field(:budget, :value => format_deliverable_value_fields(fixed_budget.object.budget), :class => 'financial') %>
|
||||
</p>
|
||||
|
||||
<p class="inline-hints">
|
||||
<%= fixed_budget.label(:markup, l(:field_markup)) %> <%= l(:field_discount_hint) %>
|
||||
<%= fixed_budget.text_field(:markup, :value => format_deliverable_value_fields_as_dollar_or_percent(fixed_budget.object.markup), :class => 'financial') %>
|
||||
</p>
|
||||
|
||||
<p class="inline-hints">
|
||||
<%= fixed_budget.label(:paid, l(:field_paid)) %>
|
||||
<%= fixed_budget.check_box(:paid) %>
|
||||
</p>
|
||||
|
||||
<p class="inline-hints" style="display:none;"><%= fixed_budget.label(:description, l(:field_description), :for => "fixed-description#{fixed_budget.object.object_id}")%></p>
|
||||
<%= fixed_budget.text_area(:description, :class => 'wiki-edit', :rows => '5', :id => "fixed-description#{fixed_budget.object.object_id}") %>
|
||||
<%= wikitoolbar_for "fixed-description#{fixed_budget.object.object_id}" %>
|
||||
|
||||
<p><%= release(3, "Green Add button for multiple records") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
||||
</li>
|
||||
|
||||
<%= form.input :total, :input_html => {:size => 10}, :wrapper_html => {:class => 'deliverable_total_input'}, :hint => l(:text_dollar_sign) %>
|
||||
|
||||
<% end %>
|
||||
|
||||
@@ -2,8 +2,19 @@
|
||||
<%= javascript_tag("var i18nEndDateEmpty = '#{l(:text_end_date_empty)}'") %>
|
||||
<%= javascript_tag("var i18nChangedPeriodMessage = '#{l(:text_changed_period_message)}'") %>
|
||||
|
||||
<% if resource.locked? || resource.closed? || resource.contract_locked? || resource.contract_closed? %>
|
||||
<div class="error_msg">
|
||||
<% if resource.contract_locked? || resource.contract_closed? %>
|
||||
<p><%= resource.contract_locked? ? l(:text_contract_locked_warning) : l(:text_contract_closed_warning) %></p>
|
||||
<% end %>
|
||||
<% if resource.locked? || resource.closed? %>
|
||||
<p><%= resource.locked? ? l(:text_deliverable_locked_warning) : l(:text_deliverable_closed_warning) %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="box tabular">
|
||||
<% form.inputs :name => l(:text_deliverable_details_legend) do %>
|
||||
<% form.inputs :name => l(:text_deliverable_details_legend), :id => 'deliverable-details' do %>
|
||||
<%# Used by jquery to check if this is a new or existing record %>
|
||||
<%= hidden_field_tag('deliverable_id', h(resource.id), :id => 'deliverable_stored_id') %>
|
||||
<%= form.input :title, :required => true %>
|
||||
@@ -13,14 +24,19 @@
|
||||
<%= form.select(:type, Deliverable.valid_types_to_select, {:include_blank => false}, {:class => 'type'}) %>
|
||||
</li>
|
||||
<% else %>
|
||||
<li class="string" id="deliverable_type_input">
|
||||
<%= form.label(:type, l(:field_type)) %>
|
||||
<span style="padding:3px"><%= h(resource.humanize_type) %></span>
|
||||
</li>
|
||||
<%= form.input :type, :as => :hidden, :class => 'type' %>
|
||||
<% end %>
|
||||
<%= form.input :status, :required => true, :collection => [["Open","open"],["Locked","locked"],["Closed","closed"]] %>
|
||||
<%= form.input :manager, :required => true, :collection => @project.users.sort %>
|
||||
|
||||
<%= form.input :start_date, :as => :string, :input_html => {:size => 10, :class => 'start-date'}, :hint => calendar_for('deliverable_start_date') %>
|
||||
<%= form.input :start_date, :as => :string, :input_html => {:size => 10, :class => 'start-date', :id => 'deliverable_start_date'}, :hint => calendar_for('deliverable_start_date') %>
|
||||
<%= hidden_field_tag('deliverable_stored_start_date', h(resource.start_date), :id => 'deliverable_stored_start_date') %>
|
||||
|
||||
<%= form.input :end_date, :as => :string, :input_html => {:size => 10, :class => 'end-date'}, :hint => calendar_for('deliverable_end_date') %>
|
||||
<%= form.input :end_date, :as => :string, :input_html => {:size => 10, :class => 'end-date', :id => 'deliverable_end_date'}, :hint => calendar_for('deliverable_end_date') %>
|
||||
<%= hidden_field_tag('deliverable_stored_end_date', h(resource.end_date), :id => 'deliverable_stored_end_date') %>
|
||||
|
||||
<%= form.input :notes, :input_html => {:class => 'wiki-edit', :rows => '5'} %>
|
||||
@@ -40,13 +56,13 @@
|
||||
<% if resource.retainer? && resource.respond_to?(:months) %>
|
||||
<% if resource.months.present? %>
|
||||
<% resource.months.each do |month| %>
|
||||
<%= render :partial => 'finance_form', :locals => {:form => form, :labor_budgets => resource.labor_budgets_for_date(month), :overhead_budgets => resource.overhead_budgets_for_date(month), :label => l(:text_deliverable_finances_date, :date => month.strftime("%B, %Y")), :fieldset_class => 'date-' + month.strftime('%Y-%m') } %>
|
||||
<%= render :partial => 'finance_form', :locals => {:form => form, :labor_budgets => resource.labor_budgets_for_date(month), :overhead_budgets => resource.overhead_budgets_for_date(month), :fixed_budgets => resource.fixed_budgets_for_date(month), :label => l(:text_deliverable_finances_date, :date => month.strftime("%B, %Y")), :fieldset_class => 'date-' + month.strftime('%Y-%m') } %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= content_tag(:p, l(:text_missing_period), :class => 'nodata') %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= render :partial => 'finance_form', :locals => {:form => form, :labor_budgets => resource.labor_budgets, :overhead_budgets => resource.overhead_budgets, :label => l(:text_deliverable_finances), :fieldset_class => '' } %>
|
||||
<%= render :partial => 'finance_form', :locals => {:form => form, :labor_budgets => resource.labor_budgets, :overhead_budgets => resource.overhead_budgets, :fixed_budgets => resource.fixed_budgets, :label => l(:text_deliverable_finances), :fieldset_class => '' } %>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
<%= content_tag(:h2, h(l(:text_edit_deliverable_title, :title => resource.title))) %>
|
||||
|
||||
<%= error_messages_for 'deliverable' %>
|
||||
|
||||
<% semantic_form_for [@project, @contract, setup_nested_deliverable_records(resource)], :url => contract_deliverable_path(@project, @contract, resource), :html => {:class => 'deliverable tabular'} do |form| %>
|
||||
<%= render :partial => 'form', :object => form, :locals => {:cancel_path => contract_path(@project, @contract)} %>
|
||||
<% end %>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
<%= content_tag(:h2, l(:text_new_deliverable)) %>
|
||||
|
||||
<%= error_messages_for 'deliverable' %>
|
||||
|
||||
<% semantic_form_for [@project, @contract, setup_nested_deliverable_records(resource)], :url => contract_deliverables_path(@project, @contract), :html => {:class => 'deliverable tabular'} do |form| %>
|
||||
<%= render :partial => 'form', :object => form, :locals => {:cancel_path => contract_path(@project, @contract)} %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
<% if project.module_enabled?(:contracts) %>
|
||||
<% if project.module_enabled?(:contracts) && User.current.allowed_to?(:assign_deliverable_to_issue, project) %>
|
||||
<p>
|
||||
<%= label_tag(:deliverable_id, l(:field_deliverable)) %>
|
||||
<% options = project.contracts.inject([]) {|data, contract|
|
||||
data << [contract.name, contract.deliverables.collect {|d| [d.title, d.id]} ]
|
||||
} %>
|
||||
|
||||
<%= select_tag('deliverable_id',
|
||||
content_tag('option', l(:label_no_change_option), :value => '') +
|
||||
content_tag('option', l(:label_none), :value => 'none') +
|
||||
grouped_options_for_select(options)) %>
|
||||
grouped_deliverable_options_for_select(project)) %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<% if project.module_enabled?(:contracts) %>
|
||||
<% if project.module_enabled?(:contracts) && User.current.allowed_to?(:assign_deliverable_to_issue, project) %>
|
||||
<p>
|
||||
<% options = project.contracts.inject([]) {|data, contract|
|
||||
data << [contract.name, contract.deliverables.collect {|d| [d.title, d.id]} ]
|
||||
} %>
|
||||
<%= form.select(:deliverable_id, grouped_options_for_select(options, issue.deliverable_id), {:include_blank => true}) %>
|
||||
<%= form.select(:deliverable_id, grouped_deliverable_options_for_select(project, issue.deliverable_id), {:include_blank => true}) %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<% if project.module_enabled?(:contracts) %>
|
||||
<tr>
|
||||
<th class="deliverable"><%= l(:field_deliverable) %>:</th>
|
||||
<td class="deliverable"><%= h(issue.deliverable.title) if issue.deliverable.present? %></td>"
|
||||
<td class="deliverable"><%= h(issue.deliverable.title) if issue.deliverable.present? %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
|
||||
|
||||
BIN
assets/images/todo1.png
Normal file
BIN
assets/images/todo1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 200 B |
BIN
assets/images/todo2.png
Normal file
BIN
assets/images/todo2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 207 B |
BIN
assets/images/todo3.png
Normal file
BIN
assets/images/todo3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 203 B |
BIN
assets/images/todo4.png
Normal file
BIN
assets/images/todo4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 197 B |
BIN
assets/images/todo5.png
Normal file
BIN
assets/images/todo5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 209 B |
@@ -22,6 +22,9 @@ jQuery(function($) {
|
||||
$('#expand_terms').click( function(){
|
||||
$(this).next().slideToggle();
|
||||
$(this).toggleClass('alt');
|
||||
|
||||
var new_height = $('#contract-terms .info').height() - $('#contract-terms .finance').height() + 30;
|
||||
$('#contract-terms .stretch').css('height', new_height);
|
||||
});
|
||||
|
||||
showDeliverableTotal = function() {
|
||||
@@ -48,12 +51,20 @@ jQuery(function($) {
|
||||
if (deliverableType == 'FixedDeliverable') {
|
||||
showDeliverableTotal();
|
||||
hideDeliverableFrequency();
|
||||
$('#retainer-finances-message').hide();
|
||||
} else if(deliverableType == "HourlyDeliverable") {
|
||||
hideDeliverableTotal();
|
||||
hideDeliverableFrequency();
|
||||
$('#retainer-finances-message').hide();
|
||||
} else if(deliverableType == "RetainerDeliverable") {
|
||||
hideDeliverableTotal();
|
||||
showDeliverableFrequency();
|
||||
if ($('form.deliverable #deliverable_stored_id').val() == '') {
|
||||
$('#retainer-finances-message').show();
|
||||
} else {
|
||||
$('#retainer-finances-message').hide();
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ html>body .tabular li {overflow:hidden;}
|
||||
/* End tabular */
|
||||
|
||||
a.contract-delete {color: red; }
|
||||
.overage { color: #A40000; }
|
||||
|
||||
/* Positioning */
|
||||
|
||||
@@ -45,6 +46,15 @@ padding-left: 120px; /*width of left column containing the label elements*/
|
||||
padding-left: 0px; /* Don't pad submit buttons */
|
||||
}
|
||||
|
||||
#retainer-finances-message label { margin-left: 0px; width: 600px; text-align: left;} /* reposition the label */
|
||||
|
||||
#content .deliverable_finance_table td { padding-left: 0px; }
|
||||
|
||||
input.financial{
|
||||
text-align: right;
|
||||
width: 65px;
|
||||
}
|
||||
|
||||
#content .error_msg{
|
||||
background: #ffd5d5;
|
||||
color: #a40000;
|
||||
@@ -54,13 +64,13 @@ padding-left: 0px; /* Don't pad submit buttons */
|
||||
|
||||
#content .error_msg p{
|
||||
padding: 6px;
|
||||
color: #a40000;
|
||||
}
|
||||
#content .error_msg p a {
|
||||
color: #a40000;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#content .error_msg p span{
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.contract .gravatar{
|
||||
float: left;
|
||||
border: 1px solid #b4d1d9;
|
||||
@@ -122,7 +132,9 @@ padding-left: 0px; /* Don't pad submit buttons */
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.deliverable_finance_table p.inline-hints label {font-weight: normal; float: none; display: auto; margin-left: 0px; } /* An inline label so it needs to be reset from .tabular */
|
||||
.deliverable-finances p.inline-hints label {font-weight: normal; float: none; display: auto; margin-left: 0px; } /* An inline label so it needs to be reset from .tabular */
|
||||
|
||||
.deliverable-finances .fixed-item-form p.inline-hints { margin-right: 13px; }
|
||||
|
||||
#expand_terms{
|
||||
margin: 10px 0 10px 0;
|
||||
@@ -382,6 +394,12 @@ padding-left: 0px; /* Don't pad submit buttons */
|
||||
|
||||
#content #deliverables .info p{
|
||||
margin: 10px 0 10px 10px;
|
||||
}
|
||||
|
||||
#content #deliverables .info ul{
|
||||
margin: 10px 0 10px 10px;
|
||||
padding-left: 10px;
|
||||
list-style: inherit;
|
||||
}
|
||||
#content #deliverables .info form fieldset{
|
||||
margin-left: 10px;
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
en:
|
||||
activerecord:
|
||||
errors:
|
||||
messages:
|
||||
cant_create_time_on_object: "Can't create a time entry on a %{reason} %{thing}"
|
||||
cant_assign_to_closed_deliverable: "Can't assign issue to a closed deliverable"
|
||||
cant_assign_to_locked_deliverable: "Can't assign issue to a locked deliverable"
|
||||
cant_assign_to_closed_contract: "Can't assign issue to a closed contract"
|
||||
cant_assign_to_locked_contract: "Can't assign issue to a locked contract"
|
||||
cant_update_locked_deliverable: "Can't update a locked deliverable"
|
||||
cant_update_closed_deliverable: "Can't update a closed deliverable"
|
||||
cant_update_locked_contract: "Can't update a locked contract"
|
||||
cant_update_closed_contract: "Can't update a closed contract"
|
||||
cant_create_deliverable_on_locked_contract: "Can't create a deliverable on a locked contract"
|
||||
cant_create_deliverable_on_closed_contract: "Can't create a deliverable on a closed contract"
|
||||
|
||||
field_end_date: End Date
|
||||
field_executed: Executed
|
||||
text_contracts: Contracts
|
||||
@@ -34,14 +49,17 @@ en:
|
||||
field_client_point_of_contact: "Point of Contact"
|
||||
field_discount_budget: "Discount"
|
||||
field_discount_spent: "Discount"
|
||||
field_estimated_hour_budget: "Est. Hours"
|
||||
field_estimated_hour_spent: "Est. Hours"
|
||||
field_estimated_hour_budget: "Total Hours"
|
||||
field_estimated_hour_spent: "Total Hours"
|
||||
field_labor_hour_spent: "Labor Hours"
|
||||
field_overhead_hour_spent: "Overhead Hours"
|
||||
field_total_hours: "Total Hours"
|
||||
field_fixed_budget: "Fixed"
|
||||
field_fixed_spent: "Fixed"
|
||||
field_labor_budget: "Labor"
|
||||
field_labor_spent: "Labor"
|
||||
field_markup_budget: "Markup"
|
||||
field_markup_spent: "Markup"
|
||||
field_fixed_markup_budget: "Markup"
|
||||
field_fixed_markup_spent: "Markup"
|
||||
field_overhead_budget: "Overhead"
|
||||
field_overhead_spent: "Overhead"
|
||||
field_profit_budget: "Profit"
|
||||
@@ -66,3 +84,19 @@ en:
|
||||
text_missing_period: "This deliverable is missing a date range so it cannot have budget items. Please save start and end dates before adding any budget items."
|
||||
text_changed_period_message: "The period for this deliverable has been changed. Would you like to expand/shrink the Deliverable Finances?"
|
||||
field_current_period: "Current period"
|
||||
text_retainer_monthly_message: "Enter budget for a representative month. Any overrides to individual months can be done via the editor after saving."
|
||||
text_flash_deliverable_created: "Deliverable: {{name}} was successfully created."
|
||||
text_flash_deliverable_updated: "Deliverable: {{name}} was successfully updated."
|
||||
text_flash_deliverable_deleted: "Deliverable: {{name}} was successfully deleted."
|
||||
field_budget: Budget
|
||||
field_markup: Markup
|
||||
field_paid: Paid
|
||||
field_spent: Spent
|
||||
field_profit: Profit
|
||||
text_error_message_orphaned_time: "There is {{amount}} worth of time clocked to issues that are not assigned to any deliverables."
|
||||
text_error_message_update_orphaned_time: "Please update the orphaned issues."
|
||||
field_estimated: Estimated
|
||||
text_deliverable_locked_warning: "This deliverable is locked and cannot be saved without changing it's status to Open."
|
||||
text_deliverable_closed_warning: "This deliverable is closed and cannot be saved without changing it's status to Open."
|
||||
text_contract_locked_warning: "This contract is locked and cannot be saved without changing it's status to Open."
|
||||
text_contract_closed_warning: "This contract is closed and cannot be saved without changing it's status to Open."
|
||||
|
||||
19
db/migrate/015_create_fixed_budgets.rb
Normal file
19
db/migrate/015_create_fixed_budgets.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
class CreateFixedBudgets < ActiveRecord::Migration
|
||||
def self.up
|
||||
create_table :fixed_budgets do |t|
|
||||
t.string :title
|
||||
t.decimal :budget, :precision => 15, :scale => 4
|
||||
t.string :markup
|
||||
t.text :description
|
||||
t.references :deliverable
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :fixed_budgets, :deliverable_id
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :fixed_budgets
|
||||
end
|
||||
end
|
||||
14
db/migrate/016_add_year_and_month_to_fixed_budgets.rb
Normal file
14
db/migrate/016_add_year_and_month_to_fixed_budgets.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
class AddYearAndMonthToFixedBudgets < ActiveRecord::Migration
|
||||
def self.up
|
||||
add_column :fixed_budgets, :year, :integer
|
||||
add_index :fixed_budgets, :year
|
||||
|
||||
add_column :fixed_budgets, :month, :integer
|
||||
add_index :fixed_budgets, :month
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :fixed_budgets, :year
|
||||
remove_column :fixed_budgets, :month
|
||||
end
|
||||
end
|
||||
10
db/migrate/017_add_paid_to_fixed_budgets.rb
Normal file
10
db/migrate/017_add_paid_to_fixed_budgets.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
class AddPaidToFixedBudgets < ActiveRecord::Migration
|
||||
def self.up
|
||||
add_column :fixed_budgets, :paid, :boolean
|
||||
add_index :fixed_budgets, :paid
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :fixed_budgets, :paid
|
||||
end
|
||||
end
|
||||
10
db/migrate/018_add_status_to_contracts.rb
Normal file
10
db/migrate/018_add_status_to_contracts.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
class AddStatusToContracts < ActiveRecord::Migration
|
||||
def self.up
|
||||
add_column :contracts, :status, :string
|
||||
add_index :contracts, :status
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :contracts, :status
|
||||
end
|
||||
end
|
||||
10
db/migrate/019_add_status_to_deliverables.rb
Normal file
10
db/migrate/019_add_status_to_deliverables.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
class AddStatusToDeliverables < ActiveRecord::Migration
|
||||
def self.up
|
||||
add_column :deliverables, :status, :string
|
||||
add_index :deliverables, :status
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :deliverables, :status
|
||||
end
|
||||
end
|
||||
51
init.rb
51
init.rb
@@ -1,11 +1,5 @@
|
||||
config.gem 'formtastic', :version => '0.9.10'
|
||||
|
||||
if Rails.env.test?
|
||||
config.gem "stackdeck"
|
||||
config.gem "johnson", :version => '2.0.0.pre3'
|
||||
config.gem "holygrail"
|
||||
end
|
||||
|
||||
require 'redmine'
|
||||
|
||||
Redmine::Plugin.register :redmine_contracts do
|
||||
@@ -24,14 +18,38 @@ Redmine::Plugin.register :redmine_contracts do
|
||||
permission(:manage_budget, {
|
||||
:contracts => [:index, :new, :create, :show, :edit, :update, :destroy],
|
||||
:deliverables => [:index, :new, :create, :show, :edit, :update, :destroy]
|
||||
}, :public => true)
|
||||
})
|
||||
end
|
||||
|
||||
project_module :issue_tracking do
|
||||
permission(:assign_deliverable_to_issue, {})
|
||||
end
|
||||
|
||||
contract_list_submenu_items = Proc.new {|project|
|
||||
if project && project.module_enabled?(:contracts)
|
||||
|
||||
project.contracts.inject([]) do |menu_items, contract|
|
||||
|
||||
menu_items << ::Redmine::MenuManager::MenuItem.new("contract-#{contract.id}",
|
||||
{ :controller => 'contracts', :action => 'show', :id => contract.id, :project_id => project},
|
||||
# TODO: http://www.redmine.org/issues/6426
|
||||
# contract_path(project, contract),
|
||||
{
|
||||
:caption => contract.name, # h-escaped in Redmine
|
||||
:param => :project_id,
|
||||
:parent => :contracts
|
||||
})
|
||||
end
|
||||
|
||||
end
|
||||
}
|
||||
|
||||
menu(:project_menu,
|
||||
:contracts,
|
||||
{:controller => 'contracts', :action => 'index'},
|
||||
:caption => :text_contracts,
|
||||
:param => :project_id)
|
||||
:param => :project_id,
|
||||
:children => contract_list_submenu_items)
|
||||
|
||||
menu(:project_menu,
|
||||
:new_contract,
|
||||
@@ -44,6 +62,9 @@ end
|
||||
|
||||
require 'dispatcher'
|
||||
Dispatcher.to_prepare :redmine_contracts do
|
||||
|
||||
require_dependency 'time_entry'
|
||||
TimeEntry.send(:include, RedmineContracts::Patches::TimeEntryPatch)
|
||||
gem 'inherited_resources', :version => '1.0.6'
|
||||
require_dependency 'inherited_resources'
|
||||
require_dependency 'inherited_resources/base'
|
||||
@@ -57,6 +78,7 @@ Dispatcher.to_prepare :redmine_contracts do
|
||||
|
||||
Formtastic::SemanticFormBuilder.all_fields_required_by_default = false
|
||||
Formtastic::SemanticFormBuilder.required_string = "<span class='required'> *</span>"
|
||||
Formtastic::SemanticFormBuilder.inline_errors = :none
|
||||
|
||||
require_dependency 'payment_term' # Load so Enumeration will pick up the subclass in dev
|
||||
|
||||
@@ -69,13 +91,20 @@ Dispatcher.to_prepare :redmine_contracts do
|
||||
Query.send(:include, RedmineContracts::Patches::QueryPatch)
|
||||
end
|
||||
|
||||
unless Query.available_columns.collect(&:name).include?(:deliverable_title)
|
||||
Query.add_available_column(QueryColumn.new(:deliverable_title, :sortable => "#{Deliverable.table_name}.title"))
|
||||
unless Query.available_columns.collect(&:name).include?(:deliverable)
|
||||
Query.add_available_column(QueryColumn.new(:deliverable, :sortable => "#{Deliverable.table_name}.title", :groupable => 'deliverable'))
|
||||
end
|
||||
|
||||
# Hack in order to get the associated contract to be grouped by name
|
||||
# * Proxy method Issue#contract_name
|
||||
# * Naming Query column contract_name
|
||||
# * Grouping by 'contracts.name'
|
||||
unless Query.available_columns.collect(&:name).include?(:contract_name)
|
||||
Query.add_available_column(QueryColumn.new(:contract_name, :sortable => "#{Contract.table_name}.name"))
|
||||
Query.add_available_column(QueryColumn.new(:contract_name, :sortable => "#{Contract.table_name}.name", :groupable => 'contracts.name'))
|
||||
end
|
||||
|
||||
require_dependency 'application_controller'
|
||||
ApplicationController.send(:helper, :contracts)
|
||||
end
|
||||
|
||||
require 'redmine_contracts/hooks/view_layouts_base_html_head_hook'
|
||||
|
||||
23
lib/dollarized_attribute.rb
Normal file
23
lib/dollarized_attribute.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# Shared module to allow seting an attribute using:
|
||||
# * Dollar amount - $1,000.00
|
||||
# * Number - 100.00
|
||||
module DollarizedAttribute
|
||||
module ClassMethods
|
||||
|
||||
# dollarized_attribute(:budget) will create a budget=(value) method
|
||||
def dollarized_attribute(attribute)
|
||||
define_method(attribute.to_s + '=') {|value|
|
||||
if value.is_a? String
|
||||
write_attribute(attribute, value.gsub(/[$ ,]/, ''))
|
||||
else
|
||||
write_attribute(attribute, value)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
end
|
||||
@@ -29,8 +29,45 @@ module RedmineContracts
|
||||
end
|
||||
|
||||
# * old_data - YAML string of deliverables to migrate
|
||||
#
|
||||
# @param [Hash] options the options to migrate with
|
||||
# @option options [String] :contract_rate the contract rate to use. Defaults to 150.0
|
||||
# @option options [String] :account_executive the id or login of the user
|
||||
# to use for the contract account executive
|
||||
# Defaults to the first user on the project.
|
||||
# @option options [String] :deliverable_manager the id or login of the user
|
||||
# to use for the deliverables manager
|
||||
# Defaults to the first user on the project.
|
||||
# @option options [boolean] :append_object_notes show the old Budget data be
|
||||
# added to the Deliverable notes (for debugging)
|
||||
# Defaults to true (will append)
|
||||
# @option options [float] :overhead_rate the overhead rate to use when calculating hours.
|
||||
# Defaults to 0
|
||||
def self.migrate(old_data, options={})
|
||||
@contract_rate = options[:contract_rate] ? options[:contract_rate].to_f : 150.0
|
||||
@account_executive = if options[:account_executive].present?
|
||||
user = User.find_by_login(options[:account_executive])
|
||||
user ||= User.find_by_id(options[:account_executive])
|
||||
end
|
||||
@deliverable_manager = if options[:deliverable_manager].present?
|
||||
user = User.find_by_login(options[:deliverable_manager])
|
||||
user ||= User.find_by_id(options[:deliverable_manager])
|
||||
end
|
||||
|
||||
@append_object_notes = if options[:append_object_notes].nil?
|
||||
true
|
||||
else
|
||||
# Simple option parsing
|
||||
if options[:append_object_notes] == false ||
|
||||
options[:append_object_notes] == 'false' ||
|
||||
options[:append_object_notes] == 0 ||
|
||||
options[:append_object_notes] == '0'
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
@overhead_rate = options[:overhead_rate].nil? ? 0 : options[:overhead_rate].to_f
|
||||
|
||||
@@data = YAML.load(old_data)
|
||||
|
||||
@@ -52,12 +89,14 @@ module RedmineContracts
|
||||
contract ||= create_new_contract(old_deliverable)
|
||||
|
||||
deliverable.contract = contract
|
||||
deliverable.manager = project.users.first
|
||||
deliverable.manager = @deliverable_manager || project.users.first
|
||||
deliverable.total = old_deliverable['budget']
|
||||
|
||||
case old_deliverable['type']
|
||||
when 'FixedDeliverable'
|
||||
@total_cost = old_deliverable['fixed_cost']
|
||||
convert_old_fixed_deliverable_to_fixed_budgets(deliverable, old_deliverable)
|
||||
|
||||
when 'HourlyDeliverable'
|
||||
@total_cost = old_deliverable['total_hours'].to_f * old_deliverable['cost_per_hour'].to_f
|
||||
|
||||
@@ -72,7 +111,7 @@ module RedmineContracts
|
||||
|
||||
convert_overhead(deliverable, old_deliverable, @total_cost)
|
||||
convert_materials(deliverable, old_deliverable, @total_cost)
|
||||
append_old_deliverable_to_notes(old_deliverable, deliverable)
|
||||
append_old_deliverable_to_notes(old_deliverable, deliverable) if @append_object_notes
|
||||
|
||||
deliverable.save!
|
||||
|
||||
@@ -107,7 +146,7 @@ module RedmineContracts
|
||||
:start_date => old_deliverable['due'],
|
||||
:end_date => old_deliverable['due']) do |c|
|
||||
c.project = project
|
||||
c.account_executive = project.users.first
|
||||
c.account_executive = @account_executive || project.users.first
|
||||
c.start_date ||= Date.today
|
||||
c.end_date ||= Date.today
|
||||
c.billable_rate = @contract_rate
|
||||
@@ -122,16 +161,27 @@ module RedmineContracts
|
||||
|
||||
def self.convert_overhead(deliverable, old_deliverable, total)
|
||||
total ||= 0
|
||||
|
||||
if old_deliverable['overhead'].present?
|
||||
if @overhead_rate != 0
|
||||
hours = old_deliverable['overhead'] / @overhead_rate
|
||||
else
|
||||
hours = 0
|
||||
end
|
||||
|
||||
deliverable.overhead_budgets << OverheadBudget.new(:deliverable => deliverable,
|
||||
:budget => old_deliverable['overhead'],
|
||||
:hours => 0)
|
||||
:hours => hours.to_f.round(2))
|
||||
elsif old_deliverable['overhead_percent'].present?
|
||||
overhead = total * (old_deliverable['overhead_percent'].to_f / 100)
|
||||
if @overhead_rate != 0
|
||||
hours = overhead / @overhead_rate
|
||||
else
|
||||
hours = 0
|
||||
end
|
||||
|
||||
deliverable.overhead_budgets << OverheadBudget.new(:deliverable => deliverable,
|
||||
:budget => overhead,
|
||||
:hours => 0)
|
||||
:hours => hours.to_f.round(2))
|
||||
|
||||
end
|
||||
end
|
||||
@@ -140,19 +190,41 @@ module RedmineContracts
|
||||
total ||= 0
|
||||
|
||||
if old_deliverable['materials'].present? && old_deliverable['materials'] > 0.0
|
||||
deliverable.overhead_budgets << OverheadBudget.new(:deliverable => deliverable,
|
||||
:budget => old_deliverable['materials'],
|
||||
:hours => 0)
|
||||
deliverable.fixed_budgets << FixedBudget.new(:deliverable => deliverable,
|
||||
:budget => old_deliverable['materials'],
|
||||
:markup => 0)
|
||||
|
||||
elsif old_deliverable['materials_percent'].present? && old_deliverable['materials_percent'] > 0.0
|
||||
materials = total * (old_deliverable['materials_percent'].to_f / 100)
|
||||
deliverable.overhead_budgets << OverheadBudget.new(:deliverable => deliverable,
|
||||
:budget => materials,
|
||||
:hours => 0)
|
||||
deliverable.fixed_budgets << FixedBudget.new(:deliverable => deliverable,
|
||||
:budget => materials,
|
||||
:markup => 0)
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
def self.convert_old_fixed_deliverable_to_fixed_budgets(deliverable, old_deliverable)
|
||||
if old_deliverable['fixed_cost'].present?
|
||||
budget = old_deliverable['fixed_cost']
|
||||
else
|
||||
budget = 0
|
||||
end
|
||||
|
||||
if old_deliverable['profit'].present?
|
||||
markup = old_deliverable['profit']
|
||||
elsif old_deliverable['profit_percent'].present?
|
||||
markup = old_deliverable['profit_percent'].to_s + "%"
|
||||
else
|
||||
markup = '0'
|
||||
end
|
||||
|
||||
deliverable.fixed_budgets << FixedBudget.new(:deliverable => deliverable,
|
||||
:budget => budget,
|
||||
:markup => markup,
|
||||
:title => "Converted Fixed Deliverable - #{old_deliverable['subject']}")
|
||||
|
||||
end
|
||||
|
||||
def self.append_old_deliverable_to_notes(old_deliverable, new_deliverable)
|
||||
new_deliverable.notes += "Converted data:\n<pre>" + old_deliverable.pretty_inspect + "</pre>"
|
||||
end
|
||||
|
||||
@@ -6,6 +6,8 @@ module RedmineContracts
|
||||
# * :params => HTML parameters
|
||||
#
|
||||
def controller_issues_bulk_edit_before_save(context={})
|
||||
return '' unless User.current.allowed_to?(:assign_deliverable_to_issue, context[:issue].project)
|
||||
|
||||
case
|
||||
when context[:params][:deliverable_id].blank?
|
||||
# Do nothing
|
||||
|
||||
@@ -4,14 +4,16 @@ module RedmineContracts
|
||||
def controller_issues_edit_before_save(context={})
|
||||
|
||||
if context[:params] && context[:params][:issue]
|
||||
if context[:params][:issue][:deliverable_id].present?
|
||||
deliverable = Deliverable.find_by_id(context[:params][:issue][:deliverable_id])
|
||||
if deliverable.contract.project == context[:issue].project
|
||||
context[:issue].deliverable = deliverable
|
||||
end
|
||||
if User.current.allowed_to?(:assign_deliverable_to_issue, context[:issue].project)
|
||||
if context[:params][:issue][:deliverable_id].present?
|
||||
deliverable = Deliverable.find_by_id(context[:params][:issue][:deliverable_id])
|
||||
if deliverable.contract.project == context[:issue].project
|
||||
context[:issue].deliverable = deliverable
|
||||
end
|
||||
|
||||
else
|
||||
context[:issue].deliverable = nil
|
||||
else
|
||||
context[:issue].deliverable = nil
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -10,6 +10,8 @@ module RedmineContracts
|
||||
def helper_issues_show_detail_after_setting(context = { })
|
||||
# TODO Later: Overwritting the caller is bad juju
|
||||
if context[:detail].prop_key == 'deliverable_id'
|
||||
context[:detail].reload
|
||||
|
||||
d = Deliverable.find_by_id(context[:detail].value)
|
||||
context[:detail].value = d.title if d.present? && d.title.present?
|
||||
|
||||
|
||||
@@ -10,7 +10,29 @@ module RedmineContracts
|
||||
belongs_to :deliverable
|
||||
|
||||
delegate :title, :to => :deliverable, :prefix => true, :allow_nil => true
|
||||
delegate :contract_name, :to => :deliverable, :allow_nil => true
|
||||
delegate :contract, :to => :deliverable, :allow_nil => true
|
||||
|
||||
def contract_name
|
||||
contract.try(:name)
|
||||
end
|
||||
|
||||
validate :validate_deliverable_status
|
||||
validate :validate_contract_status
|
||||
|
||||
def validate_deliverable_status
|
||||
if deliverable.present? && changes["deliverable_id"].present?
|
||||
errors.add_to_base(:cant_assign_to_closed_deliverable) if deliverable.closed?
|
||||
errors.add_to_base(:cant_assign_to_locked_deliverable) if deliverable.locked?
|
||||
end
|
||||
end
|
||||
|
||||
def validate_contract_status
|
||||
if deliverable.present? && changes["deliverable_id"].present? && contract.present?
|
||||
errors.add_to_base(:cant_assign_to_closed_contract) if contract.closed?
|
||||
errors.add_to_base(:cant_assign_to_locked_contract) if contract.locked?
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -12,6 +12,36 @@ module RedmineContracts
|
||||
alias_method_chain :available_filters, :contract
|
||||
|
||||
alias_method_chain :sql_for_field, :contract
|
||||
|
||||
alias_method_chain :issues, :deliverable
|
||||
alias_method_chain :issues, :contract
|
||||
|
||||
# Override Query#count_by_group to allow adding include options like
|
||||
# Query#issues
|
||||
# TODO: core bug: Query#issue_count_by_group doesn't allow setting
|
||||
# options like Query#issue does.
|
||||
def issue_count_by_group(options={})
|
||||
includes = ([:status, :project] + (options[:include] || [])).uniq
|
||||
|
||||
r = nil
|
||||
if grouped?
|
||||
begin
|
||||
# Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
|
||||
r = Issue.count(:group => group_by_statement, :include => includes, :conditions => statement)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
r = {nil => issue_count}
|
||||
end
|
||||
c = group_by_column
|
||||
if c.is_a?(QueryCustomFieldColumn)
|
||||
r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
|
||||
end
|
||||
end
|
||||
r
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise ::Query::StatementInvalid.new(e.message)
|
||||
end
|
||||
|
||||
alias_method_chain :issue_count_by_group, :contract
|
||||
end
|
||||
end
|
||||
|
||||
@@ -83,6 +113,37 @@ module RedmineContracts
|
||||
end
|
||||
end
|
||||
|
||||
# Add the deliverables into the includes
|
||||
#
|
||||
# Used with grouping
|
||||
def issues_with_deliverable(options={})
|
||||
options[:include] ||= []
|
||||
options[:include] << :deliverable
|
||||
|
||||
issues_without_deliverable(options)
|
||||
end
|
||||
|
||||
# Add the contracts into the includes
|
||||
#
|
||||
# Used with grouping
|
||||
def issues_with_contract(options={})
|
||||
options[:include] ||= []
|
||||
options[:include] << {:deliverable => :contract}
|
||||
|
||||
issues_without_contract(options)
|
||||
end
|
||||
|
||||
# Add the contracts into the includes
|
||||
#
|
||||
# Used with grouping
|
||||
def issue_count_by_group_with_contract(options={})
|
||||
options[:include] ||= []
|
||||
options[:include] << {:deliverable => :contract}
|
||||
|
||||
issue_count_by_group_without_contract(options)
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
38
lib/redmine_contracts/patches/time_entry_patch.rb
Normal file
38
lib/redmine_contracts/patches/time_entry_patch.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
module RedmineContracts
|
||||
module Patches
|
||||
module TimeEntryPatch
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
|
||||
base.send(:include, InstanceMethods)
|
||||
base.class_eval do
|
||||
unloadable
|
||||
|
||||
validate :validate_deliverable_status
|
||||
validate :validate_contract_status
|
||||
|
||||
def validate_deliverable_status
|
||||
if issue.present? && issue.deliverable.present?
|
||||
errors.add_to_base("#{l(:"activerecord.errors.messages.cant_create_time_on_object", :reason => 'locked', :thing => 'deliverable')}") if issue.deliverable.locked?
|
||||
errors.add_to_base("#{l(:"activerecord.errors.messages.cant_create_time_on_object", :reason => 'closed', :thing => 'deliverable')}") if issue.deliverable.closed?
|
||||
end
|
||||
end
|
||||
|
||||
def validate_contract_status
|
||||
if issue.present? && issue.deliverable.present? && issue.deliverable.contract.present?
|
||||
errors.add_to_base("#{l(:"activerecord.errors.messages.cant_create_time_on_object", :reason => 'locked', :thing => 'contract')}") if issue.deliverable.contract.locked?
|
||||
errors.add_to_base("#{l(:"activerecord.errors.messages.cant_create_time_on_object", :reason => 'closed', :thing => 'contract')}") if issue.deliverable.contract.closed?
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,10 @@ namespace :redmine_contracts do
|
||||
task :budget_migration => :environment do
|
||||
options = {}
|
||||
options[:contract_rate] = ENV['contract_rate']
|
||||
options[:account_executive] = ENV['account_executive']
|
||||
options[:deliverable_manager] = ENV['deliverable_manager']
|
||||
options[:append_object_notes] = ENV['append_object_notes']
|
||||
options[:overhead_rate] = ENV['overhead_rate']
|
||||
|
||||
RedmineContracts::BudgetPluginMigration.check_for_installed_budget_plugin
|
||||
data = RedmineContracts::BudgetPluginMigration.export_data
|
||||
|
||||
@@ -19,10 +19,13 @@ class BudgetPluginMigrationTest < ActionController::IntegrationTest
|
||||
Project.stubs(:find_by_id).with(1).returns(@project_one)
|
||||
Project.stubs(:find_by_id).with(2).returns(@project_one)
|
||||
|
||||
@manager = User.generate!
|
||||
@role = Role.generate!
|
||||
@manager = User.generate!
|
||||
User.add_to_project(@manager, @project_one, @role)
|
||||
User.add_to_project(@manager, @project_two, @role)
|
||||
@other_user = User.generate!
|
||||
User.add_to_project(@other_user, @project_one, @role)
|
||||
User.add_to_project(@other_user, @project_two, @role)
|
||||
|
||||
end
|
||||
|
||||
@@ -67,6 +70,27 @@ class BudgetPluginMigrationTest < ActionController::IntegrationTest
|
||||
assert_equal 100.5, @project_one.reload.contracts.first.billable_rate
|
||||
assert_equal 100.5, @project_two.reload.contracts.first.billable_rate
|
||||
end
|
||||
|
||||
should "pick the first project member as the account executive" do
|
||||
RedmineContracts::BudgetPluginMigration.migrate(@data)
|
||||
|
||||
assert_equal @manager, @project_one.reload.contracts.first.account_executive
|
||||
assert_equal @manager, @project_two.reload.contracts.first.account_executive
|
||||
end
|
||||
|
||||
should "allow overriding the account executive by login" do
|
||||
RedmineContracts::BudgetPluginMigration.migrate(@data, :account_executive => @other_user.login)
|
||||
|
||||
assert_equal @other_user, @project_one.reload.contracts.first.account_executive
|
||||
assert_equal @other_user, @project_two.reload.contracts.first.account_executive
|
||||
end
|
||||
|
||||
should "allow overriding the account executive by id" do
|
||||
RedmineContracts::BudgetPluginMigration.migrate(@data, :account_executive => @other_user.id)
|
||||
|
||||
assert_equal @other_user, @project_one.reload.contracts.first.account_executive
|
||||
assert_equal @other_user, @project_two.reload.contracts.first.account_executive
|
||||
end
|
||||
end
|
||||
|
||||
should "enable the contracts plugin for each project with a contract" do
|
||||
@@ -84,71 +108,111 @@ class BudgetPluginMigrationTest < ActionController::IntegrationTest
|
||||
assert_equal [@manager, @manager, @manager], Deliverable.all.collect(&:manager)
|
||||
end
|
||||
|
||||
should "create a new Overhead Budget record for any overhead" do
|
||||
assert_difference("OverheadBudget.count", 5) do
|
||||
should "allow overriding the deliverable manager by login" do
|
||||
RedmineContracts::BudgetPluginMigration.migrate(@data, :deliverable_manager => @other_user.login)
|
||||
|
||||
assert_equal [@other_user, @other_user, @other_user], Deliverable.all.collect(&:manager)
|
||||
end
|
||||
|
||||
should "allow overriding the deliverable manager by id" do
|
||||
RedmineContracts::BudgetPluginMigration.migrate(@data, :deliverable_manager => @other_user.id)
|
||||
|
||||
assert_equal [@other_user, @other_user, @other_user], Deliverable.all.collect(&:manager)
|
||||
end
|
||||
|
||||
context "Overhead Budgets" do
|
||||
should "create a new Overhead Budget record for any overhead" do
|
||||
assert_difference("OverheadBudget.count", 3) do
|
||||
RedmineContracts::BudgetPluginMigration.migrate(@data)
|
||||
end
|
||||
|
||||
d = Deliverable.find_by_title("Deliverable One")
|
||||
assert_equal 1, d.overhead_budgets.count
|
||||
assert_equal 200, d.overhead_budget_total
|
||||
|
||||
overhead = d.overhead_budgets.first
|
||||
assert overhead
|
||||
assert_equal 200, overhead.budget
|
||||
assert_equal 0, overhead.hours
|
||||
end
|
||||
|
||||
should "create a new Overhead Budget record for any overhead percent" do
|
||||
assert_difference("OverheadBudget.count", 3) do
|
||||
RedmineContracts::BudgetPluginMigration.migrate(@data)
|
||||
end
|
||||
|
||||
d = Deliverable.find_by_title("Deliverable 2")
|
||||
assert_equal 1, d.overhead_budgets.count
|
||||
|
||||
overhead = d.overhead_budgets.first
|
||||
assert overhead
|
||||
assert_equal 12 * 25 * 1.5, overhead.budget
|
||||
assert_equal 0, overhead.hours
|
||||
|
||||
end
|
||||
|
||||
should "allow setting an overhead rate to compute the overhead hours" do
|
||||
RedmineContracts::BudgetPluginMigration.migrate(@data, :overhead_rate => 100.0)
|
||||
|
||||
d1 = Deliverable.find_by_title("Deliverable One")
|
||||
assert_equal 1, d1.overhead_budgets.count
|
||||
assert_equal 200, d1.overhead_budget_total
|
||||
|
||||
overhead = d1.overhead_budgets.first
|
||||
assert overhead
|
||||
assert_equal 2, overhead.hours
|
||||
|
||||
d2 = Deliverable.find_by_title("Deliverable 2")
|
||||
assert_equal 1, d2.overhead_budgets.count
|
||||
|
||||
overhead = d2.overhead_budgets.first
|
||||
assert overhead
|
||||
assert_equal 4.5, overhead.hours
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
should "create a new Fixed Budget record for any materials" do
|
||||
assert_difference("FixedBudget.count", 3) do
|
||||
RedmineContracts::BudgetPluginMigration.migrate(@data)
|
||||
end
|
||||
|
||||
d = Deliverable.find_by_title("Deliverable One")
|
||||
assert_equal 2, d.overhead_budgets.count
|
||||
assert_equal 400, d.overhead_budget_total
|
||||
|
||||
overhead = d.overhead_budgets.first
|
||||
assert overhead
|
||||
assert_equal 200, overhead.budget
|
||||
assert_equal 0, overhead.hours
|
||||
assert_equal 1, d.fixed_budgets.count
|
||||
assert_equal 200, d.fixed_budget_total
|
||||
end
|
||||
|
||||
should "create a new Overhead Budget record for any overhead percent" do
|
||||
assert_difference("OverheadBudget.count", 5) do
|
||||
should "create a new Fixed Budget record for any materials percent" do
|
||||
assert_difference("FixedBudget.count", 3) do
|
||||
RedmineContracts::BudgetPluginMigration.migrate(@data)
|
||||
end
|
||||
|
||||
d = Deliverable.find_by_title("Deliverable 2")
|
||||
assert_equal 2, d.overhead_budgets.count
|
||||
|
||||
overhead = d.overhead_budgets.first
|
||||
assert overhead
|
||||
assert_equal 12 * 25 * 1.5, overhead.budget
|
||||
assert_equal 0, overhead.hours
|
||||
|
||||
end
|
||||
|
||||
should "create a new Overhead Budget record for any materials" do
|
||||
assert_difference("OverheadBudget.count", 5) do
|
||||
RedmineContracts::BudgetPluginMigration.migrate(@data)
|
||||
end
|
||||
|
||||
d = Deliverable.find_by_title("Deliverable One")
|
||||
assert_equal 2, d.overhead_budgets.count
|
||||
assert_equal 400, d.overhead_budget_total
|
||||
|
||||
materials = d.overhead_budgets.last
|
||||
assert materials
|
||||
assert_equal 200, materials.budget
|
||||
assert_equal 0, materials.hours
|
||||
end
|
||||
|
||||
should "create a new Overhead Budget record for any materials percent" do
|
||||
assert_difference("OverheadBudget.count", 5) do
|
||||
RedmineContracts::BudgetPluginMigration.migrate(@data)
|
||||
end
|
||||
|
||||
d = Deliverable.find_by_title("Deliverable 2")
|
||||
assert_equal 2, d.overhead_budgets.count
|
||||
materials = d.overhead_budgets.last
|
||||
assert_equal 1, d.fixed_budgets.count
|
||||
materials = d.fixed_budgets.first
|
||||
assert materials
|
||||
assert_equal 12 * 25 * 0.1, materials.budget
|
||||
assert_equal 0, materials.hours
|
||||
assert_equal 0, materials.markup.to_i
|
||||
|
||||
end
|
||||
|
||||
should "append the YAML dump of the old object to the notes" do
|
||||
RedmineContracts::BudgetPluginMigration.migrate(@data)
|
||||
d = Deliverable.find_by_title("Deliverable One")
|
||||
context "YAML dumping of the old object to notes" do
|
||||
should "be appended by default" do
|
||||
RedmineContracts::BudgetPluginMigration.migrate(@data)
|
||||
d = Deliverable.find_by_title("Deliverable One")
|
||||
|
||||
assert_match /Converted data/, d.notes
|
||||
assert_match /"profit"=>200.0/, d.notes
|
||||
end
|
||||
|
||||
should "be have an option to be turned off" do
|
||||
RedmineContracts::BudgetPluginMigration.migrate(@data, :append_object_notes => false)
|
||||
d = Deliverable.find_by_title("Deliverable One")
|
||||
|
||||
assert_equal nil, d.notes.match(/Converted data/)
|
||||
assert_equal nil, d.notes.match(/"profit"=>200.0/)
|
||||
end
|
||||
|
||||
assert_match /Converted data/, d.notes
|
||||
assert_match /"profit"=>200.0/, d.notes
|
||||
end
|
||||
|
||||
context "converting Fixed Deliverables" do
|
||||
@@ -158,6 +222,17 @@ class BudgetPluginMigrationTest < ActionController::IntegrationTest
|
||||
d = FixedDeliverable.find_by_title("Version 1.0")
|
||||
assert_equal 93_000, d.total
|
||||
end
|
||||
|
||||
should "add a FixedBudget item for the total deliverable" do
|
||||
RedmineContracts::BudgetPluginMigration.migrate(@data)
|
||||
d = FixedDeliverable.find_by_title("Version 1.0")
|
||||
|
||||
assert_equal 1, d.fixed_budgets.count
|
||||
fixed_budget_item = d.fixed_budgets.first
|
||||
assert_equal 30_000, fixed_budget_item.budget
|
||||
assert_equal "150%", fixed_budget_item.markup
|
||||
assert_equal "Converted Fixed Deliverable - Version 1.0", fixed_budget_item.title
|
||||
end
|
||||
end
|
||||
|
||||
context "converting Hourly Deliverables" do
|
||||
|
||||
@@ -6,6 +6,9 @@ class ContractsDeleteTest < ActionController::IntegrationTest
|
||||
def setup
|
||||
@project = Project.generate!(:identifier => 'main')
|
||||
@contract = Contract.generate!(:project => @project, :name => 'A Contract')
|
||||
@user = User.generate_user_with_permission_to_manage_budget(:project => @project)
|
||||
|
||||
login_as(@user.login, 'contracts')
|
||||
end
|
||||
|
||||
should "allow admins to delete the contract" do
|
||||
@@ -39,10 +42,7 @@ class ContractsDeleteTest < ActionController::IntegrationTest
|
||||
|
||||
assert_select "a", :text => /Delete/, :count => 0
|
||||
delete contract_path(@project, @contract)
|
||||
assert_response :redirect
|
||||
follow_redirect!
|
||||
assert_response :success
|
||||
assert_template 'account/login' # Prompt for login
|
||||
assert_forbidden
|
||||
|
||||
assert Contract.find_by_id(@contract.id), "Contract deleted"
|
||||
end
|
||||
|
||||
@@ -9,9 +9,30 @@ class ContractsEditTest < ActionController::IntegrationTest
|
||||
@role = Role.generate!
|
||||
User.add_to_project(@account_executive, @project, @role)
|
||||
@contract = Contract.generate!(:project => @project, :name => 'A Contract', :account_executive => @account_executive)
|
||||
@user = User.generate_user_with_permission_to_manage_budget(:project => @project)
|
||||
|
||||
login_as(@user.login, 'contracts')
|
||||
end
|
||||
|
||||
should "allow any user to edit the contract" do
|
||||
should "block anonymous users from editing the contract" do
|
||||
logout
|
||||
visit "/projects/#{@project.identifier}/contracts/#{@contract.id}/edit"
|
||||
|
||||
assert_requires_login
|
||||
end
|
||||
|
||||
should "block unauthorized users from editing the contract" do
|
||||
logout
|
||||
|
||||
@user = User.generate!(:password => 'test', :password_confirmation => 'test')
|
||||
login_as(@user.login, 'test')
|
||||
|
||||
visit "/projects/#{@project.identifier}/contracts/#{@contract.id}/edit"
|
||||
|
||||
assert_forbidden
|
||||
end
|
||||
|
||||
should "allow authorized users to edit the contract" do
|
||||
visit_contracts_for_project(@project)
|
||||
click_link @contract.id
|
||||
assert_response :success
|
||||
@@ -33,6 +54,133 @@ class ContractsEditTest < ActionController::IntegrationTest
|
||||
assert_template 'contracts/show'
|
||||
|
||||
assert_equal "An updated name", @contract.reload.name
|
||||
end
|
||||
|
||||
context "locked contract" do
|
||||
setup do
|
||||
assert @contract.lock!
|
||||
end
|
||||
|
||||
should "block edits" do
|
||||
visit_contract_page(@contract)
|
||||
click_link 'Update'
|
||||
assert_response :success
|
||||
|
||||
fill_in "Name", :with => 'An updated name'
|
||||
click_button 'Save Contract'
|
||||
|
||||
assert_response :success
|
||||
assert_template 'contracts/edit'
|
||||
|
||||
assert_not_equal "An updated name", @contract.reload.name
|
||||
end
|
||||
|
||||
should "block edits even when the status is changed to closed" do
|
||||
visit_contract_page(@contract)
|
||||
click_link 'Update'
|
||||
assert_response :success
|
||||
|
||||
fill_in "Name", :with => 'An updated name'
|
||||
select "Closed", :from => "Status"
|
||||
click_button 'Save Contract'
|
||||
|
||||
assert_response :success
|
||||
assert_template 'contracts/edit'
|
||||
|
||||
assert_not_equal "An updated name", @contract.reload.name
|
||||
assert @contract.reload.locked?
|
||||
end
|
||||
|
||||
should "be allowed to change the status from locked to open" do
|
||||
visit_contract_page(@contract)
|
||||
click_link 'Update'
|
||||
assert_response :success
|
||||
|
||||
select "Open", :from => "Status"
|
||||
click_button 'Save Contract'
|
||||
|
||||
assert_response :success
|
||||
assert_template 'contracts/show'
|
||||
|
||||
assert @contract.reload.open?
|
||||
end
|
||||
|
||||
should "be allowed to change the status from locked to closed" do
|
||||
visit_contract_page(@contract)
|
||||
click_link 'Update'
|
||||
assert_response :success
|
||||
|
||||
select "Closed", :from => "Status"
|
||||
click_button 'Save Contract'
|
||||
|
||||
assert_response :success
|
||||
assert_template 'contracts/show'
|
||||
|
||||
assert @contract.reload.closed?
|
||||
end
|
||||
end
|
||||
|
||||
context "closed contract" do
|
||||
setup do
|
||||
assert @contract.close!
|
||||
end
|
||||
|
||||
should "block edits" do
|
||||
visit_contract_page(@contract)
|
||||
click_link 'Update'
|
||||
assert_response :success
|
||||
|
||||
fill_in "Name", :with => 'An updated name'
|
||||
click_button 'Save Contract'
|
||||
|
||||
assert_response :success
|
||||
assert_template 'contracts/edit'
|
||||
|
||||
assert_not_equal "An updated name", @contract.reload.name
|
||||
end
|
||||
|
||||
should "block edits weven when the status is changed to locked" do
|
||||
visit_contract_page(@contract)
|
||||
click_link 'Update'
|
||||
assert_response :success
|
||||
|
||||
fill_in "Name", :with => 'An updated name'
|
||||
select "Locked", :from => "Status"
|
||||
click_button 'Save Contract'
|
||||
|
||||
assert_response :success
|
||||
assert_template 'contracts/edit'
|
||||
|
||||
assert_not_equal "An updated name", @contract.reload.name
|
||||
assert @contract.reload.closed?
|
||||
end
|
||||
|
||||
should "be allowed to change the status from closed to open" do
|
||||
visit_contract_page(@contract)
|
||||
click_link 'Update'
|
||||
assert_response :success
|
||||
|
||||
select "Open", :from => "Status"
|
||||
click_button 'Save Contract'
|
||||
|
||||
assert_response :success
|
||||
assert_template 'contracts/show'
|
||||
|
||||
assert @contract.reload.open?
|
||||
end
|
||||
|
||||
should "be allowed to change the status from closed to locked" do
|
||||
visit_contract_page(@contract)
|
||||
click_link 'Update'
|
||||
assert_response :success
|
||||
|
||||
select "Locked", :from => "Status"
|
||||
click_button 'Save Contract'
|
||||
|
||||
assert_response :success
|
||||
assert_template 'contracts/show'
|
||||
|
||||
assert @contract.reload.locked?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,8 +5,10 @@ class ContractsListTest < ActionController::IntegrationTest
|
||||
|
||||
def setup
|
||||
@project = Project.generate!(:identifier => 'main')
|
||||
@contract = Contract.generate!(:project => @project)
|
||||
@contract2 = Contract.generate!(:project => @project)
|
||||
@contract = Contract.generate!(:project => @project, :name => 'Contract1').reload
|
||||
@contract2 = Contract.generate!(:project => @project, :name => 'Contract2').reload
|
||||
@contract_locked = Contract.generate!(:project => @project, :status => 'locked', :name => 'LockedContract').reload
|
||||
@contract_closed = Contract.generate!(:project => @project, :status => 'closed', :name => 'ClosedContract').reload
|
||||
|
||||
@other_project = Project.generate!(:identifier => 'other')
|
||||
@other_contract = Contract.generate!(:project => @other_project)
|
||||
@@ -16,16 +18,38 @@ class ContractsListTest < ActionController::IntegrationTest
|
||||
@contract2,
|
||||
@other_contract
|
||||
].map {|c| c.reload }
|
||||
|
||||
@user = User.generate_user_with_permission_to_manage_budget(:project => @project)
|
||||
|
||||
login_as(@user.login, 'contracts')
|
||||
end
|
||||
|
||||
should "allow any user to list the contracts on a project" do
|
||||
should "block anonymous users from listing the contracts" do
|
||||
logout
|
||||
visit "/projects/#{@project.identifier}/contracts"
|
||||
|
||||
assert_requires_login
|
||||
end
|
||||
|
||||
should "block unauthorized users from listing contracts" do
|
||||
logout
|
||||
|
||||
@user = User.generate!(:password => 'test', :password_confirmation => 'test')
|
||||
login_as(@user.login, 'test')
|
||||
|
||||
visit "/projects/#{@project.identifier}/contracts"
|
||||
|
||||
assert_forbidden
|
||||
end
|
||||
|
||||
should "allow authorized users to list the contracts on a project" do
|
||||
visit_contracts_for_project(@project)
|
||||
end
|
||||
|
||||
should "list all contracts for the project" do
|
||||
should "list all contracts for the project grouped by status" do
|
||||
visit_contracts_for_project(@project)
|
||||
|
||||
assert_select "table#contracts" do
|
||||
assert_select "table#contracts.open" do
|
||||
[@contract, @contract2].each do |contract|
|
||||
assert_select "td.id", :text => /#{contract.id}/
|
||||
assert_select "td.name", :text => /#{contract.name}/
|
||||
@@ -34,7 +58,23 @@ class ContractsListTest < ActionController::IntegrationTest
|
||||
assert_select "td.total-budget"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
assert_select "table#contracts.locked" do
|
||||
assert_select "td.id", :text => /#{@contract_locked.id}/
|
||||
assert_select "td.name", :text => /#{@contract_locked.name}/
|
||||
assert_select "td.account-executive", :text => /#{@contract_locked.account_executive.name}/
|
||||
assert_select "td.end-date", :text => /#{format_date(@contract_locked.end_date)}/
|
||||
assert_select "td.total-budget"
|
||||
end
|
||||
|
||||
assert_select "table#contracts.closed" do
|
||||
assert_select "td.id", :text => /#{@contract_closed.id}/
|
||||
assert_select "td.name", :text => /#{@contract_closed.name}/
|
||||
assert_select "td.account-executive", :text => /#{@contract_closed.account_executive.name}/
|
||||
assert_select "td.end-date", :text => /#{format_date(@contract_closed.end_date)}/
|
||||
assert_select "td.total-budget"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "not list contracts from other projects" do
|
||||
|
||||
@@ -7,9 +7,30 @@ class ContractsNewTest < ActionController::IntegrationTest
|
||||
@project = Project.generate!(:identifier => 'main')
|
||||
PaymentTerm.generate!(:type => 'PaymentTerm', :name => 'Net 15')
|
||||
PaymentTerm.generate!(:type => 'PaymentTerm', :name => 'Net 30')
|
||||
@user = User.generate_user_with_permission_to_manage_budget(:project => @project)
|
||||
|
||||
login_as(@user.login, 'contracts')
|
||||
end
|
||||
|
||||
should "allow any user to open the new contracts form" do
|
||||
should "block anonymous users from opening the new contract form" do
|
||||
logout
|
||||
visit "/projects/#{@project.identifier}/contracts/new"
|
||||
|
||||
assert_requires_login
|
||||
end
|
||||
|
||||
should "block unauthorized users from opening the new contract form" do
|
||||
logout
|
||||
|
||||
@user = User.generate!(:password => 'test', :password_confirmation => 'test')
|
||||
login_as(@user.login, 'test')
|
||||
|
||||
visit "/projects/#{@project.identifier}/contracts/new"
|
||||
|
||||
assert_forbidden
|
||||
end
|
||||
|
||||
should "allow authorized users to open the new contracts form" do
|
||||
visit_contracts_for_project(@project)
|
||||
click_link 'New Contract'
|
||||
assert_response :success
|
||||
@@ -31,6 +52,7 @@ class ContractsNewTest < ActionController::IntegrationTest
|
||||
fill_in "Start", :with => '2010-01-01'
|
||||
fill_in "End Date", :with => '2010-12-31'
|
||||
select "Net 30", :from => "Payment Terms"
|
||||
select "Locked", :from => "Status"
|
||||
|
||||
click_button "Save Contract"
|
||||
|
||||
@@ -43,6 +65,7 @@ class ContractsNewTest < ActionController::IntegrationTest
|
||||
assert_equal '2010-01-01', @contract.start_date.to_s
|
||||
assert_equal '2010-12-31', @contract.end_date.to_s
|
||||
assert_equal 'Net 30', @contract.payment_term.name
|
||||
assert_equal "locked", @contract.status
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,9 +6,30 @@ class ContractsShowTest < ActionController::IntegrationTest
|
||||
def setup
|
||||
@project = Project.generate!(:identifier => 'main').reload
|
||||
@contract = Contract.generate!(:project => @project)
|
||||
@user = User.generate_user_with_permission_to_manage_budget(:project => @project)
|
||||
|
||||
login_as(@user.login, 'contracts')
|
||||
end
|
||||
|
||||
should "allow any user to view the contract" do
|
||||
should "block anonymous users from viewing the contract" do
|
||||
logout
|
||||
visit "/projects/#{@project.identifier}/contracts/#{@contract.id}"
|
||||
|
||||
assert_requires_login
|
||||
end
|
||||
|
||||
should "block unauthorized users from viewing the contract" do
|
||||
logout
|
||||
|
||||
@user = User.generate!(:password => 'test', :password_confirmation => 'test')
|
||||
login_as(@user.login, 'test')
|
||||
|
||||
visit "/projects/#{@project.identifier}/contracts/#{@contract.id}"
|
||||
|
||||
assert_forbidden
|
||||
end
|
||||
|
||||
should "allow authorized users to view the contract" do
|
||||
visit_contracts_for_project(@project)
|
||||
click_link @contract.id
|
||||
assert_response :success
|
||||
@@ -52,19 +73,21 @@ class ContractsShowTest < ActionController::IntegrationTest
|
||||
assert_select '.contract-overhead .budget'
|
||||
assert_select '.contract-fixed .spent'
|
||||
assert_select '.contract-fixed .budget'
|
||||
assert_select '.contract-markup .spent'
|
||||
assert_select '.contract-markup .budget'
|
||||
assert_select '.contract-profit .spent'
|
||||
assert_select '.contract-profit .budget'
|
||||
assert_select '.contract-discount .spent'
|
||||
assert_select '.contract-discount .budget'
|
||||
# assert_select '.contract-discount .spent'
|
||||
# assert_select '.contract-discount .budget'
|
||||
assert_select '.contract-total .spent'
|
||||
assert_select '.contract-total .budget'
|
||||
|
||||
assert_select '.contract-billable-rate'
|
||||
|
||||
assert_select '.contract-estimated-hour .spent'
|
||||
assert_select '.contract-estimated-hour .budget'
|
||||
assert_select '.contract-labor-hour .spent'
|
||||
assert_select '.contract-labor-hour .budget'
|
||||
assert_select '.contract-overhead-hour .spent'
|
||||
assert_select '.contract-overhead-hour .budget'
|
||||
assert_select '.contract-total-hour .spent'
|
||||
assert_select '.contract-total-hour .budget'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -210,6 +233,329 @@ class ContractsShowTest < ActionController::IntegrationTest
|
||||
|
||||
end
|
||||
|
||||
should "show the total fixed budget for a Deliverable" do
|
||||
@manager = User.generate!
|
||||
|
||||
@deliverable1 = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
|
||||
|
||||
FixedBudget.generate!(:deliverable => @deliverable1, :budget => '$1,000', :markup => '$100')
|
||||
FixedBudget.generate!(:deliverable => @deliverable1, :budget => '$2,000', :markup => '200%')
|
||||
|
||||
visit_contract_page(@contract)
|
||||
assert_select "table#deliverables" do
|
||||
assert_select "td.fixed", :text => /3,000/
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "show the total fixed budget spent for a Deliverable" do
|
||||
@manager = User.generate!
|
||||
|
||||
@deliverable1 = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
|
||||
|
||||
FixedBudget.generate!(:deliverable => @deliverable1, :budget => '$1,000', :markup => '$100', :paid => true)
|
||||
FixedBudget.generate!(:deliverable => @deliverable1, :budget => '$2,000', :markup => '200%')
|
||||
|
||||
visit_contract_page(@contract)
|
||||
assert_select "table#deliverables" do
|
||||
assert_select "td.fixed.spent-amount", :text => /1,000/
|
||||
end
|
||||
end
|
||||
|
||||
should "show each fixed budget item in the details for the Deliverable" do
|
||||
@manager = User.generate!
|
||||
|
||||
@deliverable1 = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
|
||||
|
||||
@budget1 = FixedBudget.generate!(:deliverable => @deliverable1, :title => 'Item 1', :budget => '$1,000', :markup => '$100', :paid => true)
|
||||
@budget2 = FixedBudget.generate!(:deliverable => @deliverable1, :title => 'Item 2', :budget => '$2,000', :markup => '200%')
|
||||
|
||||
visit_contract_page(@contract)
|
||||
assert_select "table#deliverables" do
|
||||
assert_select "#deliverable_details_#{@deliverable1.id}" do
|
||||
assert_select "tr#fixed_budget_#{@budget1.id}" do
|
||||
assert_select 'td.fixed_title', :text => /#{@budget1.title}/
|
||||
assert_select 'td.fixed_budget_spent', :text => '1,000'
|
||||
assert_select 'td.fixed_budget_total', :text => '1,000'
|
||||
end
|
||||
|
||||
assert_select "tr#fixed_budget_#{@budget2.id}" do
|
||||
assert_select 'td.fixed_title', :text => /#{@budget2.title}/
|
||||
assert_select 'td.fixed_budget_spent', :text => '0'
|
||||
assert_select 'td.fixed_budget_total', :text => '2,000'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "show the total fixed markup budget in the details for the Deliverable" do
|
||||
@manager = User.generate!
|
||||
|
||||
@deliverable1 = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
|
||||
|
||||
@budget1 = FixedBudget.generate!(:deliverable => @deliverable1, :title => 'Item 1', :budget => '$1,000', :markup => '$100', :paid => true)
|
||||
@budget2 = FixedBudget.generate!(:deliverable => @deliverable1, :title => 'Item 2', :budget => '$2,000', :markup => '200%')
|
||||
|
||||
visit_contract_page(@contract)
|
||||
assert_select "table#deliverables" do
|
||||
assert_select "#deliverable_details_#{@deliverable1.id}" do
|
||||
assert_select 'td.fixed_markup_budget_spent', :text => '4,100'
|
||||
assert_select 'td.fixed_markup_budget_total', :text => '4,100'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should "show the labor hours for the deliverable" do
|
||||
configure_overhead_plugin
|
||||
|
||||
@manager = User.generate!
|
||||
|
||||
@deliverable1 = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
|
||||
LaborBudget.generate!(:deliverable => @deliverable1,
|
||||
:hours => 100,
|
||||
:budget => 4000.5)
|
||||
|
||||
@issue1 = Issue.generate_for_project!(@project)
|
||||
@time_entry1 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @billable_activity,
|
||||
:spent_on => Date.today,
|
||||
:hours => 10,
|
||||
:user => @manager)
|
||||
|
||||
@rate = Rate.generate!(:project => @project,
|
||||
:user => @manager,
|
||||
:date_in_effect => Date.yesterday,
|
||||
:amount => 100)
|
||||
|
||||
@deliverable1.issues << @issue1
|
||||
|
||||
assert_equal 1, @deliverable1.issues.count
|
||||
|
||||
visit_contract_page(@contract)
|
||||
assert_select "table#deliverables" do
|
||||
assert_select "td.labor_hours_spent", :text => /10/
|
||||
assert_select "td.labor_hours", :text => /100/
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "show the overhead hours for the deliverable" do
|
||||
configure_overhead_plugin
|
||||
|
||||
@manager = User.generate!
|
||||
|
||||
@deliverable1 = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
|
||||
OverheadBudget.generate!(:deliverable => @deliverable1,
|
||||
:hours => 100,
|
||||
:budget => 4000.5)
|
||||
|
||||
@issue1 = Issue.generate_for_project!(@project)
|
||||
@time_entry1 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @non_billable_activity,
|
||||
:spent_on => Date.today,
|
||||
:hours => 5,
|
||||
:user => @manager)
|
||||
|
||||
@rate = Rate.generate!(:project => @project,
|
||||
:user => @manager,
|
||||
:date_in_effect => Date.yesterday,
|
||||
:amount => 100)
|
||||
|
||||
@deliverable1.issues << @issue1
|
||||
|
||||
assert_equal 1, @deliverable1.issues.count
|
||||
|
||||
visit_contract_page(@contract)
|
||||
assert_select "table#deliverables" do
|
||||
assert_select "td.overhead_hours_spent", :text => /5/
|
||||
assert_select "td.overhead_hours", :text => /100/
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "show the total hours for the deliverable" do
|
||||
configure_overhead_plugin
|
||||
|
||||
@manager = User.generate!
|
||||
|
||||
@deliverable1 = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
|
||||
LaborBudget.generate!(:deliverable => @deliverable1,
|
||||
:hours => 100,
|
||||
:budget => 4000.5)
|
||||
OverheadBudget.generate!(:deliverable => @deliverable1,
|
||||
:hours => 100,
|
||||
:budget => 4000.5)
|
||||
|
||||
@issue1 = Issue.generate_for_project!(@project)
|
||||
@time_entry1 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @billable_activity,
|
||||
:spent_on => Date.today,
|
||||
:hours => 10,
|
||||
:user => @manager)
|
||||
@time_entry2 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @non_billable_activity,
|
||||
:spent_on => Date.today,
|
||||
:hours => 5,
|
||||
:user => @manager)
|
||||
|
||||
@rate = Rate.generate!(:project => @project,
|
||||
:user => @manager,
|
||||
:date_in_effect => Date.yesterday,
|
||||
:amount => 100)
|
||||
|
||||
@deliverable1.issues << @issue1
|
||||
|
||||
assert_equal 1, @deliverable1.issues.count
|
||||
|
||||
visit_contract_page(@contract)
|
||||
assert_select "table#deliverables" do
|
||||
assert_select "td.total_hours_spent", :text => /15/
|
||||
assert_select "td.total_hours", :text => /200/
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "show the count of the issues by status for the deliverable" do
|
||||
configure_overhead_plugin
|
||||
|
||||
@manager = User.generate!
|
||||
|
||||
@deliverable1 = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
|
||||
@status1 = IssueStatus.generate!
|
||||
@status2 = IssueStatus.generate!
|
||||
|
||||
@issue1 = Issue.generate_for_project!(@project, :status => @status1)
|
||||
@issue2 = Issue.generate_for_project!(@project, :status => @status1)
|
||||
@issue3 = Issue.generate_for_project!(@project, :status => @status1)
|
||||
@issue4 = Issue.generate_for_project!(@project, :status => @status2)
|
||||
|
||||
@deliverable1.issues = [@issue1, @issue2, @issue3, @issue4]
|
||||
|
||||
assert_equal 4, @deliverable1.issues.count
|
||||
|
||||
visit_contract_page(@contract)
|
||||
assert_select "table#deliverables" do
|
||||
assert_select "tr" do
|
||||
assert_select "td a", :text => /#{@status1}/
|
||||
assert_select "td.number a", :text => /3/
|
||||
end
|
||||
assert_select "tr" do
|
||||
assert_select "td a", :text => /#{@status2}/
|
||||
assert_select "td.number a", :text => /1/
|
||||
end
|
||||
assert_select "tr" do
|
||||
assert_select "td strong a", "All"
|
||||
assert_select "td.number a", :text => /4/
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should "show overages in red" do
|
||||
configure_overhead_plugin
|
||||
|
||||
@manager = User.generate!
|
||||
|
||||
@deliverable1 = HourlyDeliverable.generate!(:contract => @contract, :manager => @manager)
|
||||
@issue1 = Issue.generate_for_project!(@project)
|
||||
@time_entry1 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @billable_activity,
|
||||
:spent_on => Date.today,
|
||||
:hours => 10,
|
||||
:user => @manager)
|
||||
@time_entry2 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @non_billable_activity,
|
||||
:spent_on => Date.today,
|
||||
:hours => 5,
|
||||
:user => @manager)
|
||||
@deliverable1.issues << @issue1
|
||||
@rate = Rate.generate!(:project => @project,
|
||||
:user => @manager,
|
||||
:date_in_effect => Date.yesterday,
|
||||
:amount => 100)
|
||||
|
||||
assert_equal 1, @deliverable1.issues.count
|
||||
|
||||
visit_contract_page(@contract)
|
||||
|
||||
# Overages on:
|
||||
assert_select '.overage', :count => 14
|
||||
assert_select '.contract-labor .overage', :count => 2
|
||||
assert_select '.contract-overhead .overage', :count => 2
|
||||
assert_select '.contract-labor-hour .overage'
|
||||
assert_select '.contract-overhead-hour .overage'
|
||||
assert_select '.contract-total-hour .overage'
|
||||
|
||||
assert_select '#deliverables .spent-amount.labor.overage'
|
||||
assert_select '#deliverables .spent-amount.overhead.overage'
|
||||
assert_select '#deliverables .labor_budget_spent.overage'
|
||||
assert_select '#deliverables .overhead_budget_spent.overage'
|
||||
assert_select '#deliverables .labor_hours_spent .overage'
|
||||
assert_select '#deliverables .overhead_hours_spent .overage'
|
||||
assert_select '#deliverables .total_hours_spent .overage'
|
||||
|
||||
end
|
||||
|
||||
|
||||
should "show an alert if there is orphaned time or issues" do
|
||||
configure_overhead_plugin
|
||||
|
||||
@manager = User.generate!
|
||||
|
||||
@deliverable1 = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
|
||||
|
||||
@issue1 = Issue.generate_for_project!(@project)
|
||||
@time_entry1 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @billable_activity,
|
||||
:spent_on => Date.today,
|
||||
:hours => 10,
|
||||
:user => @manager)
|
||||
@time_entry2 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @non_billable_activity,
|
||||
:spent_on => Date.today,
|
||||
:hours => 5,
|
||||
:user => @manager)
|
||||
@deliverable1.issues << @issue1
|
||||
|
||||
@orphaned_issue = Issue.generate_for_project!(@project)
|
||||
@time_entry_on_orphaned_issue = TimeEntry.generate!(:issue => @orphaned_issue,
|
||||
:project => @project,
|
||||
:activity => @billable_activity,
|
||||
:spent_on => Date.today,
|
||||
:hours => 10,
|
||||
:user => @manager)
|
||||
|
||||
@time_entry_on_project = TimeEntry.generate!(:issue => nil,
|
||||
:project => @project,
|
||||
:activity => @billable_activity,
|
||||
:spent_on => Date.today,
|
||||
:hours => 10,
|
||||
:user => @manager)
|
||||
|
||||
@rate = Rate.generate!(:project => @project,
|
||||
:user => @manager,
|
||||
:date_in_effect => Date.yesterday,
|
||||
:amount => 100)
|
||||
|
||||
|
||||
assert_equal 1, @deliverable1.issues.count
|
||||
|
||||
visit_contract_page(@contract)
|
||||
assert_select "div.error_msg" do
|
||||
assert_select "p", :text => /2,000/
|
||||
assert_select "a", :text => /update/i
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "show the current period for a Retainer" do
|
||||
today_mock = Date.new(2010,2,15)
|
||||
Date.stubs(:today).returns(today_mock)
|
||||
|
||||
@@ -12,9 +12,38 @@ class DeliverableDetailsShowTest < ActionController::IntegrationTest
|
||||
@deliverable1.overhead_budgets << OverheadBudget.spawn(:budget => 200, :hours => 10)
|
||||
|
||||
@deliverable1.save!
|
||||
@user = User.generate_user_with_permission_to_manage_budget(:project => @project)
|
||||
|
||||
login_as(@user.login, 'contracts')
|
||||
end
|
||||
|
||||
context "for a JS request" do
|
||||
context "for an anonymous JS request" do
|
||||
should "require login" do
|
||||
logout
|
||||
|
||||
visit "/projects/#{@project.id}/contracts/#{@contract.id}/deliverables/#{@deliverable1.id}", :get, {:format => 'js', :as => 'deliverable_details_row'}
|
||||
|
||||
assert_response :unauthorized
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "for an unauthorized JS request" do
|
||||
should "be forbidden" do
|
||||
logout
|
||||
|
||||
@user = User.generate!(:password => 'test', :password_confirmation => 'test')
|
||||
login_as(@user.login, 'test')
|
||||
|
||||
visit "/projects/#{@project.id}/contracts/#{@contract.id}/deliverables/#{@deliverable1.id}", :get, {:format => 'js', :as => 'deliverable_details_row'}
|
||||
|
||||
assert_response :forbidden
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
context "for an authorized JS request" do
|
||||
should "render the details for the deliverable" do
|
||||
visit "/projects/#{@project.id}/contracts/#{@contract.id}/deliverables/#{@deliverable1.id}", :get, {:format => 'js', :as => 'deliverable_details_row'}
|
||||
|
||||
@@ -32,7 +61,6 @@ class DeliverableDetailsShowTest < ActionController::IntegrationTest
|
||||
|
||||
visit "/projects/#{@project.id}/contracts/#{@contract.id}/deliverables/#{@deliverable1.id}", :get, {:format => 'js', :as => 'deliverable_details_row', :period => '2010-02'}
|
||||
|
||||
puts response.body
|
||||
assert_response :success
|
||||
assert_select ".deliverable_details_outer_wrapper_#{@deliverable1.id}" do
|
||||
assert_select "td.labor_budget_total", '100'
|
||||
@@ -40,7 +68,7 @@ class DeliverableDetailsShowTest < ActionController::IntegrationTest
|
||||
assert_select "td.total", '100'
|
||||
|
||||
assert_select "select.retainer_period_change" do
|
||||
assert_select "option[selected=selected]", "Feburary 2010"
|
||||
assert_select "option[selected=selected]", "February 2010"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -8,9 +8,53 @@ class DeliverablesDeleteTest < ActionController::IntegrationTest
|
||||
@contract = Contract.generate!(:project => @project, :name => 'A Contract')
|
||||
@manager = User.generate!
|
||||
@deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
|
||||
@user = User.generate_user_with_permission_to_manage_budget(:project => @project)
|
||||
|
||||
login_as(@user.login, 'contracts')
|
||||
end
|
||||
|
||||
should "allow anyone to delete the deliverable" do
|
||||
should "block anonymous users from deleting the deliverable" do
|
||||
logout
|
||||
delete "/projects/#{@project.identifier}/contracts/#{@contract.id}/deliverables/#{@deliverable.id}"
|
||||
follow_redirect!
|
||||
|
||||
assert_requires_login
|
||||
end
|
||||
|
||||
should "block unauthorized users from deleting the deliverable" do
|
||||
logout
|
||||
|
||||
@user = User.generate!(:password => 'test', :password_confirmation => 'test')
|
||||
login_as(@user.login, 'test')
|
||||
|
||||
delete "/projects/#{@project.identifier}/contracts/#{@contract.id}/deliverables/#{@deliverable.id}"
|
||||
|
||||
assert_forbidden
|
||||
end
|
||||
|
||||
should "allow authorized users to delete the deliverable" do
|
||||
visit_contract_page(@contract)
|
||||
|
||||
click_link_within "#deliverable_details_#{@deliverable.id}", 'Delete'
|
||||
assert_response :success
|
||||
assert_template 'contracts/show'
|
||||
|
||||
assert_select '.flash.notice', /successfully deleted/
|
||||
|
||||
|
||||
assert_nil Deliverable.find_by_id(@deliverable.id), "Deliverable not deleted"
|
||||
end
|
||||
|
||||
should "unassign issues from the deliverable when deleting" do
|
||||
issues = [
|
||||
Issue.generate_for_project!(@project, :deliverable => @deliverable),
|
||||
Issue.generate_for_project!(@project, :deliverable => @deliverable),
|
||||
Issue.generate_for_project!(@project, :deliverable => @deliverable)
|
||||
]
|
||||
assert_equal 3, @deliverable.issues.count
|
||||
|
||||
@project.reload
|
||||
|
||||
visit_contract_page(@contract)
|
||||
|
||||
click_link_within "#deliverable_details_#{@deliverable.id}", 'Delete'
|
||||
@@ -18,5 +62,9 @@ class DeliverablesDeleteTest < ActionController::IntegrationTest
|
||||
assert_template 'contracts/show'
|
||||
|
||||
assert_nil Deliverable.find_by_id(@deliverable.id), "Deliverable not deleted"
|
||||
assert_equal 0, @deliverable.issues.count
|
||||
assert issues.all? {|issue|
|
||||
issue.reload && issue.deliverable_id.nil?
|
||||
}, "Issues' deliverable was not removed #{issues.collect(&:deliverable_id).join(', ')}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,12 +9,33 @@ class DeliverablesEditTest < ActionController::IntegrationTest
|
||||
@manager = User.generate!
|
||||
@role = Role.generate!
|
||||
User.add_to_project(@manager, @project, @role)
|
||||
@fixed_deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'The Title')
|
||||
@fixed_deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'The Title', :notes => "", :feature_sign_off => false, :warranty_sign_off => false)
|
||||
@hourly_deliverable = HourlyDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'An Hourly')
|
||||
|
||||
@user = User.generate_user_with_permission_to_manage_budget(:project => @project)
|
||||
|
||||
login_as(@user.login, 'contracts')
|
||||
end
|
||||
|
||||
should "allow any user to edit the Fixed deliverable" do
|
||||
should "block anonymous users from editing the deliverable" do
|
||||
logout
|
||||
visit "/projects/#{@project.identifier}/contracts/#{@contract.id}/deliverables/#{@fixed_deliverable.id}"
|
||||
|
||||
assert_requires_login
|
||||
end
|
||||
|
||||
should "block unauthorized users from editing the deliverable" do
|
||||
logout
|
||||
|
||||
@user = User.generate!(:password => 'test', :password_confirmation => 'test')
|
||||
login_as(@user.login, 'test')
|
||||
|
||||
visit "/projects/#{@project.identifier}/contracts/#{@contract.id}/deliverables/#{@fixed_deliverable.id}"
|
||||
|
||||
assert_forbidden
|
||||
end
|
||||
|
||||
should "allow authorized users to edit the Fixed deliverable" do
|
||||
visit_contract_page(@contract)
|
||||
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
|
||||
assert_response :success
|
||||
@@ -25,12 +46,13 @@ class DeliverablesEditTest < ActionController::IntegrationTest
|
||||
end
|
||||
|
||||
assert_select "select#fixed_deliverable_type", :count => 0 # Not editable
|
||||
assert js("jQuery('#fixed_deliverable_total_input').is(':visible')"), "Total is hidden when it should be visible"
|
||||
|
||||
|
||||
fill_in "Title", :with => 'An updated title'
|
||||
check "Feature Sign Off"
|
||||
check "Warranty Sign Off"
|
||||
within("#deliverable-details") do
|
||||
fill_in "Title", :with => 'An updated title'
|
||||
select "Locked", :from => "Status"
|
||||
check "Feature Sign Off"
|
||||
check "Warranty Sign Off"
|
||||
end
|
||||
click_button "Save"
|
||||
|
||||
assert_response :success
|
||||
@@ -40,10 +62,11 @@ class DeliverablesEditTest < ActionController::IntegrationTest
|
||||
assert_equal "FixedDeliverable", @fixed_deliverable.reload.type
|
||||
assert @fixed_deliverable.reload.warranty_sign_off?
|
||||
assert @fixed_deliverable.reload.feature_sign_off?
|
||||
assert_equal "locked", @fixed_deliverable.reload.status
|
||||
|
||||
end
|
||||
|
||||
should "allow any user to edit the Hourly deliverable" do
|
||||
should "allow authorized users to edit the Hourly deliverable" do
|
||||
visit_contract_page(@contract)
|
||||
click_link_within "#deliverable_details_#{@hourly_deliverable.id}", 'Edit'
|
||||
assert_response :success
|
||||
@@ -54,11 +77,13 @@ class DeliverablesEditTest < ActionController::IntegrationTest
|
||||
end
|
||||
|
||||
assert_select "select#hourly_deliverable_type", :count => 0 # Not editable
|
||||
assert js("jQuery('#hourly_deliverable_total_input').is(':hidden')"), "Total is visible when it should be hidden"
|
||||
|
||||
fill_in "Title", :with => 'An updated title'
|
||||
check "Feature Sign Off"
|
||||
check "Warranty Sign Off"
|
||||
within("#deliverable-details") do
|
||||
fill_in "Title", :with => 'An updated title'
|
||||
select "Locked", :from => "Status"
|
||||
check "Feature Sign Off"
|
||||
check "Warranty Sign Off"
|
||||
end
|
||||
|
||||
within("#deliverable-labor") do
|
||||
fill_in "hrs", :with => '20'
|
||||
@@ -79,6 +104,7 @@ class DeliverablesEditTest < ActionController::IntegrationTest
|
||||
assert_equal "HourlyDeliverable", @hourly_deliverable.reload.type
|
||||
assert @hourly_deliverable.reload.warranty_sign_off?
|
||||
assert @hourly_deliverable.reload.feature_sign_off?
|
||||
assert_equal "locked", @hourly_deliverable.reload.status
|
||||
|
||||
assert_equal 1, @hourly_deliverable.labor_budgets.count
|
||||
@labor_budget = @hourly_deliverable.labor_budgets.first
|
||||
@@ -95,6 +121,8 @@ class DeliverablesEditTest < ActionController::IntegrationTest
|
||||
@retainer_deliverable = RetainerDeliverable.spawn(:contract => @contract, :manager => @manager, :title => "Retainer")
|
||||
@retainer_deliverable.labor_budgets << @labor_budget = LaborBudget.spawn(:deliverable => @retainer_deliverable, :budget => 1000, :hours => 10)
|
||||
@retainer_deliverable.overhead_budgets << @overhead_budget = OverheadBudget.spawn(:deliverable => @retainer_deliverable, :budget => 1000, :hours => 10)
|
||||
@retainer_deliverable.fixed_budgets << @fixed_budget = FixedBudget.spawn(:deliverable => @retainer_deliverable, :title => 'Printing supplies', :budget => 100, :markup => 0)
|
||||
|
||||
@retainer_deliverable.start_date = '2010-01-01'
|
||||
@retainer_deliverable.end_date = '2010-12-31'
|
||||
@retainer_deliverable.save!
|
||||
@@ -117,6 +145,13 @@ class DeliverablesEditTest < ActionController::IntegrationTest
|
||||
fill_in "hrs", :with => '100'
|
||||
fill_in "$", :with => '100'
|
||||
end
|
||||
|
||||
within "#deliverable-fixed" do
|
||||
fill_in "title", :with => 'Flight to NYC'
|
||||
fill_in "budget", :with => '$600'
|
||||
fill_in "markup", :with => '50%'
|
||||
fill_in "description", :with => 'Need to fly to NYC for the week'
|
||||
end
|
||||
end
|
||||
|
||||
click_button "Save"
|
||||
@@ -157,6 +192,24 @@ class DeliverablesEditTest < ActionController::IntegrationTest
|
||||
end
|
||||
end
|
||||
|
||||
@fixed_budgets = @retainer_deliverable.reload.fixed_budgets
|
||||
assert_equal 12, @fixed_budgets.length
|
||||
@fixed_budgets.each do |fixed_budget|
|
||||
if fixed_budget.year == 2010 && fixed_budget.month == 1
|
||||
|
||||
# Specific month's budget updated?
|
||||
assert_equal 600, fixed_budget.budget
|
||||
assert_equal '50%', fixed_budget.markup
|
||||
assert_equal 300, fixed_budget.markup_value
|
||||
|
||||
else
|
||||
|
||||
assert_equal 100, fixed_budget.budget
|
||||
assert_equal '$0.00', fixed_budget.markup
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "allow extending a Retainer's start and end months" do
|
||||
@@ -175,6 +228,7 @@ class DeliverablesEditTest < ActionController::IntegrationTest
|
||||
@retainer_deliverable.labor_budgets << @labor_budget = LaborBudget.spawn(:deliverable => @retainer_deliverable, :budget => labor_budget_amount_2, :hours => labor_budget_hours_2)
|
||||
@retainer_deliverable.overhead_budgets << @overhead_budget = OverheadBudget.spawn(:deliverable => @retainer_deliverable, :budget => overhead_budget_amount_1, :hours => overhead_budget_hours_1)
|
||||
@retainer_deliverable.overhead_budgets << @overhead_budget = OverheadBudget.spawn(:deliverable => @retainer_deliverable, :budget => overhead_budget_amount_2, :hours => overhead_budget_hours_2)
|
||||
@retainer_deliverable.fixed_budgets << @fixed_budget = FixedBudget.spawn(:deliverable => @retainer_deliverable, :title => 'Printing supplies', :budget => 100, :markup => 0)
|
||||
@retainer_deliverable.start_date = '2010-01-01'
|
||||
@retainer_deliverable.end_date = '2010-12-31'
|
||||
@retainer_deliverable.save!
|
||||
@@ -256,6 +310,17 @@ class DeliverablesEditTest < ActionController::IntegrationTest
|
||||
assert [overhead_budget_amount_1, overhead_budget_amount_2].include?(overhead_budget.budget), "Extended overhead budget dollars not matching template budget"
|
||||
end
|
||||
|
||||
@fixed_budgets = @retainer_deliverable.reload.fixed_budgets
|
||||
assert_equal 36, @fixed_budgets.length # 36 months * 1 record
|
||||
|
||||
@fixed_budgets_for_2009 = @fixed_budgets.select {|l| l.year == 2009 }
|
||||
@fixed_budgets_for_2010 = @fixed_budgets.select {|l| l.year == 2010 }
|
||||
@fixed_budgets_for_2011 = @fixed_budgets.select {|l| l.year == 2011 }
|
||||
|
||||
assert_equal 12, @fixed_budgets_for_2009.length
|
||||
assert_equal 12, @fixed_budgets_for_2010.length
|
||||
assert_equal 12, @fixed_budgets_for_2011.length
|
||||
|
||||
end
|
||||
|
||||
should "allow shrinking a Retainer's start and end months" do
|
||||
@@ -264,6 +329,7 @@ class DeliverablesEditTest < ActionController::IntegrationTest
|
||||
@retainer_deliverable.labor_budgets << @labor_budget = LaborBudget.spawn(:deliverable => @retainer_deliverable, :budget => 2000, :hours => 20)
|
||||
@retainer_deliverable.overhead_budgets << @overhead_budget = OverheadBudget.spawn(:deliverable => @retainer_deliverable, :budget => 1000, :hours => 10)
|
||||
@retainer_deliverable.overhead_budgets << @overhead_budget = OverheadBudget.spawn(:deliverable => @retainer_deliverable, :budget => 2000, :hours => 20)
|
||||
@retainer_deliverable.fixed_budgets << @fixed_budget = FixedBudget.spawn(:deliverable => @retainer_deliverable, :title => 'Printing supplies', :budget => 100, :markup => 0)
|
||||
@retainer_deliverable.start_date = '2010-01-01'
|
||||
@retainer_deliverable.end_date = '2010-12-31'
|
||||
@retainer_deliverable.save!
|
||||
@@ -293,6 +359,9 @@ class DeliverablesEditTest < ActionController::IntegrationTest
|
||||
@overhead_budgets = @retainer_deliverable.reload.overhead_budgets
|
||||
assert_equal 12, @overhead_budgets.length # 6 months * 2 records
|
||||
|
||||
@fixed_budgets = @retainer_deliverable.reload.fixed_budgets
|
||||
assert_equal 6, @fixed_budgets.length # 6 months * 1 records
|
||||
|
||||
end
|
||||
|
||||
should "allow editing a Retainer's start and end months inside the current period" do
|
||||
@@ -346,7 +415,7 @@ class DeliverablesEditTest < ActionController::IntegrationTest
|
||||
assert_response :success
|
||||
assert_template 'deliverables/edit'
|
||||
|
||||
# Should show 6 inputs:
|
||||
# Should show inputs:
|
||||
# * labor hidden year
|
||||
# * labor hidden month
|
||||
# * labor hours
|
||||
@@ -355,9 +424,17 @@ class DeliverablesEditTest < ActionController::IntegrationTest
|
||||
# * overhead hidden month
|
||||
# * overhead hours
|
||||
# * overhead amount
|
||||
# * fixed hidden year
|
||||
# * fixed hidden month
|
||||
# * fixed title
|
||||
# * fixed budget
|
||||
# * fixed markup
|
||||
# * fixed paid checkbox
|
||||
# * fixed paid hidden field
|
||||
# * total (hidden)
|
||||
assert_select ".date-2010-01" do
|
||||
assert_select "input", :count => 9
|
||||
assert_select "input", :count => 16
|
||||
assert_select "textarea.wiki-edit", :count => 1 # Fixed description
|
||||
end
|
||||
|
||||
|
||||
@@ -371,6 +448,14 @@ class DeliverablesEditTest < ActionController::IntegrationTest
|
||||
fill_in "hrs", :with => '100'
|
||||
fill_in "$", :with => '100'
|
||||
end
|
||||
|
||||
within "#deliverable-fixed" do
|
||||
fill_in "title", :with => 'Flight to NYC'
|
||||
fill_in "budget", :with => '$600'
|
||||
fill_in "markup", :with => '50%'
|
||||
fill_in "description", :with => 'Need to fly to NYC for the week'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
click_button "Save"
|
||||
@@ -386,5 +471,244 @@ class DeliverablesEditTest < ActionController::IntegrationTest
|
||||
assert_equal 3, @retainer_deliverable.overhead_budgets.count
|
||||
assert_equal [100, nil, nil], @retainer_deliverable.overhead_budgets.collect(&:hours)
|
||||
assert_equal [100, nil, nil], @retainer_deliverable.overhead_budgets.collect(&:budget)
|
||||
|
||||
assert_equal 3, @retainer_deliverable.fixed_budgets.count
|
||||
assert_equal [600, nil, nil], @retainer_deliverable.fixed_budgets.collect(&:budget)
|
||||
end
|
||||
|
||||
context "locked deliverable" do
|
||||
setup do
|
||||
assert @fixed_deliverable.lock!
|
||||
end
|
||||
|
||||
should "block edits to locked deliverables" do
|
||||
visit_contract_page(@contract)
|
||||
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
|
||||
assert_response :success
|
||||
|
||||
within("#deliverable-details") do
|
||||
fill_in "Title", :with => 'An updated title'
|
||||
end
|
||||
|
||||
click_button "Save"
|
||||
|
||||
assert_response :success
|
||||
assert_template 'deliverables/edit'
|
||||
|
||||
assert_not_equal "An updated title", @fixed_deliverable.reload.title
|
||||
end
|
||||
|
||||
should "block edits to locked deliverables even when status changes to closed" do
|
||||
visit_contract_page(@contract)
|
||||
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
|
||||
assert_response :success
|
||||
|
||||
within("#deliverable-details") do
|
||||
fill_in "Title", :with => 'An updated title'
|
||||
select "Closed", :from => "Status"
|
||||
end
|
||||
|
||||
click_button "Save"
|
||||
|
||||
assert_response :success
|
||||
assert_template 'deliverables/edit'
|
||||
|
||||
assert_not_equal "An updated title", @fixed_deliverable.reload.title
|
||||
assert @fixed_deliverable.reload.locked?
|
||||
end
|
||||
|
||||
should "be allowed to change the status on a locked deliverables to open" do
|
||||
visit_contract_page(@contract)
|
||||
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
|
||||
assert_response :success
|
||||
|
||||
within("#deliverable-details") do
|
||||
select "Open", :from => "Status"
|
||||
end
|
||||
|
||||
click_button "Save"
|
||||
|
||||
assert_response :success
|
||||
assert_template 'contracts/show'
|
||||
|
||||
assert @fixed_deliverable.reload.open?
|
||||
end
|
||||
|
||||
should "be allowed to change the status on a locked deliverables to closed" do
|
||||
visit_contract_page(@contract)
|
||||
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
|
||||
assert_response :success
|
||||
|
||||
within("#deliverable-details") do
|
||||
select "Closed", :from => "Status"
|
||||
end
|
||||
|
||||
click_button "Save"
|
||||
|
||||
assert_response :success
|
||||
assert_template 'contracts/show'
|
||||
|
||||
assert @fixed_deliverable.reload.closed?
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "closed deliverable" do
|
||||
setup do
|
||||
assert @fixed_deliverable.close!
|
||||
end
|
||||
|
||||
should "block edits to closed deliverables" do
|
||||
visit_contract_page(@contract)
|
||||
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
|
||||
assert_response :success
|
||||
|
||||
within("#deliverable-details") do
|
||||
fill_in "Title", :with => 'An updated title'
|
||||
end
|
||||
|
||||
click_button "Save"
|
||||
|
||||
assert_response :success
|
||||
assert_template 'deliverables/edit'
|
||||
|
||||
assert_not_equal "An updated title", @fixed_deliverable.reload.title
|
||||
end
|
||||
|
||||
should "block edits to closed deliverables even when the status is changed to locked" do
|
||||
visit_contract_page(@contract)
|
||||
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
|
||||
assert_response :success
|
||||
|
||||
within("#deliverable-details") do
|
||||
fill_in "Title", :with => 'An updated title'
|
||||
select "Locked", :from => "Status"
|
||||
end
|
||||
|
||||
click_button "Save"
|
||||
|
||||
assert_response :success
|
||||
assert_template 'deliverables/edit'
|
||||
|
||||
assert_not_equal "An updated title", @fixed_deliverable.reload.title
|
||||
assert @fixed_deliverable.reload.closed?
|
||||
end
|
||||
|
||||
should "be allowed to change the status on a closed deliverables to open" do
|
||||
visit_contract_page(@contract)
|
||||
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
|
||||
assert_response :success
|
||||
|
||||
within("#deliverable-details") do
|
||||
select "Open", :from => "Status"
|
||||
end
|
||||
|
||||
click_button "Save"
|
||||
|
||||
assert_response :success
|
||||
assert_template 'contracts/show'
|
||||
|
||||
assert @fixed_deliverable.reload.open?
|
||||
end
|
||||
|
||||
should "be allowed to change the status on a closed deliverables to Locked" do
|
||||
visit_contract_page(@contract)
|
||||
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
|
||||
assert_response :success
|
||||
|
||||
within("#deliverable-details") do
|
||||
select "Locked", :from => "Status"
|
||||
end
|
||||
|
||||
click_button "Save"
|
||||
|
||||
assert_response :success
|
||||
assert_template 'contracts/show'
|
||||
|
||||
assert @fixed_deliverable.reload.locked?
|
||||
end
|
||||
end
|
||||
|
||||
context "a Deliverable on a locked Contract" do
|
||||
setup do
|
||||
assert @contract.lock!
|
||||
end
|
||||
|
||||
should "be blocked from editing" do
|
||||
visit_contract_page(@contract)
|
||||
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
|
||||
assert_response :success
|
||||
|
||||
within("#deliverable-details") do
|
||||
fill_in "Title", :with => 'An updated title'
|
||||
end
|
||||
|
||||
click_button "Save"
|
||||
|
||||
assert_response :success
|
||||
assert_template 'deliverables/edit'
|
||||
|
||||
assert_not_equal "An updated title", @fixed_deliverable.reload.title
|
||||
end
|
||||
|
||||
should "allow status only changes" do
|
||||
visit_contract_page(@contract)
|
||||
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
|
||||
assert_response :success
|
||||
|
||||
within("#deliverable-details") do
|
||||
select "Locked", :from => "Status"
|
||||
end
|
||||
|
||||
click_button "Save"
|
||||
|
||||
assert_response :success
|
||||
assert_template 'contracts/show'
|
||||
|
||||
assert @fixed_deliverable.reload.locked?
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "a Deliverable on a closed Contract" do
|
||||
setup do
|
||||
assert @contract.close!
|
||||
end
|
||||
|
||||
should "be blocked from editing" do
|
||||
visit_contract_page(@contract)
|
||||
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
|
||||
assert_response :success
|
||||
|
||||
within("#deliverable-details") do
|
||||
fill_in "Title", :with => 'An updated title'
|
||||
end
|
||||
|
||||
click_button "Save"
|
||||
|
||||
assert_response :success
|
||||
assert_template 'deliverables/edit'
|
||||
|
||||
assert_not_equal "An updated title", @fixed_deliverable.reload.title
|
||||
end
|
||||
|
||||
should "allow status only changes" do
|
||||
visit_contract_page(@contract)
|
||||
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
|
||||
assert_response :success
|
||||
|
||||
within("#deliverable-details") do
|
||||
select "Locked", :from => "Status"
|
||||
end
|
||||
|
||||
click_button "Save"
|
||||
|
||||
assert_response :success
|
||||
assert_template 'contracts/show'
|
||||
|
||||
assert @fixed_deliverable.reload.locked?
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -8,6 +8,9 @@ class DeliverablesListTest < ActionController::IntegrationTest
|
||||
@contract = Contract.generate!(:project => @project)
|
||||
@manager = User.generate!
|
||||
@deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
|
||||
@user = User.generate_user_with_permission_to_manage_budget(:project => @project)
|
||||
|
||||
login_as(@user.login, 'contracts')
|
||||
end
|
||||
|
||||
should "redirect to the contract page" do
|
||||
|
||||
@@ -6,9 +6,30 @@ class DeliverablesNewTest < ActionController::IntegrationTest
|
||||
def setup
|
||||
@project = Project.generate!(:identifier => 'main')
|
||||
@contract = Contract.generate!(:project => @project)
|
||||
@user = User.generate_user_with_permission_to_manage_budget(:project => @project)
|
||||
|
||||
login_as(@user.login, 'contracts')
|
||||
end
|
||||
|
||||
should "allow any user to open the new deliverable form" do
|
||||
should "block anonymous users from opening the new deliverable form" do
|
||||
logout
|
||||
visit "/projects/#{@project.identifier}/contracts/#{@contract.id}/deliverables/new"
|
||||
|
||||
assert_requires_login
|
||||
end
|
||||
|
||||
should "block unauthorized users from opening the new deliverable form" do
|
||||
logout
|
||||
|
||||
@user = User.generate!(:password => 'test', :password_confirmation => 'test')
|
||||
login_as(@user.login, 'test')
|
||||
|
||||
visit "/projects/#{@project.identifier}/contracts/#{@contract.id}/deliverables/new"
|
||||
|
||||
assert_forbidden
|
||||
end
|
||||
|
||||
should "allow authorized users open the new deliverable form" do
|
||||
visit_contract_page(@contract)
|
||||
click_link 'Add New'
|
||||
assert_response :success
|
||||
@@ -50,15 +71,17 @@ class DeliverablesNewTest < ActionController::IntegrationTest
|
||||
click_link 'Add New'
|
||||
assert_response :success
|
||||
|
||||
fill_in "Title", :with => 'A New Deliverable'
|
||||
select "Fixed", :from => "Type"
|
||||
select @manager.name, :from => "Manager"
|
||||
fill_in "Start", :with => '2010-01-01'
|
||||
fill_in "End Date", :with => '2010-12-31'
|
||||
fill_in "Notes", :with => 'Some notes on the deliverable'
|
||||
within("#deliverable-details") do
|
||||
fill_in "Title", :with => 'A New Deliverable'
|
||||
select "Fixed", :from => "Type"
|
||||
select "Locked", :from => "Status"
|
||||
select @manager.name, :from => "Manager"
|
||||
fill_in "Start", :with => '2010-01-01'
|
||||
fill_in "End Date", :with => '2010-12-31'
|
||||
fill_in "Notes", :with => 'Some notes on the deliverable'
|
||||
end
|
||||
|
||||
fill_in "Total", :with => '1,000.00'
|
||||
# TODO: webrat can't trigger DOM events so it can't appear
|
||||
# assert js("jQuery('#deliverable_total').is(':visible')"), "Total is hidden when it should be visible"
|
||||
|
||||
click_button "Save"
|
||||
|
||||
@@ -73,6 +96,7 @@ class DeliverablesNewTest < ActionController::IntegrationTest
|
||||
assert_equal '2010-12-31', @deliverable.end_date.to_s
|
||||
assert_equal @manager, @deliverable.manager
|
||||
assert_equal 1000.0, @deliverable.total.to_f
|
||||
assert_equal "locked", @deliverable.status
|
||||
end
|
||||
|
||||
should "create a new Hourly deliverable" do
|
||||
@@ -84,18 +108,17 @@ class DeliverablesNewTest < ActionController::IntegrationTest
|
||||
click_link 'Add New'
|
||||
assert_response :success
|
||||
|
||||
fill_in "Title", :with => 'A New Deliverable'
|
||||
select "Hourly", :from => "Type"
|
||||
select @manager.name, :from => "Manager"
|
||||
fill_in "Start", :with => '2010-01-01'
|
||||
fill_in "End Date", :with => '2010-12-31'
|
||||
fill_in "Notes", :with => 'Some notes on the deliverable'
|
||||
within("#deliverable-details") do
|
||||
fill_in "Title", :with => 'A New Deliverable'
|
||||
select "Hourly", :from => "Type"
|
||||
select "Locked", :from => "Status"
|
||||
select @manager.name, :from => "Manager"
|
||||
fill_in "Start", :with => '2010-01-01'
|
||||
fill_in "End Date", :with => '2010-12-31'
|
||||
fill_in "Notes", :with => 'Some notes on the deliverable'
|
||||
end
|
||||
fill_in "Total", :with => '1,000.00'
|
||||
|
||||
# # Hide and clear the total
|
||||
# assert js("jQuery('#deliverable_total_input').is(':hidden')"),
|
||||
# "Total is visible when it should be hidden"
|
||||
|
||||
click_button "Save"
|
||||
|
||||
assert_response :success
|
||||
@@ -108,7 +131,8 @@ class DeliverablesNewTest < ActionController::IntegrationTest
|
||||
assert_equal '2010-01-01', @deliverable.start_date.to_s
|
||||
assert_equal '2010-12-31', @deliverable.end_date.to_s
|
||||
assert_equal @manager, @deliverable.manager
|
||||
|
||||
assert_equal "locked", @deliverable.status
|
||||
|
||||
end
|
||||
|
||||
should "create a new Retainer deliverable" do
|
||||
@@ -120,13 +144,16 @@ class DeliverablesNewTest < ActionController::IntegrationTest
|
||||
click_link 'Add New'
|
||||
assert_response :success
|
||||
|
||||
fill_in "Title", :with => 'A New Deliverable'
|
||||
select "Retainer", :from => "Type"
|
||||
select @manager.name, :from => "Manager"
|
||||
fill_in "Start", :with => '2010-01-01'
|
||||
fill_in "End Date", :with => '2010-12-31'
|
||||
fill_in "Notes", :with => 'Some notes on the deliverable'
|
||||
|
||||
within("#deliverable-details") do
|
||||
fill_in "Title", :with => 'A New Deliverable'
|
||||
select "Retainer", :from => "Type"
|
||||
select "Locked", :from => "Status"
|
||||
select @manager.name, :from => "Manager"
|
||||
fill_in "Start", :with => '2010-01-01'
|
||||
fill_in "End Date", :with => '2010-12-31'
|
||||
fill_in "Notes", :with => 'Some notes on the deliverable'
|
||||
end
|
||||
|
||||
within("#deliverable-labor") do
|
||||
fill_in "hrs", :with => '20'
|
||||
fill_in "$", :with => '$2,000'
|
||||
@@ -149,7 +176,8 @@ class DeliverablesNewTest < ActionController::IntegrationTest
|
||||
assert_equal '2010-01-01', @deliverable.start_date.to_s
|
||||
assert_equal '2010-12-31', @deliverable.end_date.to_s
|
||||
assert_equal @manager, @deliverable.manager
|
||||
|
||||
assert_equal "locked", @deliverable.status
|
||||
|
||||
# Budget items, one per month
|
||||
labor_budgets = @deliverable.labor_budgets
|
||||
assert_equal 12, labor_budgets.length
|
||||
@@ -193,12 +221,14 @@ class DeliverablesNewTest < ActionController::IntegrationTest
|
||||
click_link 'Add New'
|
||||
assert_response :success
|
||||
|
||||
fill_in "Title", :with => 'A New Deliverable'
|
||||
select "Hourly", :from => "Type"
|
||||
select @manager.name, :from => "Manager"
|
||||
fill_in "Start", :with => '2010-01-01'
|
||||
fill_in "End Date", :with => '2010-12-31'
|
||||
fill_in "Notes", :with => 'Some notes on the deliverable'
|
||||
within("#deliverable-details") do
|
||||
fill_in "Title", :with => 'A New Deliverable'
|
||||
select "Hourly", :from => "Type"
|
||||
select @manager.name, :from => "Manager"
|
||||
fill_in "Start", :with => '2010-01-01'
|
||||
fill_in "End Date", :with => '2010-12-31'
|
||||
fill_in "Notes", :with => 'Some notes on the deliverable'
|
||||
end
|
||||
|
||||
within("#deliverable-labor") do
|
||||
fill_in "hrs", :with => '20'
|
||||
@@ -210,6 +240,13 @@ class DeliverablesNewTest < ActionController::IntegrationTest
|
||||
fill_in "$", :with => '$1,000'
|
||||
end
|
||||
|
||||
within("#deliverable-fixed") do
|
||||
fill_in "title", :with => 'Flight to NYC'
|
||||
fill_in "budget", :with => '$600'
|
||||
fill_in "markup", :with => '50%'
|
||||
fill_in "description", :with => 'Need to fly to NYC for the week'
|
||||
end
|
||||
|
||||
click_button "Save"
|
||||
|
||||
assert_response :success
|
||||
@@ -226,6 +263,14 @@ class DeliverablesNewTest < ActionController::IntegrationTest
|
||||
@overhead_budget = @deliverable.overhead_budgets.first
|
||||
assert_equal 10, @overhead_budget.hours
|
||||
assert_equal 1000.0, @overhead_budget.budget
|
||||
|
||||
assert_equal 1, @deliverable.fixed_budgets.count
|
||||
@fixed_budget = @deliverable.fixed_budgets.first
|
||||
assert_equal "Flight to NYC", @fixed_budget.title
|
||||
assert_equal 600, @fixed_budget.budget
|
||||
assert_equal "50%", @fixed_budget.markup
|
||||
assert_equal 300, @fixed_budget.markup_value # 600 * 50%
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -8,6 +8,9 @@ class DeliverablesShowTest < ActionController::IntegrationTest
|
||||
@contract = Contract.generate!(:project => @project)
|
||||
@manager = User.generate!
|
||||
@deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
|
||||
@user = User.generate_user_with_permission_to_manage_budget(:project => @project)
|
||||
|
||||
login_as(@user.login, 'contracts')
|
||||
end
|
||||
|
||||
should "redirect to the contract page" do
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
require 'test_helper'
|
||||
|
||||
class DisabledContractsModuleTest < ActionController::IntegrationTest
|
||||
def setup
|
||||
@user = User.generate!(:login => 'existing', :password => 'existing', :password_confirmation => 'existing', :admin => true)
|
||||
login_as
|
||||
end
|
||||
|
||||
context "on a project with the Contracts module disabled" do
|
||||
setup do
|
||||
@project = Project.generate!
|
||||
@project.enabled_modules.find_by_name('contracts').destroy
|
||||
@project.reload
|
||||
assert !@project.module_enabled?(:contracts), "Contracts enabled on project"
|
||||
|
||||
@user = User.generate_user_with_permission_to_manage_budget(:project => @project)
|
||||
login_as(@user.login, 'contracts')
|
||||
end
|
||||
|
||||
should "not show the menu item" do
|
||||
|
||||
71
test/integration/issue_filtering_test.rb
Normal file
71
test/integration/issue_filtering_test.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
require 'test_helper'
|
||||
|
||||
class IssueFilteringTest < ActionController::IntegrationTest
|
||||
include Redmine::I18n
|
||||
|
||||
def setup
|
||||
@project = Project.generate!(:identifier => 'main')
|
||||
@contract = Contract.generate!(:project => @project)
|
||||
@manager = User.generate!
|
||||
@deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
|
||||
@user = User.generate_user_with_permission_to_manage_budget(:project => @project).reload
|
||||
@user.admin = true # Getting odd permissions issues
|
||||
@user.save
|
||||
@issue1 = Issue.generate_for_project!(@project)
|
||||
@issue2 = Issue.generate_for_project!(@project, :deliverable => @deliverable)
|
||||
assert_equal @deliverable, @issue2.deliverable
|
||||
|
||||
login_as(@user.login, 'contracts')
|
||||
end
|
||||
|
||||
should "allow grouping issues by deliverable" do
|
||||
visit_project(@project)
|
||||
click_link "Issues"
|
||||
|
||||
assert_select '#group_by' do
|
||||
assert_select 'option', "Deliverable"
|
||||
end
|
||||
|
||||
select "Deliverable", :from => 'group_by'
|
||||
|
||||
# Apply link is behind a JavaScript form
|
||||
visit "/projects/#{@project.identifier}/issues/?set_filter&group_by=deliverable"
|
||||
assert_response :success
|
||||
|
||||
assert_select "tr.group" do
|
||||
assert_select "td", :text => /None/ do
|
||||
assert_select "span.count", "(1)"
|
||||
end
|
||||
assert_select "td", :text => Regexp.new(@deliverable.title) do
|
||||
assert_select "span.count", "(1)"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "allow grouping issues by contract" do
|
||||
visit_project(@project)
|
||||
click_link "Issues"
|
||||
|
||||
assert_select '#group_by' do
|
||||
assert_select 'option', "Contract"
|
||||
end
|
||||
|
||||
select "Contract", :from => 'group_by'
|
||||
|
||||
# Apply link is behind a JavaScript form
|
||||
visit "/projects/#{@project.identifier}/issues/?set_filter&group_by=contract_name"
|
||||
assert_response :success
|
||||
|
||||
assert_select "tr.group" do
|
||||
assert_select "td", :text => /None/ do
|
||||
assert_select "span.count", "(1)"
|
||||
end
|
||||
assert_select "td", :text => Regexp.new(@contract.name) do
|
||||
assert_select "span.count", "(1)"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -19,36 +19,73 @@ class RedmineContracts::Hooks::ControllerIssuesBulkEditBeforeSaveHookTest < Acti
|
||||
@deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'The Title 1')
|
||||
@deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'The Title 2')
|
||||
@issue.deliverable = @deliverable1
|
||||
@issue.save
|
||||
|
||||
login_as('manager', 'existing')
|
||||
end
|
||||
|
||||
context "when saving multiple issues" do
|
||||
setup do
|
||||
visit_issue_bulk_edit_page(@issues)
|
||||
|
||||
context "with permission to Assign Deliverable to Issue" do
|
||||
setup do
|
||||
@role.permissions << :assign_deliverable_to_issue
|
||||
@role.save!
|
||||
visit_issue_bulk_edit_page(@issues)
|
||||
end
|
||||
|
||||
should "allow clearing all of the deliverables" do
|
||||
select "none", :from => "Deliverable"
|
||||
click_button "Submit"
|
||||
|
||||
assert_response :success
|
||||
|
||||
@issues.each do |issue|
|
||||
assert_equal nil, issue.reload.deliverable
|
||||
end
|
||||
end
|
||||
|
||||
should "allow assigning a deliverable" do
|
||||
select @deliverable2.title, :from => "Deliverable"
|
||||
click_button "Submit"
|
||||
|
||||
assert_response :success
|
||||
|
||||
@issues.each do |issue|
|
||||
assert_equal @deliverable2, issue.reload.deliverable
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "allow clearing all of the deliverables" do
|
||||
select "none", :from => "Deliverable"
|
||||
click_button "Submit"
|
||||
|
||||
assert_response :success
|
||||
|
||||
@issues.each do |issue|
|
||||
assert_equal nil, issue.reload.deliverable
|
||||
context "with no permission to Assign Deliverable to Issue" do
|
||||
setup do
|
||||
@role.permissions.delete(:assign_deliverable_to_issue)
|
||||
@role.save!
|
||||
visit_issue_bulk_edit_page(@issues)
|
||||
end
|
||||
|
||||
should "not allow clearing deliverables" do
|
||||
# Simulate form post since the field is hidden
|
||||
post "/issues/bulk_edit", :ids => @issues.collect(&:id), :deliverable_id => 'none'
|
||||
|
||||
assert_response :redirect
|
||||
|
||||
assert_equal @deliverable1, @issue.reload.deliverable
|
||||
end
|
||||
|
||||
should "not allow assigning a deliverable" do
|
||||
# Simulate form post since the field is hidden
|
||||
post "/issues/bulk_edit", :ids => @issues.collect(&:id), :deliverable_id => @deliverable2.id
|
||||
|
||||
assert_response :redirect
|
||||
|
||||
@issues.each do |issue|
|
||||
assert_not_equal @deliverable2, issue.reload.deliverable
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "allow assigning a deliverable" do
|
||||
select @deliverable2.title, :from => "Deliverable"
|
||||
click_button "Submit"
|
||||
|
||||
assert_response :success
|
||||
|
||||
@issues.each do |issue|
|
||||
assert_equal @deliverable2, issue.reload.deliverable
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,8 +11,8 @@ class RedmineContracts::Hooks::ControllerIssuesEditBeforeSaveTest < ActionContro
|
||||
@contract1 = Contract.generate!(:project => @project)
|
||||
@contract2 = Contract.generate!(:project => @project)
|
||||
|
||||
@manager = User.generate!(:login => 'manager', :password => 'existing', :password_confirmation => 'existing', :admin => true)
|
||||
@role = Role.generate!(:permissions => [:view_issues, :add_issues, :edit_issues])
|
||||
@manager = User.generate!(:login => 'manager', :password => 'existing', :password_confirmation => 'existing', :admin => false)
|
||||
@role = Role.generate!(:permissions => [:view_issues, :add_issues, :edit_issues, :assign_deliverable_to_issue])
|
||||
User.add_to_project(@manager, @project, @role)
|
||||
@deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'The Title for 1')
|
||||
@deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'The Title for 2')
|
||||
@@ -24,10 +24,10 @@ class RedmineContracts::Hooks::ControllerIssuesEditBeforeSaveTest < ActionContro
|
||||
context "for a new issue" do
|
||||
setup do
|
||||
visit_project(@project)
|
||||
click_link "New issue"
|
||||
end
|
||||
|
||||
should "set the issue's deliverable" do
|
||||
click_link "New issue"
|
||||
fill_in "Subject", :with => 'Hook test'
|
||||
select @deliverable2.title, :from => "Deliverable"
|
||||
click_button "Create"
|
||||
@@ -37,6 +37,84 @@ class RedmineContracts::Hooks::ControllerIssuesEditBeforeSaveTest < ActionContro
|
||||
assert_equal @deliverable2, Issue.last.deliverable
|
||||
|
||||
end
|
||||
|
||||
should "not allow setting a locked Deliverable" do
|
||||
assert @deliverable2.lock!
|
||||
click_link "New issue"
|
||||
|
||||
fill_in "Subject", :with => 'Hook test'
|
||||
select @deliverable2.title, :from => "Deliverable"
|
||||
assert_no_difference("Issue.count") do
|
||||
click_button "Create"
|
||||
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
assert_equal nil, Issue.last.deliverable
|
||||
|
||||
end
|
||||
|
||||
should "not allow setting a closed Deliverable" do
|
||||
assert @deliverable2.close!
|
||||
click_link "New issue"
|
||||
|
||||
fill_in "Subject", :with => 'Hook test'
|
||||
select @deliverable2.title, :from => "Deliverable"
|
||||
assert_no_difference("Issue.count") do
|
||||
click_button "Create"
|
||||
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
assert_equal nil, Issue.last.deliverable
|
||||
|
||||
end
|
||||
|
||||
should "not allow setting a Deliverable on a locked Contract" do
|
||||
assert @contract2.lock!
|
||||
click_link "New issue"
|
||||
|
||||
fill_in "Subject", :with => 'Hook test'
|
||||
select @deliverable2.title, :from => "Deliverable"
|
||||
assert_no_difference("Issue.count") do
|
||||
click_button "Create"
|
||||
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
assert_equal nil, Issue.last.deliverable
|
||||
|
||||
end
|
||||
|
||||
should "not allow setting a Deliverable on a closed Contract" do
|
||||
assert @contract2.close!
|
||||
click_link "New issue"
|
||||
|
||||
fill_in "Subject", :with => 'Hook test'
|
||||
select @deliverable2.title, :from => "Deliverable"
|
||||
assert_no_difference("Issue.count") do
|
||||
click_button "Create"
|
||||
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
assert_equal nil, Issue.last.deliverable
|
||||
|
||||
end
|
||||
|
||||
context "with no permission to Assign Deliverable" do
|
||||
should "not allow setting the Deliverable (force HTTP request)" do
|
||||
@role.permissions.delete(:assign_deliverable_to_issue)
|
||||
@role.save!
|
||||
|
||||
assert_difference('Issue.count', 1) do
|
||||
post "/projects/#{@project.identifier}/issues", :issue => {:subject => 'Force', :deliverable_id => @deliverable1.id, :priority_id => IssuePriority.first.id}
|
||||
end
|
||||
|
||||
assert_equal nil, Issue.last.deliverable
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "for an existing issue" do
|
||||
@@ -54,6 +132,89 @@ class RedmineContracts::Hooks::ControllerIssuesEditBeforeSaveTest < ActionContro
|
||||
assert_equal @deliverable2, @issue.deliverable
|
||||
|
||||
end
|
||||
|
||||
should "not allow updating to a locked deliverable" do
|
||||
assert @deliverable2.lock!
|
||||
select @deliverable2.title, :from => "Deliverable"
|
||||
click_button "Submit"
|
||||
|
||||
assert_response :success
|
||||
|
||||
@issue.reload
|
||||
assert_equal nil, @issue.deliverable
|
||||
|
||||
end
|
||||
|
||||
should "not allow updating to a closed deliverable" do
|
||||
assert @deliverable2.close!
|
||||
select @deliverable2.title, :from => "Deliverable"
|
||||
click_button "Submit"
|
||||
|
||||
assert_response :success
|
||||
|
||||
@issue.reload
|
||||
assert_equal nil, @issue.deliverable
|
||||
|
||||
end
|
||||
|
||||
should "not allow updating to a deliverable on a locked contract" do
|
||||
assert @contract2.lock!
|
||||
select @deliverable2.title, :from => "Deliverable"
|
||||
click_button "Submit"
|
||||
|
||||
assert_response :success
|
||||
|
||||
@issue.reload
|
||||
assert_equal nil, @issue.deliverable
|
||||
|
||||
end
|
||||
|
||||
should "not allow updating to a deliverable on a closed contract" do
|
||||
assert @contract2.close!
|
||||
select @deliverable2.title, :from => "Deliverable"
|
||||
click_button "Submit"
|
||||
|
||||
assert_response :success
|
||||
|
||||
@issue.reload
|
||||
assert_equal nil, @issue.deliverable
|
||||
|
||||
end
|
||||
|
||||
should "allow updating an issue, even if the deliverable is locked as long as the deliverable isn't changed" do
|
||||
select @deliverable2.title, :from => "Deliverable"
|
||||
click_button "Submit"
|
||||
|
||||
assert_response :success
|
||||
|
||||
@issue.reload
|
||||
assert_equal @deliverable2, @issue.deliverable
|
||||
|
||||
# Now normal update after locking
|
||||
assert @deliverable2.lock!
|
||||
fill_in "Subject", :with => 'Change subject'
|
||||
click_button "Submit"
|
||||
|
||||
assert_response :success
|
||||
@issue.reload
|
||||
assert_equal "Change subject", @issue.subject
|
||||
assert_equal @deliverable2, @issue.deliverable
|
||||
|
||||
end
|
||||
|
||||
context "with no permission to Assign Deliverable" do
|
||||
should "not allow setting the Deliverable (force HTTP request)" do
|
||||
@role.permissions.delete(:assign_deliverable_to_issue)
|
||||
@role.save!
|
||||
|
||||
assert_difference('Journal.count', 1) do
|
||||
put "/issues/#{@issue.id}", :issue => {:subject => 'Force', :deliverable_id => @deliverable1.id}
|
||||
end
|
||||
|
||||
assert_equal nil, @issue.reload.deliverable
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,15 +19,15 @@ class RedmineContracts::Hooks::HelperIssuesShowDetailAfterSettingHookTest < Acti
|
||||
# Set first
|
||||
@issue.init_journal(@manager)
|
||||
@issue.deliverable = @deliverable1
|
||||
@issue.save!
|
||||
@issue.save! && @issue.reload
|
||||
# Change
|
||||
@issue.init_journal(@manager)
|
||||
@issue.deliverable = @deliverable2
|
||||
@issue.save!
|
||||
@issue.save! && @issue.reload
|
||||
# Unset
|
||||
@issue.init_journal(@manager)
|
||||
@issue.deliverable = nil
|
||||
@issue.save!
|
||||
@issue.save! && @issue.reload
|
||||
|
||||
login_as('manager', 'existing')
|
||||
|
||||
|
||||
@@ -11,34 +11,91 @@ class RedmineContracts::Hooks::ViewIssuesBulkEditDetailsBottomHookTest < ActionC
|
||||
@issue3 = Issue.generate_for_project!(@project)
|
||||
@contract1 = Contract.generate!(:project => @project)
|
||||
@contract2 = Contract.generate!(:project => @project)
|
||||
@locked_contract = Contract.generate!(:project => @project)
|
||||
@closed_contract = Contract.generate!(:project => @project)
|
||||
|
||||
@manager = User.generate!(:login => 'manager', :password => 'existing', :password_confirmation => 'existing')
|
||||
@role = Role.generate!(:permissions => [:view_issues, :edit_issues])
|
||||
User.add_to_project(@manager, @project, @role)
|
||||
@deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'The Title')
|
||||
@deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'The Title')
|
||||
@locked_deliverable = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Locked Deliverable', :status => 'locked')
|
||||
@closed_deliverable = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Closed Deliverable', :status => 'closed')
|
||||
@deliverable1_on_locked_contract = FixedDeliverable.generate!(:contract => @locked_contract, :manager => @manager, :title => 'Deliverable 1 on locked contract')
|
||||
@deliverable2_on_locked_contract = FixedDeliverable.generate!(:contract => @locked_contract, :manager => @manager, :title => 'Deliverable 2 on locked contract')
|
||||
@deliverable_on_closed_contract = FixedDeliverable.generate!(:contract => @closed_contract, :manager => @manager, :title => 'Deliverable on closed contract')
|
||||
@issue.deliverable = @deliverable1
|
||||
|
||||
# Set contract statuses now that all deliverables are created
|
||||
assert @locked_contract.lock!
|
||||
assert @closed_contract.close!
|
||||
|
||||
login_as('manager', 'existing')
|
||||
end
|
||||
|
||||
context "with Contracts Enabled" do
|
||||
setup do
|
||||
visit_issue_bulk_edit_page([@issue, @issue2, @issue3])
|
||||
end
|
||||
|
||||
should "render the a select field for the deliverables with all of the deliverables grouped by contract" do
|
||||
context "with permission to Assign Deliverable" do
|
||||
setup do
|
||||
@role.permissions << :assign_deliverable_to_issue
|
||||
@role.save!
|
||||
visit_issue_bulk_edit_page([@issue, @issue2, @issue3])
|
||||
end
|
||||
|
||||
assert_select "select#deliverable_id" do
|
||||
assert_select "optgroup[label=?]", @contract1.name do
|
||||
assert_select "option", :text => /#{@deliverable1.title}/
|
||||
end
|
||||
should "render the a select field for the deliverables with all of the deliverables grouped by contract" do
|
||||
|
||||
assert_select "optgroup[label=?]", @contract2.name do
|
||||
assert_select "option", :text => /#{@deliverable2.title}/
|
||||
assert_select "select#deliverable_id" do
|
||||
assert_select "optgroup[label=?]", @contract1.name do
|
||||
assert_select "option", :text => /#{@deliverable1.title}/
|
||||
end
|
||||
|
||||
assert_select "optgroup[label=?]", @contract2.name do
|
||||
assert_select "option", :text => /#{@deliverable2.title}/
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should "disable all locked deliverables" do
|
||||
assert_select "select#deliverable_id" do
|
||||
assert_select "option[disabled=disabled]", :text => /#{@locked_deliverable.title}/
|
||||
end
|
||||
end
|
||||
|
||||
should "disable all deliverables on locked contracts" do
|
||||
assert_select "select#deliverable_id" do
|
||||
assert_select "optgroup[label=?]", @locked_contract.name do
|
||||
assert_select "option[disabled=disabled]", :text => /#{@deliverable1_on_locked_contract.title}/
|
||||
assert_select "option[disabled=disabled]", :text => /#{@deliverable2_on_locked_contract.title}/
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should "not show closed deliverables" do
|
||||
assert_select "select#deliverable_id" do
|
||||
assert_select "option", :text => /#{@closed_deliverable.title}/, :count => 0
|
||||
end
|
||||
end
|
||||
|
||||
should "not show deliverables on closed contracts" do
|
||||
assert_select "select#deliverable_id" do
|
||||
assert_select "optgroup[label=?]", @closed_contract.name, :count => 0
|
||||
assert_select "option", :text => /#{@deliverable_on_closed_contract.title}/, :count => 0
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "with no permission to Assign Deliverable" do
|
||||
setup do
|
||||
@role.permissions.delete(:assign_deliverable_to_issue)
|
||||
@role.save!
|
||||
visit_issue_bulk_edit_page([@issue, @issue2, @issue3])
|
||||
end
|
||||
|
||||
should "not render the deliverable select field" do
|
||||
assert_select 'select#deliverable_id', :count => 0
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "with Contracts Disabled" do
|
||||
|
||||
@@ -9,35 +9,135 @@ class RedmineContracts::Hooks::ViewIssuesFormDetailsBottomTest < ActionControlle
|
||||
@issue = Issue.generate_for_project!(@project)
|
||||
@contract1 = Contract.generate!(:project => @project)
|
||||
@contract2 = Contract.generate!(:project => @project)
|
||||
@locked_contract = Contract.generate!(:project => @project)
|
||||
@closed_contract = Contract.generate!(:project => @project)
|
||||
|
||||
@manager = User.generate!(:login => 'manager', :password => 'existing', :password_confirmation => 'existing')
|
||||
@role = Role.generate!(:permissions => [:view_issues, :edit_issues])
|
||||
User.add_to_project(@manager, @project, @role)
|
||||
@deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'The Title')
|
||||
@deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'The Title')
|
||||
@deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Deliverable1')
|
||||
@deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'Deliverable2')
|
||||
@locked_deliverable = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Locked Deliverable', :status => 'locked')
|
||||
@closed_deliverable = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Closed Deliverable', :status => 'closed')
|
||||
@deliverable1_on_locked_contract = FixedDeliverable.generate!(:contract => @locked_contract, :manager => @manager, :title => 'Deliverable 1 on locked contract')
|
||||
@deliverable2_on_locked_contract = FixedDeliverable.generate!(:contract => @locked_contract, :manager => @manager, :title => 'Deliverable 2 on locked contract')
|
||||
@deliverable_on_closed_contract = FixedDeliverable.generate!(:contract => @closed_contract, :manager => @manager, :title => 'Deliverable on closed contract')
|
||||
@issue.deliverable = @deliverable1
|
||||
assert @issue.save
|
||||
|
||||
# Set contract statuses now that all deliverables are created
|
||||
assert @locked_contract.lock!
|
||||
assert @closed_contract.close!
|
||||
|
||||
login_as('manager', 'existing')
|
||||
end
|
||||
|
||||
context "with Contracts Enabled" do
|
||||
setup do
|
||||
visit_issue_page(@issue)
|
||||
end
|
||||
|
||||
should "render the a select field for the deliverables with all of the deliverables grouped by contract" do
|
||||
assert_select "select#issue_deliverable_id" do
|
||||
assert_select "optgroup[label=?]", @contract1.name do
|
||||
assert_select "option", :text => /#{@deliverable1.title}/
|
||||
context "with permission to Assign Deliverable" do
|
||||
setup do
|
||||
@role.permissions << :assign_deliverable_to_issue
|
||||
@role.save!
|
||||
visit_issue_page(@issue)
|
||||
end
|
||||
|
||||
should "render the a select field for the deliverables with all of the deliverables grouped by contract" do
|
||||
assert_select "select#issue_deliverable_id" do
|
||||
assert_select "optgroup[label=?]", @contract1.name do
|
||||
assert_select "option", :text => /#{@deliverable1.title}/
|
||||
end
|
||||
|
||||
assert_select "optgroup[label=?]", @contract2.name do
|
||||
assert_select "option", :text => /#{@deliverable2.title}/
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should "disable all locked deliverables" do
|
||||
assert_select "select#issue_deliverable_id" do
|
||||
assert_select "option[disabled=disabled]", :text => /#{@locked_deliverable.title}/
|
||||
end
|
||||
end
|
||||
|
||||
should "disable all deliverables on locked contracts" do
|
||||
assert_select "select#issue_deliverable_id" do
|
||||
assert_select "optgroup[label=?]", @locked_contract.name do
|
||||
assert_select "option[disabled=disabled]", :text => /#{@deliverable1_on_locked_contract.title}/
|
||||
assert_select "option[disabled=disabled]", :text => /#{@deliverable2_on_locked_contract.title}/
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should "not show closed deliverables" do
|
||||
assert_select "select#issue_deliverable_id" do
|
||||
assert_select "option", :text => /#{@closed_deliverable.title}/, :count => 0
|
||||
end
|
||||
end
|
||||
|
||||
should "not show deliverables on closed contracts" do
|
||||
assert_select "select#issue_deliverable_id" do
|
||||
assert_select "optgroup[label=?]", @closed_contract.name, :count => 0
|
||||
assert_select "option", :text => /#{@deliverable_on_closed_contract.title}/, :count => 0
|
||||
end
|
||||
end
|
||||
|
||||
should "show the assigned deliverable as an option, even if it's locked" do
|
||||
@deliverable1.lock!
|
||||
visit_issue_page(@issue)
|
||||
|
||||
assert_select "select#issue_deliverable_id" do
|
||||
assert_select "option[disabled=disabled]", :text => /#{@deliverable1.title}/, :count => 0 # Not disabled
|
||||
assert_select "option", :text => /#{@deliverable1.title}/, :count => 1 # Present
|
||||
end
|
||||
|
||||
assert_select "optgroup[label=?]", @contract2.name do
|
||||
assert_select "option", :text => /#{@deliverable2.title}/
|
||||
end
|
||||
|
||||
should "show the assigned deliverable as an option, even if it's closed" do
|
||||
@deliverable1.close!
|
||||
visit_issue_page(@issue)
|
||||
|
||||
assert_select "select#issue_deliverable_id" do
|
||||
assert_select "option[disabled=disabled]", :text => /#{@deliverable1.title}/, :count => 0 # Not disabled
|
||||
assert_select "option", :text => /#{@deliverable1.title}/, :count => 1 # Present
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "show the assigned deliverable as an option, even if it's contract is locked" do
|
||||
@contract1.lock!
|
||||
visit_issue_page(@issue)
|
||||
|
||||
assert_select "select#issue_deliverable_id" do
|
||||
assert_select "option[disabled=disabled]", :text => /#{@deliverable1.title}/, :count => 0 # Not disabled
|
||||
assert_select "option", :text => /#{@deliverable1.title}/, :count => 1 # Present
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "show the assigned deliverable as an option, even if it's contract is closed" do
|
||||
@contract1.close!
|
||||
visit_issue_page(@issue)
|
||||
|
||||
assert_select "select#issue_deliverable_id" do
|
||||
assert_select "option[disabled=disabled]", :text => /#{@deliverable1.title}/, :count => 0 # Not disabled
|
||||
assert_select "option", :text => /#{@deliverable1.title}/, :count => 1 # Present
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
context "with no permission to Assign Deliverable" do
|
||||
setup do
|
||||
@role.permissions.delete(:assign_deliverable_to_issue)
|
||||
@role.save!
|
||||
visit_issue_page(@issue)
|
||||
end
|
||||
|
||||
should "not render the deliverable select field" do
|
||||
assert_select 'select#issue_deliverable_id', :count => 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
context "with Contracts Disabled" do
|
||||
setup do
|
||||
@project.enabled_modules.collect {|m| m.destroy if m.name == 'contracts' }
|
||||
|
||||
@@ -7,12 +7,12 @@ class ContractShowTest < ActionController::PerformanceTest
|
||||
def setup
|
||||
@project = Project.generate!(:identifier => 'main').reload
|
||||
@contract = Contract.generate!(:project => @project)
|
||||
@manager = User.generate!(:login => 'user', :password => 'password', :password_confirmation => 'password')
|
||||
@role = Role.generate!
|
||||
User.add_to_project(@manager, @project, @role)
|
||||
@manager = User.generate_user_with_permission_to_manage_budget(:project => @project).reload
|
||||
@fixed_deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'The Title')
|
||||
@hourly_deliverable = HourlyDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'An Hourly')
|
||||
|
||||
@rate = Rate.generate!(:project => @project, :user => @manager, :date_in_effect => Date.today, :amount => 100)
|
||||
|
||||
configure_overhead_plugin
|
||||
100.times do
|
||||
generate_issues_and_time_entries_for_deliverable(@hourly_deliverable, @project)
|
||||
@@ -20,7 +20,7 @@ class ContractShowTest < ActionController::PerformanceTest
|
||||
end
|
||||
|
||||
# Load the app
|
||||
login_as 'user', 'password'
|
||||
login_as @manager.login, 'contracts'
|
||||
visit_contracts_for_project(@project)
|
||||
end
|
||||
|
||||
@@ -44,6 +44,7 @@ class ContractShowTest < ActionController::PerformanceTest
|
||||
:spent_on => Date.today,
|
||||
:hours => 20,
|
||||
:user => @manager)
|
||||
|
||||
deliverable.issues << @issue1
|
||||
|
||||
end
|
||||
|
||||
@@ -10,30 +10,20 @@ Webrat.configure do |config|
|
||||
config.mode = :rails
|
||||
end
|
||||
|
||||
require 'holygrail'
|
||||
|
||||
module HolyGrail
|
||||
module Extensions
|
||||
# Need to rewrite the javascript for the engine too
|
||||
def rewrite_script_paths(body)
|
||||
body.
|
||||
gsub(%r%src=("|')/?plugin_assets/redmine_contracts/javascripts/(.*)("|')%) { %|src=#{$1}%s#{$1}"| % Rails.root.join("vendor/plugins/redmine_contracts/assets/javascripts/#{$2}") }.
|
||||
gsub(%r%src=("|')/?javascripts/(.*)("|')%) { %|src=#{$1}%s#{$1}"| % Rails.root.join("public/javascripts/#{$2}") }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ActionController::TestCase
|
||||
include HolyGrail::Extensions
|
||||
end
|
||||
class ActionController::IntegrationTest
|
||||
include HolyGrail::Extensions
|
||||
end
|
||||
|
||||
def User.add_to_project(user, project, role)
|
||||
Member.generate!(:principal => user, :project => project, :roles => [role])
|
||||
end
|
||||
|
||||
def User.generate_user_with_permission_to_manage_budget(options={})
|
||||
project = options[:project]
|
||||
|
||||
user = User.generate!(:password => 'contracts', :password_confirmation => 'contracts')
|
||||
role = Role.generate!(:permissions => [:view_issues, :edit_issues, :add_issues, :manage_budget])
|
||||
User.add_to_project(user, project, role)
|
||||
user
|
||||
end
|
||||
|
||||
|
||||
module IntegrationTestHelper
|
||||
def login_as(user="existing", password="existing")
|
||||
visit "/login"
|
||||
@@ -44,6 +34,12 @@ module IntegrationTestHelper
|
||||
assert User.current.logged?
|
||||
end
|
||||
|
||||
def logout
|
||||
visit '/logout'
|
||||
assert_response :success
|
||||
assert !User.current.logged?
|
||||
end
|
||||
|
||||
def visit_project(project)
|
||||
visit '/'
|
||||
assert_response :success
|
||||
@@ -81,7 +77,12 @@ module IntegrationTestHelper
|
||||
|
||||
def assert_forbidden
|
||||
assert_response :forbidden
|
||||
assert_template 'common/403'
|
||||
assert_template 'common/error'
|
||||
end
|
||||
|
||||
def assert_requires_login
|
||||
assert_response :success
|
||||
assert_template 'account/login'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -17,6 +17,9 @@ class ContractTest < ActiveSupport::TestCase
|
||||
should_allow_values_for :discount_type, "$", "%", nil, ''
|
||||
should_not_allow_values_for :discount_type, ["amount", "percent", "bar"]
|
||||
|
||||
should_allow_values_for :status, "", nil, 'open', 'locked', 'closed'
|
||||
should_not_allow_values_for :status, "other", "things", "1"
|
||||
|
||||
context "end_date" do
|
||||
should "be after start_date" do
|
||||
@contract = Contract.new(:start_date => Date.today, :end_date => Date.yesterday)
|
||||
@@ -32,6 +35,12 @@ class ContractTest < ActiveSupport::TestCase
|
||||
assert_equal false, @contract.executed
|
||||
end
|
||||
|
||||
should "default status to open" do
|
||||
@contract = Contract.new
|
||||
|
||||
assert_equal "open", @contract.status
|
||||
end
|
||||
|
||||
context "#labor_budget" do
|
||||
should "sum all of the labor budgets of the Deliverables" do
|
||||
contract = Contract.generate!
|
||||
@@ -247,6 +256,7 @@ class ContractTest < ActiveSupport::TestCase
|
||||
TimeEntry.generate!(:hours => 4, :issue => @issue2, :project => @project,
|
||||
:activity => @non_billable_activity,
|
||||
:user => @manager)
|
||||
@deliverable_2.fixed_budgets << FixedBudget.spawn(:budget => 200, :markup => '$100', :paid => true)
|
||||
|
||||
assert_equal 875, @deliverable_1.profit_left
|
||||
assert_equal 1125, @deliverable_2.profit_left
|
||||
@@ -254,4 +264,54 @@ class ContractTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#fixed_budget" do
|
||||
should "sum all fixed budget amounts on the Deliverables" do
|
||||
contract = Contract.generate!
|
||||
contract.deliverables << @deliverable_1 = FixedDeliverable.generate!
|
||||
FixedBudget.generate!(:deliverable => @deliverable_1, :budget => '$1,000')
|
||||
contract.deliverables << @deliverable_2 = HourlyDeliverable.generate!
|
||||
FixedBudget.generate!(:deliverable => @deliverable_2, :budget => '$2,000')
|
||||
|
||||
assert_equal 3000, contract.fixed_budget
|
||||
end
|
||||
end
|
||||
|
||||
context "#fixed_spent" do
|
||||
should "sum all fixed budget amounts on the Deliverables which are paid" do
|
||||
contract = Contract.generate!
|
||||
contract.deliverables << @deliverable_1 = FixedDeliverable.generate!
|
||||
FixedBudget.generate!(:deliverable => @deliverable_1, :budget => '$1,000', :paid => true)
|
||||
contract.deliverables << @deliverable_2 = HourlyDeliverable.generate!
|
||||
FixedBudget.generate!(:deliverable => @deliverable_2, :budget => '$2,000')
|
||||
|
||||
assert_equal 1000, contract.fixed_spent
|
||||
end
|
||||
end
|
||||
|
||||
context "#fixed_markup_budget" do
|
||||
should "sum all fixed budget markup values on the Deliverables" do
|
||||
contract = Contract.generate!
|
||||
contract.deliverables << @deliverable_1 = FixedDeliverable.generate!
|
||||
FixedBudget.generate!(:deliverable => @deliverable_1, :budget => '$1,000', :markup => '$100')
|
||||
contract.deliverables << @deliverable_2 = HourlyDeliverable.generate!
|
||||
FixedBudget.generate!(:deliverable => @deliverable_2, :budget => '$2,000', :markup => '200%')
|
||||
|
||||
assert_equal (100) + (2.00 * 2000), contract.fixed_markup_budget
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#fixed_markup_spent" do
|
||||
should "sum all fixed budget markup values on the Deliverables which are paid" do
|
||||
contract = Contract.generate!
|
||||
contract.deliverables << @deliverable_1 = FixedDeliverable.generate!
|
||||
FixedBudget.generate!(:deliverable => @deliverable_1, :budget => '$1,000', :markup => '$100', :paid => true)
|
||||
contract.deliverables << @deliverable_2 = HourlyDeliverable.generate!
|
||||
FixedBudget.generate!(:deliverable => @deliverable_2, :budget => '$2,000', :markup => '200%')
|
||||
|
||||
assert_equal (100) + (0), contract.fixed_markup_spent
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -5,12 +5,20 @@ class DeliverableTest < ActiveSupport::TestCase
|
||||
should_belong_to :manager
|
||||
should_have_many :labor_budgets
|
||||
should_have_many :overhead_budgets
|
||||
should_have_many :fixed_budgets
|
||||
should_have_many :issues
|
||||
|
||||
should_validate_presence_of :title
|
||||
should_validate_presence_of :type
|
||||
should_validate_presence_of :manager
|
||||
|
||||
should_allow_values_for :status, "", nil, 'open', 'locked', 'closed'
|
||||
should_not_allow_values_for :status, "other", "things", "1"
|
||||
|
||||
should "default status to open" do
|
||||
assert_equal "open", Deliverable.new.status
|
||||
end
|
||||
|
||||
context "#total=" do
|
||||
should "strip dollar signs when writing" do
|
||||
d = Deliverable.new
|
||||
@@ -34,4 +42,45 @@ class DeliverableTest < ActiveSupport::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
context "with a locked contract" do
|
||||
should "block creating a new deliverable" do
|
||||
contract = Contract.generate!(:status => "locked")
|
||||
deliverable = FixedDeliverable.spawn(:contract => contract)
|
||||
|
||||
assert !deliverable.valid?
|
||||
assert deliverable.errors.on_base.include?("Can't create a deliverable on a locked contract")
|
||||
end
|
||||
|
||||
should "block deleting a deliverable" do
|
||||
contract = Contract.generate!
|
||||
deliverable = FixedDeliverable.generate!(:contract => contract).reload
|
||||
assert contract.lock!
|
||||
|
||||
assert_no_difference("Deliverable.count") do
|
||||
deliverable.destroy
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
context "with a closed contract" do
|
||||
should "block creating a new deliverable" do
|
||||
contract = Contract.generate!(:status => "closed")
|
||||
deliverable = FixedDeliverable.spawn(:contract => contract)
|
||||
|
||||
assert !deliverable.valid?
|
||||
assert deliverable.errors.on_base.include?("Can't create a deliverable on a closed contract")
|
||||
end
|
||||
|
||||
should "block deleting a deliverable" do
|
||||
contract = Contract.generate!
|
||||
deliverable = FixedDeliverable.generate!(:contract => contract).reload
|
||||
assert contract.close!
|
||||
|
||||
assert_no_difference("Deliverable.count") do
|
||||
deliverable.destroy
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
69
test/unit/fixed_budget_test.rb
Normal file
69
test/unit/fixed_budget_test.rb
Normal file
@@ -0,0 +1,69 @@
|
||||
require File.dirname(__FILE__) + '/../test_helper'
|
||||
|
||||
class FixedBudgetTest < ActiveSupport::TestCase
|
||||
should_belong_to :deliverable
|
||||
|
||||
context "#markup_value" do
|
||||
setup do
|
||||
@fixed_budget = FixedBudget.new(:budget => 1000)
|
||||
end
|
||||
|
||||
context "with no markup" do
|
||||
should "be 0" do
|
||||
assert_equal nil, @fixed_budget.markup
|
||||
assert_equal 0, @fixed_budget.markup_value
|
||||
end
|
||||
end
|
||||
|
||||
context "with a % markup" do
|
||||
should "equal the budget times the %" do
|
||||
@fixed_budget.markup = '50%'
|
||||
assert_equal 500, @fixed_budget.markup_value
|
||||
end
|
||||
end
|
||||
|
||||
context "with a $ markup" do
|
||||
should "equal the $ markup (straight markup)" do
|
||||
@fixed_budget.markup = '$4,000.57'
|
||||
assert_equal 4000.57, @fixed_budget.markup_value
|
||||
end
|
||||
|
||||
should "work without the $ sign" do
|
||||
@fixed_budget.markup = '4,000.57'
|
||||
assert_equal 4000.57, @fixed_budget.markup_value
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "with a straight amount of markup" do
|
||||
should "equal the markup" do
|
||||
@fixed_budget.markup = 4000.57
|
||||
assert_equal 4000.57, @fixed_budget.markup_value
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#budget=" do
|
||||
setup do
|
||||
@fixed_budget = FixedBudget.new
|
||||
end
|
||||
|
||||
should "allow a $ string" do
|
||||
@fixed_budget.budget = '$1,000.00'
|
||||
assert_equal 1000.00, @fixed_budget.budget
|
||||
end
|
||||
|
||||
should "allow a plain string" do
|
||||
@fixed_budget.budget = '1,000.00'
|
||||
assert_equal 1000.00, @fixed_budget.budget
|
||||
end
|
||||
|
||||
should "allow a numeric value" do
|
||||
@fixed_budget.budget = 1000.00
|
||||
assert_equal 1000.00, @fixed_budget.budget
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -13,8 +13,9 @@ class FixedDeliverableTest < ActiveSupport::TestCase
|
||||
LaborBudget.generate!(:deliverable => deliverable, :budget => 200)
|
||||
LaborBudget.generate!(:deliverable => deliverable, :budget => 200)
|
||||
OverheadBudget.generate!(:deliverable => deliverable, :budget => 200)
|
||||
FixedBudget.generate!(:deliverable => deliverable, :budget => '$100', :markup => '50%') # $50 markup
|
||||
|
||||
assert_equal 400, deliverable.profit_budget
|
||||
assert_equal 400 - 150, deliverable.profit_budget
|
||||
end
|
||||
|
||||
should "be 0 if there is no total" do
|
||||
@@ -64,4 +65,15 @@ class FixedDeliverableTest < ActiveSupport::TestCase
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
context "#fixed_markup_budget_total_spent" do
|
||||
should "be the total markup from fixed budgets because FixedDeliverables are considered 100% paid" do
|
||||
@deliverable = FixedDeliverable.generate!
|
||||
FixedBudget.generate!(:deliverable => @deliverable, :budget => '$1,000', :markup => '$100', :paid => true)
|
||||
FixedBudget.generate!(:deliverable => @deliverable, :budget => '$1,000', :markup => '$100', :paid => false)
|
||||
|
||||
assert_equal 200, @deliverable.fixed_markup_budget_total_spent
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
31
test/unit/helpers/contracts_helper_test.rb
Normal file
31
test/unit/helpers/contracts_helper_test.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
require 'test_helper'
|
||||
|
||||
class ContractsHelperTest < ActionView::TestCase
|
||||
context "#validate_period" do
|
||||
should "with a HourlyDeliverable should return nil" do
|
||||
assert_equal nil, validate_period(HourlyDeliverable.new, '2010-01')
|
||||
end
|
||||
|
||||
should "with a FixedDeliverable should return nil" do
|
||||
assert_equal nil, validate_period(FixedDeliverable.new, '2010-01')
|
||||
end
|
||||
|
||||
context "with a RetainerDeliverable" do
|
||||
should "return nil when there period is not within the Deliverable's date range" do
|
||||
retainer = RetainerDeliverable.new(:start_date => Date.new(2011,1,1),
|
||||
:end_date => Date.new(2012,1,1))
|
||||
|
||||
assert_equal nil, validate_period(retainer, '2010-01')
|
||||
end
|
||||
|
||||
should "return the period when it's within the Deliverable's date range" do
|
||||
retainer = RetainerDeliverable.new(:start_date => Date.new(2001,1,1),
|
||||
:end_date => Date.new(2003,1,1))
|
||||
|
||||
assert_equal '2001-02', validate_period(retainer, '2001-02')
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -20,18 +20,19 @@ class HourlyDeliverableTest < ActiveSupport::TestCase
|
||||
assert_equal 0, d.total
|
||||
end
|
||||
|
||||
should "multiply the total number of labor budget hours by the contract billable rate" do
|
||||
should "multiply the total number of labor budget hours by the contract billable rate and add the fixed budget and markup" do
|
||||
contract = Contract.generate!(:billable_rate => 100.0)
|
||||
d = HourlyDeliverable.generate!(:contract => contract)
|
||||
d.labor_budgets << LaborBudget.generate!(:hours => 10)
|
||||
d.overhead_budgets << OverheadBudget.generate!(:hours => 20)
|
||||
d.fixed_budgets << FixedBudget.generate!(:budget => '$100', :markup => '50%') # $50 markup
|
||||
|
||||
assert_equal 100.0 * 10, d.total
|
||||
assert_equal (100.0 * 10) + (100 + 50), d.total
|
||||
end
|
||||
end
|
||||
|
||||
context "#total_spent" do
|
||||
should "be equal to the number of hours used multipled by the contract rate" do
|
||||
should "be equal to the number of hours used multipled by the contract rate and adding the fixed budget and markup spent" do
|
||||
configure_overhead_plugin
|
||||
|
||||
contract = Contract.generate!(:billable_rate => 150.0)
|
||||
@@ -45,8 +46,11 @@ class HourlyDeliverableTest < ActiveSupport::TestCase
|
||||
TimeEntry.generate!(:hours => 15, :issue => @issue1, :project => @project,
|
||||
:activity => @billable_activity,
|
||||
:user => @developer)
|
||||
# Only paid fixed budgets counted
|
||||
d.fixed_budgets << FixedBudget.generate!(:budget => '$100', :markup => '50%') # $50 markup
|
||||
d.fixed_budgets << FixedBudget.generate!(:budget => '$100', :markup => '50%', :paid => true) # $50 markup
|
||||
|
||||
assert_equal 2250, d.total_spent
|
||||
assert_equal 2250 + 150, d.total_spent
|
||||
end
|
||||
end
|
||||
|
||||
@@ -86,9 +90,10 @@ class HourlyDeliverableTest < ActiveSupport::TestCase
|
||||
LaborBudget.generate!(:deliverable => @deliverable, :hours => 5, :budget => 250)
|
||||
LaborBudget.generate!(:deliverable => @deliverable, :hours => 5, :budget => 250)
|
||||
OverheadBudget.generate!(:deliverable => @deliverable, :hours => 3, :budget => 225)
|
||||
FixedBudget.generate!(:deliverable => @deliverable, :budget => '$100', :markup => '50%') # $50 markup
|
||||
|
||||
assert_equal 1500, @deliverable.total
|
||||
assert_equal 1500 - (225 + 250 + 250), @deliverable.profit_budget
|
||||
assert_equal 1650, @deliverable.total # has the FixedBudget items added to the total also
|
||||
assert_equal 1650 - (225 + 250 + 250 + 100 + 50), @deliverable.profit_budget
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ class RedmineContracts::Hooks::ViewIssuesShowDetailsBottomTest < ActionControlle
|
||||
@controller ||= ApplicationController.new
|
||||
@controller.class.send(:include, ::Redmine::I18n)
|
||||
@controller.response ||= ActionController::TestResponse.new
|
||||
def @controller.api_request?
|
||||
false
|
||||
end
|
||||
# Hack to support render_on
|
||||
@controller.instance_variable_set('@template', template)
|
||||
@controller.response = response
|
||||
|
||||
123
test/unit/lib/redmine_contracts/patches/time_entry_patch_test.rb
Normal file
123
test/unit/lib/redmine_contracts/patches/time_entry_patch_test.rb
Normal file
@@ -0,0 +1,123 @@
|
||||
require File.dirname(__FILE__) + '/../../../../test_helper'
|
||||
|
||||
class RedmineContracts::Patches::TimeEntryTest < ActionController::TestCase
|
||||
|
||||
def setup
|
||||
@project = Project.generate!
|
||||
@contract = Contract.generate!(:project => @project, :status => 'open')
|
||||
@deliverable = FixedDeliverable.generate!(:contract => @contract, :status => 'open').reload
|
||||
@issue = Issue.generate_for_project!(@project, :deliverable => @deliverable).reload
|
||||
assert_equal @deliverable, @issue.deliverable
|
||||
@user = User.generate!
|
||||
@role = Role.generate!
|
||||
User.add_to_project(@user, @project, @role)
|
||||
@activity = TimeEntryActivity.generate!
|
||||
end
|
||||
|
||||
def create_time_entry
|
||||
@issue.reload
|
||||
@time_entry = TimeEntry.create(:issue => @issue,
|
||||
:project => @project,
|
||||
:spent_on => Date.today,
|
||||
:activity => @activity,
|
||||
:hours => 10,
|
||||
:user => @user)
|
||||
end
|
||||
|
||||
def assert_error_about_locked_deliverable(time_entry)
|
||||
assert_equal "Can't create a time entry on a locked deliverable", time_entry.errors.on_base
|
||||
end
|
||||
|
||||
def assert_error_about_locked_contract(time_entry)
|
||||
assert_equal "Can't create a time entry on a locked contract", time_entry.errors.on_base
|
||||
end
|
||||
|
||||
def assert_error_about_closed_deliverable(time_entry)
|
||||
assert_equal "Can't create a time entry on a closed deliverable", time_entry.errors.on_base
|
||||
end
|
||||
|
||||
def assert_error_about_closed_contract(time_entry)
|
||||
assert_equal "Can't create a time entry on a closed contract", time_entry.errors.on_base
|
||||
end
|
||||
|
||||
should "allow logging time to an issue on an open deliverable, open contract" do
|
||||
assert_difference("TimeEntry.count") { create_time_entry }
|
||||
end
|
||||
|
||||
should "block logging time to an issue on a locked deliverable, open contract" do
|
||||
assert @deliverable.lock!
|
||||
assert @deliverable.locked?
|
||||
|
||||
assert_no_difference("TimeEntry.count") { create_time_entry }
|
||||
assert_error_about_locked_deliverable(@time_entry)
|
||||
end
|
||||
|
||||
should "block logging time to an issue on an open deliverable, locked contract" do
|
||||
assert @contract.lock!
|
||||
assert @contract.locked?
|
||||
|
||||
assert_no_difference("TimeEntry.count") { create_time_entry }
|
||||
assert_error_about_locked_contract(@time_entry)
|
||||
end
|
||||
|
||||
should "block logging time to an issue on a locked deliverable, locked contract" do
|
||||
assert @deliverable.lock!
|
||||
assert @deliverable.locked?
|
||||
assert @contract.lock!
|
||||
assert @contract.locked?
|
||||
|
||||
assert_no_difference("TimeEntry.count") { create_time_entry }
|
||||
assert @time_entry.errors.on_base.include?("Can't create a time entry on a locked deliverable")
|
||||
assert @time_entry.errors.on_base.include?("Can't create a time entry on a locked contract")
|
||||
end
|
||||
|
||||
should "block logging time to an issue on a closed deliverable, open contract" do
|
||||
assert @deliverable.close!
|
||||
assert @deliverable.closed?
|
||||
|
||||
assert_no_difference("TimeEntry.count") { create_time_entry }
|
||||
assert_error_about_closed_deliverable(@time_entry)
|
||||
end
|
||||
|
||||
should "block logging time to an issue on a closed deliverable, locked contract" do
|
||||
assert @deliverable.close!
|
||||
assert @deliverable.closed?
|
||||
assert @contract.lock!
|
||||
assert @contract.locked?
|
||||
|
||||
assert_no_difference("TimeEntry.count") { create_time_entry }
|
||||
assert @time_entry.errors.on_base.include?("Can't create a time entry on a closed deliverable")
|
||||
assert @time_entry.errors.on_base.include?("Can't create a time entry on a locked contract")
|
||||
end
|
||||
|
||||
should "block logging time to an issue on an open deliverable, closed contract" do
|
||||
assert @contract.close!
|
||||
assert @contract.closed?
|
||||
|
||||
assert_no_difference("TimeEntry.count") { create_time_entry }
|
||||
assert_error_about_closed_contract(@time_entry)
|
||||
end
|
||||
|
||||
should "block logging time to an issue on a locked deliverable, closed contract" do
|
||||
assert @deliverable.lock!
|
||||
assert @deliverable.locked?
|
||||
assert @contract.close!
|
||||
assert @contract.closed?
|
||||
|
||||
assert_no_difference("TimeEntry.count") { create_time_entry }
|
||||
assert @time_entry.errors.on_base.include?("Can't create a time entry on a locked deliverable")
|
||||
assert @time_entry.errors.on_base.include?("Can't create a time entry on a closed contract")
|
||||
end
|
||||
|
||||
should "block logging time to an issue on a closed deliverable, closed contract" do
|
||||
assert @deliverable.close!
|
||||
assert @deliverable.closed?
|
||||
assert @contract.close!
|
||||
assert @contract.closed?
|
||||
|
||||
assert_no_difference("TimeEntry.count") { create_time_entry }
|
||||
assert @time_entry.errors.on_base.include?("Can't create a time entry on a closed deliverable")
|
||||
assert @time_entry.errors.on_base.include?("Can't create a time entry on a closed contract")
|
||||
end
|
||||
|
||||
end
|
||||
@@ -81,16 +81,66 @@ class RetainerDeliverableTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
context "#labor_budget_spent" do
|
||||
setup do
|
||||
@project = Project.generate!
|
||||
@contract = Contract.generate!(:billable_rate => 100, :project => @project)
|
||||
@deliverable = RetainerDeliverable.generate!(:start_date => '2010-01-01', :end_date => '2010-03-31', :contract => @contract)
|
||||
@deliverable.labor_budgets << LaborBudget.spawn(:budget => 100, :hours => 10)
|
||||
@deliverable.overhead_budgets << OverheadBudget.spawn(:budget => 100, :hours => 10)
|
||||
@deliverable.save!
|
||||
|
||||
@manager = User.generate!
|
||||
@role = Role.generate!
|
||||
User.add_to_project(@manager, @project, @role)
|
||||
|
||||
configure_overhead_plugin
|
||||
|
||||
@issue1 = Issue.generate_for_project!(@project)
|
||||
@time_entry1 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @billable_activity,
|
||||
:spent_on => Date.new(2010,1,2),
|
||||
:hours => 10,
|
||||
:user => @manager)
|
||||
@time_entry2 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @billable_activity,
|
||||
:spent_on => Date.new(2010,2,1),
|
||||
:hours => 20,
|
||||
:user => @manager)
|
||||
|
||||
@rate = Rate.generate!(:project => @project,
|
||||
:user => @manager,
|
||||
:date_in_effect => Date.new(2010,1,1),
|
||||
:amount => 100)
|
||||
|
||||
@deliverable.issues << @issue1
|
||||
|
||||
|
||||
end
|
||||
|
||||
context "with a empty period" do
|
||||
should "use all periods"
|
||||
should "use all periods" do
|
||||
assert_equal (10+20) * 100, @deliverable.labor_budget_spent(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period out of the retainer range" do
|
||||
should "use all periods"
|
||||
should "filter the records periods" do
|
||||
assert_equal 0, @deliverable.labor_budget_spent(Date.new(2011,1,1))
|
||||
end
|
||||
end
|
||||
|
||||
context "with an invalid period" do
|
||||
should "return 0" do
|
||||
assert_equal 0, @deliverable.labor_budget_spent('1')
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period in the retainer range" do
|
||||
should "filter the records"
|
||||
should "filter the records" do
|
||||
assert_equal 20 * 100, @deliverable.labor_budget_spent(Date.new(2010,2,1))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -126,8 +176,249 @@ class RetainerDeliverableTest < ActiveSupport::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
# context "#overhead_spent"
|
||||
|
||||
context "#labor_hours_spent_total" do
|
||||
setup do
|
||||
@project = Project.generate!
|
||||
@contract = Contract.generate!(:billable_rate => 100, :project => @project)
|
||||
@deliverable = RetainerDeliverable.generate!(:start_date => '2010-01-01', :end_date => '2010-03-31', :contract => @contract)
|
||||
|
||||
@manager = User.generate!
|
||||
@role = Role.generate!
|
||||
User.add_to_project(@manager, @project, @role)
|
||||
|
||||
configure_overhead_plugin
|
||||
|
||||
@issue1 = Issue.generate_for_project!(@project)
|
||||
@time_entry1 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @billable_activity,
|
||||
:spent_on => Date.new(2010,1,2),
|
||||
:hours => 10,
|
||||
:user => @manager)
|
||||
@time_entry2 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @billable_activity,
|
||||
:spent_on => Date.new(2010,2,1),
|
||||
:hours => 20,
|
||||
:user => @manager)
|
||||
|
||||
@rate = Rate.generate!(:project => @project,
|
||||
:user => @manager,
|
||||
:date_in_effect => Date.new(2010,1,1),
|
||||
:amount => 100)
|
||||
|
||||
@deliverable.issues << @issue1
|
||||
assert_equal 30, @deliverable.labor_hours_spent_total
|
||||
end
|
||||
|
||||
context "with a empty period" do
|
||||
should "use all periods" do
|
||||
assert_equal 30.0, @deliverable.labor_hours_spent_total(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period out of the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal 0, @deliverable.labor_hours_spent_total(Date.new(2011,1,1))
|
||||
end
|
||||
end
|
||||
|
||||
context "with an invalid period" do
|
||||
should "return 0" do
|
||||
assert_equal 0, @deliverable.labor_hours_spent_total('1')
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period in the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal 20.0, @deliverable.labor_hours_spent_total(Date.new(2010,2,1))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#overhead_hours_spent_total" do
|
||||
setup do
|
||||
@project = Project.generate!
|
||||
@contract = Contract.generate!(:billable_rate => 100, :project => @project)
|
||||
@deliverable = RetainerDeliverable.generate!(:start_date => '2010-01-01', :end_date => '2010-03-31', :contract => @contract)
|
||||
|
||||
@manager = User.generate!
|
||||
@role = Role.generate!
|
||||
User.add_to_project(@manager, @project, @role)
|
||||
|
||||
configure_overhead_plugin
|
||||
|
||||
@issue1 = Issue.generate_for_project!(@project)
|
||||
@time_entry1 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @non_billable_activity,
|
||||
:spent_on => Date.new(2010,1,2),
|
||||
:hours => 10,
|
||||
:user => @manager)
|
||||
@time_entry2 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @non_billable_activity,
|
||||
:spent_on => Date.new(2010,2,1),
|
||||
:hours => 20,
|
||||
:user => @manager)
|
||||
|
||||
@rate = Rate.generate!(:project => @project,
|
||||
:user => @manager,
|
||||
:date_in_effect => Date.new(2010,1,1),
|
||||
:amount => 100)
|
||||
|
||||
@deliverable.issues << @issue1
|
||||
assert_equal 30, @deliverable.overhead_hours_spent_total
|
||||
end
|
||||
|
||||
context "with a empty period" do
|
||||
should "use all periods" do
|
||||
assert_equal 30.0, @deliverable.overhead_hours_spent_total(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period out of the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal 0, @deliverable.overhead_hours_spent_total(Date.new(2011,1,1))
|
||||
end
|
||||
end
|
||||
|
||||
context "with an invalid period" do
|
||||
should "return 0" do
|
||||
assert_equal 0, @deliverable.overhead_hours_spent_total('1')
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period in the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal 20.0, @deliverable.overhead_hours_spent_total(Date.new(2010,2,1))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#hours_spent_total" do
|
||||
setup do
|
||||
@project = Project.generate!
|
||||
@contract = Contract.generate!(:billable_rate => 100, :project => @project)
|
||||
@deliverable = RetainerDeliverable.generate!(:start_date => '2010-01-01', :end_date => '2010-03-31', :contract => @contract)
|
||||
|
||||
@manager = User.generate!
|
||||
@role = Role.generate!
|
||||
User.add_to_project(@manager, @project, @role)
|
||||
|
||||
configure_overhead_plugin
|
||||
|
||||
@issue1 = Issue.generate_for_project!(@project)
|
||||
@time_entry1 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @billable_activity,
|
||||
:spent_on => Date.new(2010,1,2),
|
||||
:hours => 10,
|
||||
:user => @manager)
|
||||
@time_entry2 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @non_billable_activity,
|
||||
:spent_on => Date.new(2010,2,1),
|
||||
:hours => 20,
|
||||
:user => @manager)
|
||||
|
||||
@rate = Rate.generate!(:project => @project,
|
||||
:user => @manager,
|
||||
:date_in_effect => Date.new(2010,1,1),
|
||||
:amount => 100)
|
||||
|
||||
@deliverable.issues << @issue1
|
||||
assert_equal 30, @deliverable.hours_spent_total
|
||||
end
|
||||
|
||||
context "with a empty period" do
|
||||
should "use all periods" do
|
||||
assert_equal 30.0, @deliverable.hours_spent_total(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period out of the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal 0, @deliverable.hours_spent_total(Date.new(2011,1,1))
|
||||
end
|
||||
end
|
||||
|
||||
context "with an invalid period" do
|
||||
should "return 0" do
|
||||
assert_equal 0, @deliverable.hours_spent_total('1')
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period in the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal 20.0, @deliverable.hours_spent_total(Date.new(2010,2,1))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#overhead_spent" do
|
||||
setup do
|
||||
@project = Project.generate!
|
||||
@contract = Contract.generate!(:billable_rate => 100, :project => @project)
|
||||
@deliverable = RetainerDeliverable.generate!(:start_date => '2010-01-01', :end_date => '2010-03-31', :contract => @contract)
|
||||
@deliverable.overhead_budgets << OverheadBudget.spawn(:budget => 100, :hours => 10)
|
||||
@deliverable.save!
|
||||
|
||||
@manager = User.generate!
|
||||
@role = Role.generate!
|
||||
User.add_to_project(@manager, @project, @role)
|
||||
|
||||
configure_overhead_plugin
|
||||
|
||||
@issue1 = Issue.generate_for_project!(@project)
|
||||
@time_entry1 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @non_billable_activity,
|
||||
:spent_on => Date.new(2010,1,2),
|
||||
:hours => 10,
|
||||
:user => @manager)
|
||||
@time_entry2 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @non_billable_activity,
|
||||
:spent_on => Date.new(2010,2,1),
|
||||
:hours => 20,
|
||||
:user => @manager)
|
||||
|
||||
@rate = Rate.generate!(:project => @project,
|
||||
:user => @manager,
|
||||
:date_in_effect => Date.new(2010,1,1),
|
||||
:amount => 100)
|
||||
|
||||
@deliverable.issues << @issue1
|
||||
|
||||
|
||||
end
|
||||
|
||||
context "with a empty period" do
|
||||
should "use all periods" do
|
||||
assert_equal (10+20) * 100, @deliverable.overhead_spent(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period out of the retainer range" do
|
||||
should "filter the records periods" do
|
||||
assert_equal 0, @deliverable.overhead_spent(Date.new(2011,1,1))
|
||||
end
|
||||
end
|
||||
|
||||
context "with an invalid period" do
|
||||
should "return 0" do
|
||||
assert_equal 0, @deliverable.overhead_spent('1')
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period in the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal 20 * 100, @deliverable.overhead_spent(Date.new(2010,2,1))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#overhead_budget_total" do
|
||||
setup do
|
||||
@deliverable = RetainerDeliverable.generate!(:start_date => '2010-01-01', :end_date => '2010-03-31')
|
||||
@@ -160,7 +451,71 @@ class RetainerDeliverableTest < ActiveSupport::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
# context "#profit_left"
|
||||
# (Labor used * contract rate) - (labor used * time rate) - (overhead used * time rate)
|
||||
context "#profit_left" do
|
||||
setup do
|
||||
@project = Project.generate!
|
||||
@contract = Contract.generate!(:billable_rate => 200, :project => @project)
|
||||
@deliverable = RetainerDeliverable.generate!(:start_date => '2010-01-01', :end_date => '2010-03-31', :contract => @contract)
|
||||
@deliverable.labor_budgets << LaborBudget.spawn(:budget => 100, :hours => 10)
|
||||
@deliverable.overhead_budgets << OverheadBudget.spawn(:budget => 100, :hours => 10)
|
||||
@deliverable.save!
|
||||
|
||||
@manager = User.generate!
|
||||
@role = Role.generate!
|
||||
User.add_to_project(@manager, @project, @role)
|
||||
|
||||
configure_overhead_plugin
|
||||
|
||||
@issue1 = Issue.generate_for_project!(@project)
|
||||
@time_entry1 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @billable_activity,
|
||||
:spent_on => Date.new(2010,1,2),
|
||||
:hours => 10,
|
||||
:user => @manager)
|
||||
@time_entry2 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @non_billable_activity,
|
||||
:spent_on => Date.new(2010,2,1),
|
||||
:hours => 20,
|
||||
:user => @manager)
|
||||
|
||||
@rate = Rate.generate!(:project => @project,
|
||||
:user => @manager,
|
||||
:date_in_effect => Date.new(2010,1,1),
|
||||
:amount => 100)
|
||||
|
||||
@deliverable.issues << @issue1
|
||||
|
||||
|
||||
end
|
||||
|
||||
context "with a empty period" do
|
||||
should "use all periods" do
|
||||
assert_equal (10 * 200) - (10 * 100) - (20 * 100), @deliverable.profit_left(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period out of the retainer range" do
|
||||
should "filter the records periods" do
|
||||
assert_equal 0, @deliverable.profit_left(Date.new(2011,1,1))
|
||||
end
|
||||
end
|
||||
|
||||
context "with an invalid period" do
|
||||
should "return 0" do
|
||||
assert_equal 0, @deliverable.profit_left('1')
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period in the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal (0 * 200) - (0 * 100) - (20 * 100), @deliverable.profit_left(Date.new(2010,2,1))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
context "#profit_budget" do
|
||||
setup do
|
||||
@@ -200,8 +555,73 @@ class RetainerDeliverableTest < ActiveSupport::TestCase
|
||||
|
||||
end
|
||||
|
||||
# context "#total_spent"
|
||||
|
||||
context "#total_spent" do
|
||||
setup do
|
||||
@project = Project.generate!
|
||||
@contract = Contract.generate!(:billable_rate => 200, :project => @project)
|
||||
@deliverable = RetainerDeliverable.generate!(:start_date => '2010-01-01', :end_date => '2010-03-31', :contract => @contract)
|
||||
@deliverable.labor_budgets << LaborBudget.spawn(:budget => 100, :hours => 10)
|
||||
@deliverable.overhead_budgets << OverheadBudget.spawn(:budget => 100, :hours => 10)
|
||||
# Only paid fixed budgets counted
|
||||
@deliverable.fixed_budgets << FixedBudget.generate!(:budget => '$100', :markup => '50%') # $50 markup
|
||||
@deliverable.fixed_budgets << FixedBudget.generate!(:budget => '$100', :markup => '50%', :paid => true) # $50 markup
|
||||
|
||||
@deliverable.save!
|
||||
|
||||
@manager = User.generate!
|
||||
@role = Role.generate!
|
||||
User.add_to_project(@manager, @project, @role)
|
||||
|
||||
configure_overhead_plugin
|
||||
|
||||
@issue1 = Issue.generate_for_project!(@project)
|
||||
@time_entry1 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @billable_activity,
|
||||
:spent_on => Date.new(2010,1,2),
|
||||
:hours => 10,
|
||||
:user => @manager)
|
||||
@time_entry2 = TimeEntry.generate!(:issue => @issue1,
|
||||
:project => @project,
|
||||
:activity => @billable_activity,
|
||||
:spent_on => Date.new(2010,2,1),
|
||||
:hours => 20,
|
||||
:user => @manager)
|
||||
|
||||
@rate = Rate.generate!(:project => @project,
|
||||
:user => @manager,
|
||||
:date_in_effect => Date.new(2010,1,1),
|
||||
:amount => 100)
|
||||
|
||||
@deliverable.issues << @issue1
|
||||
end
|
||||
|
||||
context "with a empty period" do
|
||||
should "use all periods" do
|
||||
# (Labor used * contract rate) + fixed
|
||||
assert_equal ((10+20) * 200) + (150 * 3), @deliverable.total_spent(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period out of the retainer range" do
|
||||
should "filter the records periods" do
|
||||
assert_equal 0, @deliverable.total_spent(Date.new(2011,1,1))
|
||||
end
|
||||
end
|
||||
|
||||
context "with an invalid period" do
|
||||
should "return 0" do
|
||||
assert_equal 0, @deliverable.total_spent('1')
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period in the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal (20 * 200) + 150, @deliverable.total_spent(Date.new(2010,2,1))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#total" do
|
||||
setup do
|
||||
@contract = Contract.generate!(:billable_rate => 100)
|
||||
@@ -238,4 +658,152 @@ class RetainerDeliverableTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#fixed_budget_total" do
|
||||
setup do
|
||||
@contract = Contract.generate!(:billable_rate => 100)
|
||||
@deliverable = RetainerDeliverable.generate!(:start_date => '2010-01-01', :end_date => '2010-03-31', :contract => @contract)
|
||||
@deliverable.fixed_budgets << FixedBudget.spawn(:budget => 1000)
|
||||
@deliverable.fixed_budgets << FixedBudget.spawn(:budget => 2000)
|
||||
@deliverable.save!
|
||||
|
||||
assert_equal 3000 * 3, @deliverable.fixed_budget_total
|
||||
end
|
||||
|
||||
context "with a empty period" do
|
||||
should "use all periods" do
|
||||
assert_equal 9000, @deliverable.fixed_budget_total(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period out of the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal 0, @deliverable.fixed_budget_total(Date.new(2011,1,1))
|
||||
end
|
||||
end
|
||||
|
||||
context "with an invalid period" do
|
||||
should "return 0" do
|
||||
assert_equal 0, @deliverable.fixed_budget_total('1')
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period in the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal 3000, @deliverable.fixed_budget_total(Date.new(2010,2,1))
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#fixed_budget_total_spent" do
|
||||
setup do
|
||||
@contract = Contract.generate!(:billable_rate => 100)
|
||||
@deliverable = RetainerDeliverable.generate!(:start_date => '2010-01-01', :end_date => '2010-03-31', :contract => @contract)
|
||||
@deliverable.fixed_budgets << FixedBudget.spawn(:budget => 1000, :paid => true)
|
||||
@deliverable.fixed_budgets << FixedBudget.spawn(:budget => 2000)
|
||||
@deliverable.save!
|
||||
|
||||
assert_equal 1000 * 3, @deliverable.fixed_budget_total_spent
|
||||
end
|
||||
|
||||
context "with a empty period" do
|
||||
should "use all periods" do
|
||||
assert_equal 3000, @deliverable.fixed_budget_total_spent(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period out of the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal 0, @deliverable.fixed_budget_total_spent(Date.new(2011,1,1))
|
||||
end
|
||||
end
|
||||
|
||||
context "with an invalid period" do
|
||||
should "return 0" do
|
||||
assert_equal 0, @deliverable.fixed_budget_total_spent('1')
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period in the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal 1000, @deliverable.fixed_budget_total_spent(Date.new(2010,2,1))
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#fixed_markup_budget_total" do
|
||||
setup do
|
||||
@contract = Contract.generate!(:billable_rate => 100)
|
||||
@deliverable = RetainerDeliverable.generate!(:start_date => '2010-01-01', :end_date => '2010-03-31', :contract => @contract)
|
||||
@deliverable.fixed_budgets << FixedBudget.spawn(:budget => 1000, :markup => '50%')
|
||||
@deliverable.fixed_budgets << FixedBudget.spawn(:budget => 2000, :markup => '$1000')
|
||||
@deliverable.save!
|
||||
|
||||
assert_equal (500 + 1000) * 3, @deliverable.fixed_markup_budget_total
|
||||
end
|
||||
|
||||
context "with a empty period" do
|
||||
should "use all periods" do
|
||||
assert_equal 4500, @deliverable.fixed_markup_budget_total(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period out of the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal 0, @deliverable.fixed_markup_budget_total(Date.new(2011,1,1))
|
||||
end
|
||||
end
|
||||
|
||||
context "with an invalid period" do
|
||||
should "return 0" do
|
||||
assert_equal 0, @deliverable.fixed_markup_budget_total('1')
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period in the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal 1500, @deliverable.fixed_markup_budget_total(Date.new(2010,2,1))
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#fixed_markup_budget_total_spent" do
|
||||
setup do
|
||||
@contract = Contract.generate!(:billable_rate => 100)
|
||||
@deliverable = RetainerDeliverable.generate!(:start_date => '2010-01-01', :end_date => '2010-03-31', :contract => @contract)
|
||||
@deliverable.fixed_budgets << FixedBudget.spawn(:budget => 1000, :markup => '50%', :paid => true)
|
||||
@deliverable.fixed_budgets << FixedBudget.spawn(:budget => 2000, :markup => '$1000')
|
||||
@deliverable.save!
|
||||
|
||||
assert_equal (500) * 3, @deliverable.fixed_markup_budget_total_spent
|
||||
end
|
||||
|
||||
context "with a empty period" do
|
||||
should "use all periods" do
|
||||
assert_equal 1500, @deliverable.fixed_markup_budget_total_spent(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period out of the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal 0, @deliverable.fixed_markup_budget_total_spent(Date.new(2011,1,1))
|
||||
end
|
||||
end
|
||||
|
||||
context "with an invalid period" do
|
||||
should "return 0" do
|
||||
assert_equal 0, @deliverable.fixed_markup_budget_total_spent('1')
|
||||
end
|
||||
end
|
||||
|
||||
context "with a period in the retainer range" do
|
||||
should "filter the records" do
|
||||
assert_equal 500, @deliverable.fixed_markup_budget_total_spent(Date.new(2010,2,1))
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user