50 Commits

Author SHA1 Message Date
Eric Davis
437909b621 [#4272] Added a performance test for showing a contract to profile the code. 2010-08-16 12:26:28 -07:00
Eric Davis
720da2c554 [#4389] Added the Contract filter to the Time Entry report. 2010-08-16 09:54:09 -07:00
Eric Davis
620eaa262f [#4389] Add the Deliverable filter to the Time Entry report. 2010-08-16 09:39:27 -07:00
Eric Davis
a3e1aa277f [#4390] Added a Contract filter to the issues list. 2010-08-12 12:04:29 -07:00
Eric Davis
25436910fb [#4391] Add a Deliverable filter to the issues list. 2010-08-12 11:27:33 -07:00
Eric Davis
6924b32048 [#4390] Add the Contract name as an issue column. 2010-08-12 10:38:48 -07:00
Eric Davis
b83696f49a [#4391] Add the Deliverable title as an issue column. 2010-08-12 10:36:01 -07:00
Eric Davis
8906639273 [#4412] Added HTML page titles. 2010-08-12 10:25:14 -07:00
Eric Davis
dfcc31c83e [#4411] Format the Contracts list too. 2010-08-12 10:06:43 -07:00
Eric Davis
fa12fcd129 [#4411] Round currency to 0 decimal places on the Contracts page. 2010-08-12 10:03:37 -07:00
Eric Davis
82d46bd12a [#4411] Round time and currencies on the Deliverable form to 2 decimal places. 2010-08-12 09:49:45 -07:00
Eric Davis
a2b810a4d8 [#4410] Replace Payment Terms with an Enumeration
Instead of using a hard coded value for Payment Terms, they will use
Redmine's Enumeration table.  This provides an admin gui to managing
the value as well as ordering them.
2010-08-12 09:26:09 -07:00
Eric Davis
c17e8eaf1b [#4409] Old HourlyDeliverables should be converted over to FixedDeliverables. 2010-08-12 08:11:11 -07:00
Eric Davis
e734430deb [#4409] Convert contracts with the billable rate of 50. 2010-08-12 08:00:31 -07:00
Eric Davis
61270c60d5 [#4178] Fixed some more tests from the UI changes. 2010-08-09 12:47:45 -07:00
Eric Davis
cb8fd95f29 [#4178] Fixed a JS syntax error from the skinning. 2010-08-09 12:34:42 -07:00
Eric Davis
074d6a47e7 [#4178] Fixing integration tests based on the UI changes. 2010-08-09 12:29:04 -07:00
Eric Davis
de0752ed02 [#4178] CSS stripe the contracts list. 2010-08-09 12:00:38 -07:00
Eric Davis
ed1d04d18d [#4178] Fix a css rule that leaked into the table cell title. 2010-08-09 12:00:20 -07:00
Eric Davis
0cb4b1941a Removed extra TODO, migration process was decided. 2010-08-09 11:50:14 -07:00
Eric Davis
66f3a07e75 [#4178] TODO flagging sections of the views with their release version. 2010-08-09 11:49:42 -07:00
Eric Davis
4af0860542 [#4178] Zebra stripe deliverable table. 2010-08-09 11:34:36 -07:00
Eric Davis
118b0d7622 [#4178] Fix a width issue in the contract terms in Chrome webkit. 2010-08-09 11:28:59 -07:00
Eric Davis
74795a2788 Untabify 2010-08-09 11:16:15 -07:00
Eric Davis
a2dfc1c92b [#4178] Removed emtpy table cells that are for future features. 2010-08-09 11:16:00 -07:00
Eric Davis
67069dc60a [#4178] Tweak the Deliverable Finances so it's one table per child record. 2010-08-09 11:12:51 -07:00
Eric Davis
90d2aeccea [#4178] Added styles for the Deliverable form. 2010-08-09 11:07:55 -07:00
Eric Davis
78c43a8821 [#4178] Added Contract title bar to the deliverable form. 2010-08-09 10:37:00 -07:00
Eric Davis
73e92d2970 [#4178] Extract contract title bar to partial. 2010-08-09 10:33:29 -07:00
Eric Davis
c652360a6d [#4178] Fixed the Contract form's submit button layout. 2010-08-09 10:24:46 -07:00
Eric Davis
5958285410 [#4178] Updated the Contract form from the design. 2010-08-03 15:17:50 -07:00
Eric Davis
fcf012c828 [#4178] First port of the Contracts#show skin. 2010-08-03 14:58:08 -07:00
Eric Davis
fe2c058def [#4178] Skinned Contracts#list. 2010-08-03 13:23:51 -07:00
Eric Davis
ea00b89af5 [#4178] Adding JS and CSS assets. 2010-08-03 13:12:20 -07:00
Eric Davis
84c679dc67 [#4327] Show the deliverable's title in the Journal notes. 2010-08-03 12:02:37 -07:00
Eric Davis
848616853d [#4327] Save deliverable bulk edits. 2010-08-03 11:38:17 -07:00
Eric Davis
72aeaac59a [#4327] Added the Deliverable field to the bulk edit. 2010-08-03 11:24:23 -07:00
Eric Davis
6ad50c57b2 [#4184] Add a compatibility wrapper for Deliverable#due 2010-08-03 10:48:17 -07:00
Eric Davis
1d644178bd [#4184] Fix the issue mapper so each issue is migrated only once. 2010-08-03 10:17:57 -07:00
Eric Davis
389fbf3d13 [#4327] Save the deliverable changes to the issue using the hooks. 2010-08-02 16:36:14 -07:00
Eric Davis
729acf200d [#4327] Add a select field to selecting the deliverable for an issue. 2010-08-02 16:07:59 -07:00
Eric Davis
ad8dec8638 [#4327] Show the current deliverable on the issue details page. 2010-08-02 15:14:05 -07:00
Eric Davis
39edad3e07 Fix broken test. 2010-08-02 13:39:07 -07:00
Eric Davis
76a0573341 [#4281] Change the title for editing a contract. 2010-07-19 17:45:44 -07:00
Eric Davis
231066098b [#4281] Change the title for editing a Deliverable. 2010-07-19 17:44:41 -07:00
Eric Davis
7209871e87 [#4280] Add the $ as a hint to the Deliverable total form. 2010-07-19 17:39:35 -07:00
Eric Davis
1670fb5a73 [#4279] Move the total field to the bottom of the deliverable form. 2010-07-19 17:36:59 -07:00
Eric Davis
4100972d46 [#4278] Increase the size of the Contract's small wiki fields. 2010-07-19 17:33:55 -07:00
Eric Davis
e4ce899cd5 Sort deliverable managers in the select field. 2010-07-19 17:31:05 -07:00
Eric Davis
c8ede6a05d [#4277] Only show project members for account executives. 2010-07-19 17:30:51 -07:00
56 changed files with 2070 additions and 202 deletions

View File

@@ -6,12 +6,14 @@ module ContractsHelper
end
end
def format_budget_for_deliverable(deliverable, spent, total)
def format_budget_for_deliverable(deliverable, spent, total, options={})
extra_css_class = options[:class] || ''
if total > 0 || spent > 0
content_tag(:div, h(number_to_currency(spent, :unit => '')), :class => 'spent-amount') +
content_tag(:div, h(number_to_currency(total, :unit => '')), :class => 'total-amount')
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)
end
end
@@ -23,10 +25,12 @@ module ContractsHelper
# </p>
def show_field(object, field, options={}, &block)
html_options = options[:html_options] || {}
label = content_tag(:span, l(("field_" + field.to_s.gsub(/\_id$/, "")).to_sym) + ": ", :class => 'contract-details-label')
label_html_options = options[:label_html_options] || {}
label = content_tag(:strong, l(("field_" + field.to_s.gsub(/\_id$/, "")).to_sym) + ": ", :class => 'contract-details-label')
formatter = options[:format]
raw_content = options[:raw] || false
wrap_in_td = options[:wrap_in_td] || true
content = ''
@@ -39,10 +43,16 @@ module ContractsHelper
object.send(field)
end
end
if raw_content
field_content = content
else
field_content = h(content)
end
content_tag(:p,
label +
(raw_content ? content : h(content)),
content_tag(:tr,
content_tag(:td, label, label_html_options) +
(wrap_in_td ? content_tag(:td, field_content) : field_content),
html_options)
end
@@ -52,10 +62,10 @@ module ContractsHelper
spent_content = send(formatter, object.send(spent_field))
total_content = send(formatter, object.send(total_field))
show_field(object, spent_field, options.merge(:raw => true)) do
show_field(object, spent_field, options.merge(:raw => true, :wrap_in_td => false)) do
content_tag(:span, h(spent_content), :class => 'spent') +
content_tag(:span, h(total_content), :class => 'budget')
content_tag(:td, h(spent_content), :class => 'spent') +
content_tag(:td, h(total_content), :class => 'budget')
end
end
@@ -65,6 +75,14 @@ module ContractsHelper
def format_payment_terms(value)
return '' if value.blank?
return l(Contract::PaymentTerms[value.to_sym])
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
end

View File

@@ -1,9 +1,12 @@
class Contract < ActiveRecord::Base
unloadable
ViewPrecision = 0
# Associations
belongs_to :project
belongs_to :account_executive, :class_name => 'User', :foreign_key => 'account_executive_id'
belongs_to :payment_term, :class_name => "PaymentTerm", :foreign_key => "payment_term_id"
has_many :deliverables, :dependent => :destroy
# Validations
@@ -24,12 +27,14 @@ class Contract < ActiveRecord::Base
attr_accessible :billable_rate
attr_accessible :discount
attr_accessible :discount_note
attr_accessible :payment_terms
attr_accessible :payment_term_id
attr_accessible :client_ap_contact_information
attr_accessible :po_number
attr_accessible :client_point_of_contact
attr_accessible :details
named_scope :by_name, {:order => "#{Contract.table_name}.name ASC"}
[:status, :contract_type,
:fixed_spent, :fixed_budget,
:markup_spent, :markup_budget,
@@ -91,19 +96,6 @@ class Contract < ActiveRecord::Base
end
alias_method :profit_spent, :profit_left
PaymentTerms = {
:net_0 => :text_payment_terms_net_0,
:net_15 => :text_payment_terms_net_15,
:net_30 => :text_payment_terms_net_30,
:net_45 => :text_payment_terms_net_45
}
def payment_terms_for_select
PaymentTerms.collect {|value, label|
[l(label), value.to_s]
}
end
def after_initialize
self.executed = false unless self.executed.present?
end
@@ -114,6 +106,10 @@ class Contract < ActiveRecord::Base
end
end
def to_s
name
end
if Rails.env.test?
generator_for :name, :method => :next_name
generator_for :executed => true

View File

@@ -1,6 +1,8 @@
class Deliverable < ActiveRecord::Base
unloadable
ViewPrecision = 2
# Associations
belongs_to :contract
belongs_to :manager, :class_name => 'User', :foreign_key => 'manager_id'
@@ -18,10 +20,18 @@ class Deliverable < ActiveRecord::Base
# Accessors
delegate :name, :to => :contract, :prefix => true, :allow_nil => true
named_scope :by_title, {:order => "#{Deliverable.table_name}.title ASC"}
def short_type
''
end
def to_s
title
end
def to_underscore
self.class.to_s.underscore
end
@@ -53,6 +63,11 @@ class Deliverable < ActiveRecord::Base
issues.inject(0) {|total, issue| total += issue.spent_hours }
end
# Wrapper for the old Budget plugins' API
def due
end_date
end
if Rails.env.test?
generator_for :title, :method => :next_title

View File

@@ -0,0 +1,19 @@
class PaymentTerm < Enumeration
unloadable
has_many :contracts, :foreign_key => 'payment_term_id'
OptionName = :enumeration_payment_term
def option_name
OptionName
end
def objects_count
contracts.count
end
def transfer_relations(to)
contracts.update_all("payment_term_id = #{to.id}")
end
end

View File

@@ -1,24 +1,37 @@
<% form.inputs do %>
<div class="box tabular">
<% form.inputs :name => l(:text_general_legend) do %>
<%= form.input :name, :required => true %>
<%= form.input :account_executive, :required => true %>
<%= form.input :account_executive, :required => true, :collection => @project.users.sort %>
<li class="boolean optional">
<%= label('contract', 'executed') %>
<%= check_box 'contract', 'executed' %>
</li>
<%= form.input :start_date, :required => true, :as => :string, :input_html => {:size => 10}, :hint => calendar_for('contract_start_date') %>
<%= form.input :end_date, :required => true, :as => :string, :input_html => {:size => 10}, :hint => calendar_for('contract_end_date') %>
<%= form.input :billable_rate, :input_html => {:size => 20}, :hint => l(:field_billable_rate_hint) %>
<%= form.input :discount, :input_html => {:size => 20}, :hint => l(:field_discount_hint) %>
<%= form.input :discount_note, :input_html => {:class => 'wiki-edit', :rows => '3'} %>
<%= form.input :payment_terms, :as => :select, :collection => resource.payment_terms_for_select %>
<%= form.input :client_ap_contact_information, :input_html => {:class => 'wiki-edit', :rows => '3'} %>
<%= form.input :po_number %>
<%= form.input :client_point_of_contact, :input_html => {:class => 'wiki-edit', :rows => '3'} %>
<%= form.input :details, :input_html => {:class => 'wiki-edit'} %>
<% end %>
<% form.inputs :name => l(:text_budget_legend) do %>
<%= form.input :billable_rate, :input_html => {:size => 20}, :hint => l(:field_billable_rate_hint) %>
<%= form.input :discount, :input_html => {:size => 20}, :hint => l(:field_discount_hint) %>
<%= form.input :discount_note, :input_html => {:class => 'wiki-edit', :rows => '5'} %>
<% end %>
<% form.inputs :name => l(:text_account_legend) do %>
<%= form.input :payment_term %>
<%= form.input :po_number %>
<%= form.input :client_ap_contact_information, :input_html => {:class => 'wiki-edit', :rows => '5'} %>
<%= form.input :client_point_of_contact, :input_html => {:class => 'wiki-edit', :rows => '5'} %>
<% end %>
</div>
<% form.buttons do %>
<%= form.commit_button %>
<%= link_to(l(:button_cancel), cancel_path) %>
<ol>
<li class="commit">
<%= submit_tag(l(:text_save_contract)) %>
<%= link_to(l(:button_cancel), cancel_path) %>
</li>
</ol>
<% end %>
<%= wikitoolbar_for 'contract_discount_note' %>

View File

@@ -0,0 +1,25 @@
<div class="title-bar" id="upper-title-bar">
<%= 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 class="meta-sep"> | </span>
<span id="watcher"><a class="icon icon-fav-off" href="#TODO-release-3" onclick="">Watch (Release 3)</a></span>
<span class="meta-sep"> | </span>
<span><a href="#TODO">Copy (Release ?)</a></span>
<span class="meta-sep"> | </span>
<div class="update button-large">
<%= link_to(l(:button_update), edit_contract_path(@project, contract)) %>
</div>
</div>
</div>
<%# TODO: release 3/4, contract messages
<div class="error_msg">
<p>There is $243 worth of time clocked to issues that are not assigned to any deliverables. <span>Please update the orphaned issues.</span> </p>
</div>
%>

View File

@@ -4,8 +4,10 @@
</div>
<% end %>
<%= content_tag(:h2, h(resource.name)) %>
<%= content_tag(:h2, h(l(:text_edit_contract_name, :name => resource.name))) %>
<% 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 %>
<% html_title "#{l(:text_contracts)} - #{h(l(:text_edit_contract_name, :name => resource.name))}" %>

View File

@@ -1,26 +1,52 @@
<% if collection.empty? %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% else %>
<table class="list" cellspacing="0" border="0" cellpadding="0" id="contracts">
<thead>
<th><%= l(:field_id) %></th>
<th><%= l(:field_name) %></th>
<%# TODO: Status %>
<%# TODO: Type %>
<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| %>
<% content_tag_for(:tr, contract) 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="account-executive"><%= h contract.account_executive.name %></td>
<td class="total-budget"><%= h(number_to_currency(contract.total_budget)) %></td>
<td class="end-date"><%= h format_date(contract.end_date) %></td>
<% end %>
<% end %>
</tbody>
</table>
<% end %>
<div id="contract_list">
<div class="title-bar">
<div class="contextual">
<div class="new-issue button-large">
<%= link_to(l(:text_new_contract), new_contract_path) %>
</div>
</div>
<h2>Active Contacts</h2>
</div>
<% if collection.empty? %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% else %>
<table class="list" 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_account_executive_short) %></th>
<th><%= l(:field_total_budget) %></th>
<th><%= l(:field_end_date) %></th>
</thead>
<tbody>
<% collection.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="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>Inactive 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>
</div>
<% html_title "#{l(:text_contracts)}" %>

View File

@@ -3,3 +3,5 @@
<% semantic_form_for resource, :html => {:class => 'tabular'} do |form| %>
<%= render :partial => 'form', :object => form, :locals => {:cancel_path => contracts_path} %>
<% end %>
<% html_title "#{l(:text_new_contract)}" %>

View File

@@ -1,82 +1,296 @@
<%# TODO: Need skinning %>
<%= render :partial => 'title', :locals => {:contract => resource} %>
<% div_for(resource) do %>
<%= content_tag(:h2, h(resource.name)) %>
<%= 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'}) %>
<%= show_field(resource, :start_date, :format => :format_date, :html_options => {:class => 'contract-start-date'}) %>
<%= show_field(resource, :end_date, :format => :format_date, :html_options => {:class => 'contract-end-date'}) %>
<%= avatar(resource.account_executive, :size => 40) %>
<%= show_budget_field(resource, :labor_spent, :labor_budget, :html_options => {:class => 'contract-labor'}) %>
<%= show_budget_field(resource, :overhead_spent, :overhead_budget, :html_options => {:class => 'contract-overhead'}) %>
<%= show_budget_field(resource, :total_spent, :total_budget, :html_options => {:class => 'contract-total'}) %>
<div class="c_overview">
<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'}) %>
</table>
<table class="middle">
<%= show_field(resource, :start_date, :format => :format_date, :html_options => {:class => 'contract-start-date'}) %>
<%= show_field(resource, :end_date, :format => :format_date, :html_options => {:class => 'contract-end-date'}) %>
</table>
<table class="right">
<%= show_budget_field(resource, :labor_spent, :labor_budget, :format => :format_value_field_for_contracts, :html_options => {:class => 'contract-labor'}, :label_html_options => {:width => '49%'}) %>
<%= show_budget_field(resource, :overhead_spent, :overhead_budget, :format => :format_value_field_for_contracts, :html_options => {:class => 'contract-overhead'}) %>
<%= show_budget_field(resource, :total_spent, :total_budget, :format => :format_value_field_for_contracts, :html_options => {:class => 'contract-total'}) %>
</table>
<div class="clear"></div>
<a href="#" id="expand_terms" class="expand"><%= l(:text_terms_and_financial_overview) %></a>
<div id="contract-terms">
<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, :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, :po_number, :html_options => {:class => 'contract-po-number'}) %>
<%= show_field(resource, :details, :format => :textilizable, :raw => true, :html_options => {:class => 'contract-details'}) %>
</table>
</div>
<div id="contract-terms" style="border: 1px solid #000">
<%= show_field(resource, :client_point_of_contact, :format => :textilizable, :raw => true, :html_options => {:class => 'contract-client-point-of-contact'}) %>
<%= 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, :payment_terms, :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, :po_number, :html_options => {:class => 'contract-po-number'}) %>
<%= show_field(resource, :details, :format => :textilizable, :raw => true, :html_options => {:class => 'contract-details'}) %>
<table class="finance">
<%= show_budget_field(resource, :labor_spent, :labor_budget, :format => :format_value_field_for_contracts, :html_options => {:class => 'contract-labor'}, :label_html_options => {:width => '46%'}) %>
<%= show_budget_field(resource, :overhead_spent, :overhead_budget, :format => :format_value_field_for_contracts, :html_options => {:class => 'contract-overhead'}) %>
<%= show_budget_field(resource, :fixed_spent, :fixed_budget, :format => :format_value_field_for_contracts, :html_options => {:class => 'contract-fixed'}) %>
<%= show_budget_field(resource, :markup_spent, :markup_budget, :format => :format_value_field_for_contracts, :html_options => {:class => 'contract-markup'}) %>
<%= 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'}) %>
<div id="contract-finances" style="border: 1px solid #000">
<%= show_budget_field(resource, :labor_spent, :labor_budget, :html_options => {:class => 'contract-labor'}) %>
<%= show_budget_field(resource, :overhead_spent, :overhead_budget, :html_options => {:class => 'contract-overhead'}) %>
<%= show_budget_field(resource, :fixed_spent, :fixed_budget, :html_options => {:class => 'contract-fixed'}) %>
<%= show_budget_field(resource, :markup_spent, :markup_budget, :html_options => {:class => 'contract-markup'}) %>
<%= show_budget_field(resource, :profit_spent, :profit_budget, :html_options => {:class => 'contract-profit'}) %>
<%= show_budget_field(resource, :discount_spent, :discount_budget, :html_options => {:class => 'contract-discount'}) %>
<%= show_budget_field(resource, :total_spent, :total_budget, :html_options => {:class => 'contract-total'}) %>
<tr>
<td colspan="3">
<div class="hr"></div>
</td>
</tr>
<%= show_field(resource, :billable_rate, :format => :format_hourly_rate, :html_options => {:class => 'contract-billable-rate'}) %>
<%= show_budget_field(resource, :estimated_hour_spent, :estimated_hour_budget, :format => :l_hours, :html_options => {:class => 'contract-estimated-hour'}) %>
<%= 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, :estimated_hour_spent, :estimated_hour_budget, :format => :l_hours, :html_options => {:class => 'contract-estimated-hour total'}) %>
</table>
<div class="clear"></div>
</div>
</div>
<div class="clear"></div>
<div id="deliverables">
<%= 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') %>
</div>
</div>
<% end %>
<div class="clear"></div>
<%= link_to(l(:button_update), edit_contract_path(@project, resource)) %>
<% if resource.deliverables.empty? %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% else %>
<table width="100%" class="texpand" id="deliverables">
<thead>
<tr>
<th><%= l(:field_end_date) %></th>
<th>&nbsp;</th>
<th><%= l(:field_title) %></th>
<th><%= l(:field_status) %></th>
<th><%= l(:field_manager) %></th>
<th colspan="2"><%= l(:field_labor) %></th>
<th colspan="2"><%= l(:field_overhead) %></th>
<th colspan="2"><%= l(:field_fixed) %></th>
</tr>
</thead>
<%= link_to(l(:button_add_new), new_contract_deliverable_path(@project, resource), :id => 'new-deliverable') %>
<tbody>
<% resource.deliverables.each do |deliverable| %>
<% content_tag_for(:tr, deliverable, :class => 'click ' + cycle('even','')) do %>
<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="manager"><%= h deliverable.manager.try(:name) %></td>
<%= format_budget_for_deliverable(deliverable, deliverable.labor_budget_spent, deliverable.labor_budget_total, :class => 'labor') %>
<%= format_budget_for_deliverable(deliverable, deliverable.overhead_spent, deliverable.overhead_budget_total, :class => 'overhead') %>
<%= format_budget_for_deliverable(deliverable, 0, 0) %><%# TODO: Release 2, Fixed Budgets %>
<% end %>
<% if resource.deliverables.empty? %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% else %>
<table class="list" cellspacing="0" border="0" cellpadding="0" id="deliverables">
<thead>
<th><%= l(:field_end_date) %></th>
<th>&nbsp;</th>
<th><%= l(:field_title) %></th>
<%# TODO: Status %>
<th><%= l(:field_manager) %></th>
<th><%= l(:field_labor) %></th>
<th><%= l(:field_overhead) %></th>
<th><%= l(:field_fixed) %></th>
<th><%= l(:button_edit) %></th>
<th><%= l(:button_delete) %></th>
</thead>
<tbody>
<% resource.deliverables.each do |deliverable| %>
<% content_tag_for(:tr, deliverable) do %>
<td class="end-date"><%= h format_date(deliverable.end_date) %></td>
<td class="type"><%= h deliverable.short_type %></td>
<td class="title"><%= h deliverable.title %></td>
<%# TODO: Status %>
<td class="manager"><%= h deliverable.manager.try(:name) %></td>
<td class="labor">
<%= format_budget_for_deliverable(deliverable, deliverable.labor_budget_spent, deliverable.labor_budget_total) %>
</td>
<td class="overhead">
<%= format_budget_for_deliverable(deliverable, deliverable.overhead_spent, deliverable.overhead_budget_total) %>
</td>
<td class="fixed">---</td>
<td><%= link_to(l(:button_edit), edit_contract_deliverable_path(@project, resource, deliverable), :class => 'icon icon-edit') %></td>
<td><%= link_to(l(:button_delete), contract_deliverable_path(@project, resource, deliverable), :method => :delete, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %></td>
<tr id="deliverable_details_<%= h(deliverable.id) %>" class="ign">
<td colspan="11">
<div class="expanded">
<div class="info">
<div class="title">
<%= link_to(l(:button_edit), edit_contract_deliverable_path(@project, resource, deliverable), :class => 'icon icon-edit') %>
<%= link_to(l(:button_delete), contract_deliverable_path(@project, resource, deliverable), :method => :delete, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %>
</div>
<%= textilizable(deliverable, :notes) %>
<table>
<%= 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>
<% end %>
</div>
<%# TODO: Release 2, skinning port %>
<div class="finance">
<table>
<thead>
<tr>
<th> </th>
<th>Spent</th>
<th>Budget</th>
<th>Hours</th>
</tr>
</thead>
<tfoot>
<tr class="fill">
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tfoot>
<tbody>
<tr>
<td class="l"><a href="#"><strong>Labor</strong></a></td>
<td><%= h(format_value_field_for_contracts(deliverable.labor_budget_spent)) %></td>
<td><%= h(format_value_field_for_contracts(deliverable.labor_budget_total)) %></td>
<td> TODO: Release 2 / TODO hrs </td>
</tr>
<tr>
<td class="l"><a href="#"><strong>Overhead</strong></a></td>
<td><%= h(format_value_field_for_contracts(deliverable.overhead_spent)) %></td>
<td><%= h(format_value_field_for_contracts(deliverable.overhead_budget_total)) %></td>
<td> TODO: Release 2 / TODO hrs </td>
</tr>
<%# TODO: Release 2, Fixed %>
<%# TODO: Release 2, Markup %>
<tr>
<td class="l">Profit</td>
<td><%= h(format_value_field_for_contracts(deliverable.profit_left)) %></td>
<td><%= h(format_value_field_for_contracts(deliverable.profit_budget)) %></td>
<td></td>
</tr>
<tr class="total">
<td class="l"><strong>Total:</strong></td>
<td><strong><%= h(format_value_field_for_contracts(deliverable.total_spent)) %></strong></td>
<td><strong><%= h(format_value_field_for_contracts(deliverable.total)) %></strong></td>
<td><strong>TODO: Release 2</strong></td>
</tr>
</tbody>
</table>
</div>
<div class="issue_status">
<table>
<thead>
<tr>
<th colspan="2"><%= l(:label_issue_status_plural) %></th>
</tr>
</thead>
<%# TODO: Release 2 Issue status counters
<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>
%>
</table>
</div>
<div class="clear"></div>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
</tbody>
</table>
<% end %>
<div class="clear"></div>
</div>
<% end %> <%# div_for(resource) %>
<p>TODO: Release 4, Milestones</p>
<!--
<div id="milestones">
<h3>Milestones</h3>
<div class="options">
<input type="checkbox" checked="checked" /> Event
<input type="checkbox" checked="checked" /> Financial
<input type="checkbox" checked="checked" /> Dependency
</div>
<div class="actions">
<a href="#">iCal</a>
<a href="#">View All (37)</a>
<a href="#" id="new-deliverable">Add New</a>
</div>
<div class="clear"></div>
<table>
<thead>
<tr>
<th class="c">Done</th>
<th>Date</th>
<th>Title</th>
<th>Status</th>
<th>Type</th>
<th>Alert</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td class="c"><input type="checkbox" value="1" /></td>
<td>11/19/2009</td>
<td>ACME delivers branding guide</td>
<td><span class="red"><strong>Overdue</strong></span></td>
<td>Dependency</td>
<td>1 day</td>
</tr>
<tr class="even">
<td class="c"><input type="checkbox" value="1" /></td>
<td>12/01/2009</td>
<td>Send Invoice #2</td>
<td><span class="red"><strong>Overdue</strong></span></td>
<td>Financial</td>
<td>EoD</td>
</tr>
<tr class="odd highlight">
<td class="c"><input type="checkbox" value="1" /></td>
<td>12/11/2009</td>
<td>QA Signoff on 1.2</td>
<td>Pending</td>
<td>Event</td>
<td>3 days</td>
</tr>
<tr class="even">
<td class="c"><input type="checkbox" value="1" /></td>
<td>12/15/2009</td>
<td>Release 1.2</td>
<td>Pending</td>
<td>Event</td>
<td></td>
</tr>
<tr class="odd">
<td class="c"><input type="checkbox" value="1" /></td>
<td>12/15/2009</td>
<td>Deliverable: Load and Release Software</td>
<td>Pending</td>
<td>Event</td>
<td>BoD</td>
</tr>
</tbody>
</table>
</div>
-->
<p>TODO: Release 2+, history</p>
<% html_title "#{l(:text_contracts)} - #{h(resource.name)}" %>

View File

@@ -1,11 +1,12 @@
<% form.inputs do %>
<div class="box tabular">
<% form.inputs :name => l(:text_deliverable_details_legend) do %>
<%= form.input :title, :required => true %>
<% if resource.new_record? %>
<%= form.input(:type, :required => true, :as => :select, :collection => [["Fixed", "FixedDeliverable"],["Hourly", "HourlyDeliverable"]], :include_blank => false, :input_html => {:class => 'type'}) %>
<% else %>
<%= form.input :type, :as => :hidden, :class => 'type' %>
<% end %>
<%= form.input :manager, :required => true, :collection => @project.users %>
<%= form.input :manager, :required => true, :collection => @project.users.sort %>
<%= form.input :start_date, :as => :string, :input_html => {:size => 10}, :hint => calendar_for('deliverable_start_date') %>
<%= form.input :end_date, :as => :string, :input_html => {:size => 10}, :hint => calendar_for('deliverable_end_date') %>
<%= form.input :notes, :input_html => {:class => 'wiki-edit', :rows => '5'} %>
@@ -20,30 +21,77 @@
<%= check_box resource.class.to_s.underscore, 'warranty_sign_off' %>
</li>
<% end %>
<% end %>
<%= form.input :total, :input_html => {:size => 10}, :wrapper_html => {:class => 'deliverable_total_input'} %>
<% form.inputs :name => l(:text_deliverable_finances), :id => 'deliverable-finances' do %>
<% form.inputs :name => l(:text_deliverable_finances), :id => 'deliverable-finances' do %>
<% form.semantic_fields_for :labor_budgets do |labor_budget| %>
<% labor_budget.inputs :name => l(:field_labor), :id => 'deliverable-labor' do %>
<%= labor_budget.input :hours, :label => l(:text_short_hours), :input_html => {:size => 10} %>
<%= labor_budget.input :budget, :label => l(:text_dollar_sign), :input_html => {:size => 10} %>
<li class="numeric optional">
<%= content_tag(:label, l(:field_labor)) %>
<table id="deliverable-labor" class="deliverable_finance_table">
<% form.fields_for :labor_budgets do |labor_budget| %>
<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>
<% end %>
<% end %>
</table>
</li>
<% form.semantic_fields_for :overhead_budgets do |overhead_budget| %>
<% overhead_budget.inputs :name => l(:field_overhead), :id => 'deliverable-overhead' do %>
<%= overhead_budget.input :hours, :label => l(:text_short_hours), :input_html => {:size => 10} %>
<%= overhead_budget.input :budget, :label => l(:text_dollar_sign), :input_html => {:size => 10} %>
<li class="numeric optional">
<%= content_tag(:label, l(:field_overhead)) %>
<table id="deliverable-overhead" class="deliverable_finance_table">
<% form.fields_for :overhead_budgets do |overhead_budget| %>
<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>
<% end %>
<% end %>
<% end %>
</table>
</li>
<%= form.input :total, :input_html => {:size => 10}, :wrapper_html => {:class => 'deliverable_total_input'}, :hint => l(:text_dollar_sign) %>
<% end %>
</div>
<% form.buttons do %>
<%= form.commit_button :label => l(:button_save) %>
<%= link_to(l(:button_cancel), cancel_path) %>
<ol>
<li class="commit">
<%= submit_tag(l(:button_save)) %>
<%= link_to(l(:button_cancel), cancel_path) %>
</li>
</ol>
<% end %>
<%= wikitoolbar_for resource.to_underscore + '_notes' %>

View File

@@ -1,5 +1,9 @@
<%= content_tag(:h2, h(resource.title)) %>
<%= render :partial => 'contracts/title', :locals => {:contract => @contract} %>
<%= content_tag(:h2, h(l(:text_edit_deliverable_title, :title => resource.title))) %>
<% 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 %>
<% html_title "#{l(:field_deliverable_plural)} - #{h(l(:text_edit_deliverable_title, :title => resource.title))}" %>

View File

@@ -1,5 +1,9 @@
<%= render :partial => 'contracts/title', :locals => {:contract => @contract} %>
<%= content_tag(:h2, l(:text_new_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 %>
<% html_title "#{l(:field_deliverable_plural)} - #{l(:text_new_deliverable)}" %>

View File

@@ -0,0 +1,14 @@
<% if project.module_enabled?(:contracts) %>
<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)) %>
</p>
<% end %>

View File

@@ -0,0 +1,9 @@
<% if project.module_enabled?(:contracts) %>
<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}) %>
</p>
<% end %>

View File

@@ -0,0 +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>"
</tr>
<% end %>

View File

@@ -1,4 +1,27 @@
jQuery(function($) {
var right_align = $('#contract-terms .finance tr td:nth-child ~ td, .c_overview table.right tr td:nth-child ~ td, #deliverables table tr.click td:nth-child(5) ~ td, .deliverable_finance_table tr.aright td:nth-child ~ td');
if (right_align.length > 0) {
right_align.after().css("text-align", "right");
}
$("#deliverables table tbody tr td:contains('---')").css("text-align", "center");
$(".texpand").jExpand();
$(".texpand").find("tr.even").next('tr:first').addClass("even");
$(window).resize(function() {
});
$('#expand_terms').click( function(){
$(this).next().slideToggle();
$(this).toggleClass('alt');
});
toggleSpecificDeliverableFields = function(form) {
var deliverableType = form.find('.type').val();
@@ -17,3 +40,26 @@ jQuery(function($) {
toggleSpecificDeliverableFields($('form.deliverable'));
});
});
/* Jquery Table Expander Plugin */
(function($){
$.fn.jExpand = function(){
var element = this;
$(element).find("tr.ign").hide();
$(element).find("tr.click").click(function() {
$(this).toggleClass("noborder");
$(this).next("tr").toggle();
$(this).find('.arrow').toggleClass("alt");
var box_height = $(this).next().find('.expanded').height();
var table_height = $(this).next().find('.finance table').height();
if(box_height-table_height > 0){
$(this).next().find('.finance table .fill td').css("padding-top", box_height-table_height+2);
}
});
}
})(jQuery);

View File

@@ -12,6 +12,14 @@ html>body .tabular li {overflow:hidden;}
.tabular .inline-hints { color:#666; margin:0.5em 0 0 0; padding: 0px; display: inline; }
.deliverable_finance_table .inline-hints {
padding-top:4px;
}
.no-value {
text-align:center;
color:#999;
}
.tabular li.required label { color: #484848; }
.tabular li.required label span.required {color: #bb0000;}
@@ -21,11 +29,477 @@ html>body .tabular li {overflow:hidden;}
a.contract-delete {color: red; }
/* Positioning */
#deliverable-finances ol li { display: inline; }
#deliverable-finances ol li label { float: none; margin-left: 0; }
#deliverables td.labor, #deliverables td.overhead, #deliverables td.fixed { text-align: center; }
#deliverables td.labor div, #deliverables td.overhead div, #deliverables td.fixed div { text-align: right; display: inline; padding: 0 5px; width: 50%; }
.spent, .budget { text-align: right; padding: 0 5px; }
span.contract-details-label { font-weight: bold; }
/* Design */
.tabular li{
padding-left: 120px; /*width of left column containing the label elements*/
}
.tabular li.commit {
padding-left: 0px; /* Don't pad submit buttons */
}
#content .error_msg{
background: #ffd5d5;
color: #a40000;
border: 1px solid #a40000;
margin: 5px 0 10px 0;
}
#content .error_msg p{
padding: 6px;
}
#content .error_msg p span{
font-weight: bold;
text-decoration: underline;
}
.contract .gravatar{
float: left;
border: 1px solid #b4d1d9;
padding: 0;
margin-top: 10px;
margin-left: 6px;
margin-right: 25px;
}
#content .contract .fixed-item,
#content .contract .fixed-item-form {
border-top: 1px dotted #999;
padding-top: 15px;
}
#content .contract .fixed-items {
padding-bottom: 15px;
border-bottom: 1px dotted #999;
}
.add_fixed a{
display: block;
margin: 15px 0 15px 0;
color: #2A5685;
text-decoration:none;
}
.select_dropdown{
width: 190px;
}
#content .contract table td,
#content .contract table th{
padding: 2px 5px 2px 5px;
}
#deliverable-overhead ol li,
#deliverable-labor ol li{
padding-left: 155px
}
#content table.deliverable_finance_table{
width: 600px;
}
#content table.deliverable_finance_table select{
width: 145px;
}
#content table.deliverable_finance_table input{
text-align: right;
width: 65px;
}
#content table.deliverable_finance_table,
#content table.deliverable_finance_table tr,
#content table.deliverable_finance_table tr td{
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 */
#expand_terms{
margin: 10px 0 10px 0;
display: block;
}
#content #contract_list table{
margin-bottom: 20px;
}
#content #contract_list table tbody tr td.name a{
color: #2f8fa5;
text-decoration: none;
}
#content #contract_list table tbody tr td.name a:hover{
text-decoration: underline;
}
#content #contract_list table tbody tr td.priority_1{
background: #ffaaaa;
}
#content .c_overview{
background: #edf6fa;
border: 1px solid #dcebf0;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
padding: 6px;
margin-bottom: 20px;
}
#content .c_overview table{
width: 230px;
float: left;
border: 0;
}
#content .c_overview table.left{
float:left;
width:30%;
}
#content .c_overview table.middle{
margin-right:3%;
margin-left:3%;
width:auto;
}
#content .c_overview table.right{
width:30%;
max-width:300px;
float:right;
}
#content .c_overview table td{
border: 0;
text-align: left;
}
#content .c_overview table tr:hover td,
#content .c_overview table tr:hover,
#content .c_overview table td:hover{
background: transparent;
}
#content .c_overview a.expand{
background: url(../../../images/arrow_collapsed.png) no-repeat;
padding-left: 15px;
}
#content .c_overview a.expand.alt{
background: url(../../../images/arrow_expanded.png) no-repeat;
}
#deliverables .arrow{
background: url(../../../images/arrow_collapsed.png) no-repeat;
background-position: 0 6px;
}
#deliverables .arrow.alt{
background: url(../../../images/arrow_expanded.png) no-repeat;
background-position: 0 6px;
}
#deliverables .arrow span{
padding-left: 15px;
}
#contract-terms,
.deliverable_overview{
background: #f6fbfd;
border: 1px solid #d5dde1;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
border-radius: 2px;
display: none;
}
#contract-terms table .hr{
border-top: 1px solid #dee0e1;
}
#content #contract-terms table tr.padd td{
padding-top: 10px;
}
#contract-terms table.info{
width: 60%;
background: #f6fbfd;
}
#contract-terms table.finance{
width: 30%;
max-width:300px;
border-left: 1px solid #bed3dd;
float: right;
margin: 0;
background: #fafdfe;
}
#contract-terms table.info td,
#contract-terms table.finance td{
padding: 5px;
}
#contract-terms table tr.grey td,
#content .contract .grey{
color: #808080;
}
#contract-terms span.red,
#deliverables span.red,
#milestones span.red{
color: #990000;
}
#deliverables h3,
#milestones h3,
#content #history h3{
float: left;
font-size: 22px;
border: 0;
padding-left: 0;
}
#content #deliverables .actions,
#content #milestones .actions{
float: right;
margin-top: 20px;
}
#content #deliverables .actions a,
#content #milestones .actions a{
font-size: 13px;
color: #3e3e3e;
margin-left: 20px;
}
#deliverables table{
width: 100%;
margin-bottom: 20px;
}
#content #deliverables table thead tr th{
border-right: 1px solid #e6e6e6;
text-align: center;
font-weight: bold;
}
#content #deliverables table thead tr{
border-bottom: 1px solid #e6e6e6;
}
#content #deliverables table th,
#content #deliverables table td{
padding: 6px;
}
#content #deliverables table td, #content #deliverables table th,
#content #milestones table td, #content #milestones table th,
#content #contract_list table td, #content #contract_list table th {
border:1px solid #e6e6e6;
border-style: solid;
text-align:left;
position: static;
vertical-align:top;
z-index: 99;
}
#content #deliverables table tr.noborder td{
border-bottom: 0;
}
#content #deliverables table tr.ign td{
border-top: 0;
padding: 0;
}
#content #deliverables table tr.ign td table td{
padding: 2px 5px 2px 5px;
}
#content #deliverables table tr{
background-color: #edf6fa;
}
#content #deliverables table tr td.white{
background-color: #f6fafd;
}
#content #deliverables table tr.even,
#content #deliverables table tr.even:hover {
background-color:#dff3fc;
}
#content #deliverables table tr.ign tr{
background: transparent;
}
#content #deliverables div.title{
background: #FFF;
padding: 6px;
border-bottom: 1px solid #e4eff4;
}
#content #deliverables .title a {
margin-right: 6px;
}
#content #deliverables .title img{
vertical-align: middle;
}
#content #deliverables .click td{
cursor: pointer;
}
#content #deliverables .expanded{
border: 1px solid #c8d0d4;
margin: 5px;
margin-top: 0;
background: #f6fbfd;
}
#content #deliverables .expanded fieldset{
border: none;
}
#content #deliverables .info{
width: 45%;
float: left;
}
#content #deliverables .info p{
margin: 10px 0 10px 10px;
}
#content #deliverables .info form fieldset{
margin-left: 10px;
margin-bottom: 10px;
}
#content #deliverables .info table{
width: auto;
margin: 0 0 0 10px;
}
#content #deliverables .info table td{
border: 0;
padding: 2px 5px 2px 5px;
}
#content #deliverables .finance table{
border-left: 1px solid #e4eff4;
}
#content #deliverables .finance table td,
#content #deliverables .issue_status table td{
border: 0px;
border-right: 1px solid #e4eff4;
text-align: right;
padding: 2px 5px 2px 5px;
}
#content #deliverables .finance table td.l,
#content #deliverables .issue_status table td.l{
text-align: left;
}
#content #deliverables .finance{
float: left;
width: 39%;
}
#content #deliverables .finance table{
margin: 0;
padding: 0;
}
#content #deliverables .finance table thead th,
#content #deliverables .issue_status table thead th{
background: #FFF;
border: 0;
font-weight: bold;
border-bottom: 1px solid #e4eff4;
text-align: center;
padding: 6px;
}
#content #deliverables .finance table thead th{
border-right: 1px solid #e4eff4;
}
#content #deliverables .finance table tbody tr.total td,
#contract-terms table tbody tr.total td{
border-top: 1px solid #bed3dd;
}
#content #deliverables .issue_status{
float: left;
width: 16%;
}
#content #deliverables .issue_status table tbody tr td{
border-right: 0;
color: #277d94;
text-align: left;
}
#content #deliverables .issue_status table tbody tr td.number {
text-align: right;
}
#content #deliverables a,
#content .c_overview a{
color: #277d94;
}
#content #milestones .options{
float: left;
width: 300px;
margin-top: 20px;
margin-left: 10px;
}
#content #milestones table{
margin: 0;padding: 0;
}
#content #milestones table thead th{
font-weight: bold;
text-align: center;
}
#content #milestones table tr.odd{
background: #dff3fc;
}
#content #milestones table tr.even{
background: #edf6fa;
}
#content #milestones table tr.highlight td{
border-top: 2px solid #5fccf9;
}
#content #milestones .c{
text-align: center;
}
#edit_contract_1 input#contract_start_date,
#edit_contract_1 input#contract_end_date{
width: 85px;
}
#edit_contract_1 li#contract_billable_rate_input div{
margin-bottom: 10px;
}

View File

@@ -7,21 +7,19 @@ en:
field_account_executive_short: "Acct. Mgr."
field_end_date: "End Date"
text_new_contract: "New Contract"
text_edit_contract_name: "Edit {{name}}"
field_billable_rate: "Billable Rate"
field_billable_rate_hint: "$"
field_discount: "Discount"
field_discount_hint: "$, %"
field_discount_note: "Discounts Notes"
field_payment_terms: "Payment Terms"
field_payment_term: "Payment Terms"
field_client_ap_contact_information: "AP Contact Info"
field_po_number: "PO Number"
field_details: "Details"
text_payment_terms_net_0: "Net 0"
text_payment_terms_net_15: "Net 15"
text_payment_terms_net_30: "Net 30"
text_payment_terms_net_45: "Net 45"
button_add_new: Add New
text_new_deliverable: New Deliverable
text_edit_deliverable_title: "Edit {{title}}"
field_manager: Manager
field_labor: Labor
field_overhead: Overhead
@@ -50,3 +48,15 @@ en:
field_total_budget: "Total Budget"
field_total_spent: "Contract Total"
field_contract_type: "Type"
field_deliverable: "Deliverable"
field_deliverable_plural: "Deliverables"
text_terms_and_financial_overview: "Contract Terms and Financial Overview"
text_general_legend: "General"
text_budget_legend: "Budget"
text_account_legend: "Account Management"
text_deliverable_details_legend: "Deliverable Details"
text_save_contract: "Save Contract"
enumeration_payment_term: "Payment Terms"
field_deliverable_title: "Deliverable"
field_contract_name: "Contract"
field_contract: "Contract"

View File

@@ -0,0 +1,9 @@
class AddPaymentTermIdToContracts < ActiveRecord::Migration
def self.up
add_column :contracts, :payment_term_id, :integer
end
def self.down
remove_column :contracts, :payment_term_id
end
end

View File

@@ -0,0 +1,9 @@
class RemovePaymentTermsFromContracts < ActiveRecord::Migration
def self.up
remove_column :contracts, :payment_terms
end
def self.down
add_column :contracts, :payment_terms, :string
end
end

View File

@@ -0,0 +1,14 @@
class PopulatePaymentTerms < ActiveRecord::Migration
def self.up
[0, 15, 30, 45, 60, 75, 90].each_with_index do |days, index|
name = "Net #{days}"
unless PaymentTerm.find_by_name(name)
PaymentTerm.create!(:name => name, :position => index + 1)
end
end
end
def self.down
# No-op
end
end

22
init.rb
View File

@@ -44,7 +44,6 @@ end
require 'dispatcher'
Dispatcher.to_prepare :redmine_contracts do
gem 'inherited_resources', :version => '1.0.6'
require_dependency 'inherited_resources'
require_dependency 'inherited_resources/base'
@@ -58,11 +57,32 @@ Dispatcher.to_prepare :redmine_contracts do
Formtastic::SemanticFormBuilder.all_fields_required_by_default = false
Formtastic::SemanticFormBuilder.required_string = "<span class='required'> *</span>"
require_dependency 'payment_term' # Load so Enumeration will pick up the subclass in dev
require_dependency 'project'
Project.send(:include, RedmineContracts::Patches::ProjectPatch)
require_dependency 'issue'
Issue.send(:include, RedmineContracts::Patches::IssuePatch)
require_dependency 'query'
unless Query.included_modules.include? RedmineContracts::Patches::QueryPatch
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"))
end
unless Query.available_columns.collect(&:name).include?(:contract_name)
Query.add_available_column(QueryColumn.new(:contract_name, :sortable => "#{Contract.table_name}.name"))
end
end
require 'redmine_contracts/hooks/view_layouts_base_html_head_hook'
require 'redmine_contracts/hooks/view_issues_show_details_bottom_hook'
require 'redmine_contracts/hooks/view_issues_form_details_bottom_hook'
require 'redmine_contracts/hooks/controller_issues_edit_before_save_hook'
require 'redmine_contracts/hooks/view_issues_bulk_edit_details_bottom_hook'
require 'redmine_contracts/hooks/controller_issues_bulk_edit_before_save_hook'
require 'redmine_contracts/hooks/helper_issues_show_detail_after_setting_hook'
require 'redmine_contracts/hooks/controller_timelog_available_criterias_hook'

View File

@@ -1,5 +1,3 @@
require 'pp'
module RedmineContracts
class BudgetPluginInstalledError < StandardError; end
@@ -7,10 +5,6 @@ module RedmineContracts
set_table_name 'deliverables'
end
# TODO: decide how this is activated.
# Have the user dump yaml themselves? Or rename the tables and
# SQL select the data out into yaml?
# Deliverable.all.collect {|d| d.attributes}.to_yaml
class BudgetPluginMigration
@@data = nil
@@ -35,7 +29,9 @@ module RedmineContracts
end
# * old_data - YAML string of deliverables to migrate
def self.migrate(old_data)
def self.migrate(old_data, options={})
@contract_rate = options[:contract_rate] ? options[:contract_rate].to_f : 150.0
@@data = YAML.load(old_data)
# Map old deliverable ids to the new ones
@@ -49,31 +45,33 @@ module RedmineContracts
:end_date => old_deliverable['due'],
:notes => old_deliverable['description']
)
deliverable.type = old_deliverable['type']
# All deliverables are converted over to FixedDeliverable
deliverable.type = 'FixedDeliverable'
project = Project.find(old_deliverable['project_id'])
contract = Contract.find_by_project_id(project.id)
contract ||= create_new_contract(old_deliverable)
deliverable.contract = contract
deliverable.manager = project.users.first
deliverable.total = old_deliverable['budget']
case old_deliverable['type']
when 'FixedDeliverable'
@total = deliverable.total = old_deliverable['fixed_cost']
@total_cost = old_deliverable['fixed_cost']
when 'HourlyDeliverable'
@total = old_deliverable['total_hours'].to_f * old_deliverable['cost_per_hour'].to_f
@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,
:budget => @total_cost,
:hours => old_deliverable['total_hours'])
end
else
@total = 0
@total_cost = 0
end
convert_overhead(deliverable, old_deliverable, @total)
convert_materials(deliverable, old_deliverable, @total)
convert_overhead(deliverable, old_deliverable, @total_cost)
convert_materials(deliverable, old_deliverable, @total_cost)
append_old_deliverable_to_notes(old_deliverable, deliverable)
deliverable.save!
@@ -82,8 +80,17 @@ module RedmineContracts
end
end
@deliverable_mapper.each do |old, new|
Issue.update_all(["deliverable_id = ?", new], ["deliverable_id = ?", old])
# Slower than update_all but update_all could potentially hit an issue
# multiple times depending on the migration order. Example:
#
# - Issue 1 has Deliverable 1
# - Deliverable 1 updates Issue 1 to have the new deliverable id of 3
# - Deliverable 3 runs and updates Issue 1 again to have the new deliverable id of 5
#
Issue.all.each do |issue|
next if issue.deliverable_id.blank?
issue.update_attribute(:deliverable_id, @deliverable_mapper[issue.deliverable_id])
end
end
@@ -103,6 +110,7 @@ module RedmineContracts
c.account_executive = project.users.first
c.start_date ||= Date.today
c.end_date ||= Date.today
c.billable_rate = @contract_rate
end
contract.save!

View File

@@ -0,0 +1,23 @@
module RedmineContracts
module Hooks
class ControllerIssuesBulkEditBeforeSaveHook < Redmine::Hook::ViewListener
# Context:
# * :issue => Issue being saved
# * :params => HTML parameters
#
def controller_issues_bulk_edit_before_save(context={})
case
when context[:params][:deliverable_id].blank?
# Do nothing
when context[:params][:deliverable_id] == 'none'
# Unassign deliverable
context[:issue].deliverable = nil
else
context[:issue].deliverable = Deliverable.find(context[:params][:deliverable_id])
end
return ''
end
end
end
end

View File

@@ -0,0 +1,25 @@
module RedmineContracts
module Hooks
class ControllerIssuesEditBeforeSaveHook < Redmine::Hook::ViewListener
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
else
context[:issue].deliverable = nil
end
end
return ''
end
alias_method :controller_issues_new_before_save, :controller_issues_edit_before_save
end
end
end

View File

@@ -0,0 +1,19 @@
module RedmineContracts
module Hooks
class ControllerTimelogAvailableCriteriasHook < Redmine::Hook::ViewListener
def controller_timelog_available_criterias(context={})
context[:available_criterias]["deliverable_id"] = {
:sql => "#{Issue.table_name}.deliverable_id",
:klass => Deliverable,
:label => :field_deliverable
}
context[:available_criterias]["contract_id"] = {
:sql => "(SELECT deliverable.contract_id FROM #{Deliverable.table_name} deliverable WHERE deliverable.id = issues.deliverable_id)",
:klass => Contract,
:label => :field_contract
}
return ''
end
end
end
end

View File

@@ -0,0 +1,23 @@
module RedmineContracts
module Hooks
class HelperIssuesShowDetailAfterSettingHook < Redmine::Hook::ViewListener
# Deliverable changes for the journal use the Deliverable subject
# instead of the id
#
# Context:
# * :detail => Detail about the journal change
#
def helper_issues_show_detail_after_setting(context = { })
# TODO Later: Overwritting the caller is bad juju
if context[:detail].prop_key == 'deliverable_id'
d = Deliverable.find_by_id(context[:detail].value)
context[:detail].value = d.title if d.present? && d.title.present?
d = Deliverable.find_by_id(context[:detail].old_value)
context[:detail].old_value = d.title if d.present? && d.title.present?
end
''
end
end
end
end

View File

@@ -0,0 +1,9 @@
module RedmineContracts
module Hooks
class ViewIssuesBulkEditDetailsBottomHook < Redmine::Hook::ViewListener
render_on(:view_issues_bulk_edit_details_bottom, :partial => 'issues/bulk_edit_deliverable', :layout => false)
end
end
end

View File

@@ -0,0 +1,7 @@
module RedmineContracts
module Hooks
class ViewIssuesFormDetailsBottomHook < Redmine::Hook::ViewListener
render_on(:view_issues_form_details_bottom, :partial => 'issues/edit_deliverable', :layout => false)
end
end
end

View File

@@ -0,0 +1,9 @@
module RedmineContracts
module Hooks
class ViewIssuesShowDetailsBottomHook < Redmine::Hook::ViewListener
include Redmine::I18n
render_on(:view_issues_show_details_bottom, :partial => 'issues/show_deliverable', :layout => false)
end
end
end

View File

@@ -8,6 +8,9 @@ module RedmineContracts
base.class_eval do
unloadable
belongs_to :deliverable
delegate :title, :to => :deliverable, :prefix => true, :allow_nil => true
delegate :contract_name, :to => :deliverable, :allow_nil => true
end
end

View File

@@ -7,8 +7,8 @@ module RedmineContracts
base.send(:include, InstanceMethods)
base.class_eval do
unloadable
has_many :contracts
has_many :deliverables, :through => :contracts
end
end

View File

@@ -0,0 +1,89 @@
module RedmineContracts
module Patches
module QueryPatch
def self.included(base)
base.extend(ClassMethods)
base.send(:include, InstanceMethods)
base.class_eval do
unloadable
alias_method_chain :available_filters, :deliverable
alias_method_chain :available_filters, :contract
alias_method_chain :sql_for_field, :contract
end
end
module ClassMethods
end
module InstanceMethods
# TODO: Should have an API on the Redmine core for this
def available_filters_with_deliverable
@available_filters = available_filters_without_deliverable
if project
deliverable_filters = {
"deliverable_id" => {
:type => :list_optional,
:order => 15,
:values => project.deliverables.by_title.collect { |d| [d.title, d.id.to_s] }
}
}
return @available_filters.merge(deliverable_filters)
else
return @available_filters
end
end
# TODO: Should have an API on the Redmine core for this
def available_filters_with_contract
@available_filters = available_filters_without_contract
if project
contract_filters = {
"contract_id" => {
:type => :list_optional,
:order => 16,
:values => project.contracts.by_name.collect { |d| [d.name, d.id.to_s] }
}
}
return @available_filters.merge(contract_filters)
else
return @available_filters
end
end
def sql_for_field_with_contract(field, operator, value, db_table, db_field, is_custom_filter=false)
if field != "contract_id"
return sql_for_field_without_contract(field, operator, value, db_table, db_field, is_custom_filter)
else
# Contracts > Deliverables > Issue
case operator
when "="
contracts = value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",")
inner_select = "(SELECT id from deliverables where deliverables.contract_id IN (#{contracts}))"
sql = "#{Issue.table_name}.deliverable_id IN (#{inner_select})"
when "!"
contracts = value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",")
inner_select = "(SELECT id from deliverables where deliverables.contract_id IN (#{contracts}))"
sql = "(#{Issue.table_name}.deliverable_id IS NULL OR #{Issue.table_name}.deliverable_id NOT IN (#{inner_select}))"
when "!*"
# If it doesn't have a deliverable, it can't have a contract
sql = "#{Issue.table_name}.deliverable_id IS NULL"
when "*"
# If it has a deliverable, it must have a contract
sql = "#{Issue.table_name}.deliverable_id IS NOT NULL"
end
return sql
end
end
end
end
end
end

View File

@@ -1,10 +1,13 @@
namespace :redmine_contracts do
desc "Migrate data from the budget_plugin to redmine_contracts"
task :budget_migration => :environment do
options = {}
options[:contract_rate] = ENV['contract_rate']
RedmineContracts::BudgetPluginMigration.check_for_installed_budget_plugin
data = RedmineContracts::BudgetPluginMigration.export_data
RedmineContracts::BudgetPluginMigration.rename_old_tables
RedmineContracts::BudgetPluginMigration.migrate_contracts
RedmineContracts::BudgetPluginMigration.migrate(data)
RedmineContracts::BudgetPluginMigration.migrate(data, options)
end
end

View File

@@ -35,8 +35,8 @@ class BudgetPluginMigrationTest < ActionController::IntegrationTest
should "create a new Deliverable for each old Deliverable" do
assert_difference("Deliverable.count", 3) do
assert_difference("HourlyDeliverable.count", 2) do
assert_difference("FixedDeliverable.count", 1) do
assert_difference("HourlyDeliverable.count", 0) do
assert_difference("FixedDeliverable.count", 3) do
RedmineContracts::BudgetPluginMigration.migrate(@data)
end
end
@@ -53,6 +53,22 @@ class BudgetPluginMigrationTest < ActionController::IntegrationTest
assert_equal 2, @project_two.reload.contracts.first.deliverables.count
end
context "on new Contracts" do
should "default the contract billable rate to $150" do
RedmineContracts::BudgetPluginMigration.migrate(@data)
assert_equal 150, @project_one.reload.contracts.first.billable_rate
assert_equal 150, @project_two.reload.contracts.first.billable_rate
end
should "allow overriding the contract billable rate" do
RedmineContracts::BudgetPluginMigration.migrate(@data, :contract_rate => '100.50')
assert_equal 100.5, @project_one.reload.contracts.first.billable_rate
assert_equal 100.5, @project_two.reload.contracts.first.billable_rate
end
end
should "enable the contracts plugin for each project with a contract" do
@no_deliverables = Project.generate!(:enabled_modules => [])
RedmineContracts::BudgetPluginMigration.migrate(@data)
@@ -136,15 +152,28 @@ class BudgetPluginMigrationTest < ActionController::IntegrationTest
end
context "converting Fixed Deliverables" do
should "convert fixed_cost to total" do
should "convert the budget field to total" do
RedmineContracts::BudgetPluginMigration.migrate(@data)
d = FixedDeliverable.find_by_title("Version 1.0")
assert_equal 30_000, d.total
assert_equal 93_000, d.total
end
end
context "converting Hourly Deliverables" do
should "convert over into Fixed Deliverables" do
assert_difference("FixedDeliverable.count",3) do
RedmineContracts::BudgetPluginMigration.migrate(@data)
end
end
should "convert the old 'budget' field into the total" do
RedmineContracts::BudgetPluginMigration.migrate(@data)
assert_equal 5600, FixedDeliverable.find_by_title("Deliverable One").total
assert_equal 900, FixedDeliverable.find_by_title("Deliverable 2").total
end
should "create a new Labor Budget" do
assert_difference("LaborBudget.count", 2) do
RedmineContracts::BudgetPluginMigration.migrate(@data)
@@ -175,6 +204,7 @@ class BudgetPluginMigrationTest < ActionController::IntegrationTest
setup do
@issue1 = Issue.generate_for_project!(@project_two, :deliverable_id => 2)
@issue2 = Issue.generate_for_project!(@project_two, :deliverable_id => 4)
@issue3 = Issue.generate_for_project!(@project_one, :deliverable_id => 1)
end
@@ -189,7 +219,9 @@ class BudgetPluginMigrationTest < ActionController::IntegrationTest
RedmineContracts::BudgetPluginMigration.migrate(@data)
# The "third" deliverable has an id of 4
assert_equal "Deliverable 2", @issue1.reload.deliverable.title
assert_equal "Version 1.0", @issue2.reload.deliverable.title
assert_equal "Deliverable One", @issue3.reload.deliverable.title
end
end

View File

@@ -5,7 +5,7 @@ class ContractsDeleteTest < ActionController::IntegrationTest
def setup
@project = Project.generate!(:identifier => 'main')
@contract = Contract.generate!(:project => @project, :name => 'A Contract', :payment_terms => 'net_15')
@contract = Contract.generate!(:project => @project, :name => 'A Contract')
end
should "allow admins to delete the contract" do

View File

@@ -5,7 +5,10 @@ class ContractsEditTest < ActionController::IntegrationTest
def setup
@project = Project.generate!(:identifier => 'main')
@contract = Contract.generate!(:project => @project, :name => 'A Contract', :payment_terms => 'net_15')
@account_executive = User.generate!
@role = Role.generate!
User.add_to_project(@account_executive, @project, @role)
@contract = Contract.generate!(:project => @project, :name => 'A Contract', :account_executive => @account_executive)
end
should "allow any user to edit the contract" do
@@ -17,16 +20,14 @@ class ContractsEditTest < ActionController::IntegrationTest
assert_response :success
assert_template 'contracts/edit'
assert_select "h2", :text => @contract.name
assert_select "h2", :text => /#{@contract.name}/
assert_select "form#edit_contract_#{@contract.id}.contract" do
assert_select "input[value=?]", /#{@contract.name}/
assert_select "select#contract_payment_terms" do
assert_select "option[selected=selected][value=net_15]"
end
assert_select "select#contract_payment_term_id"
end
fill_in "Name", :with => 'An updated name'
click_button "Update Contract"
click_button "Save Contract"
assert_response :success
assert_template 'contracts/show'

View File

@@ -5,6 +5,8 @@ class ContractsNewTest < ActionController::IntegrationTest
def setup
@project = Project.generate!(:identifier => 'main')
PaymentTerm.generate!(:type => 'PaymentTerm', :name => 'Net 15')
PaymentTerm.generate!(:type => 'PaymentTerm', :name => 'Net 30')
end
should "allow any user to open the new contracts form" do
@@ -17,6 +19,8 @@ class ContractsNewTest < ActionController::IntegrationTest
should "create a new contract" do
@account_executive = User.generate!
@role = Role.generate!
User.add_to_project(@account_executive, @project, @role)
visit_contracts_for_project(@project)
click_link 'New Contract'
@@ -28,7 +32,7 @@ class ContractsNewTest < ActionController::IntegrationTest
fill_in "End Date", :with => '2010-12-31'
select "Net 30", :from => "Payment Terms"
click_button "Create Contract"
click_button "Save Contract"
assert_response :success
assert_template 'contracts/show'
@@ -38,7 +42,7 @@ class ContractsNewTest < ActionController::IntegrationTest
assert_equal @account_executive, @contract.account_executive
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_terms
assert_equal 'Net 30', @contract.payment_term.name
end
end

View File

@@ -15,7 +15,7 @@ class ContractsShowTest < ActionController::IntegrationTest
assert_template 'contracts/show'
assert_equal "/projects/main/contracts/#{@contract.id}", current_url
assert_select "div#contract_#{@contract.id}.contract" do
assert_select "div.title-bar" do
assert_select 'h2', :text => @contract.name
end
end
@@ -119,7 +119,7 @@ class ContractsShowTest < ActionController::IntegrationTest
visit_contract_page(@contract)
assert_select "table#deliverables" do
assert_select "td.labor", :text => /4,200.50/
assert_select "td.labor", :text => /4,201/
end
end
@@ -137,7 +137,7 @@ class ContractsShowTest < ActionController::IntegrationTest
visit_contract_page(@contract)
assert_select "table#deliverables" do
assert_select "td.overhead", :text => /4,200.50/
assert_select "td.overhead", :text => /4,201/
end
end
@@ -171,7 +171,7 @@ class ContractsShowTest < ActionController::IntegrationTest
visit_contract_page(@contract)
assert_select "table#deliverables" do
assert_select "td.labor", :text => /1,000.00/
assert_select "td.labor", :text => /1,000/
end
end
@@ -205,7 +205,7 @@ class ContractsShowTest < ActionController::IntegrationTest
visit_contract_page(@contract)
assert_select "table#deliverables" do
assert_select "td.overhead", :text => /2,000.00/
assert_select "td.overhead", :text => /2,000/
end
end

View File

@@ -5,7 +5,7 @@ class DeliverablesDeleteTest < ActionController::IntegrationTest
def setup
@project = Project.generate!(:identifier => 'main')
@contract = Contract.generate!(:project => @project, :name => 'A Contract', :payment_terms => 'net_15')
@contract = Contract.generate!(:project => @project, :name => 'A Contract')
@manager = User.generate!
@deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager)
end
@@ -13,7 +13,7 @@ class DeliverablesDeleteTest < ActionController::IntegrationTest
should "allow anyone to delete the deliverable" do
visit_contract_page(@contract)
click_link_within "#fixed_deliverable_#{@deliverable.id}", 'Delete'
click_link_within "#deliverable_details_#{@deliverable.id}", 'Delete'
assert_response :success
assert_template 'contracts/show'

View File

@@ -5,7 +5,7 @@ class DeliverablesEditTest < ActionController::IntegrationTest
def setup
@project = Project.generate!(:identifier => 'main')
@contract = Contract.generate!(:project => @project, :name => 'A Contract', :payment_terms => 'net_15')
@contract = Contract.generate!(:project => @project, :name => 'A Contract')
@manager = User.generate!
@role = Role.generate!
User.add_to_project(@manager, @project, @role)
@@ -16,7 +16,7 @@ class DeliverablesEditTest < ActionController::IntegrationTest
should "allow any user to edit the Fixed deliverable" do
visit_contract_page(@contract)
click_link_within "#fixed_deliverable_#{@fixed_deliverable.id}", 'Edit'
click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit'
assert_response :success
assert_template 'deliverables/edit'
@@ -45,7 +45,7 @@ class DeliverablesEditTest < ActionController::IntegrationTest
should "allow any user to edit the Hourly deliverable" do
visit_contract_page(@contract)
click_link_within "#hourly_deliverable_#{@hourly_deliverable.id}", 'Edit'
click_link_within "#deliverable_details_#{@hourly_deliverable.id}", 'Edit'
assert_response :success
assert_template 'deliverables/edit'

View File

@@ -3,7 +3,7 @@ require 'test_helper'
class OverheadPluginIntegrationTest < ActionController::IntegrationTest
def setup
@project = Project.generate!(:identifier => 'main')
@contract = Contract.generate!(:project => @project, :name => 'A Contract', :payment_terms => 'net_15')
@contract = Contract.generate!(:project => @project, :name => 'A Contract')
@manager = User.generate!
@role = Role.generate!
User.add_to_project(@manager, @project, @role)

View File

@@ -0,0 +1,54 @@
require File.dirname(__FILE__) + '/../../../test_helper'
class RedmineContracts::Hooks::ControllerIssuesBulkEditBeforeSaveHookTest < ActionController::IntegrationTest
include Redmine::Hook::Helper
context "#view_issues_bulk_edit_details_bottom" do
setup do
@project = Project.generate!
@issue = Issue.generate_for_project!(@project)
@issue2 = Issue.generate_for_project!(@project)
@issue3 = Issue.generate_for_project!(@project)
@issues = [@issue, @issue2, @issue3]
@contract1 = Contract.generate!(:project => @project)
@contract2 = 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 1')
@deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'The Title 2')
@issue.deliverable = @deliverable1
login_as('manager', 'existing')
end
context "when saving multiple issues" do
setup do
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
end
end

View File

@@ -0,0 +1,59 @@
require File.dirname(__FILE__) + '/../../../test_helper'
class RedmineContracts::Hooks::ControllerIssuesEditBeforeSaveTest < ActionController::IntegrationTest
include Redmine::Hook::Helper
context "#controller_issues_edit_before_save" do
setup do
@project = Project.generate!
IssueStatus.generate!(:is_default => true)
@issue = Issue.generate_for_project!(@project)
@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])
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')
login_as('manager', 'existing')
@project.reload
end
context "for a new issue" do
setup do
visit_project(@project)
click_link "New issue"
end
should "set the issue's deliverable" do
fill_in "Subject", :with => 'Hook test'
select @deliverable2.title, :from => "Deliverable"
click_button "Create"
assert_response :success
assert_equal @deliverable2, Issue.last.deliverable
end
end
context "for an existing issue" do
setup do
visit_issue_page(@issue)
end
should "update the issue's deliverable" do
select @deliverable2.title, :from => "Deliverable"
click_button "Submit"
assert_response :success
@issue.reload
assert_equal @deliverable2, @issue.deliverable
end
end
end
end

View File

@@ -0,0 +1,58 @@
require File.dirname(__FILE__) + '/../../../test_helper'
class RedmineContracts::Hooks::HelperIssuesShowDetailAfterSettingHookTest < ActionController::IntegrationTest
include Redmine::Hook::Helper
context "#helper_issues_show_detail_after_setting" do
setup do
@project = Project.generate!
@issue = Issue.generate_for_project!(@project)
@contract1 = Contract.generate!(:project => @project)
@contract2 = 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 1')
@deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'The Title 2')
# Set first
@issue.init_journal(@manager)
@issue.deliverable = @deliverable1
@issue.save!
# Change
@issue.init_journal(@manager)
@issue.deliverable = @deliverable2
@issue.save!
# Unset
@issue.init_journal(@manager)
@issue.deliverable = nil
@issue.save!
login_as('manager', 'existing')
visit_issue_page(@issue)
assert_response :success
end
should "show when a deliverable is set" do
assert_select ".details" do
assert_select "li", :text => /Deliverable set to #{@deliverable1.title}/
end
end
should "show when a deliverable is changed" do
assert_select ".details" do
assert_select "li", :text => /Deliverable changed from #{@deliverable1.title} to #{@deliverable2.title}/
end
end
should "show when a deliverable is removed" do
assert_select ".details" do
assert_select "li", :text => /Deliverable deleted .*#{@deliverable2.title}/
end
end
end
end

View File

@@ -0,0 +1,55 @@
require File.dirname(__FILE__) + '/../../../test_helper'
class RedmineContracts::Hooks::ViewIssuesBulkEditDetailsBottomHookTest < ActionController::IntegrationTest
include Redmine::Hook::Helper
context "#view_issues_bulk_edit_details_bottom" do
setup do
@project = Project.generate!
@issue = Issue.generate_for_project!(@project)
@issue2 = Issue.generate_for_project!(@project)
@issue3 = Issue.generate_for_project!(@project)
@contract1 = Contract.generate!(:project => @project)
@contract2 = 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')
@issue.deliverable = @deliverable1
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
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
end
context "with Contracts Disabled" do
setup do
@project.enabled_modules.collect {|m| m.destroy if m.name == 'contracts' }
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
end

View File

@@ -0,0 +1,52 @@
require File.dirname(__FILE__) + '/../../../test_helper'
class RedmineContracts::Hooks::ViewIssuesFormDetailsBottomTest < ActionController::IntegrationTest
include Redmine::Hook::Helper
context "#view_issues_form_details_bottom" do
setup do
@project = Project.generate!
@issue = Issue.generate_for_project!(@project)
@contract1 = Contract.generate!(:project => @project)
@contract2 = 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')
@issue.deliverable = @deliverable1
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}/
end
assert_select "optgroup[label=?]", @contract2.name do
assert_select "option", :text => /#{@deliverable2.title}/
end
end
end
end
context "with Contracts Disabled" do
setup do
@project.enabled_modules.collect {|m| m.destroy if m.name == 'contracts' }
visit_issue_page(@issue)
end
should "not render the deliverable select field" do
assert_select 'select#issue_deliverable_id', :count => 0
end
end
end
end

View File

@@ -0,0 +1,50 @@
require 'test_helper'
require 'performance_test_help'
# Performance logs
#
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)
@fixed_deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'The Title')
@hourly_deliverable = HourlyDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'An Hourly')
configure_overhead_plugin
100.times do
generate_issues_and_time_entries_for_deliverable(@hourly_deliverable, @project)
generate_issues_and_time_entries_for_deliverable(@fixed_deliverable, @project)
end
# Load the app
login_as 'user', 'password'
visit_contracts_for_project(@project)
end
def test_contract_show
click_link @contract.id
end
private
def generate_issues_and_time_entries_for_deliverable(deliverable, project)
@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 => 20,
:user => @manager)
deliverable.issues << @issue1
end
end

View File

@@ -71,6 +71,14 @@ module IntegrationTestHelper
assert_template 'contracts/show'
end
def visit_issue_page(issue)
visit '/issues/' + issue.id.to_s
end
def visit_issue_bulk_edit_page(issues)
visit url_for(:controller => 'issues', :action => 'bulk_edit', :ids => issues.collect(&:id))
end
def assert_forbidden
assert_response :forbidden
assert_template 'common/403'

View File

@@ -3,6 +3,7 @@ require File.dirname(__FILE__) + '/../test_helper'
class ContractTest < ActiveSupport::TestCase
should_belong_to :account_executive
should_belong_to :project
should_belong_to :payment_term
should_have_many :deliverables
should_validate_presence_of :name

View File

@@ -0,0 +1,77 @@
require File.dirname(__FILE__) + '/../../../../test_helper'
class RedmineContracts::Hooks::ControllerTimelogAvailableCriteriasTest < ActionController::TestCase
include Redmine::Hook::Helper
def controller
@controller ||= ApplicationController.new
@controller.response ||= ActionController::TestResponse.new
@controller
end
def request
@request ||= ActionController::TestRequest.new
end
def hook(args={})
call_hook :controller_timelog_available_criterias, args
end
def context
@context ||= {
:available_criterias => {"existing" => {:label => 'existing'}}
}
end
context "#controller_timelog_available_criterias" do
should "return an empty string" do
@response.body = hook(context)
assert @response.body.blank?
end
context "Deliverables" do
should "add a deliverable_id to the available criterias" do
@response.body = hook(context)
assert context[:available_criterias]['deliverable_id']
end
should "add the deliverable sql to the available criterias" do
@response.body = hook(context)
assert "issues.deliverable_id", context[:available_criterias]['deliverable_id'][:sql]
end
should "add the deliverable Class to the available criterias" do
@response.body = hook(context)
assert Deliverable, context[:available_criterias]['deliverable_id'][:klass]
end
should "add the deliverable label to the available criterias" do
@response.body = hook(context)
assert :field_deliverable, context[:available_criterias]['deliverable_id'][:label]
end
end
context "Contracts" do
should "add a contract_id to the available criterias" do
@response.body = hook(context)
assert context[:available_criterias]['contract_id']
end
should "add the contact sql to the available criterias" do
@response.body = hook(context)
assert "issues.deliverable_id", context[:available_criterias]['contract_id'][:sql]
end
should "add the deliverable Class to the available criterias" do
@response.body = hook(context)
assert Contract, context[:available_criterias]['contract_id'][:klass]
end
should "add the deliverable label to the available criterias" do
@response.body = hook(context)
assert :field_contract, context[:available_criterias]['contract_id'][:label]
end
end
end
end

View File

@@ -0,0 +1,77 @@
require File.dirname(__FILE__) + '/../../../../test_helper'
class RedmineContracts::Hooks::ViewIssuesShowDetailsBottomTest < ActionController::TestCase
include Redmine::Hook::Helper
# Bloody bloody hack to work around Rails coupling of _VC.
def template
t = ActionView::Base.new(ActionController::Base.view_paths, {}, @controller)
def t.template_format
"html"
end
# Rendered views aren't getting access to the controller's I18n module
t.send(:extend, Redmine::I18n)
t
end
def controller
@controller ||= ApplicationController.new
@controller.class.send(:include, ::Redmine::I18n)
@controller.response ||= ActionController::TestResponse.new
# Hack to support render_on
@controller.instance_variable_set('@template', template)
@controller.response = response
@controller
end
def request
@request ||= ActionController::TestRequest.new
end
# Hack to support render_on
def response
@response.template ||= template
@response
end
def hook(args={})
call_hook :view_issues_show_details_bottom, args
end
context "#view_issues_show_details_bottom" do
setup do
@project = Project.generate!
@issue = Issue.generate_for_project!(@project)
@contract = Contract.generate!(:project => @project)
@manager = User.generate!
@role = Role.generate!
User.add_to_project(@manager, @project, @role)
@deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'The Title')
@issue.deliverable = @deliverable
end
context "with Contracts Enabled" do
should "render the deliverable's name" do
@response.body = hook(:project => @project, :issue => @issue, :controller => controller)
assert_select "tr" do
assert_select "td", :text => /#{@deliverable.title}/
end
end
end
context "with Contracts Disabled" do
setup do
@project.enabled_modules.destroy_all
end
should "not render the deliverable's name" do
@response.body = hook(:project => @project, :issue => @issue, :controller => controller)
assert_no_match /#{@deliverable.title}/, @response.body
end
end
end
end

View File

@@ -5,5 +5,6 @@ class RedmineContracts::Patches::ProjectTest < ActionController::TestCase
context "Project" do
subject { Project.new }
should_have_many :contracts
should_have_many :deliverables
end
end

View File

@@ -0,0 +1,83 @@
require File.dirname(__FILE__) + '/../../../../test_helper'
class RedmineContracts::Patches::QueryTest < ActionController::TestCase
context "Query" do
subject {Query.new}
context "#available_filters with project" do
setup do
@query = Query.new
@query.project = @project = Project.generate!
@contract = Contract.generate!(:project => @project, :name => 'A Contract')
@manager = User.generate!
@deliverable1 = FixedDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'One')
@deliverable2 = FixedDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'Two')
end
should "add a deliverable_id filter" do
filters = @query.available_filters
assert filters.keys.include?("deliverable_id")
deliverable_filter = filters["deliverable_id"]
assert_equal :list_optional, deliverable_filter[:type]
assert_equal [
["One", @deliverable1.id.to_s],
["Two", @deliverable2.id.to_s]
], deliverable_filter[:values]
end
should "add a contract_id filter" do
filters = @query.available_filters
assert filters.keys.include?("contract_id")
contract_filter = filters["contract_id"]
assert_equal :list_optional, contract_filter[:type]
assert_equal [["A Contract", @contract.id.to_s]], contract_filter[:values]
end
end
# TODO: Dragons in this test
context "#sql_for_field_with_contract" do
context "for contract_id fields" do
setup do
@query = Query.new
end
context "with the equal operator" do
should "return the SQL snippet for checking for deliverables on the specific contracts" do
sql = @query.send(:sql_for_field, 'contract_id', '=', ['1','2'], '', '')
assert_equal "issues.deliverable_id IN ((SELECT id from deliverables where deliverables.contract_id IN ('1','2')))", sql
end
end
context "with is not operator" do
should "return the SQL snippet for checking for null deliverables or deliverables no on the specific contracts" do
sql = @query.send(:sql_for_field, 'contract_id', '!', ['1','2'], '', '')
assert_equal "(issues.deliverable_id IS NULL OR issues.deliverable_id NOT IN ((SELECT id from deliverables where deliverables.contract_id IN ('1','2'))))", sql
end
end
context "with none operator" do
should "return the SQL snippet for checking for null deliverables" do
sql = @query.send(:sql_for_field, 'contract_id', '!*', '', '', '')
assert_equal "issues.deliverable_id IS NULL", sql
end
end
context "with all operator" do
should "return the SQL snippet for checking for not null deliverables" do
sql = @query.send(:sql_for_field, 'contract_id', '*', '', '', '')
assert_equal "issues.deliverable_id IS NOT NULL", sql
end
end
end
end
end
end

View File

@@ -0,0 +1,40 @@
require File.dirname(__FILE__) + '/../test_helper'
class PaymentTermTest < ActiveSupport::TestCase
include Redmine::I18n
should_have_many(:contracts)
should "be a subclass of Enumeration" do
assert_equal Enumeration, PaymentTerm.superclass
end
context "#option_name" do
should "be Payment Terms" do
assert_equal "Payment Terms", l(PaymentTerm.new.option_name)
end
end
context "#objects_count" do
should "count the number of contracts with this payment term" do
@payment_term = PaymentTerm.generate!(:type => 'PaymentTerm')
Contract.generate!(:payment_term => @payment_term)
Contract.generate!(:payment_term => @payment_term)
assert_equal 2, @payment_term.objects_count
end
end
context "#transfer_relations" do
should "update all contracts to use a new PaymentTerm" do
@old_payment_term = PaymentTerm.generate!(:type => 'PaymentTerm')
@new_payment_term = PaymentTerm.generate!(:type => 'PaymentTerm')
@contract1 = Contract.generate!(:payment_term => @old_payment_term)
@contract2 = Contract.generate!(:payment_term => @old_payment_term)
@old_payment_term.transfer_relations(@new_payment_term)
assert_equal @new_payment_term, @contract1.reload.payment_term
assert_equal @new_payment_term, @contract2.reload.payment_term
end
end
end