132 Commits

Author SHA1 Message Date
Eric Davis
5e50eb7558 [#6441] Block deleting deliverables on closed or locked contracts 2011-08-10 16:07:07 -07:00
Eric Davis
bf1339fc61 [#6441] Block creating deliverables on closed or locked contracts 2011-08-10 15:53:36 -07:00
Eric Davis
256325a39d [#6441] Refactor: extract method 2011-08-10 15:21:45 -07:00
Eric Davis
e0ecae8220 [#6441] Remove dead code 2011-08-10 15:18:37 -07:00
Eric Davis
b99f57f51c [#6441] Use a string so the flow is clearer 2011-08-10 15:16:54 -07:00
Eric Davis
edc9e84719 [#6441] Refactor: extract and move method 2011-08-10 15:15:26 -07:00
Eric Davis
2f77619fef [#6441] Refactor: remove unneeded contract arg 2011-08-10 15:13:01 -07:00
Eric Davis
6357fd83e4 [#6441] Refactor: extract method 2011-08-10 15:11:45 -07:00
Eric Davis
1b4191dc15 [#6441] Don't hide or disable the issue's current deliverable 2011-08-10 15:07:44 -07:00
Eric Davis
e019a9435e [#6441] Hide and disable unassignable deliverables from the issue
* Hide - Closed deliverables
* Hide - Closed contract deliverables
* Disable - Locked deliverables
* Disable - Locked contract deliverables
2011-08-10 13:10:05 -07:00
Eric Davis
f70ade5011 [#6441] Block assigning issues to deliverable on a locked or closed contract 2011-08-10 12:09:09 -07:00
Eric Davis
495407f127 [#6441] Block editing deliverables on locked and closed contracts 2011-08-10 11:56:08 -07:00
Eric Davis
be37e00f10 [#6441] Block editing locked and closed contracts 2011-08-10 11:31:54 -07:00
Eric Davis
f8fe23a68d [#6441] Refactor: extract method in the complex validation 2011-08-09 16:17:18 -07:00
Eric Davis
7c23725bbd [#6441] Refactor: merge i18n strings by passing in a reason and object 2011-08-09 15:59:24 -07:00
Eric Davis
2f79493cca [#6441] Block logging time on locked contracts and deliverables 2011-08-09 15:48:46 -07:00
Eric Davis
62cdfd35de [#6441] Refactor: only call validation on updates 2011-08-09 15:34:53 -07:00
Eric Davis
5c1e254181 [#6441] Remove test code that is now in a setup block 2011-08-09 15:24:58 -07:00
Eric Davis
0a916fea70 [#6441] Fix a Deliverable validation error when editing with a status change 2011-08-09 15:23:08 -07:00
Eric Davis
ac24ae08e6 [#6441] Block editing Locked or Closed Deliverables. 2011-08-08 15:52:13 -07:00
Eric Davis
51f62ca5a3 [#6441] Block assigning issues to a locked or closed Deliverable 2011-08-08 15:14:22 -07:00
Eric Davis
07c3c42ca8 [#6441] Refactor logic in view to helpers 2011-08-08 14:12:06 -07:00
Eric Davis
953587ce46 [#6441] Block logging time on closed contracts or deliverables 2011-08-08 13:46:28 -07:00
Eric Davis
5e2e309304 [#6441] Add a manual Deliverable#status 2011-08-08 12:08:46 -07:00
Eric Davis
9453b52c10 [#6441] Add a manual Contract#status 2011-08-08 11:48:35 -07:00
Eric Davis
fbd38b3678 Fix tests due to core changes 2011-08-08 10:32:04 -07:00
Eric Davis
4ade8de2c4 Add Gemfile for ChiliProject 2011-07-30 21:57:04 -07:00
Eric Davis
2153220675 Merge branch '5482-issue-grouping' 2011-02-22 10:58:39 -08:00
Eric Davis
496c6bada7 [#5482] Fix counts when grouping by contracts in the issues list 2011-02-22 10:58:19 -08:00
Eric Davis
3384dd3dbd Refactor: merge assertions in tests 2011-02-22 10:35:32 -08:00
Eric Davis
7d7d6da8b5 [#5482] Add tests for the grouping counts 2011-02-22 10:34:42 -08:00
Eric Davis
96fa509a1e [#5482] Change deliverable QueryColumn so the grouped counts are shown 2011-02-22 10:11:04 -08:00
Eric Davis
a0b92851a7 [#5482] Add issue grouping by contract 2011-02-22 09:41:15 -08:00
Eric Davis
d2aa87fdd2 [#5482] Add issue grouping by deliverable 2011-02-22 09:10:15 -08:00
Eric Davis
f514b15361 Added some comments to make the source easier to scan 2011-02-17 11:47:59 -08:00
Eric Davis
889a825426 [#5442] Add rdocs to Contract to make it easier to understand next time 2011-02-17 11:44:28 -08:00
Eric Davis
a92559c133 [#5442] Add contract totals for labor and overhead hours spent and budgeted 2011-02-17 11:30:40 -08:00
Eric Davis
af0eecb6dc [#5421] Only bulk assign deliverables to issues when the user has permission 2011-02-08 10:02:07 -08:00
Eric Davis
70fad4d8e2 [#5421] Only assign the issue deliverable when a user has permission. 2011-02-08 09:43:02 -08:00
Eric Davis
ff363f08f3 [#5421] Add Assign Deliverable to Issue permission for the issue bulk edit form 2011-02-08 09:29:54 -08:00
Eric Davis
40fc6e9751 [#5421] Add Assign Deliverable to Issue permission for the new/edit issue form 2011-02-08 09:25:37 -08:00
Eric Davis
75680e5a49 [#5417] Show the deliverable type on the edit form. 2011-02-04 09:45:27 -08:00
Eric Davis
39073905c3 Remove typo, extra quotes 2010-12-04 11:09:29 -08:00
Eric Davis
abeba76f40 [#4803] Reload the Journal details when showing the deliverable name
This caused an odd bug that occurred in Postgres. What would happen is that
the first call would find the Deliverable correctly and set the value/old_value
on the Journal.  But on the second call, the Journal would have the Deliverable
subjects in the value and old_value (as string), which would cause Postgres to
error since it won't allow searching id (int) with the subject (string).

MySQL and SQLite worked because the finder would run but return nil and the next
statements are guarded against nils.
2010-12-04 11:00:58 -08:00
Eric Davis
726c7a206f [#4690] Clear the issue/deliverable relation when deleting deliverables 2010-10-27 14:29:50 -07:00
Eric Davis
6f1ce637c5 Update the error template for the latest Redmine 2010-10-27 14:29:11 -07:00
Eric Davis
bef015f4ea [#4674] Round calculated Overhead Budget hours to two places 2010-10-21 11:10:13 -07:00
Eric Davis
b74ed932db [#4673] Handle edge cases parameters from rake 2010-10-21 11:09:48 -07:00
Eric Davis
2957d8e2cc [#4674] Add an option for an overhead rate which computes overhead hours 2010-10-21 10:30:28 -07:00
Eric Davis
a302c17b04 [#4673] Add an option to remove the converted data from deliverables 2010-10-21 09:50:46 -07:00
Eric Davis
1fafe8ea92 [#4653] Don't show a $ when a value field is blank 2010-10-14 15:35:33 -07:00
Eric Davis
2aea176df0 [#4650] Added labels to the Fixed Budget form 2010-10-14 13:05:02 -07:00
Eric Davis
5a0cb948c9 [#4649] Change the flash message to say deleted and not destroyed 2010-10-14 12:46:10 -07:00
Eric Davis
71c3d114ed [#4648] Remove the overage from Profit calculations 2010-10-14 12:40:29 -07:00
Eric Davis
46a5aaeabe [#4647] Split the hours and estimates in the Deliverable details row 2010-10-14 12:26:40 -07:00
Eric Davis
80d4c7bfa6 [#4510] Stretch the right rail of the Contract Finances.
Contributed by Philo Hermans
2010-10-14 11:36:17 -07:00
Eric Davis
3b5c008a65 [#4646] Memoize Deliverable subclasses 2010-10-13 15:18:52 -07:00
Eric Davis
650296bd52 [#4646] Memoize some calculated values in the Deliverable 2010-10-13 15:07:44 -07:00
Eric Davis
bf270623d2 [#4646] Memoize some calculated values in the Contract 2010-10-13 15:07:30 -07:00
Eric Davis
691adf521e Add a Rate so TimeEntry costs are measured 2010-10-06 17:36:06 -07:00
Eric Davis
a5552845a5 Update the performance test for the latest code 2010-10-06 13:29:16 -07:00
Eric Davis
ac2a9ddbf0 Remove HolyGrail, not worth it yet 2010-10-05 14:41:03 -07:00
Eric Davis
67469c9d94 Refactor: extract methods to utility method 2010-10-05 14:34:54 -07:00
Eric Davis
ed9f828efa Refactor: move helper methods to a formatter module 2010-10-05 14:19:49 -07:00
Eric Davis
0f9308267c [#4602] Show overage values in red 2010-10-05 14:07:57 -07:00
Eric Davis
d3f9255eef [#4601] Change Manage Budget to a givable permission 2010-10-05 11:30:19 -07:00
Eric Davis
5a277c723f [#4600] Replaced todo and later release notes with icons 2010-10-05 10:39:44 -07:00
Eric Davis
b0b434dbfb [#4570] Add the $ to the orphaned time message 2010-10-05 09:58:24 -07:00
Eric Davis
d6aee43b4b [#4570] Replace two queries with a single query 2010-09-29 13:58:11 -07:00
Eric Davis
39c04b9896 [#4570] Add an alert message about orphaned time on a project 2010-09-29 13:50:17 -07:00
Eric Davis
15d626b6d8 [#4552] Link the issue counts to filtered Issue lists 2010-09-29 12:06:53 -07:00
Eric Davis
20f89adf39 [#4552] Add issue counts to the Deliverable details 2010-09-29 11:48:21 -07:00
Eric Davis
632cff8f7f [#4552] Add the Total hours to the Deliverable details 2010-09-29 11:29:04 -07:00
Eric Davis
f37306aace [#4552] Add the Overhead hours spent to the Deliverable details 2010-09-29 10:54:33 -07:00
Eric Davis
38f2bcb17f [#4552] Add the Labor hours spent to the Deliverable details 2010-09-29 10:44:54 -07:00
Eric Davis
7a5599b4f9 [#4568] Change FixedDeliverable so it's markup is automatically paid 2010-09-29 10:18:25 -07:00
Eric Davis
0fd801c709 [#4568] Style lists in the deliverable details 2010-09-29 10:05:50 -07:00
Eric Davis
f3fc82d9dc [#4567] Replace inline errors with regular Redmine error messages 2010-09-29 09:50:18 -07:00
Eric Davis
2a60d283a4 Refactor: extract method 2010-09-23 13:38:04 -07:00
Eric Davis
cd382fd784 [#4558] Allow overriding the Contract account executive and Deliverable manager when converting 2010-09-23 13:30:44 -07:00
Eric Davis
1350a47593 [#4518] Fix the Deliverable finances field spacing 2010-09-23 11:42:21 -07:00
Eric Davis
56a38fbd93 Refactor: extract methods to a module and class helper, dollarized_attribute 2010-09-23 11:25:16 -07:00
Eric Davis
83e3023d85 [#4557] Hide markup displays when it's 0 2010-09-23 11:11:50 -07:00
Eric Davis
7bc31d6be0 Refactor: extract method 2010-09-23 11:00:05 -07:00
Eric Davis
d778201eff [#4556] Formatted the Fixed Budget values in the deliverable form 2010-09-23 10:51:56 -07:00
Eric Davis
893ad7943d [#4556] Use a CSS class for financial inputs on Deliverable form 2010-09-23 10:31:16 -07:00
Eric Davis
059b971e54 Refactor: pull up method 2010-09-23 10:22:46 -07:00
Eric Davis
b8d7c15b39 Refactor: pull up method 2010-09-23 10:18:44 -07:00
Eric Davis
ed9f73e8a0 [#4555] Include the spent FixedBudget in Hourly and Retainers total_spent 2010-09-23 10:14:12 -07:00
Eric Davis
0a80a54be3 [#4554] Fix the markup calculation to work with plain numerics 2010-09-23 09:40:47 -07:00
Eric Davis
8582ad0ecd [#4551] Use the old deliverable subject as part of the FixedBudget item 2010-09-22 13:03:14 -07:00
Eric Davis
c9861835e7 [#4551] Change the Budget conversion to create new FixedBudget items for old FixedDeliverables 2010-09-22 12:45:04 -07:00
Eric Davis
0ef87e7b9c [#4551] Change the Budget conversion to convert materials as FixedBudget items 2010-09-22 12:27:52 -07:00
Eric Davis
c6d43d9149 Fix the FixedBudget displays with a validated period 2010-09-22 12:05:58 -07:00
Eric Davis
7c6c2797c4 Merge branch '4477-fixed-expenses' 2010-09-22 11:54:41 -07:00
Eric Davis
967ef6bc7a [#4777] Add fixed budgets to the profit_left calculations 2010-09-22 11:53:55 -07:00
Eric Davis
4a7484e1ea [#4477] Added the amounts spent for FixedBudgets 2010-09-22 11:27:42 -07:00
Eric Davis
d4de581b3c [#4477] Added a paid field to track when a FixedBudget is spent 2010-09-22 10:49:36 -07:00
Eric Davis
f8270498f3 [#4477] Add a simple title/tooltip for the FixedBudget description 2010-09-17 15:37:30 -07:00
Eric Davis
2c04ef4f45 [#4477] Add FixedBudget items into the total and profit Deliverable calculations 2010-09-17 15:22:53 -07:00
Eric Davis
d78e7b1dc3 [#4477] Scope fixed budget items by period too (for Retainers) 2010-09-17 14:51:48 -07:00
Eric Davis
67dd75a980 [#4477] Skip blank FixedBudget items in the deliverable details 2010-09-17 14:23:29 -07:00
Eric Davis
55c9f93c6c [#4477] Change Retainers to scope #fixed_budget_total and #fixed_markup_budget_total by date 2010-09-17 14:15:08 -07:00
Eric Davis
081df7874d [#4477] Added the total FixedBudget markup to the deliverable details 2010-09-17 13:43:35 -07:00
Eric Davis
ed7536897e [#4477] Show each fixed budget item in the deliverable details 2010-09-17 13:38:44 -07:00
Eric Davis
86be29cecc [#4477] Show the total fixed budget in the deliverable row 2010-09-17 13:27:35 -07:00
Eric Davis
294c88f11d [#4477] Hook up FixedBudgets to Contract#fixed_budget and #fixed_markup_budget 2010-09-17 13:16:59 -07:00
Eric Davis
ce523df7eb [#4519] Add all contracts as child menu items 2010-09-17 11:33:31 -07:00
Eric Davis
306e4cfc80 [#4518] Align the total field with the other finance forms. 2010-09-17 10:56:52 -07:00
Eric Davis
19ed07b617 [#4517] Default the details row to All periods when it's out of the date range 2010-09-17 10:44:18 -07:00
Eric Davis
74d707a1a4 [#9370] Apply the wiki styles to the text fields on the Contract details page. 2010-09-14 15:22:59 -07:00
Eric Davis
ee282a42de [#4508] Update the Retainer single month label to work with larger content. 2010-09-14 15:03:19 -07:00
Eric Davis
30f90b7c8d [#4508] Update the Retainer single month label content. 2010-09-14 15:01:44 -07:00
Eric Davis
e1a7297312 [#4508] Reposition the Retainer single month label. 2010-09-14 14:44:55 -07:00
Eric Davis
4ba5ac7f8f [#4507] Round billable rate display to nearest dollar value. 2010-09-14 14:28:44 -07:00
Eric Davis
a7128fc15b [#4507] Use show_budget_field for billable rate so it's in 3 columns. (using nil hack) 2010-09-14 14:24:55 -07:00
Eric Davis
c9e699e9fa [#4507] Remove an extra td wrapper in the contract fields. 2010-09-14 14:17:25 -07:00
Eric Davis
893d83a03d [#4506] Format the executed contract as Yes or No. 2010-09-14 13:57:26 -07:00
Eric Davis
9b49959bcf [#4505] Fix the start/end calendar when editing an STI deliverable. 2010-09-14 13:51:01 -07:00
Eric Davis
edc8db7fe2 [#4477] Hook FixedBudgets into Retainer's periods. 2010-09-09 17:27:32 -07:00
Eric Davis
feee5e8b43 [#4477] Added FixedBudgets to the Deliverable finance form. 2010-09-09 17:06:35 -07:00
Eric Davis
07da5d98d8 [#4477] Added the FixedBudget markup calculations. 2010-09-09 16:12:48 -07:00
Eric Davis
7f7e5b3042 [#4477] Added model and migration for FixedBudget. 2010-09-09 15:57:58 -07:00
Eric Davis
d315252565 Refactor: use #scope_date_status. 2010-09-09 15:44:36 -07:00
Eric Davis
6b38dfca91 Refactor: use utility method. 2010-09-09 15:43:38 -07:00
Eric Davis
75046cfdd1 Refactor: extract utility method. 2010-09-09 15:42:12 -07:00
Eric Davis
12fffee314 Refactor: extract scope_date_status method to find how to scope the periods. 2010-09-09 15:39:25 -07:00
Eric Davis
362610dc77 [#4420] Updated the flash messages for Deliverables. 2010-09-09 15:12:01 -07:00
Eric Davis
1b08487a0d [#4420] Add a message for new Retainers that Finances are for a single month. 2010-09-09 15:04:26 -07:00
Eric Davis
5ef7d0271d [#4420] Display per period Retainer values on the Contract details page. 2010-09-09 14:56:20 -07:00
Eric Davis
0a1bb9169d [#4420] Hook up Retainer's total_spent and profit_left for periods. 2010-09-09 14:22:25 -07:00
Eric Davis
91d8c1818e [#4420] Hook up Retainer's overhead_spent and labor_budget_spent for periods 2010-09-09 13:50:50 -07:00
80 changed files with 4459 additions and 535 deletions

6
Gemfile Normal file
View File

@@ -0,0 +1,6 @@
gem 'formtastic', "0.9.10"
gem 'inherited_resources', '1.0.6'
group :test do
gem 'webrat'
end

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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') %>

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)}" %>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

BIN
assets/images/todo2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

BIN
assets/images/todo3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

BIN
assets/images/todo4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

BIN
assets/images/todo5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

View File

@@ -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();
}
}
},

View File

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

View File

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

View 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

View 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

View 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

View 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

View 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
View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View 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

View File

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

View File

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

View 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

View File

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