Author SHA1 Message Date
Eric Davis f269f4ae9b Test update for new field 2011-10-11 16:17:37 -07:00
Eric Davis 6fa8b3f2e7 [#6574] Generate new ids for fixed descriptions for the jsToolBar 2011-10-11 16:03:25 -07:00
Eric Davis 0d6ee39f42 [#6574] Add wiki toolbars to each new finance item 2011-10-11 15:53:56 -07:00
Eric Davis 3479ded9a6 [#6574] Allow adding and removing Fixed Budgets 2011-10-11 15:19:12 -07:00
Eric Davis 7e818d75a7 [#6574] Refactor: extract fixed budget form to partial 2011-10-11 14:50:31 -07:00
Eric Davis 2ef8cd9b4e [#6574] Allow creating and deleting multiple overhead budgets 2011-10-11 14:45:49 -07:00
Eric Davis ce6c89cfa7 [#6574] Extract method from addNewDeliverableLaborItem 2011-10-11 14:21:18 -07:00
Eric Davis ca84747cb9 [#6574] Refactor: extract overhead budget form to partial 2011-10-11 14:12:52 -07:00
Eric Davis ffaab6b92d [#6574] Fix test 2011-10-11 14:07:04 -07:00
Eric Davis d170eb1d18 [#6574] Handle a missing labor budget template 2011-10-11 13:55:27 -07:00
Eric Davis 7894bf204f [#6574] Store deleted finances in a hidden HTML table 2011-10-11 13:50:40 -07:00
Eric Davis 2653e80a5f [#6574] Confirm removal in JS and handle removing the last labor item 2011-10-11 13:49:49 -07:00
Eric Davis d633b32b30 [#6574] Allow removing labor budgets 2011-10-11 11:44:16 -07:00
Eric Davis 7d0abc641a [#6574[ Scope the Labor Budget Add button with a css class 2011-10-11 10:19:17 -07:00
Eric Davis 237b220795 [#6574] Add button for multiple Labor Budgets 2011-10-11 10:17:47 -07:00
Eric Davis f0445324ea Remove metrics task 2011-10-11 10:17:03 -07:00
Eric Davis 77b105530f Fix typo in the GC patch 2011-09-20 09:31:37 -07:00
Eric Davis b98703cddf [#6574] Move LaborBudget sort to Ruby so new records aren't excluded 2011-09-15 13:44:13 -07:00
Eric Davis 6bff73db93 [#6574] Add a Labor Budget JS template for adding more budget items
The JS template is generated by Rails for adding a new record
and gets appended to the finance section using
addNewDeliverableFinance() (JS).
2011-09-15 13:26:33 -07:00
Eric Davis a6582c9d01 Refactor: extract to common method 2011-09-15 10:58:27 -07:00
Eric Davis 33e07ee10b [#6574] Add a TimeEntryActivity to OverheadBudgets 2011-09-15 10:38:16 -07:00
Eric Davis b3b7fa11b0 Add tweaks for Ruby's GC to speed up tests 2011-09-14 19:02:50 -07:00
Eric Davis df61a1fe9e [#6574] Associate LaborBudgets with TimeEntryActivities 2011-09-14 18:39:49 -07:00
Eric Davis 38c22e2f92 Workaround a regression in ChiliProject 2.x with setting journal attributes
https://www.chiliproject.org/issues/622
2011-09-14 13:31:43 -07:00
Eric Davis 668b1e0991 [#6573] Add the total spent and total budget to the deliverable row 2011-09-14 13:13:46 -07:00
Eric Davis f5ca74e420 [#6441] Don't include a blank status as an option 2011-08-12 11:02:13 -07:00
Eric Davis 066f46b9bf Merge branch '6441-contract-and-deliverable-status' 2011-08-10 16:12:11 -07:00
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 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
89 changed files with 3883 additions and 589 deletions
+6
View File
@@ -0,0 +1,6 @@
gem 'formtastic', "0.9.10"
gem 'inherited_resources', '1.0.6'
group :test do
gem 'webrat'
end
+1 -1
View File
@@ -6,7 +6,7 @@ Dir[File.expand_path(File.dirname(__FILE__)) + "/lib/tasks/**/*.rake"].sort.each
RedminePluginSupport::Base.setup do |plugin|
plugin.project_name = 'redmine_contracts'
plugin.default_task = [:test]
plugin.tasks = [:db, :doc, :release, :clean, :test, :stats, :metrics]
plugin.tasks = [:db, :doc, :release, :clean, :test, :stats]
# TODO: gem not getting this automaticly
plugin.redmine_root = File.expand_path(File.dirname(__FILE__) + '/../../../')
end
+5 -1
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
+2 -1
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)
@@ -36,7 +37,7 @@ class DeliverablesController < InheritedResources::Base
end
def destroy
destroy!(:notice => l(:text_flash_deliverable_destroyed, :name => resource.title)) { contract_url(@project, @contract) }
destroy!(:notice => l(:text_flash_deliverable_deleted, :name => resource.title)) { contract_url(@project, @contract) }
end
protected
+57
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
+111 -33
View File
@@ -1,23 +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?
d.fixed_budgets.build if d.fixed_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>
@@ -31,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 = ''
@@ -60,33 +90,21 @@ 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 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, method_options={})
selected = method_options[:selected]
if selected && selected.is_a?(Date)
@@ -103,4 +121,64 @@ module ContractsHelper
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
+228 -27
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,105 +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_markup_spent,
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
deliverables.inject(0) {|total, deliverable| total += deliverable.fixed_budget_total }
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
deliverables.inject(0) {|total, deliverable| total += deliverable.fixed_markup_budget_total }
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
+219 -27
View File
@@ -9,23 +9,38 @@ class Deliverable < ActiveRecord::Base
has_many :labor_budgets
has_many :overhead_budgets
has_many :fixed_budgets
has_many :issues
has_many :issues, :dependent => :nullify
accepts_nested_attributes_for :labor_budgets
accepts_nested_attributes_for :overhead_budgets
accepts_nested_attributes_for :fixed_budgets
accepts_nested_attributes_for :labor_budgets, :allow_destroy => true
accepts_nested_attributes_for :overhead_budgets, :allow_destroy => true
accepts_nested_attributes_for :fixed_budgets, :allow_destroy => true
# 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
delegate :project, :to => :contract, :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
@@ -35,16 +50,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
@@ -53,54 +159,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)
fixed_budgets.sum(:budget)
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)
fixed_budgets.inject(0) {|total, fixed_budget| total += fixed_budget.markup_value }
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
@@ -116,6 +281,11 @@ class Deliverable < ActiveRecord::Base
end
end
# Required attribute for AAJ's JournalFormatter
def name
title
end
# Accessors from the budget plugin that need to be wrapped
def subject
warn "[DEPRECATION] Deliverable#subject is deprecated. Please use Deliverable#title instead."
@@ -149,12 +319,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
+21 -10
View File
@@ -7,13 +7,8 @@ class FixedBudget < 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
named_scope :by_period, lambda {|date|
if date
@@ -23,6 +18,8 @@ class FixedBudget < ActiveRecord::Base
end
}
named_scope :paid, {:conditions => {:paid => true}}
def markup_value
return 0 if budget.blank? || markup.blank?
@@ -30,20 +27,34 @@ class FixedBudget < ActiveRecord::Base
when percent_markup?
percent = markup.gsub('%','').to_f
return budget.to_f * (percent / 100)
when straight_markup?
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.match(/%/)
markup && markup.to_s.match(/%/)
end
def dollar_markup?
markup && markup.to_s.match(/[$,]+/)
end
def straight_markup?
markup && markup.match(/\$/)
markup && markup.to_s.match(/[\d.]/)
end
# Is this a blank budget item. Retainers will create blank ones when
+6 -11
View File
@@ -20,19 +20,14 @@ class FixedDeliverable < Deliverable
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) + fixed_budget_total(date) + fixed_markup_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(date=nil)
total_spent(date) - labor_budget_spent(date) - overhead_spent(date)
end
# Hardcoded value used as a wrapper for the old Budget plugin API.
#
# The Overhead plugin uses this in it's calcuations.
+29 -23
View File
@@ -16,28 +16,30 @@ class HourlyDeliverable < Deliverable
# 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
fixed_budget_amount = fixed_budget_total(date) + fixed_markup_budget_total(date)
return (contract.billable_rate * labor_budget_hours(date)) + fixed_budget_amount
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(date=nil)
return 0 if contract.nil?
return 0 if contract.billable_rate.blank?
return 0 unless self.issues.count > 0
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
@@ -49,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) + fixed_budget_total(date) + fixed_markup_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(date=nil)
total_spent(date) - labor_budget_spent(date) - overhead_spent(date)
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
+4 -8
View File
@@ -3,16 +3,12 @@ class LaborBudget < ActiveRecord::Base
# Associations
belongs_to :deliverable
belongs_to :time_entry_activity
# Validations
validates_presence_of :time_entry_activity_id
# 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
+4 -8
View File
@@ -3,16 +3,12 @@ class OverheadBudget < ActiveRecord::Base
# Associations
belongs_to :deliverable
belongs_to :time_entry_activity
# Validations
validates_presence_of :time_entry_activity_id
# 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
+142 -55
View File
@@ -47,6 +47,17 @@ class RetainerDeliverable < HourlyDeliverable
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 = []
@@ -92,7 +103,9 @@ class RetainerDeliverable < HourlyDeliverable
def labor_budget_total(date=nil)
case scope_date_status(date)
when :in
labor_budgets.sum(:budget, :conditions => {:year => date.year, :month => date.month})
memoize_by_date("@labor_budget_total", date) do
labor_budgets.sum(:budget, :conditions => {:year => date.year, :month => date.month})
end
when :out
0
else
@@ -103,7 +116,9 @@ class RetainerDeliverable < HourlyDeliverable
def overhead_budget_total(date=nil)
case scope_date_status(date)
when :in
overhead_budgets.sum(:budget, :conditions => {:year => date.year, :month => date.month})
memoize_by_date("@overhead_budget_total", date) do
overhead_budgets.sum(:budget, :conditions => {:year => date.year, :month => date.month})
end
when :out
0
else
@@ -114,7 +129,56 @@ class RetainerDeliverable < HourlyDeliverable
def labor_budget_hours(date=nil)
case scope_date_status(date)
when :in
labor_budgets.sum(:hours, :conditions => {:year => date.year, :month => date.month})
memoize_by_date("@labor_budget_hours", date) do
labor_budgets.sum(:hours, :conditions => {:year => date.year, :month => date.month})
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
@@ -125,7 +189,22 @@ class RetainerDeliverable < HourlyDeliverable
def fixed_budget_total(date=nil)
case scope_date_status(date)
when :in
fixed_budgets.sum(:budget, :conditions => {:year => date.year, :month => date.month})
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
@@ -136,32 +215,52 @@ class RetainerDeliverable < HourlyDeliverable
def fixed_markup_budget_total(date=nil)
case scope_date_status(date)
when :in
fixed_budgets.
all(:conditions => {:year => date.year, :month => date.month}).
inject(0) {|total, fixed_budget| total += fixed_budget.markup_value }
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
return 0 if contract.nil?
return 0 if contract.billable_rate.blank?
return 0 unless self.issues.count > 0
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)
issue_ids = self.issues.collect(&:id)
time_logs = time_entries_for_date_and_issue_ids(date, issue_ids)
hours = time_logs.inject(0) {|total, time_entry|
total += time_entry.hours if time_entry.billable?
total
}
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
when :out
0
else
@@ -211,9 +310,11 @@ class RetainerDeliverable < HourlyDeliverable
def labor_budget_spent(date=nil)
case scope_date_status(date)
when :in
labor_budget_spent_with_filter do
issue_ids = self.issues.collect(&:id)
time_entries_for_date_and_issue_ids(date, issue_ids)
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
@@ -225,9 +326,11 @@ class RetainerDeliverable < HourlyDeliverable
def overhead_spent(date=nil)
case scope_date_status(date)
when :in
overhead_spent_with_filter do
issue_ids = self.issues.collect(&:id)
time_entries_for_date_and_issue_ids(date, issue_ids)
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
@@ -283,42 +386,26 @@ 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
fixed_budgets.all.each do |fixed_budget|
# Purge un-dated budgets, should not be saved at all
fixed_budget.destroy unless fixed_budget.year.present?
fixed_budget.destroy unless fixed_budget.month.present?
# Purge budgets outside the new beginning/ending range
unless (beginning_date..ending_date).to_a.include?(Date.new(fixed_budget.year, fixed_budget.month, 1))
fixed_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
+7
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"]], :include_blank => false %>
<%= form.input :account_executive, :required => true, :collection => @project.users.sort %>
<li class="boolean optional">
<%= label('contract', 'executed') %>
+3 -8
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>
%>
+2
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 %>
+70 -9
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>
+2
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 %>
+48 -16
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'}) %>
<% 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>
@@ -91,6 +121,7 @@
<th colspan="2"><%= l(:field_labor) %></th>
<th colspan="2"><%= l(:field_overhead) %></th>
<th colspan="2"><%= l(:field_fixed) %></th>
<th colspan="2"><%= l(:field_total) %></th>
</tr>
</thead>
@@ -100,11 +131,12 @@
<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, deliverable.fixed_budget_total, :class => 'fixed') %>
<%= format_budget_for_deliverable(deliverable, deliverable.fixed_budget_total_spent, deliverable.fixed_budget_total, :class => 'fixed') %>
<%= format_budget_for_deliverable(deliverable, deliverable.total_spent, deliverable.total, :class => 'total') %>
<% end %>
<tr id="deliverable_details_<%= h(deliverable.id) %>" class="ign">
@@ -119,7 +151,7 @@
</div>
<% end %> <%# div_for(resource) %>
<p>TODO: Release 4, Milestones</p>
<p><%= release(4, "Milestones") %></p>
<!--
<div id="milestones">
@@ -194,6 +226,6 @@
</div>
-->
<p>TODO: Release 2+, history</p>
<p><%= release(3, "history") %></p>
<% html_title "#{l(:text_contracts)} - #{h(resource.name)}" %>
+95 -44
View File
@@ -1,13 +1,13 @@
<% period ||= nil %>
<% validated_period = validate_period(deliverable, period) %>
<td colspan="11" class="deliverable_details_outer_wrapper_<%= h(deliverable.id) %>">
<td colspan="13" class="deliverable_details_outer_wrapper_<%= h(deliverable.id) %>">
<div class="expanded">
<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, :selected => period) %>
<%= 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,50 +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(period))) %></td>
<td class="labor_budget_total"><%= h(format_value_field_for_contracts(deliverable.labor_budget_total(period))) %></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(period))) %></td>
<td class="overhead_budget_total"><%= h(format_value_field_for_contracts(deliverable.overhead_budget_total(period))) %></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>
<% deliverable.fixed_budgets.by_period(period).each do |fixed_budget| %>
<% 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">0</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">0</td>
<td class="fixed_markup_budget_total"><%= h(format_value_field_for_contracts(deliverable.fixed_markup_budget_total(period))) %></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">Profit</td>
<td><%= h(format_value_field_for_contracts(deliverable.profit_left(period))) %></td>
<td><%= h(format_value_field_for_contracts(deliverable.profit_budget(period))) %></td>
<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(period))) %></strong></td>
<td class="total"><strong><%= h(format_value_field_for_contracts(deliverable.total(period))) %></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>
@@ -104,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>
+29 -68
View File
@@ -1,64 +1,46 @@
<script type="text/html" id="labor-budget-template">
<% form.fields_for :labor_budgets, resource.labor_budgets.new(:id => 0) do |labor_budget| %>
<%= render :partial => 'labor_budget_form', :locals => {:labor_budget => labor_budget} %>
<% end %>
</script>
<script type="text/html" id="overhead-budget-template">
<% form.fields_for :overhead_budgets, resource.overhead_budgets.new(:id => 0) do |overhead_budget| %>
<%= render :partial => 'overhead_budget_form', :locals => {:overhead_budget => overhead_budget} %>
<% end %>
</script>
<script type="text/html" id="fixed-budget-template">
<% form.fields_for :fixed_budgets, resource.fixed_budgets.new(:id => 0) do |fixed_budget| %>
<%= render :partial => 'fixed_budget_form', :locals => {:fixed_budget => fixed_budget} %>
<% end %>
</script>
<% form.inputs :name => label, :class => "deliverable-finances #{fieldset_class}" do %>
<li style="display:none;" id='retainer-finances-message'>
<%= content_tag(:p, l(:text_retainer_monthly_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">
<% form.fields_for :labor_budgets, labor_budgets do |labor_budget| %>
<%= 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>
</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) %>
</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) %>
</td>
<%# TODO: Green Add button for multiple records %>
<td>
Todo: Add button (Release 3)
</td>
</tr>
<tbody>
<% form.fields_for :labor_budgets, labor_budgets.sort_by(&:id) do |labor_budget| %>
<%= render :partial => 'labor_budget_form', :locals => {:labor_budget => labor_budget} %>
<% end %>
</tbody>
</table>
</li>
<li class="numeric optional">
<%= content_tag(:label, l(:field_overhead)) %>
<table id="deliverable-overhead" class="deliverable_finance_table">
<% form.fields_for :overhead_budgets, overhead_budgets do |overhead_budget| %>
<%= 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>
</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) %>
</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) %>
</td>
<%# TODO: Green Add button for multiple records %>
<td>
Todo: Add button (Release 3)
</td>
</tr>
<tbody>
<% form.fields_for :overhead_budgets, overhead_budgets.sort_by(&:id) do |overhead_budget| %>
<%= render :partial => 'overhead_budget_form', :locals => {:overhead_budget => overhead_budget} %>
<% end %>
</tbody>
</table>
</li>
@@ -66,30 +48,9 @@
<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" style="display:none;"><%= fixed_budget.label(:title, l(:field_title))%></p>
<%= fixed_budget.text_field(:title) %>
<p class="inline-hints">
<%= fixed_budget.label(:budget, l(:field_budget), :style => 'display: none;')%> <%# Hidden label :| %>
<%= l(:text_dollar_sign) %>
</p>
<%= fixed_budget.text_field(:budget) %>
<p class="inline-hints">
<%= fixed_budget.label(:markup, l(:field_markup), :style => 'display: none;')%> <%# Hidden label :| %>
<%= l(:field_discount_hint) %>
</p>
<%= fixed_budget.text_field(:markup) %>
<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}") %>
<% form.fields_for :fixed_budgets, fixed_budgets.sort_by(&:id) do |fixed_budget| %>
<%= render :partial => 'fixed_budget_form', :locals => {:fixed_budget => fixed_budget} %>
<%= wikitoolbar_for "fixed-description#{fixed_budget.object.object_id}" %>
<p>Todo: Add button (Release 3)</p>
<% end %>
</div>
@@ -0,0 +1,33 @@
<div class="fixed-budget-form">
<%= fixed_budget.hidden_field(:id) unless fixed_budget.object.new_record? %>
<%= 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}") %>
<p class="inline-hints add-fixed">
<%= fixed_budget.hidden_field "_destroy", :class=> "delete-flag" %>
<%= link_to_function(l(:button_delete), 'deleteDeliverableFinance(this)', :class => 'delete icon icon-del') %>
<%= link_to_function(l(:button_add), 'addNewDeliverableFixedItem()', :class => 'add icon icon-add', :style => 'display:none;') %>
</p>
</div>
+20 -3
View File
@@ -1,6 +1,18 @@
<%= javascript_tag("var i18nStartDateEmpty = '#{l(:text_start_date_empty)}'") %>
<%= javascript_tag("var i18nEndDateEmpty = '#{l(:text_end_date_empty)}'") %>
<%= javascript_tag("var i18nChangedPeriodMessage = '#{l(:text_changed_period_message)}'") %>
<%= javascript_tag("var i18nAreYouSure = '#{l(:text_are_you_sure)}'") %>
<% 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), :id => 'deliverable-details' do %>
@@ -13,14 +25,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"]], :include_blank => false %>
<%= 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,7 +57,7 @@
<% 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), :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') } %>
<%= render :partial => 'finance_form', :locals => {:resource => resource, :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') %>
@@ -0,0 +1,23 @@
<tr class="labor-budget-form">
<td>
<%= labor_budget.hidden_field(:id) unless labor_budget.object.new_record? %>
<%= labor_budget.hidden_field(:year) %>
<%= labor_budget.hidden_field(:month) %>
<%= labor_budget.label(:time_entry_activity_id, :class => "hidden") %>
<%= labor_budget.select(:time_entry_activity_id, options_from_collection_for_select(@project.billable_activities, :id, :name, labor_budget.object.time_entry_activity_id), {:include_blank => false}, {:class => 'financial'}) %>
</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), :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), :class => 'financial') %>
</td>
<td class="add-labor">
<%= labor_budget.hidden_field "_destroy", :class=> "delete-flag" %>
<%= link_to_function(l(:button_delete), 'deleteDeliverableFinance(this)', :class => 'delete icon icon-del') %>
<%= link_to_function(l(:button_add), 'addNewDeliverableLaborItem()', :class => 'add icon icon-add', :style => 'display:none;') %>
</td>
</tr>
@@ -0,0 +1,22 @@
<tr class="overhead-budget-form">
<td>
<%= overhead_budget.hidden_field(:year) %>
<%= overhead_budget.hidden_field(:month) %>
<%= overhead_budget.label(:time_entry_activity_id, :class => "hidden") %>
<%= overhead_budget.select(:time_entry_activity_id, options_from_collection_for_select(@project.non_billable_activities, :id, :name, overhead_budget.object.time_entry_activity_id), {:include_blank => false}, {:class => 'financial'}) %>
</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),: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), :class => 'financial') %>
</td>
<td class="add-overhead">
<%= overhead_budget.hidden_field "_destroy", :class=> "delete-flag" %>
<%= link_to_function(l(:button_delete), 'deleteDeliverableFinance(this)', :class => 'delete icon icon-del') %>
<%= link_to_function(l(:button_add), 'addNewDeliverableOverheadItem()', :class => 'add icon icon-add', :style => 'display:none;') %>
</td>
</tr>
+2
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 %>
+2
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 %>
@@ -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 %>
+2 -5
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 %>
+1 -1
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 %>
Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

+91
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() {
@@ -65,6 +68,88 @@ jQuery(function($) {
}
},
showDeliverableAddButtons = function() {
var laborLinks = $('table.deliverable_finance_table .add-labor a.add')
if (laborLinks.length == 0) {
// No link, add a blank form
addNewDeliverableLaborItem();
} else {
laborLinks.hide().last().show();
}
var overheadLinks = $('table.deliverable_finance_table .add-overhead a.add')
if (overheadLinks.length == 0) {
// No link, add a blank form
addNewDeliverableOverheadItem();
} else {
overheadLinks.hide().last().show();
}
var fixedLinks = $('#deliverable-fixed .fixed-budget-form .add-fixed a.add')
if (fixedLinks.length == 0) {
// No link, add a blank form
addNewDeliverableFixedItem();
} else {
fixedLinks.hide().last().show();
}
},
addNewDeliverableLaborItem = function() {
addNewDeliverableFinance('#labor-budget-template',
'#deliverable-labor tbody',
$("tr.labor-budget-form").size(),
'<tr class="labor-budget-form">');
},
addNewDeliverableOverheadItem = function() {
addNewDeliverableFinance('#overhead-budget-template',
'#deliverable-overhead tbody',
$("tr.overhead-budget-form").size(),
'<tr class="overhead-budget-form">');
},
addNewDeliverableFixedItem = function() {
addNewDeliverableFinance('#fixed-budget-template',
'#deliverable-fixed.fixed-item-form',
$("div.fixed-budget-form").size(),
'<div class="fixed-budget-form">');
},
addNewDeliverableFinance = function(templateSelector, appendTemplateTo, countOfExisting, wrapperElement) {
var t = $(templateSelector).tmpl({});
if (t.length > 0) {
var recordLocation = countOfExisting + 1; // increments the Rails [n] placeholder
var newContent = t.html().replace(/\[0\]/g, "[" + recordLocation + "]");
// New ids for textareas for the jsToolBar to attach to
newContent = newContent.replace(/fixed-description\d*/g, "fixed-description" + Math.floor(Math.random() * 100000000))
var newItem = $(wrapperElement).html(newContent)
newItem.appendTo(appendTemplateTo);
newItem.find("textarea.wiki-edit").each(function () {
attachWikiToolbar(this.id);
});
showDeliverableAddButtons();
}
},
// Set the deleted flag for Rails and move it out of the row into
// a hidden table
deleteDeliverableFinance = function(deleteLink) {
if (confirm(i18nAreYouSure)) {
$(deleteLink).parent().find('.delete-flag').val('1')
if ($('#deleted-finances').length == 0) {
$(deleteLink).
closest("form").
append($("<table style='display:none' id='deleted-finances'></table>"));
}
$('#deleted-finances').append(
$(deleteLink). // <a>
parent(). // <td>
parent().hide()
); // <tr>
showDeliverableAddButtons();
}
},
showDeliverableAddButtons();
toggleSpecificDeliverableFields($('form.deliverable'));
$('select#deliverable_type').change(function() {
@@ -122,3 +207,9 @@ jQuery(function($) {
}
})(jQuery);
// Global functions outside of jQuery scoping
function attachWikiToolbar(id) {
var jsToolBarInstance = new jsToolBar($(id));
jsToolBarInstance.draw();
}
File diff suppressed because one or more lines are too long
+27 -6
View File
@@ -26,7 +26,9 @@ html>body .tabular li {overflow:hidden;}
.tabular li.hidden { height: 0; padding: 0; margin: 0; }
/* End tabular */
.hidden { display: none; }
a.contract-delete {color: red; }
.overage { color: #A40000; }
/* Positioning */
@@ -45,6 +47,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 +65,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 +133,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 +395,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;
@@ -503,3 +522,5 @@ padding-left: 0px; /* Don't pad submit buttons */
#edit_contract_1 li#contract_billable_rate_input div{
margin-bottom: 10px;
}
div.fixed-budget-form {padding: 5px 0;}
+33 -4
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,8 +49,11 @@ 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"
@@ -66,9 +84,20 @@ 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: "For a single month"
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_destroyed: "Deliverable: {{name}} was successfully destroyed."
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."
field_time_entry_activity: "Activity"
@@ -0,0 +1,10 @@
class AddPaidToFixedBudgets < ActiveRecord::Migration
def self.up
add_column :fixed_budgets, :paid, :boolean
add_index :fixed_budgets, :paid
end
def self.down
remove_column :fixed_budgets, :paid
end
end
+10
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
@@ -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
@@ -0,0 +1,10 @@
class AddTimeEntryActivityIdToLaborBudgets < ActiveRecord::Migration
def self.up
add_column :labor_budgets, :time_entry_activity_id, :integer
add_index :labor_budgets, :time_entry_activity_id
end
def self.down
remove_column :labor_budgets, :time_entry_activity_id
end
end
@@ -0,0 +1,10 @@
class AddTimeEntryActivityIdToOverheadBudgets < ActiveRecord::Migration
def self.up
add_column :overhead_budgets, :time_entry_activity_id, :integer
add_index :overhead_budgets, :time_entry_activity_id
end
def self.down
remove_column :overhead_budgets, :time_entry_activity_id
end
end
+40 -11
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'
+23
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
@@ -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,19 +89,22 @@ 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
if old_deliverable['total_hours'].present? || old_deliverable['cost_per_hour'].present?
deliverable.labor_budgets << LaborBudget.new(:deliverable => deliverable,
:budget => @total_cost,
:hours => old_deliverable['total_hours'])
:hours => old_deliverable['total_hours'],
:time_entry_activity => first_billable_activity(project))
end
else
@total_cost = 0
@@ -72,7 +112,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 +147,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 +162,29 @@ 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),
:time_entry_activity => first_non_billable_activity(deliverable.project))
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),
:time_entry_activity => first_non_billable_activity(deliverable.project))
end
end
@@ -140,21 +193,51 @@ 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
def self.first_billable_activity(project)
project.billable_activities.first || TimeEntryActivity.first
end
def self.first_non_billable_activity(project)
project.non_billable_activities.first || TimeEntryActivity.first
end
end
end
@@ -6,6 +6,8 @@ module RedmineContracts
# * :params => HTML parameters
#
def controller_issues_bulk_edit_before_save(context={})
return '' unless User.current.allowed_to?(:assign_deliverable_to_issue, context[:issue].project)
case
when context[:params][:deliverable_id].blank?
# Do nothing
@@ -4,14 +4,16 @@ module RedmineContracts
def controller_issues_edit_before_save(context={})
if context[:params] && context[:params][:issue]
if context[:params][:issue][:deliverable_id].present?
deliverable = Deliverable.find_by_id(context[:params][:issue][:deliverable_id])
if deliverable.contract.project == context[:issue].project
context[:issue].deliverable = deliverable
end
if User.current.allowed_to?(:assign_deliverable_to_issue, context[:issue].project)
if context[:params][:issue][:deliverable_id].present?
deliverable = Deliverable.find_by_id(context[:params][:issue][:deliverable_id])
if deliverable.contract.project == context[:issue].project
context[:issue].deliverable = deliverable
end
else
context[:issue].deliverable = nil
else
context[:issue].deliverable = nil
end
end
end
@@ -8,8 +8,15 @@ module RedmineContracts
# * :detail => Detail about the journal change
#
def helper_issues_show_detail_after_setting(context = { })
# This will be skipped in ChiliProject 2.x because
# acts_as_journalized overrides the prop_key with the label
# 'deliverable_id' becomes 'Deliverable' (i18n)
#
# register_on_journal_formatter is used for ChiliProject 2.x support
# 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?
@@ -9,6 +9,7 @@ module RedmineContracts
return stylesheet_link_tag("redmine_contracts", :plugin => "redmine_contracts", :media => "screen") +
javascript_include_tag('jquery-1.4.2.min.js', :plugin => 'redmine_contracts') +
javascript_include_tag('jquery.tmpl.min.js', :plugin => 'redmine_contracts') +
javascript_tag('jQuery.noConflict();') +
javascript_include_tag('contracts.js', :plugin => 'redmine_contracts')
+34 -1
View File
@@ -10,7 +10,40 @@ 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
# ChiliProject 2.x support for acts_as_journalized.
# Used to format the journal details on the Issue page
#
# See RedmineContracts::Hooks::HelperIssuesShowDetailAfterSettingHook
# for <2.x and Redmine version
#
# TODO: Will not support permissions or custom code in the formatter.
if Issue.respond_to?(:register_on_journal_formatter)
register_on_journal_formatter(:named_association, 'deliverable_id')
end
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
@@ -16,6 +16,27 @@ module RedmineContracts
end
module InstanceMethods
def billable_activities
activities_sorted_by_billable[:billable]
end
def non_billable_activities
activities_sorted_by_billable[:non_billable]
end
private
def activities_sorted_by_billable
split_activities = activities.partition do |activity|
activity.billable?
end
{
:billable => split_activities.first,
:non_billable => split_activities.second
}
end
end
end
end
@@ -12,6 +12,36 @@ module RedmineContracts
alias_method_chain :available_filters, :contract
alias_method_chain :sql_for_field, :contract
alias_method_chain :issues, :deliverable
alias_method_chain :issues, :contract
# Override Query#count_by_group to allow adding include options like
# Query#issues
# TODO: core bug: Query#issue_count_by_group doesn't allow setting
# options like Query#issue does.
def issue_count_by_group(options={})
includes = ([:status, :project] + (options[:include] || [])).uniq
r = nil
if grouped?
begin
# Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
r = Issue.count(:group => group_by_statement, :include => includes, :conditions => statement)
rescue ActiveRecord::RecordNotFound
r = {nil => issue_count}
end
c = group_by_column
if c.is_a?(QueryCustomFieldColumn)
r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
end
end
r
rescue ::ActiveRecord::StatementInvalid => e
raise ::Query::StatementInvalid.new(e.message)
end
alias_method_chain :issue_count_by_group, :contract
end
end
@@ -83,6 +113,37 @@ module RedmineContracts
end
end
# Add the deliverables into the includes
#
# Used with grouping
def issues_with_deliverable(options={})
options[:include] ||= []
options[:include] << :deliverable
issues_without_deliverable(options)
end
# Add the contracts into the includes
#
# Used with grouping
def issues_with_contract(options={})
options[:include] ||= []
options[:include] << {:deliverable => :contract}
issues_without_contract(options)
end
# Add the contracts into the includes
#
# Used with grouping
def issue_count_by_group_with_contract(options={})
options[:include] ||= []
options[:include] << {:deliverable => :contract}
issue_count_by_group_without_contract(options)
end
end
end
end
@@ -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
+4
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
+125 -50
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
+4 -4
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
+149 -1
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
+46 -6
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
+24 -1
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
+343 -13
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
@@ -225,14 +248,71 @@ class ContractsShowTest < ActionController::IntegrationTest
end
should "QUESTION: show the total fixed budget spent for a Deliverable"
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 the total budget for a Deliverable" do
@manager = User.generate!
@deliverable1 = FixedDeliverable.generate!(:contract => @contract, :manager => @manager, :total => '5404')
visit_contract_page(@contract)
assert_select "table#deliverables" do
assert_select "td.total.total-amount", :text => /5,404/
end
end
should "show the total spent for a Deliverable" do
configure_overhead_plugin
@manager = User.generate!
@contract.billable_rate = 200
assert @contract.save
@deliverable1 = HourlyDeliverable.generate!(:contract => @contract, :manager => @manager, :total => '1504')
@issue1 = Issue.generate_for_project!(@project)
@time_entry1 = TimeEntry.generate!(:issue => @issue1,
:project => @project,
:activity => @billable_activity,
:spent_on => Date.today,
:hours => 15,
: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
# Using the contract billable rate and not the user rate because it's income, not an expense
assert_select "td.total.spent-amount", :text => /3,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')
@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)
@@ -240,7 +320,7 @@ class ContractsShowTest < ActionController::IntegrationTest
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 => '0'
assert_select 'td.fixed_budget_spent', :text => '1,000'
assert_select 'td.fixed_budget_total', :text => '1,000'
end
@@ -259,18 +339,268 @@ class ContractsShowTest < ActionController::IntegrationTest
@deliverable1 = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
@budget1 = FixedBudget.generate!(:deliverable => @deliverable1, :title => 'Item 1', :budget => '$1,000', :markup => '$100')
@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 => '0'
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)
+30 -1
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'}
+49 -1
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
+272 -7
View File
@@ -9,12 +9,34 @@ 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)
configure_overhead_plugin
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,10 +47,10 @@ 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"
within("#deliverable-details") do
fill_in "Title", :with => 'An updated title'
select "Locked", :from => "Status"
check "Feature Sign Off"
check "Warranty Sign Off"
end
@@ -41,10 +63,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
@@ -55,10 +78,10 @@ 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"
within("#deliverable-details") do
fill_in "Title", :with => 'An updated title'
select "Locked", :from => "Status"
check "Feature Sign Off"
check "Warranty Sign Off"
end
@@ -82,6 +105,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
@@ -182,7 +206,7 @@ class DeliverablesEditTest < ActionController::IntegrationTest
else
assert_equal 100, fixed_budget.budget
assert_equal '0', fixed_budget.markup
assert_equal '$0.00', fixed_budget.markup
end
end
@@ -397,18 +421,23 @@ class DeliverablesEditTest < ActionController::IntegrationTest
# * labor hidden month
# * labor hours
# * labor amount
# * labor deleted (hidden)
# * overhead hidden year
# * overhead hidden month
# * overhead hours
# * overhead amount
# * overhead deleted (hidden)
# * fixed hidden year
# * fixed hidden month
# * fixed title
# * fixed budget
# * fixed markup
# * fixed paid checkbox
# * fixed paid hidden field
# * fixed deleted (hidden)
# * total (hidden)
assert_select ".date-2010-01" do
assert_select "input", :count => 14
assert_select "input", :count => 19
assert_select "textarea.wiki-edit", :count => 1 # Fixed description
end
@@ -450,4 +479,240 @@ class DeliverablesEditTest < ActionController::IntegrationTest
assert_equal 3, @retainer_deliverable.fixed_budgets.count
assert_equal [600, nil, nil], @retainer_deliverable.fixed_budgets.collect(&:budget)
end
context "locked deliverable" do
setup do
assert @fixed_deliverable.lock!
end
should "block edits to locked deliverables" do
visit_contract_page(@contract)
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
assert_response :success
within("#deliverable-details") do
fill_in "Title", :with => 'An updated title'
end
click_button "Save"
assert_response :success
assert_template 'deliverables/edit'
assert_not_equal "An updated title", @fixed_deliverable.reload.title
end
should "block edits to locked deliverables even when status changes to closed" do
visit_contract_page(@contract)
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
assert_response :success
within("#deliverable-details") do
fill_in "Title", :with => 'An updated title'
select "Closed", :from => "Status"
end
click_button "Save"
assert_response :success
assert_template 'deliverables/edit'
assert_not_equal "An updated title", @fixed_deliverable.reload.title
assert @fixed_deliverable.reload.locked?
end
should "be allowed to change the status on a locked deliverables to open" do
visit_contract_page(@contract)
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
assert_response :success
within("#deliverable-details") do
select "Open", :from => "Status"
end
click_button "Save"
assert_response :success
assert_template 'contracts/show'
assert @fixed_deliverable.reload.open?
end
should "be allowed to change the status on a locked deliverables to closed" do
visit_contract_page(@contract)
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
assert_response :success
within("#deliverable-details") do
select "Closed", :from => "Status"
end
click_button "Save"
assert_response :success
assert_template 'contracts/show'
assert @fixed_deliverable.reload.closed?
end
end
context "closed deliverable" do
setup do
assert @fixed_deliverable.close!
end
should "block edits to closed deliverables" do
visit_contract_page(@contract)
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
assert_response :success
within("#deliverable-details") do
fill_in "Title", :with => 'An updated title'
end
click_button "Save"
assert_response :success
assert_template 'deliverables/edit'
assert_not_equal "An updated title", @fixed_deliverable.reload.title
end
should "block edits to closed deliverables even when the status is changed to locked" do
visit_contract_page(@contract)
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
assert_response :success
within("#deliverable-details") do
fill_in "Title", :with => 'An updated title'
select "Locked", :from => "Status"
end
click_button "Save"
assert_response :success
assert_template 'deliverables/edit'
assert_not_equal "An updated title", @fixed_deliverable.reload.title
assert @fixed_deliverable.reload.closed?
end
should "be allowed to change the status on a closed deliverables to open" do
visit_contract_page(@contract)
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
assert_response :success
within("#deliverable-details") do
select "Open", :from => "Status"
end
click_button "Save"
assert_response :success
assert_template 'contracts/show'
assert @fixed_deliverable.reload.open?
end
should "be allowed to change the status on a closed deliverables to Locked" do
visit_contract_page(@contract)
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
assert_response :success
within("#deliverable-details") do
select "Locked", :from => "Status"
end
click_button "Save"
assert_response :success
assert_template 'contracts/show'
assert @fixed_deliverable.reload.locked?
end
end
context "a Deliverable on a locked Contract" do
setup do
assert @contract.lock!
end
should "be blocked from editing" do
visit_contract_page(@contract)
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
assert_response :success
within("#deliverable-details") do
fill_in "Title", :with => 'An updated title'
end
click_button "Save"
assert_response :success
assert_template 'deliverables/edit'
assert_not_equal "An updated title", @fixed_deliverable.reload.title
end
should "allow status only changes" do
visit_contract_page(@contract)
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
assert_response :success
within("#deliverable-details") do
select "Locked", :from => "Status"
end
click_button "Save"
assert_response :success
assert_template 'contracts/show'
assert @fixed_deliverable.reload.locked?
end
end
context "a Deliverable on a closed Contract" do
setup do
assert @contract.close!
end
should "be blocked from editing" do
visit_contract_page(@contract)
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
assert_response :success
within("#deliverable-details") do
fill_in "Title", :with => 'An updated title'
end
click_button "Save"
assert_response :success
assert_template 'deliverables/edit'
assert_not_equal "An updated title", @fixed_deliverable.reload.title
end
should "allow status only changes" do
visit_contract_page(@contract)
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
assert_response :success
within("#deliverable-details") do
select "Locked", :from => "Status"
end
click_button "Save"
assert_response :success
assert_template 'contracts/show'
assert @fixed_deliverable.reload.locked?
end
end
end
@@ -8,6 +8,9 @@ class DeliverablesListTest < ActionController::IntegrationTest
@contract = Contract.generate!(:project => @project)
@manager = User.generate!
@deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
@user = User.generate_user_with_permission_to_manage_budget(:project => @project)
login_as(@user.login, 'contracts')
end
should "redirect to the contract page" do
+35 -9
View File
@@ -6,9 +6,31 @@ 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)
configure_overhead_plugin
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
@@ -53,6 +75,7 @@ class DeliverablesNewTest < ActionController::IntegrationTest
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'
@@ -60,8 +83,6 @@ class DeliverablesNewTest < ActionController::IntegrationTest
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"
@@ -76,6 +97,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
@@ -90,6 +112,7 @@ class DeliverablesNewTest < ActionController::IntegrationTest
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'
@@ -97,10 +120,6 @@ class DeliverablesNewTest < ActionController::IntegrationTest
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
@@ -113,7 +132,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
@@ -128,6 +148,7 @@ class DeliverablesNewTest < ActionController::IntegrationTest
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'
@@ -156,7 +177,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
@@ -210,11 +232,13 @@ class DeliverablesNewTest < ActionController::IntegrationTest
end
within("#deliverable-labor") do
select @billable_activity.name, :from => 'Activity'
fill_in "hrs", :with => '20'
fill_in "$", :with => '$2,000'
end
within("#deliverable-overhead") do
select @non_billable_activity.name, :from => 'Activity'
fill_in "hrs", :with => '10'
fill_in "$", :with => '$1,000'
end
@@ -237,11 +261,13 @@ class DeliverablesNewTest < ActionController::IntegrationTest
@labor_budget = @deliverable.labor_budgets.first
assert_equal 20, @labor_budget.hours
assert_equal 2000.0, @labor_budget.budget
assert_equal @billable_activity, @labor_budget.time_entry_activity
assert_equal 1, @deliverable.overhead_budgets.count
@overhead_budget = @deliverable.overhead_budgets.first
assert_equal 10, @overhead_budget.hours
assert_equal 1000.0, @overhead_budget.budget
assert_equal @non_billable_activity, @overhead_budget.time_entry_activity
assert_equal 1, @deliverable.fixed_budgets.count
@fixed_budget = @deliverable.fixed_budgets.first
@@ -8,6 +8,9 @@ class DeliverablesShowTest < ActionController::IntegrationTest
@contract = Contract.generate!(:project => @project)
@manager = User.generate!
@deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
@user = User.generate_user_with_permission_to_manage_budget(:project => @project)
login_as(@user.login, 'contracts')
end
should "redirect to the contract page" do
@@ -1,17 +1,15 @@
require 'test_helper'
class DisabledContractsModuleTest < ActionController::IntegrationTest
def setup
@user = User.generate!(:login => 'existing', :password => 'existing', :password_confirmation => 'existing', :admin => true)
login_as
end
context "on a project with the Contracts module disabled" do
setup do
@project = Project.generate!
@project.enabled_modules.find_by_name('contracts').destroy
@project.reload
assert !@project.module_enabled?(:contracts), "Contracts enabled on project"
@user = User.generate_user_with_permission_to_manage_budget(:project => @project)
login_as(@user.login, 'contracts')
end
should "not show the menu item" do
+71
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
@@ -19,36 +19,73 @@ class RedmineContracts::Hooks::ControllerIssuesBulkEditBeforeSaveHookTest < Acti
@deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'The Title 1')
@deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'The Title 2')
@issue.deliverable = @deliverable1
@issue.save
login_as('manager', 'existing')
end
context "when saving multiple issues" do
setup do
visit_issue_bulk_edit_page(@issues)
context "with permission to Assign Deliverable to Issue" do
setup do
@role.permissions << :assign_deliverable_to_issue
@role.save!
visit_issue_bulk_edit_page(@issues)
end
should "allow clearing all of the deliverables" do
select "none", :from => "Deliverable"
click_button "Submit"
assert_response :success
@issues.each do |issue|
assert_equal nil, issue.reload.deliverable
end
end
should "allow assigning a deliverable" do
select @deliverable2.title, :from => "Deliverable"
click_button "Submit"
assert_response :success
@issues.each do |issue|
assert_equal @deliverable2, issue.reload.deliverable
end
end
end
should "allow clearing all of the deliverables" do
select "none", :from => "Deliverable"
click_button "Submit"
assert_response :success
@issues.each do |issue|
assert_equal nil, issue.reload.deliverable
context "with no permission to Assign Deliverable to Issue" do
setup do
@role.permissions.delete(:assign_deliverable_to_issue)
@role.save!
visit_issue_bulk_edit_page(@issues)
end
should "not allow clearing deliverables" do
# Simulate form post since the field is hidden
post "/issues/bulk_edit", :ids => @issues.collect(&:id), :deliverable_id => 'none'
assert_response :redirect
assert_equal @deliverable1, @issue.reload.deliverable
end
should "not allow assigning a deliverable" do
# Simulate form post since the field is hidden
post "/issues/bulk_edit", :ids => @issues.collect(&:id), :deliverable_id => @deliverable2.id
assert_response :redirect
@issues.each do |issue|
assert_not_equal @deliverable2, issue.reload.deliverable
end
end
end
should "allow assigning a deliverable" do
select @deliverable2.title, :from => "Deliverable"
click_button "Submit"
assert_response :success
@issues.each do |issue|
assert_equal @deliverable2, issue.reload.deliverable
end
end
end
end
end
@@ -11,8 +11,8 @@ class RedmineContracts::Hooks::ControllerIssuesEditBeforeSaveTest < ActionContro
@contract1 = Contract.generate!(:project => @project)
@contract2 = Contract.generate!(:project => @project)
@manager = User.generate!(:login => 'manager', :password => 'existing', :password_confirmation => 'existing', :admin => true)
@role = Role.generate!(:permissions => [:view_issues, :add_issues, :edit_issues])
@manager = User.generate!(:login => 'manager', :password => 'existing', :password_confirmation => 'existing', :admin => false)
@role = Role.generate!(:permissions => [:view_issues, :add_issues, :edit_issues, :assign_deliverable_to_issue])
User.add_to_project(@manager, @project, @role)
@deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'The Title for 1')
@deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'The Title for 2')
@@ -24,10 +24,10 @@ class RedmineContracts::Hooks::ControllerIssuesEditBeforeSaveTest < ActionContro
context "for a new issue" do
setup do
visit_project(@project)
click_link "New issue"
end
should "set the issue's deliverable" do
click_link "New issue"
fill_in "Subject", :with => 'Hook test'
select @deliverable2.title, :from => "Deliverable"
click_button "Create"
@@ -37,6 +37,84 @@ class RedmineContracts::Hooks::ControllerIssuesEditBeforeSaveTest < ActionContro
assert_equal @deliverable2, Issue.last.deliverable
end
should "not allow setting a locked Deliverable" do
assert @deliverable2.lock!
click_link "New issue"
fill_in "Subject", :with => 'Hook test'
select @deliverable2.title, :from => "Deliverable"
assert_no_difference("Issue.count") do
click_button "Create"
assert_response :success
end
assert_equal nil, Issue.last.deliverable
end
should "not allow setting a closed Deliverable" do
assert @deliverable2.close!
click_link "New issue"
fill_in "Subject", :with => 'Hook test'
select @deliverable2.title, :from => "Deliverable"
assert_no_difference("Issue.count") do
click_button "Create"
assert_response :success
end
assert_equal nil, Issue.last.deliverable
end
should "not allow setting a Deliverable on a locked Contract" do
assert @contract2.lock!
click_link "New issue"
fill_in "Subject", :with => 'Hook test'
select @deliverable2.title, :from => "Deliverable"
assert_no_difference("Issue.count") do
click_button "Create"
assert_response :success
end
assert_equal nil, Issue.last.deliverable
end
should "not allow setting a Deliverable on a closed Contract" do
assert @contract2.close!
click_link "New issue"
fill_in "Subject", :with => 'Hook test'
select @deliverable2.title, :from => "Deliverable"
assert_no_difference("Issue.count") do
click_button "Create"
assert_response :success
end
assert_equal nil, Issue.last.deliverable
end
context "with no permission to Assign Deliverable" do
should "not allow setting the Deliverable (force HTTP request)" do
@role.permissions.delete(:assign_deliverable_to_issue)
@role.save!
assert_difference('Issue.count', 1) do
post "/projects/#{@project.identifier}/issues", :issue => {:subject => 'Force', :deliverable_id => @deliverable1.id, :priority_id => IssuePriority.first.id}
end
assert_equal nil, Issue.last.deliverable
end
end
end
context "for an existing issue" do
@@ -54,6 +132,89 @@ class RedmineContracts::Hooks::ControllerIssuesEditBeforeSaveTest < ActionContro
assert_equal @deliverable2, @issue.deliverable
end
should "not allow updating to a locked deliverable" do
assert @deliverable2.lock!
select @deliverable2.title, :from => "Deliverable"
click_button "Submit"
assert_response :success
@issue.reload
assert_equal nil, @issue.deliverable
end
should "not allow updating to a closed deliverable" do
assert @deliverable2.close!
select @deliverable2.title, :from => "Deliverable"
click_button "Submit"
assert_response :success
@issue.reload
assert_equal nil, @issue.deliverable
end
should "not allow updating to a deliverable on a locked contract" do
assert @contract2.lock!
select @deliverable2.title, :from => "Deliverable"
click_button "Submit"
assert_response :success
@issue.reload
assert_equal nil, @issue.deliverable
end
should "not allow updating to a deliverable on a closed contract" do
assert @contract2.close!
select @deliverable2.title, :from => "Deliverable"
click_button "Submit"
assert_response :success
@issue.reload
assert_equal nil, @issue.deliverable
end
should "allow updating an issue, even if the deliverable is locked as long as the deliverable isn't changed" do
select @deliverable2.title, :from => "Deliverable"
click_button "Submit"
assert_response :success
@issue.reload
assert_equal @deliverable2, @issue.deliverable
# Now normal update after locking
assert @deliverable2.lock!
fill_in "Subject", :with => 'Change subject'
click_button "Submit"
assert_response :success
@issue.reload
assert_equal "Change subject", @issue.subject
assert_equal @deliverable2, @issue.deliverable
end
context "with no permission to Assign Deliverable" do
should "not allow setting the Deliverable (force HTTP request)" do
@role.permissions.delete(:assign_deliverable_to_issue)
@role.save!
assert_difference('Journal.count', 1) do
put "/issues/#{@issue.id}", :issue => {:subject => 'Force', :deliverable_id => @deliverable1.id}
end
assert_equal nil, @issue.reload.deliverable
end
end
end
end
end
@@ -19,15 +19,15 @@ class RedmineContracts::Hooks::HelperIssuesShowDetailAfterSettingHookTest < Acti
# Set first
@issue.init_journal(@manager)
@issue.deliverable = @deliverable1
@issue.save!
@issue.save! && @issue.reload
# Change
@issue.init_journal(@manager)
@issue.deliverable = @deliverable2
@issue.save!
@issue.save! && @issue.reload
# Unset
@issue.init_journal(@manager)
@issue.deliverable = nil
@issue.save!
@issue.save! && @issue.reload
login_as('manager', 'existing')
@@ -11,34 +11,91 @@ class RedmineContracts::Hooks::ViewIssuesBulkEditDetailsBottomHookTest < ActionC
@issue3 = Issue.generate_for_project!(@project)
@contract1 = Contract.generate!(:project => @project)
@contract2 = Contract.generate!(:project => @project)
@locked_contract = Contract.generate!(:project => @project)
@closed_contract = Contract.generate!(:project => @project)
@manager = User.generate!(:login => 'manager', :password => 'existing', :password_confirmation => 'existing')
@role = Role.generate!(:permissions => [:view_issues, :edit_issues])
User.add_to_project(@manager, @project, @role)
@deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'The Title')
@deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'The Title')
@locked_deliverable = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Locked Deliverable', :status => 'locked')
@closed_deliverable = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Closed Deliverable', :status => 'closed')
@deliverable1_on_locked_contract = FixedDeliverable.generate!(:contract => @locked_contract, :manager => @manager, :title => 'Deliverable 1 on locked contract')
@deliverable2_on_locked_contract = FixedDeliverable.generate!(:contract => @locked_contract, :manager => @manager, :title => 'Deliverable 2 on locked contract')
@deliverable_on_closed_contract = FixedDeliverable.generate!(:contract => @closed_contract, :manager => @manager, :title => 'Deliverable on closed contract')
@issue.deliverable = @deliverable1
# Set contract statuses now that all deliverables are created
assert @locked_contract.lock!
assert @closed_contract.close!
login_as('manager', 'existing')
end
context "with Contracts Enabled" do
setup do
visit_issue_bulk_edit_page([@issue, @issue2, @issue3])
end
should "render the a select field for the deliverables with all of the deliverables grouped by contract" do
context "with permission to Assign Deliverable" do
setup do
@role.permissions << :assign_deliverable_to_issue
@role.save!
visit_issue_bulk_edit_page([@issue, @issue2, @issue3])
end
assert_select "select#deliverable_id" do
assert_select "optgroup[label=?]", @contract1.name do
assert_select "option", :text => /#{@deliverable1.title}/
end
should "render the a select field for the deliverables with all of the deliverables grouped by contract" do
assert_select "optgroup[label=?]", @contract2.name do
assert_select "option", :text => /#{@deliverable2.title}/
assert_select "select#deliverable_id" do
assert_select "optgroup[label=?]", @contract1.name do
assert_select "option", :text => /#{@deliverable1.title}/
end
assert_select "optgroup[label=?]", @contract2.name do
assert_select "option", :text => /#{@deliverable2.title}/
end
end
end
should "disable all locked deliverables" do
assert_select "select#deliverable_id" do
assert_select "option[disabled=disabled]", :text => /#{@locked_deliverable.title}/
end
end
should "disable all deliverables on locked contracts" do
assert_select "select#deliverable_id" do
assert_select "optgroup[label=?]", @locked_contract.name do
assert_select "option[disabled=disabled]", :text => /#{@deliverable1_on_locked_contract.title}/
assert_select "option[disabled=disabled]", :text => /#{@deliverable2_on_locked_contract.title}/
end
end
end
should "not show closed deliverables" do
assert_select "select#deliverable_id" do
assert_select "option", :text => /#{@closed_deliverable.title}/, :count => 0
end
end
should "not show deliverables on closed contracts" do
assert_select "select#deliverable_id" do
assert_select "optgroup[label=?]", @closed_contract.name, :count => 0
assert_select "option", :text => /#{@deliverable_on_closed_contract.title}/, :count => 0
end
end
end
context "with no permission to Assign Deliverable" do
setup do
@role.permissions.delete(:assign_deliverable_to_issue)
@role.save!
visit_issue_bulk_edit_page([@issue, @issue2, @issue3])
end
should "not render the deliverable select field" do
assert_select 'select#deliverable_id', :count => 0
end
end
end
context "with Contracts Disabled" do
@@ -9,35 +9,135 @@ class RedmineContracts::Hooks::ViewIssuesFormDetailsBottomTest < ActionControlle
@issue = Issue.generate_for_project!(@project)
@contract1 = Contract.generate!(:project => @project)
@contract2 = Contract.generate!(:project => @project)
@locked_contract = Contract.generate!(:project => @project)
@closed_contract = Contract.generate!(:project => @project)
@manager = User.generate!(:login => 'manager', :password => 'existing', :password_confirmation => 'existing')
@role = Role.generate!(:permissions => [:view_issues, :edit_issues])
User.add_to_project(@manager, @project, @role)
@deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'The Title')
@deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'The Title')
@deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Deliverable1')
@deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'Deliverable2')
@locked_deliverable = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Locked Deliverable', :status => 'locked')
@closed_deliverable = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Closed Deliverable', :status => 'closed')
@deliverable1_on_locked_contract = FixedDeliverable.generate!(:contract => @locked_contract, :manager => @manager, :title => 'Deliverable 1 on locked contract')
@deliverable2_on_locked_contract = FixedDeliverable.generate!(:contract => @locked_contract, :manager => @manager, :title => 'Deliverable 2 on locked contract')
@deliverable_on_closed_contract = FixedDeliverable.generate!(:contract => @closed_contract, :manager => @manager, :title => 'Deliverable on closed contract')
@issue.deliverable = @deliverable1
assert @issue.save
# Set contract statuses now that all deliverables are created
assert @locked_contract.lock!
assert @closed_contract.close!
login_as('manager', 'existing')
end
context "with Contracts Enabled" do
setup do
visit_issue_page(@issue)
end
should "render the a select field for the deliverables with all of the deliverables grouped by contract" do
assert_select "select#issue_deliverable_id" do
assert_select "optgroup[label=?]", @contract1.name do
assert_select "option", :text => /#{@deliverable1.title}/
context "with permission to Assign Deliverable" do
setup do
@role.permissions << :assign_deliverable_to_issue
@role.save!
visit_issue_page(@issue)
end
should "render the a select field for the deliverables with all of the deliverables grouped by contract" do
assert_select "select#issue_deliverable_id" do
assert_select "optgroup[label=?]", @contract1.name do
assert_select "option", :text => /#{@deliverable1.title}/
end
assert_select "optgroup[label=?]", @contract2.name do
assert_select "option", :text => /#{@deliverable2.title}/
end
end
end
should "disable all locked deliverables" do
assert_select "select#issue_deliverable_id" do
assert_select "option[disabled=disabled]", :text => /#{@locked_deliverable.title}/
end
end
should "disable all deliverables on locked contracts" do
assert_select "select#issue_deliverable_id" do
assert_select "optgroup[label=?]", @locked_contract.name do
assert_select "option[disabled=disabled]", :text => /#{@deliverable1_on_locked_contract.title}/
assert_select "option[disabled=disabled]", :text => /#{@deliverable2_on_locked_contract.title}/
end
end
end
should "not show closed deliverables" do
assert_select "select#issue_deliverable_id" do
assert_select "option", :text => /#{@closed_deliverable.title}/, :count => 0
end
end
should "not show deliverables on closed contracts" do
assert_select "select#issue_deliverable_id" do
assert_select "optgroup[label=?]", @closed_contract.name, :count => 0
assert_select "option", :text => /#{@deliverable_on_closed_contract.title}/, :count => 0
end
end
should "show the assigned deliverable as an option, even if it's locked" do
@deliverable1.lock!
visit_issue_page(@issue)
assert_select "select#issue_deliverable_id" do
assert_select "option[disabled=disabled]", :text => /#{@deliverable1.title}/, :count => 0 # Not disabled
assert_select "option", :text => /#{@deliverable1.title}/, :count => 1 # Present
end
assert_select "optgroup[label=?]", @contract2.name do
assert_select "option", :text => /#{@deliverable2.title}/
end
should "show the assigned deliverable as an option, even if it's closed" do
@deliverable1.close!
visit_issue_page(@issue)
assert_select "select#issue_deliverable_id" do
assert_select "option[disabled=disabled]", :text => /#{@deliverable1.title}/, :count => 0 # Not disabled
assert_select "option", :text => /#{@deliverable1.title}/, :count => 1 # Present
end
end
should "show the assigned deliverable as an option, even if it's contract is locked" do
@contract1.lock!
visit_issue_page(@issue)
assert_select "select#issue_deliverable_id" do
assert_select "option[disabled=disabled]", :text => /#{@deliverable1.title}/, :count => 0 # Not disabled
assert_select "option", :text => /#{@deliverable1.title}/, :count => 1 # Present
end
end
should "show the assigned deliverable as an option, even if it's contract is closed" do
@contract1.close!
visit_issue_page(@issue)
assert_select "select#issue_deliverable_id" do
assert_select "option[disabled=disabled]", :text => /#{@deliverable1.title}/, :count => 0 # Not disabled
assert_select "option", :text => /#{@deliverable1.title}/, :count => 1 # Present
end
end
end
context "with no permission to Assign Deliverable" do
setup do
@role.permissions.delete(:assign_deliverable_to_issue)
@role.save!
visit_issue_page(@issue)
end
should "not render the deliverable select field" do
assert_select 'select#issue_deliverable_id', :count => 0
end
end
end
context "with Contracts Disabled" do
setup do
@project.enabled_modules.collect {|m| m.destroy if m.name == 'contracts' }
+5 -4
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
+28 -21
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
@@ -91,6 +92,12 @@ class ActionController::IntegrationTest
end
class ActiveSupport::TestCase
begin
require 'ruby_gc_test_patch'
include RubyGcTestPatch
rescue LoadError
end
def configure_overhead_plugin
@custom_field = TimeEntryActivityCustomField.generate!
Setting['plugin_redmine_overhead'] = {
+28 -2
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
@@ -268,7 +278,15 @@ class ContractTest < ActiveSupport::TestCase
end
context "#fixed_spent" do
should "QUESTION: how to compute the amount spent"
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
@@ -285,7 +303,15 @@ class ContractTest < ActiveSupport::TestCase
end
context "#fixed_markup_spent" do
should "QUESTION: how to compute the amount spent"
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
+48
View File
@@ -12,6 +12,13 @@ class DeliverableTest < ActiveSupport::TestCase
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
@@ -35,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
+36
View File
@@ -27,7 +27,43 @@ class FixedBudgetTest < ActiveSupport::TestCase
@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
+11
View File
@@ -65,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
@@ -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
+5 -2
View File
@@ -32,7 +32,7 @@ class HourlyDeliverableTest < ActiveSupport::TestCase
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)
@@ -46,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
+3
View File
@@ -2,6 +2,9 @@ require File.dirname(__FILE__) + '/../test_helper'
class LaborBudgetTest < ActiveSupport::TestCase
should_belong_to :deliverable
should_belong_to :time_entry_activity
should_validate_presence_of :time_entry_activity_id
context "#budget=" do
should "strip dollar signs when writing" do
@@ -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
@@ -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
+3
View File
@@ -2,6 +2,9 @@ require File.dirname(__FILE__) + '/../test_helper'
class OverheadBudgetTest < ActiveSupport::TestCase
should_belong_to :deliverable
should_belong_to :time_entry_activity
should_validate_presence_of :time_entry_activity_id
context "#budget=" do
should "strip dollar signs when writing" do
+261 -5
View File
@@ -176,6 +176,186 @@ class RetainerDeliverableTest < ActiveSupport::TestCase
end
end
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!
@@ -382,6 +562,10 @@ class RetainerDeliverableTest < ActiveSupport::TestCase
@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!
@@ -410,14 +594,12 @@ class RetainerDeliverableTest < ActiveSupport::TestCase
:amount => 100)
@deliverable.issues << @issue1
end
context "with a empty period" do
should "use all periods" do
# Labor used * contract rate
assert_equal (10+20) * 200, @deliverable.total_spent(nil)
# (Labor used * contract rate) + fixed
assert_equal ((10+20) * 200) + (150 * 3), @deliverable.total_spent(nil)
end
end
@@ -435,7 +617,7 @@ class RetainerDeliverableTest < ActiveSupport::TestCase
context "with a period in the retainer range" do
should "filter the records" do
assert_equal 20 * 200, @deliverable.total_spent(Date.new(2010,2,1))
assert_equal (20 * 200) + 150, @deliverable.total_spent(Date.new(2010,2,1))
end
end
end
@@ -514,6 +696,43 @@ class RetainerDeliverableTest < ActiveSupport::TestCase
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)
@@ -550,4 +769,41 @@ class RetainerDeliverableTest < ActiveSupport::TestCase
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