├── .gitignore ├── COPYRIGHT.txt ├── CREDITS.txt ├── GPL.txt ├── Gemfile ├── README.rdoc ├── Rakefile ├── app ├── controllers │ ├── contracts_controller.rb │ └── deliverables_controller.rb ├── helpers │ ├── contract_formatter_helper.rb │ ├── contracts_helper.rb │ └── deliverables_helper.rb ├── models │ ├── contract.rb │ ├── deliverable.rb │ ├── fixed_budget.rb │ ├── fixed_deliverable.rb │ ├── hourly_deliverable.rb │ ├── labor_budget.rb │ ├── overhead_budget.rb │ ├── payment_term.rb │ └── retainer_deliverable.rb └── views │ ├── contracts │ ├── _form.html.erb │ ├── _title.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── new.html.erb │ └── show.html.erb │ ├── deliverables │ ├── _details_row.html.erb │ ├── _finance_form.html.erb │ ├── _finances.html.erb │ ├── _fixed_budget_form.html.erb │ ├── _form.html.erb │ ├── _labor_budget_form.html.erb │ ├── _overhead_budget_form.html.erb │ ├── edit.html.erb │ ├── finances.html.erb │ └── new.html.erb │ └── issues │ ├── _bulk_edit_deliverable.html.erb │ ├── _edit_deliverable.html.erb │ └── _show_deliverable.html.erb ├── assets ├── images │ ├── todo1.png │ ├── todo2.png │ ├── todo3.png │ ├── todo4.png │ └── todo5.png ├── javascripts │ ├── contracts.js │ ├── jquery-1.4.4.min.js │ ├── jquery-ui-1.8.15.custom.min.js │ └── jquery.tmpl.min.js └── stylesheets │ ├── redmine_contracts.css │ └── smoothness │ ├── images │ ├── ui-bg_flat_0_aaaaaa_40x100.png │ ├── ui-bg_flat_75_ffffff_40x100.png │ ├── ui-bg_glass_55_fbf9ee_1x400.png │ ├── ui-bg_glass_65_ffffff_1x400.png │ ├── ui-bg_glass_75_dadada_1x400.png │ ├── ui-bg_glass_75_e6e6e6_1x400.png │ ├── ui-bg_glass_95_fef1ec_1x400.png │ ├── ui-bg_highlight-soft_75_cccccc_1x100.png │ ├── ui-icons_222222_256x240.png │ ├── ui-icons_2e83ff_256x240.png │ ├── ui-icons_454545_256x240.png │ ├── ui-icons_888888_256x240.png │ └── ui-icons_cd0a0a_256x240.png │ └── jquery-ui-1.8.15.custom.css ├── autotest └── discover.rb ├── config ├── locales │ └── en.yml └── routes.rb ├── db └── migrate │ ├── 001_create_contracts.rb │ ├── 002_create_deliverables.rb │ ├── 003_add_total_to_deliverables.rb │ ├── 004_create_labor_budgets.rb │ ├── 005_create_overhead_budgets.rb │ ├── 006_add_deliverable_id_to_issues.rb │ ├── 007_add_client_point_of_contact_to_contracts.rb │ ├── 008_add_payment_term_id_to_contracts.rb │ ├── 009_remove_payment_terms_from_contracts.rb │ ├── 010_populate_payment_terms.rb │ ├── 011_add_frequency_to_deliverables.rb │ ├── 012_add_year_and_month_to_labor_budgets.rb │ ├── 013_add_year_and_month_to_overhead_budgets.rb │ ├── 014_remove_frequency_from_deliverables.rb │ ├── 015_create_fixed_budgets.rb │ ├── 016_add_year_and_month_to_fixed_budgets.rb │ ├── 017_add_paid_to_fixed_budgets.rb │ ├── 018_add_status_to_contracts.rb │ ├── 019_add_status_to_deliverables.rb │ ├── 020_add_time_entry_activity_id_to_labor_budgets.rb │ └── 021_add_time_entry_activity_id_to_overhead_budgets.rb ├── init.rb ├── lang └── en.yml ├── lib ├── dollarized_attribute.rb ├── redmine_contracts │ ├── budget_plugin_migration.rb │ ├── hooks │ │ ├── controller_issues_bulk_edit_before_save_hook.rb │ │ ├── controller_issues_edit_before_save_hook.rb │ │ ├── controller_timelog_available_criterias_hook.rb │ │ ├── helper_issues_show_detail_after_setting_hook.rb │ │ ├── view_issues_bulk_edit_details_bottom_hook.rb │ │ ├── view_issues_form_details_bottom_hook.rb │ │ ├── view_issues_show_details_bottom_hook.rb │ │ └── view_layouts_base_html_head_hook.rb │ └── patches │ │ ├── issue_patch.rb │ │ ├── project_patch.rb │ │ ├── query_patch.rb │ │ └── time_entry_patch.rb └── tasks │ └── budget_plugin_migration.rake └── test ├── fixtures └── budget_plugin_migration │ └── budget.yml ├── functional └── contracts_controller_test.rb ├── integration ├── budget_plugin_migration_test.rb ├── contracts_delete_test.rb ├── contracts_edit_test.rb ├── contracts_list_test.rb ├── contracts_new_test.rb ├── contracts_show_test.rb ├── deliverable_details_test.rb ├── deliverable_finances_test.rb ├── deliverables_delete_test.rb ├── deliverables_edit_test.rb ├── deliverables_list_test.rb ├── deliverables_new_test.rb ├── deliverables_show_test.rb ├── disabled_contracts_module_test.rb ├── issue_filtering_test.rb ├── overhead_plugin_integration_test.rb ├── redmine_contracts │ └── hooks │ │ ├── controller_issues_bulk_edit_before_save_hook_test.rb │ │ ├── controller_issues_edit_before_save.rb │ │ ├── helper_issues_show_detail_after_setting_hook_test.rb │ │ ├── view_issues_bulk_edit_details_bottom_hook_test.rb │ │ └── view_issues_form_details_bottom_hook_test.rb └── routing_test.rb ├── performance └── contract_show_test.rb ├── test_helper.rb └── unit ├── contract_test.rb ├── deliverable_test.rb ├── fixed_budget_test.rb ├── fixed_deliverable_test.rb ├── helpers └── contracts_helper_test.rb ├── hourly_deliverable_test.rb ├── labor_budget_test.rb ├── lib └── redmine_contracts │ ├── hooks │ ├── controller_timelog_available_criterias_hook_test.rb │ ├── view_issues_show_details_bottom_hook_test.rb │ └── view_layouts_base_html_head_hook_test.rb │ └── patches │ ├── issue_patch_test.rb │ ├── project_patch_test.rb │ ├── query_patch_test.rb │ └── time_entry_patch_test.rb ├── overhead_budget_test.rb ├── payment_term_test.rb └── retainer_deliverable_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | webrat* 2 | tmp/ 3 | -------------------------------------------------------------------------------- /COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | Contacts is a Redmine plugin that provides a system to manage the execution 2 | of a client contract by separating it into deliverables and milestones. 3 | 4 | Copyright (C) 2010 Eric Davis 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation; either version 2 9 | of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | -------------------------------------------------------------------------------- /CREDITS.txt: -------------------------------------------------------------------------------- 1 | Thanks go to the following people for patches and contributions: 2 | 3 | * Eric Davis - Maintainer 4 | * Shane Pearlman - Sponsor 5 | * Peter Chester - Sponsor 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | gem 'formtastic', "0.9.10" 2 | gem 'inherited_resources', '1.0.6' 3 | 4 | group :test do 5 | gem 'webrat' 6 | end 7 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Contracts 2 | 3 | A system to manage the execution of a client contract by separating it into deliverables and milestones. 4 | 5 | == Features 6 | 7 | TODO: fill in features 8 | 9 | == Getting the plugin 10 | 11 | A copy of the plugin can be downloaded from {Little Stream Software}[https://projects.littlestreamsoftware.com/projects/redmine-contracts/files] or from {GitHub}[http://github.com/edavis10/redmine_contracts] 12 | 13 | 14 | == Installation and Setup 15 | 16 | 1. Follow the Redmine plugin installation steps at: http://www.redmine.org/wiki/redmine/Plugins 17 | 2. Install the Redmine Rate plugin 18 | 3. Configure the Redmine Rate plugin to be loaded before this plugin (config.plugins = [ :redmine_rate, :all ] ) 19 | 4. Run the plugin migrations +rake db:migrate_plugins+ 20 | 5. Restart your Redmine web servers (e.g. mongrel, thin, mod_rails) 21 | 22 | == Usage 23 | 24 | TODO: Add usage 25 | 26 | == License 27 | 28 | This plugin is licensed under the GNU GPL v2. See COPYRIGHT.txt and GPL.txt for details. 29 | 30 | == Project help 31 | 32 | If you need help you can contact the maintainer at the Bug Tracker. The bug tracker is located at https://projects.littlestreamsoftware.com 33 | 34 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'redmine_plugin_support' 3 | 4 | Dir[File.expand_path(File.dirname(__FILE__)) + "/lib/tasks/**/*.rake"].sort.each { |ext| load ext } 5 | 6 | RedminePluginSupport::Base.setup do |plugin| 7 | plugin.project_name = 'redmine_contracts' 8 | plugin.default_task = [:test] 9 | plugin.tasks = [:db, :doc, :release, :clean, :test, :stats] 10 | # TODO: gem not getting this automaticly 11 | plugin.redmine_root = File.expand_path(File.dirname(__FILE__) + '/../../../') 12 | end 13 | 14 | begin 15 | require 'jeweler' 16 | Jeweler::Tasks.new do |s| 17 | s.name = "redmine_contracts" 18 | s.summary = "A system to manage the execution of a client contract by separating it into deliverables and milestones." 19 | s.email = "edavis@littlestreamsoftware.com" 20 | s.homepage = "https://projects.littlestreamsoftware.com/projects/redmine-contracts" 21 | s.description = "A system to manage the execution of a client contract by separating it into deliverables and milestones." 22 | s.authors = ["Eric Davis"] 23 | s.rubyforge_project = "TODO" # TODO 24 | s.files = FileList[ 25 | "[A-Z]*", 26 | "init.rb", 27 | "rails/init.rb", 28 | "{bin,generators,lib,test,app,assets,config,lang}/**/*", 29 | 'lib/jeweler/templates/.gitignore' 30 | ] 31 | end 32 | Jeweler::GemcutterTasks.new 33 | Jeweler::RubyforgeTasks.new do |rubyforge| 34 | rubyforge.doc_task = "rdoc" 35 | end 36 | rescue LoadError 37 | puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com" 38 | end 39 | 40 | -------------------------------------------------------------------------------- /app/controllers/contracts_controller.rb: -------------------------------------------------------------------------------- 1 | class ContractsController < InheritedResources::Base 2 | unloadable 3 | 4 | respond_to :html 5 | 6 | before_filter :find_project 7 | before_filter :authorize 8 | before_filter :require_admin, :only => :destroy 9 | 10 | helper :contract_formatter 11 | 12 | def create 13 | create! do |success, failure| 14 | success.html { redirect_to contract_url(@project, resource) } 15 | end 16 | end 17 | 18 | def update 19 | update! { contract_url(@project, resource) } 20 | end 21 | 22 | protected 23 | 24 | def begin_of_association_chain 25 | @project 26 | end 27 | 28 | private 29 | 30 | def find_project 31 | @project = Project.find(params[:project_id]) 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/deliverables_controller.rb: -------------------------------------------------------------------------------- 1 | class DeliverablesController < InheritedResources::Base 2 | unloadable 3 | 4 | respond_to :html 5 | 6 | before_filter :find_contract 7 | before_filter :authorize 8 | 9 | helper :contracts 10 | helper :contract_formatter 11 | 12 | def index 13 | redirect_to contract_url(@project, @contract) 14 | end 15 | 16 | def create 17 | remove_empty_budget_items(params) 18 | @deliverable = begin_of_association_chain.deliverables.build(params[:deliverable]) 19 | if params[:deliverable] && params[:deliverable][:type] && Deliverable.valid_types.include?(params[:deliverable][:type]) 20 | @deliverable.type = params[:deliverable][:type] 21 | end 22 | create!(:notice => l(:text_flash_deliverable_created, :name => @deliverable.title)) { contract_url(@project, @contract) } 23 | end 24 | 25 | def update 26 | @deliverable = begin_of_association_chain.deliverables.find_by_id(params[:id]) 27 | params[:deliverable] = params[:fixed_deliverable] || params[:hourly_deliverable] || params[:retainer_deliverable] 28 | remove_empty_budget_items(params) 29 | update!(:notice => l(:text_flash_deliverable_updated, :name => @deliverable.title)) { contract_url(@project, @contract) } 30 | end 31 | 32 | def show 33 | if show_partial? 34 | @period = extract_period(params[:period]) 35 | render :partial => 'deliverables/details_row', :locals => {:contract => @contract, :deliverable => @contract.deliverables.find(params[:id]), :period => @period} 36 | else 37 | redirect_to contract_url(@project, @contract) 38 | end 39 | end 40 | 41 | def finances 42 | respond_to do |format| 43 | format.js { render :partial => 'deliverables/finances', :locals => {:contract => @contract, :deliverable => @contract.deliverables.find(params[:id])} } 44 | format.html { } 45 | end 46 | 47 | end 48 | 49 | def destroy 50 | destroy!(:notice => l(:text_flash_deliverable_deleted, :name => resource.title)) { contract_url(@project, @contract) } 51 | end 52 | 53 | protected 54 | 55 | def begin_of_association_chain 56 | @contract 57 | end 58 | 59 | # Is only a partial requested? 60 | def show_partial? 61 | params[:format] == 'js' && params[:as] == 'deliverable_details_row' 62 | end 63 | 64 | private 65 | 66 | def find_contract 67 | @contract = Contract.find(params[:contract_id]) 68 | @project = @contract.project 69 | end 70 | 71 | def extract_period(param) 72 | period = nil 73 | if param.present? && param.match(/\A\d{4}-\d{2}\z/) # "YYYY-MM" 74 | year, month = param.split('-') 75 | period = Date.new(year.to_i, month.to_i, 1) 76 | end 77 | period 78 | end 79 | 80 | # Remove empty budgets. Will prevent validation errors 81 | # from empty fields submitted from the bulk adding form. 82 | # 83 | # LSS Clients #6714 84 | def remove_empty_budget_items(params) 85 | params["deliverable"]["labor_budgets_attributes"].reject! {|key, b| budget_item_empty?(b) } 86 | params["deliverable"]["overhead_budgets_attributes"].reject! {|key, b| budget_item_empty?(b) } 87 | params["deliverable"]["fixed_budgets_attributes"].reject! {|key, b| fixed_budget_item_empty?(b) } 88 | end 89 | 90 | def budget_item_empty?(item) 91 | (item["time_entry_activity_id"].blank?) && 92 | (item["hours"].blank? || item["hours"].to_f == 0.0) && 93 | (item["budget"].blank? || item["budget"].gsub('$','').to_f == 0.0) 94 | end 95 | 96 | def fixed_budget_item_empty?(item) 97 | (item["title"].blank?) && 98 | (item["budget"].blank? || item["budget"].gsub('$','').to_f == 0.0) && 99 | (item["markup"].blank? || item["markup"].gsub('$','').to_d == 0.0) 100 | 101 | end 102 | 103 | end 104 | -------------------------------------------------------------------------------- /app/helpers/contract_formatter_helper.rb: -------------------------------------------------------------------------------- 1 | # Formatting helpers 2 | module ContractFormatterHelper 3 | def format_as_yes_or_no(value) 4 | if value 5 | l(:general_text_Yes) 6 | else 7 | l(:general_text_No) 8 | end 9 | end 10 | 11 | def format_budget_for_deliverable(deliverable, spent, total, options={}) 12 | extra_css_class = options[:class] || '' 13 | 14 | if total > 0 || spent > 0 15 | spent_css_classes = 'spent-amount' 16 | spent_css_classes << " #{overage_class(spent, total)}" 17 | spent_css_classes << ' ' << extra_css_class 18 | total_css_classes = 'total-amount white' 19 | total_css_classes << ' ' << extra_css_class 20 | 21 | content_tag(:td, h(format_value_field_for_contracts(spent)), :class => spent_css_classes) + 22 | content_tag(:td, h(format_value_field_for_contracts(total)), :class => total_css_classes) 23 | else 24 | content_tag(:td, '----', :colspan => '2', :class => 'no-value ' + extra_css_class) 25 | end 26 | end 27 | 28 | def format_deliverable_value_fields(value) 29 | number_with_precision(value, :precision => Deliverable::ViewPrecision, :delimiter => '') 30 | end 31 | 32 | def format_deliverable_value_fields_as_dollar_or_percent(value) 33 | case 34 | when value.blank? || value.to_s.delete('$%').blank? 35 | '' 36 | when value.to_s.match('%') 37 | h(value) 38 | else # currency or straight amount 39 | number_to_currency(value.to_s.gsub('$',''), :precision => Deliverable::ViewPrecision, :delimiter => '', :unit => '$') 40 | end 41 | end 42 | 43 | def format_hourly_rate(decimal) 44 | number_to_currency(decimal, :precision => 0) + "/hr" if decimal 45 | end 46 | 47 | def format_payment_terms(value) 48 | return '' if value.blank? 49 | return h(value.name) 50 | end 51 | 52 | def format_value_field_for_contracts(value, options={}) 53 | opt = {:unit => '', :precision => Contract::ViewPrecision, :delimiter => ','}.merge(options) 54 | number_to_currency(value, opt) 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /app/helpers/contracts_helper.rb: -------------------------------------------------------------------------------- 1 | module ContractsHelper 2 | def setup_nested_deliverable_records(deliverable) 3 | deliverable.labor_budgets.build if deliverable.labor_budgets.empty? 4 | deliverable.overhead_budgets.build if deliverable.overhead_budgets.empty? 5 | deliverable.fixed_budgets.build if deliverable.fixed_budgets.empty? 6 | deliverable 7 | end 8 | 9 | def group_contracts_by_status(contracts) 10 | grouped_contracts = contracts.inject({}) do |grouped, contract| 11 | grouped[contract.status] ||= [] 12 | grouped[contract.status] << contract 13 | grouped 14 | end 15 | grouped_contracts["open"] ||= [] 16 | grouped_contracts["locked"] ||= [] 17 | grouped_contracts["closed"] ||= [] 18 | grouped_contracts 19 | end 20 | 21 | def grouped_deliverable_options_for_select(project, selected_key=nil) 22 | project.contracts.all(:include => :deliverables).inject("") do |html, contract| 23 | if contract.closed? && !contract.includes_deliverable_id?(selected_key) 24 | html 25 | else 26 | html << content_tag(:optgroup, 27 | deliverable_options_for_contract(contract, selected_key).join("\n"), 28 | :label => h(contract.name)) 29 | end 30 | end 31 | end 32 | 33 | def deliverable_options_for_contract(contract, selected_key) 34 | contract.deliverables.collect do |deliverable| 35 | deliverable_option(deliverable, selected_key) 36 | end 37 | end 38 | 39 | def deliverable_option(deliverable, selected_key) 40 | option_attributes = {} 41 | option_attributes[:value] = h(deliverable.id) 42 | option_attributes[:selected] = "selected" if selected_key.to_i == deliverable.id 43 | option_attributes[:disabled] = "disabled" if (deliverable.locked? || deliverable.contract_locked?) && selected_key.to_i != deliverable.id 44 | 45 | return "" if deliverable.closed? && option_attributes[:selected].blank? # Skip unselected, closed 46 | 47 | content_tag(:option, h(deliverable.title), option_attributes) 48 | end 49 | 50 | # Simple helper to show the values of a field on an object in a standard format 51 | # 52 | #

53 | # Label: 54 | # Field value 55 | #

56 | def show_field(object, field, options={}, &block) 57 | html_options = options[:html_options] || {} 58 | label_html_options = options[:label_html_options] || {} 59 | label = content_tag(:strong, l(("field_" + field.to_s.gsub(/\_id$/, "")).to_sym) + ": ", :class => 'contract-details-label') 60 | 61 | formatter = options[:format] 62 | raw_content = options[:raw] || false 63 | wrap_in_td = options[:wrap_in_td] 64 | wrap_in_td = true if wrap_in_td.nil? 65 | 66 | content = '' 67 | 68 | if block_given? 69 | content = yield 70 | else 71 | content = if formatter 72 | send(formatter, object.send(field)) 73 | else 74 | object.send(field) 75 | end 76 | end 77 | 78 | if raw_content 79 | field_content = content 80 | else 81 | field_content = h(content) 82 | end 83 | 84 | content_tag(:tr, 85 | content_tag(:td, label, label_html_options) + 86 | (wrap_in_td ? content_tag(:td, field_content) : field_content), 87 | html_options) 88 | end 89 | 90 | def show_budget_field(object, spent_field, total_field, options={}) 91 | 92 | formatter = options[:format] || :number_to_currency 93 | spent_value = object.send(spent_field) 94 | total_value = object.send(total_field) 95 | spent_content = send(formatter, spent_value) 96 | total_content = send(formatter, total_value) 97 | 98 | # Show overages except for profit fields 99 | overage_css_class = overage_class(spent_value, total_value) unless spent_field.to_s.match(/profit/i) 100 | 101 | show_field(object, spent_field, options.merge(:raw => true, :wrap_in_td => false)) do 102 | 103 | content_tag(:td, h(spent_content), :class => "spent #{overage_css_class}") + 104 | content_tag(:td, h(total_content), :class => 'budget') 105 | end 106 | end 107 | 108 | def retainer_period_options(deliverable, method_options={}) 109 | selected = method_options[:selected] 110 | if selected && selected.is_a?(Date) 111 | selected = selected.strftime("%Y-%m") 112 | end 113 | 114 | options = [] 115 | options << content_tag(:option, l(:label_all).capitalize, :value => '') 116 | 117 | deliverable.months.collect do |month| 118 | value = month.strftime("%Y-%m") 119 | options << content_tag(:option, month.strftime("%B %Y"), :value => value, :selected => (selected == value) ? 'selected' : nil) 120 | end 121 | 122 | options 123 | end 124 | 125 | # Given a deliverable and period, validate the period 126 | # TODO: could use a better name 127 | def validate_period(deliverable, period) 128 | if deliverable.current_date && deliverable.within_period_range?(period) 129 | return period 130 | end 131 | end 132 | 133 | # Should the markup be display? 134 | # 135 | # On Contracts and Deliverables, markup is hidden if both the spent 136 | # and budget is 0. 137 | def show_markup_for?(object, date=nil) 138 | if object.is_a?(Contract) 139 | !(object.fixed_markup_spent == 0 && object.fixed_markup_budget == 0) 140 | elsif object.is_a?(Deliverable) 141 | !(object.fixed_markup_budget_total_spent(date) == 0 && object.fixed_markup_budget_total(date) == 0) 142 | else 143 | true 144 | end 145 | 146 | end 147 | 148 | def link_to_issue_list_with_filter(text, options={}) 149 | deliverable_id = options[:deliverable_id] || '*' 150 | status_id = options[:status_id] || '*' 151 | 152 | link_to(h(text), { 153 | :controller => 'issues', 154 | :action => 'index', 155 | :project_id => @project, 156 | :set_filter => 't', 157 | :status_id => status_id, 158 | :deliverable_id => deliverable_id 159 | }) 160 | 161 | end 162 | 163 | def release(version=5, message='') 164 | return '' unless (1..5).include?(version) 165 | image_tag("todo#{version}.png", :plugin => 'redmine_contracts', :title => "Coming in release #{version}. #{message}") 166 | end 167 | 168 | # Overage occurs when spent is negative or spent is greater than budget 169 | def overage?(spent, budget, options={}) 170 | return false unless spent && budget 171 | return true if spent < 0 172 | 173 | spent.to_f > budget.to_f 174 | end 175 | 176 | def overage_class(spent, budget, options={}) 177 | if overage?(spent, budget, options) 178 | 'overage' 179 | else 180 | '' 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /app/helpers/deliverables_helper.rb: -------------------------------------------------------------------------------- 1 | # Defined for redmine_overhead compatibility 2 | module DeliverablesHelper 3 | end 4 | -------------------------------------------------------------------------------- /app/models/fixed_budget.rb: -------------------------------------------------------------------------------- 1 | class FixedBudget < ActiveRecord::Base 2 | unloadable 3 | 4 | # Associations 5 | belongs_to :deliverable 6 | 7 | # Validations 8 | 9 | # Accessors 10 | include DollarizedAttribute 11 | dollarized_attribute :budget 12 | 13 | named_scope :by_period, lambda {|date| 14 | if date 15 | { 16 | :conditions => {:year => date.year, :month => date.month} 17 | } 18 | end 19 | } 20 | 21 | named_scope :paid, {:conditions => {:paid => true}} 22 | 23 | def markup_value 24 | return 0 if budget.blank? || markup.blank? 25 | 26 | case 27 | when percent_markup? 28 | percent = markup.gsub('%','').to_f 29 | return budget.to_f * (percent / 100) 30 | when dollar_markup? 31 | markup.gsub('$','').gsub(',','').to_f 32 | when straight_markup? 33 | markup.to_f 34 | else 35 | 0 # Invalid markup 36 | end 37 | 38 | end 39 | 40 | def budget_spent 41 | if paid? 42 | budget 43 | else 44 | 0 45 | end 46 | end 47 | 48 | def percent_markup? 49 | markup && markup.to_s.match(/%/) 50 | end 51 | 52 | def dollar_markup? 53 | markup && markup.to_s.match(/[$,]+/) 54 | end 55 | 56 | def straight_markup? 57 | markup && markup.to_s.match(/[\d.]/) 58 | end 59 | 60 | # Is this a blank budget item. Retainers will create blank ones when 61 | # they are copied. (RetainerDeliverable#create_budgets_for_periods) 62 | def blank_record? 63 | return true if new_record? 64 | return title.blank? && budget.blank? && markup.blank? 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /app/models/fixed_deliverable.rb: -------------------------------------------------------------------------------- 1 | class FixedDeliverable < Deliverable 2 | unloadable 3 | 4 | # Associations 5 | 6 | # Validations 7 | 8 | # Accessors 9 | 10 | def short_type 11 | 'F' 12 | end 13 | 14 | def total(date=nil) 15 | read_attribute(:total) || 0.0 16 | end 17 | 18 | # Fixed deliverables are always 100% spent 19 | def total_spent(date=nil) 20 | total 21 | end 22 | 23 | # Fixed deliverables are always 100% spent so they markup is captured 24 | # right away. 25 | def fixed_markup_budget_total_spent(date=nil) 26 | memoize_by_date("@fixed_markup_budget_total_spent", date) do 27 | fixed_markup_budget_total(date) 28 | end 29 | end 30 | 31 | # Hardcoded value used as a wrapper for the old Budget plugin API. 32 | # 33 | # The Overhead plugin uses this in it's calcuations. 34 | def fixed_cost 35 | 0 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/models/hourly_deliverable.rb: -------------------------------------------------------------------------------- 1 | class HourlyDeliverable < Deliverable 2 | unloadable 3 | 4 | # Associations 5 | 6 | # Validations 7 | 8 | # Accessors 9 | 10 | # Callbacks 11 | before_save :clear_total 12 | 13 | def short_type 14 | 'H' 15 | end 16 | 17 | # Total = ( Labor Hours * Billing Rate ) + ( Fixed + Markup ) 18 | def total(date=nil) 19 | memoize_by_date("@total", date) do 20 | return 0 if contract.nil? 21 | return 0 if contract.billable_rate.blank? 22 | return 0 if labor_budgets.count == 0 && overhead_budgets.count == 0 23 | 24 | fixed_budget_amount = fixed_budget_total(date) + fixed_markup_budget_total(date) 25 | return (contract.billable_rate * labor_budget_hours(date)) + fixed_budget_amount 26 | end 27 | end 28 | 29 | # Total amount to be billed on the deliverable, using the total time logged 30 | # and the contract rate 31 | def total_spent(date=nil) 32 | memoize_by_date("@total_spent", date) do 33 | return 0 if contract.nil? 34 | return 0 if contract.billable_rate.blank? 35 | return 0 unless self.issues.count > 0 36 | 37 | time_logs = self.issues.collect(&:time_entries).flatten 38 | hours = billable_hours_on_time_entries(time_logs) 39 | 40 | fixed_budget_amount = fixed_budget_total_spent(date) + fixed_markup_budget_total_spent(date) 41 | return (hours * contract.billable_rate) + fixed_budget_amount 42 | end 43 | end 44 | 45 | # Block setting the total on HourlyDeliverables 46 | def total=(v) 47 | nil 48 | end 49 | 50 | def clear_total 51 | write_attribute(:total, nil) 52 | end 53 | 54 | protected 55 | 56 | def billable_hours_on_time_entries(time_entries) 57 | hours_on_time_entries_with_billable_option(true, time_entries) 58 | end 59 | 60 | def nonbillable_hours_on_time_entries(time_entries) 61 | hours_on_time_entries_with_billable_option(false, time_entries) 62 | end 63 | 64 | def hours_on_time_entries_with_billable_option(billable, time_entries) 65 | time_entries.inject(0) {|total, time_entry| 66 | total += time_entry.hours if (time_entry.billable? == billable) 67 | total 68 | } 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /app/models/labor_budget.rb: -------------------------------------------------------------------------------- 1 | class LaborBudget < ActiveRecord::Base 2 | unloadable 3 | 4 | # Associations 5 | belongs_to :deliverable 6 | belongs_to :time_entry_activity 7 | 8 | # Validations 9 | validates_presence_of :time_entry_activity_id 10 | 11 | # Accessors 12 | include DollarizedAttribute 13 | dollarized_attribute :budget 14 | end 15 | -------------------------------------------------------------------------------- /app/models/overhead_budget.rb: -------------------------------------------------------------------------------- 1 | class OverheadBudget < ActiveRecord::Base 2 | unloadable 3 | 4 | # Associations 5 | belongs_to :deliverable 6 | belongs_to :time_entry_activity 7 | 8 | # Validations 9 | validates_presence_of :time_entry_activity_id 10 | 11 | # Accessors 12 | include DollarizedAttribute 13 | dollarized_attribute :budget 14 | end 15 | -------------------------------------------------------------------------------- /app/models/payment_term.rb: -------------------------------------------------------------------------------- 1 | class PaymentTerm < Enumeration 2 | unloadable 3 | 4 | has_many :contracts, :foreign_key => 'payment_term_id' 5 | 6 | OptionName = :enumeration_payment_term 7 | 8 | def option_name 9 | OptionName 10 | end 11 | 12 | def objects_count 13 | contracts.count 14 | end 15 | 16 | def transfer_relations(to) 17 | contracts.update_all("payment_term_id = #{to.id}") 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/views/contracts/_form.html.erb: -------------------------------------------------------------------------------- 1 | <% if resource.locked? || resource.closed? %> 2 |
3 |

<%= resource.locked? ? l(:text_contract_locked_warning) : l(:text_contract_closed_warning) %>

4 |
5 | <% end %> 6 | 7 |
8 | <% form.inputs :name => l(:text_general_legend) do %> 9 | <%= form.input :name, :required => true %> 10 | <%= form.input :status, :required => true, :collection => [["Open","open"],["Locked","locked"],["Closed","closed"]], :include_blank => false %> 11 | <%= form.input :account_executive, :required => true, :collection => @project.users.sort %> 12 |
  • 13 | <%= label('contract', 'executed') %> 14 | <%= check_box 'contract', 'executed' %> 15 |
  • 16 | <%= form.input :start_date, :required => true, :as => :string, :input_html => {:size => 10}, :hint => calendar_for('contract_start_date') %> 17 | <%= form.input :end_date, :required => true, :as => :string, :input_html => {:size => 10}, :hint => calendar_for('contract_end_date') %> 18 | <%= form.input :details, :input_html => {:class => 'wiki-edit'} %> 19 | <% end %> 20 | 21 | <% form.inputs :name => l(:text_budget_legend) do %> 22 | <%= form.input :billable_rate, :input_html => {:size => 20}, :hint => l(:field_billable_rate_hint) %> 23 | <%= form.input :discount, :input_html => {:size => 20}, :hint => l(:field_discount_hint) %> 24 | <%= form.input :discount_note, :input_html => {:class => 'wiki-edit', :rows => '5'} %> 25 | <% end %> 26 | 27 | <% form.inputs :name => l(:text_account_legend) do %> 28 | <%= form.input :payment_term %> 29 | <%= form.input :po_number %> 30 | <%= form.input :client_ap_contact_information, :input_html => {:class => 'wiki-edit', :rows => '5'} %> 31 | <%= form.input :client_point_of_contact, :input_html => {:class => 'wiki-edit', :rows => '5'} %> 32 | <% end %> 33 |
    34 | 35 | <% form.buttons do %> 36 |
      37 |
    1. 38 | <%= submit_tag(l(:text_save_contract)) %> 39 | <%= link_to(l(:button_cancel), cancel_path) %> 40 |
    2. 41 |
    42 | <% end %> 43 | 44 | <%= wikitoolbar_for 'contract_discount_note' %> 45 | <%= wikitoolbar_for 'contract_client_ap_contact_information' %> 46 | <%= wikitoolbar_for 'contract_client_point_of_contact' %> 47 | <%= wikitoolbar_for 'contract_details' %> 48 | 49 | -------------------------------------------------------------------------------- /app/views/contracts/_title.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= content_tag(:h2, h(contract.name)) %> 3 | 4 |
    5 | <%= release(4, "Log time") %> 6 | 7 | | 8 | 9 | <%= release(3, "Watch") %> 10 | 11 | | 12 | 13 | <%= release(5, "Copy") %> 14 | | 15 | 16 |
    17 | <%= link_to(l(:button_update), edit_contract_path(@project, contract)) %> 18 |
    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /app/views/contracts/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% if User.current.admin? %> 2 |
    3 | <%= link_to(l(:button_delete), contract_path(@project, resource), :method => :delete, :confirm => l(:text_are_you_sure), :class => 'icon icon-del contract-delete') %> 4 |
    5 | <% end %> 6 | 7 | <%= content_tag(:h2, h(l(:text_edit_contract_name, :name => resource.name))) %> 8 | 9 | <%= error_messages_for 'contract' %> 10 | 11 | <% semantic_form_for resource, :url => contract_path(@project, resource), :html => {:class => 'tabular'} do |form| %> 12 | <%= render :partial => 'form', :object => form, :locals => {:cancel_path => contract_path(@project, resource)} %> 13 | <% end %> 14 | 15 | <% html_title "#{l(:text_contracts)} - #{h(l(:text_edit_contract_name, :name => resource.name))}" %> 16 | -------------------------------------------------------------------------------- /app/views/contracts/index.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | 5 |
    6 |
    7 | <%= link_to(l(:text_new_contract), new_contract_path) %> 8 |
    9 |
    10 | 11 |

    Active Contacts

    12 | 13 |
    14 | 15 | <% if group_contracts_by_status(collection)["open"].empty? %> 16 |

    <%= l(:label_no_data) %>

    17 | <% else %> 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | <% group_contracts_by_status(collection)["open"].each do |contract| %> 30 | <% content_tag_for(:tr, contract, :class => cycle('','odd')) do %> 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | <% end %> 39 | <% end %> 40 | 41 |
    <%= l(:field_id) %><%= l(:field_name) %><%= l(:field_status) %><%= l(:field_type) %><%= l(:field_account_executive_short) %><%= l(:field_total_budget) %><%= l(:field_end_date) %>
    <%= link_to(h(contract.id), contract_path(@project, contract)) %><%= link_to(h(contract.name), contract_path(@project, contract)) %><%= h(contract.status) %><%= release(5, "Contract Type") %><%= h(format_value_field_for_contracts(contract.total_budget)) %><%= h format_date(contract.end_date) %>
    42 | <% end %> 43 | 44 |
    45 |

    Locked Contracts

    46 |
    47 | 48 | <% if group_contracts_by_status(collection)["locked"].empty? %> 49 |

    <%= l(:label_no_data) %>

    50 | <% else %> 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | <% group_contracts_by_status(collection)["locked"].each do |contract| %> 63 | <% content_tag_for(:tr, contract, :class => cycle('','odd')) do %> 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | <% end %> 72 | <% end %> 73 | 74 |
    <%= l(:field_id) %><%= l(:field_name) %><%= l(:field_status) %><%= l(:field_type) %><%= l(:field_account_executive_short) %><%= l(:field_total_budget) %><%= l(:field_end_date) %>
    <%= link_to(h(contract.id), contract_path(@project, contract)) %><%= link_to(h(contract.name), contract_path(@project, contract)) %><%= h(contract.status) %><%= release(5, "Contract Type") %><%= h(format_value_field_for_contracts(contract.total_budget)) %><%= h format_date(contract.end_date) %>
    75 | <% end %> 76 | 77 |
    78 |

    Closed Contracts

    79 |
    80 | 81 | <% if group_contracts_by_status(collection)["closed"].empty? %> 82 |

    <%= l(:label_no_data) %>

    83 | <% else %> 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | <% group_contracts_by_status(collection)["closed"].each do |contract| %> 96 | <% content_tag_for(:tr, contract, :class => cycle('','odd')) do %> 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | <% end %> 105 | <% end %> 106 | 107 |
    <%= l(:field_id) %><%= l(:field_name) %><%= l(:field_status) %><%= l(:field_type) %><%= l(:field_account_executive_short) %><%= l(:field_total_budget) %><%= l(:field_end_date) %>
    <%= link_to(h(contract.id), contract_path(@project, contract)) %><%= link_to(h(contract.name), contract_path(@project, contract)) %><%= h(contract.status) %><%= release(5, "Contract Type") %><%= h(format_value_field_for_contracts(contract.total_budget)) %><%= h format_date(contract.end_date) %>
    108 | <% end %> 109 | 110 | 111 |
    112 | 113 | <% html_title "#{l(:text_contracts)}" %> 114 | -------------------------------------------------------------------------------- /app/views/contracts/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_tag(:h2, l(:text_new_contract)) %> 2 | 3 | <%= error_messages_for 'contract' %> 4 | 5 | <% semantic_form_for resource, :html => {:class => 'tabular'} do |form| %> 6 | <%= render :partial => 'form', :object => form, :locals => {:cancel_path => contracts_path} %> 7 | <% end %> 8 | 9 | <% html_title "#{l(:text_new_contract)}" %> 10 | -------------------------------------------------------------------------------- /app/views/deliverables/_finance_form.html.erb: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 18 | 19 | <% form.inputs :name => label, :class => "deliverable-finances #{fieldset_class}" do %> 20 | 21 | 24 | 25 |
  • 26 | <%= content_tag(:label, l(:field_labor)) %> 27 | 28 | 29 | <% form.fields_for :labor_budgets, labor_budgets.sort_by {|b| b.id || 0 } do |labor_budget| %> 30 | <%= render :partial => 'labor_budget_form', :locals => {:labor_budget => labor_budget} %> 31 | <% end %> 32 | 33 |
    34 |
  • 35 | 36 |
  • 37 | <%= content_tag(:label, l(:field_overhead)) %> 38 | 39 | 40 | <% form.fields_for :overhead_budgets, overhead_budgets.sort_by {|b| b.id || 0 } do |overhead_budget| %> 41 | <%= render :partial => 'overhead_budget_form', :locals => {:overhead_budget => overhead_budget} %> 42 | <% end %> 43 | 44 |
    45 |
  • 46 | 47 |
  • 48 |
    49 | 50 | 51 | <% form.fields_for :fixed_budgets, fixed_budgets.sort_by {|b| b.id || 0 } do |fixed_budget| %> 52 | <%= render :partial => 'fixed_budget_form', :locals => {:fixed_budget => fixed_budget} %> 53 | <%= wikitoolbar_for "fixed-description#{fixed_budget.object.object_id}" %> 54 | <% end %> 55 |
    56 | 57 | 58 |
  • 59 | 60 | <%= form.input :total, :input_html => {:size => 10}, :wrapper_html => {:class => 'deliverable_total_input'}, :hint => l(:text_dollar_sign) %> 61 | 62 | <% end %> 63 | -------------------------------------------------------------------------------- /app/views/deliverables/_fixed_budget_form.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= fixed_budget.hidden_field(:id) unless fixed_budget.object.new_record? %> 3 | <%= fixed_budget.hidden_field(:year) %> 4 | <%= fixed_budget.hidden_field(:month) %> 5 | 6 |

    <%= fixed_budget.label(:title, l(:field_title))%> 7 | <%= fixed_budget.text_field(:title) %> 8 |

    9 | 10 |

    11 | <%= fixed_budget.label(:budget, l(:field_budget))%> <%= l(:text_dollar_sign) %> 12 | <%= fixed_budget.text_field(:budget, :value => format_deliverable_value_fields(fixed_budget.object.budget), :class => 'financial') %> 13 |

    14 | 15 |

    16 | <%= fixed_budget.label(:markup, l(:field_markup)) %> <%= l(:field_discount_hint) %> 17 | <%= fixed_budget.text_field(:markup, :value => format_deliverable_value_fields_as_dollar_or_percent(fixed_budget.object.markup), :class => 'financial') %> 18 |

    19 | 20 |

    21 | <%= fixed_budget.label(:paid, l(:field_paid)) %> 22 | <%= fixed_budget.check_box(:paid) %> 23 |

    24 | 25 | 26 | <%= fixed_budget.text_area(:description, :class => 'wiki-edit', :rows => '5', :id => "fixed-description#{fixed_budget.object.object_id}") %> 27 | 28 |

    29 | <%= fixed_budget.hidden_field "_destroy", :class=> "delete-flag" %> 30 | <%= link_to_function(l(:button_delete), 'deleteDeliverableFinance(this)', :class => 'delete icon icon-del') %> 31 | <%= link_to_function(l(:button_add), 'addNewDeliverableFixedItem()', :class => 'add icon icon-add', :style => 'display:none;') %> 32 |

    33 |
    34 | -------------------------------------------------------------------------------- /app/views/deliverables/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= javascript_tag("var i18nStartDateEmpty = '#{l(:text_start_date_empty)}'") %> 2 | <%= javascript_tag("var i18nEndDateEmpty = '#{l(:text_end_date_empty)}'") %> 3 | <%= javascript_tag("var i18nChangedPeriodMessage = '#{l(:text_changed_period_message)}'") %> 4 | <%= javascript_tag("var i18nAreYouSure = '#{l(:text_are_you_sure)}'") %> 5 | 6 | <% if resource.locked? || resource.closed? || resource.contract_locked? || resource.contract_closed? %> 7 |
    8 | <% if resource.contract_locked? || resource.contract_closed? %> 9 |

    <%= resource.contract_locked? ? l(:text_contract_locked_warning) : l(:text_contract_closed_warning) %>

    10 | <% end %> 11 | <% if resource.locked? || resource.closed? %> 12 |

    <%= resource.locked? ? l(:text_deliverable_locked_warning) : l(:text_deliverable_closed_warning) %>

    13 | <% end %> 14 |
    15 | <% end %> 16 | 17 |
    18 | <% form.inputs :name => l(:text_deliverable_details_legend), :id => 'deliverable-details' do %> 19 | <%# Used by jquery to check if this is a new or existing record %> 20 | <%= hidden_field_tag('deliverable_id', h(resource.id), :id => 'deliverable_stored_id') %> 21 | <%= form.input :title, :required => true %> 22 | <% if resource.new_record? %> 23 |
  • 24 | <%= form.label(:type, l(:field_type)) %> 25 | <%= form.select(:type, Deliverable.valid_types_to_select, {:include_blank => false}, {:class => 'type'}) %> 26 |
  • 27 | <% else %> 28 |
  • 29 | <%= form.label(:type, l(:field_type)) %> 30 | <%= h(resource.humanize_type) %> 31 |
  • 32 | <%= form.input :type, :as => :hidden, :class => 'type' %> 33 | <% end %> 34 | <%= form.input :status, :required => true, :collection => [["Open","open"],["Locked","locked"],["Closed","closed"]], :include_blank => false %> 35 | <%= form.input :manager, :required => true, :collection => @project.users.sort %> 36 | 37 | <%= form.input :start_date, :as => :string, :input_html => {:size => 10, :class => 'start-date', :id => 'deliverable_start_date'}, :hint => calendar_for('deliverable_start_date') %> 38 | <%= hidden_field_tag('deliverable_stored_start_date', h(resource.start_date), :id => 'deliverable_stored_start_date') %> 39 | 40 | <%= form.input :end_date, :as => :string, :input_html => {:size => 10, :class => 'end-date', :id => 'deliverable_end_date'}, :hint => calendar_for('deliverable_end_date') %> 41 | <%= hidden_field_tag('deliverable_stored_end_date', h(resource.end_date), :id => 'deliverable_stored_end_date') %> 42 | 43 | <%= form.input :notes, :input_html => {:class => 'wiki-edit', :rows => '5'} %> 44 | 45 | <% unless resource.new_record? %> 46 |
  • 47 | <%= label(resource.class.to_s.underscore, 'feature_sign_off') %> 48 | <%= check_box resource.class.to_s.underscore, 'feature_sign_off' %> 49 |
  • 50 |
  • 51 | <%= label(resource.class.to_s.underscore, 'warranty_sign_off') %> 52 | <%= check_box resource.class.to_s.underscore, 'warranty_sign_off' %> 53 |
  • 54 | <% end %> 55 | <% end %> 56 | 57 | <% if resource.retainer? && resource.respond_to?(:months) %> 58 | <% if resource.months.present? %> 59 | <% resource.months.each do |month| %> 60 | <%= 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') } %> 61 | <% end %> 62 | <% else %> 63 | <%= content_tag(:p, l(:text_missing_period), :class => 'nodata') %> 64 | <% end %> 65 | <% else %> 66 | <%= render :partial => 'finance_form', :locals => {:form => form, :labor_budgets => resource.labor_budgets, :overhead_budgets => resource.overhead_budgets, :fixed_budgets => resource.fixed_budgets, :label => l(:text_deliverable_finances), :fieldset_class => '' } %> 67 | <% end %> 68 | 69 |
    70 | 71 | <% form.buttons do %> 72 |
      73 |
    1. 74 | <%= submit_tag(l(:button_save)) %> 75 | <%= link_to(l(:button_cancel), cancel_path) %> 76 |
    2. 77 |
    78 | <% end %> 79 | 80 | <%= wikitoolbar_for resource.to_underscore + '_notes' %> 81 | -------------------------------------------------------------------------------- /app/views/deliverables/_labor_budget_form.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= labor_budget.hidden_field(:id) unless labor_budget.object.new_record? %> 4 | <%= labor_budget.hidden_field(:year) %> 5 | <%= labor_budget.hidden_field(:month) %> 6 | 7 | <%= labor_budget.label(:time_entry_activity_id, :class => "hidden") %> 8 | <%= 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'}) %> 9 | 10 | 11 |

    <%= labor_budget.label(:hours, l(:text_short_hours)) %>

    12 | <%= labor_budget.text_field(:hours, :value => format_deliverable_value_fields(labor_budget.object.hours), :class => 'financial') %> 13 | 14 | 15 |

    <%= labor_budget.label(:budget, l(:text_dollar_sign)) %>

    16 | <%= labor_budget.text_field(:budget, :value => format_deliverable_value_fields(labor_budget.object.budget), :class => 'financial') %> 17 | 18 | 19 | <%= labor_budget.hidden_field "_destroy", :class=> "delete-flag" %> 20 | <%= link_to_function(l(:button_delete), 'deleteDeliverableFinance(this)', :class => 'delete icon icon-del') %> 21 | <%= link_to_function(l(:button_add), 'addNewDeliverableLaborItem()', :class => 'add icon icon-add', :style => 'display:none;') %> 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/views/deliverables/_overhead_budget_form.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= overhead_budget.hidden_field(:year) %> 4 | <%= overhead_budget.hidden_field(:month) %> 5 | 6 | <%= overhead_budget.label(:time_entry_activity_id, :class => "hidden") %> 7 | <%= 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'}) %> 8 | 9 | 10 |

    <%= overhead_budget.label(:hours, l(:text_short_hours)) %>

    11 | <%= overhead_budget.text_field(:hours, :value => format_deliverable_value_fields(overhead_budget.object.hours),:class => 'financial') %> 12 | 13 | 14 |

    <%= overhead_budget.label(:budget, l(:text_dollar_sign)) %>

    15 | <%= overhead_budget.text_field(:budget, :value => format_deliverable_value_fields(overhead_budget.object.budget), :class => 'financial') %> 16 | 17 | 18 | <%= overhead_budget.hidden_field "_destroy", :class=> "delete-flag" %> 19 | <%= link_to_function(l(:button_delete), 'deleteDeliverableFinance(this)', :class => 'delete icon icon-del') %> 20 | <%= link_to_function(l(:button_add), 'addNewDeliverableOverheadItem()', :class => 'add icon icon-add', :style => 'display:none;') %> 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/views/deliverables/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= render :partial => 'contracts/title', :locals => {:contract => @contract} %> 2 | 3 | <%= content_tag(:h2, h(l(:text_edit_deliverable_title, :title => resource.title))) %> 4 | 5 | <%= error_messages_for 'deliverable' %> 6 | 7 | <% semantic_form_for [@project, @contract, setup_nested_deliverable_records(resource)], :url => contract_deliverable_path(@project, @contract, resource), :html => {:class => 'deliverable tabular'} do |form| %> 8 | <%= render :partial => 'form', :object => form, :locals => {:cancel_path => contract_path(@project, @contract)} %> 9 | <% end %> 10 | 11 | <% html_title "#{l(:field_deliverable_plural)} - #{h(l(:text_edit_deliverable_title, :title => resource.title))}" %> 12 | -------------------------------------------------------------------------------- /app/views/deliverables/finances.html.erb: -------------------------------------------------------------------------------- 1 | <%= render :partial => 'finances', :locals => {:contract => @contract, :deliverable => @contract.deliverables.find(params[:id])} %> 2 | -------------------------------------------------------------------------------- /app/views/deliverables/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= render :partial => 'contracts/title', :locals => {:contract => @contract} %> 2 | 3 | <%= content_tag(:h2, l(:text_new_deliverable)) %> 4 | 5 | <%= error_messages_for 'deliverable' %> 6 | 7 | <% semantic_form_for [@project, @contract, setup_nested_deliverable_records(resource)], :url => contract_deliverables_path(@project, @contract), :html => {:class => 'deliverable tabular'} do |form| %> 8 | <%= render :partial => 'form', :object => form, :locals => {:cancel_path => contract_path(@project, @contract)} %> 9 | <% end %> 10 | 11 | <% html_title "#{l(:field_deliverable_plural)} - #{l(:text_new_deliverable)}" %> 12 | -------------------------------------------------------------------------------- /app/views/issues/_bulk_edit_deliverable.html.erb: -------------------------------------------------------------------------------- 1 | <% if project && project.module_enabled?(:contracts) && User.current.allowed_to?(:assign_deliverable_to_issue, project) %> 2 |

    3 | <%= label_tag(:deliverable_id, l(:field_deliverable)) %> 4 | <%= select_tag('deliverable_id', 5 | content_tag('option', l(:label_no_change_option), :value => '') + 6 | content_tag('option', l(:label_none), :value => 'none') + 7 | grouped_deliverable_options_for_select(project)) %> 8 |

    9 | <% end %> 10 | 11 | -------------------------------------------------------------------------------- /app/views/issues/_edit_deliverable.html.erb: -------------------------------------------------------------------------------- 1 | <% if project.module_enabled?(:contracts) && User.current.allowed_to?(:assign_deliverable_to_issue, project) %> 2 |

    3 | <%= form.select(:deliverable_id, grouped_deliverable_options_for_select(project, issue.deliverable_id), {:include_blank => true}) %> 4 |

    5 | <% end %> 6 | 7 | -------------------------------------------------------------------------------- /app/views/issues/_show_deliverable.html.erb: -------------------------------------------------------------------------------- 1 | <% if project.module_enabled?(:contracts) %> 2 | 3 | <%= l(:field_deliverable) %>: 4 | <%= h(issue.deliverable.title) if issue.deliverable.present? %> 5 | 6 | <% end %> 7 | 8 | -------------------------------------------------------------------------------- /assets/images/todo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/images/todo1.png -------------------------------------------------------------------------------- /assets/images/todo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/images/todo2.png -------------------------------------------------------------------------------- /assets/images/todo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/images/todo3.png -------------------------------------------------------------------------------- /assets/images/todo4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/images/todo4.png -------------------------------------------------------------------------------- /assets/images/todo5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/images/todo5.png -------------------------------------------------------------------------------- /assets/javascripts/contracts.js: -------------------------------------------------------------------------------- 1 | jQuery(function($) { 2 | $("#ajax-indicator").ajaxStart(function(){ $(this).show().css('z-index', '9999'); }); 3 | $("#ajax-indicator").ajaxStop(function(){ $(this).hide(); }); 4 | 5 | var right_align = $('#contract-terms .finance tr td:nth-child(1) ~ td, .c_overview table.right tr td:nth-child(1) ~ td, #deliverables table tr.click td:nth-child(5) ~ td, .deliverable_finance_table tr.aright td:nth-child(1) ~ td'); 6 | 7 | if (right_align.length > 0) { 8 | right_align.after().css("text-align", "right"); 9 | } 10 | 11 | $("#deliverables table tbody tr td:contains('---')").css("text-align", "center"); 12 | 13 | 14 | $(".texpand").jExpand(); 15 | 16 | $(".texpand").find("tr.even").next('tr:first').addClass("even"); 17 | 18 | $(window).resize(function() { 19 | 20 | }); 21 | 22 | $('#expand_terms').click( function(){ 23 | $(this).next().slideToggle(); 24 | $(this).toggleClass('alt'); 25 | 26 | var new_height = $('#contract-terms .info').height() - $('#contract-terms .finance').height() + 30; 27 | $('#contract-terms .stretch').css('height', new_height); 28 | }); 29 | 30 | showDeliverableTotal = function() { 31 | $('.deliverable_total_input').show(); 32 | }, 33 | 34 | hideDeliverableTotal = function() { 35 | $('.deliverable_total_input'). 36 | children('input').val('').end(). 37 | hide(); 38 | }, 39 | 40 | showDeliverableFrequency = function() { 41 | $('#deliverable_frequency').show(); 42 | }, 43 | 44 | hideDeliverableFrequency = function() { 45 | $('#deliverable_frequency').hide(); 46 | }, 47 | 48 | toggleSpecificDeliverableFields = function(form) { 49 | var deliverableType = form.find('.type').val(); 50 | 51 | if (deliverableType == 'FixedDeliverable') { 52 | showDeliverableTotal(); 53 | hideDeliverableFrequency(); 54 | $('#retainer-finances-message').hide(); 55 | } else if(deliverableType == "HourlyDeliverable") { 56 | hideDeliverableTotal(); 57 | hideDeliverableFrequency(); 58 | $('#retainer-finances-message').hide(); 59 | } else if(deliverableType == "RetainerDeliverable") { 60 | hideDeliverableTotal(); 61 | showDeliverableFrequency(); 62 | if ($('form.deliverable #deliverable_stored_id').val() == '') { 63 | $('#retainer-finances-message').show(); 64 | } else { 65 | $('#retainer-finances-message').hide(); 66 | } 67 | 68 | } 69 | }, 70 | 71 | showDeliverableAddButtons = function() { 72 | var laborLinks = $('table.deliverable_finance_table .add-labor a.add') 73 | if (laborLinks.length == 0) { 74 | // No link, add a blank form 75 | addNewDeliverableLaborItem(); 76 | } else { 77 | laborLinks.hide().last().show(); 78 | } 79 | var overheadLinks = $('table.deliverable_finance_table .add-overhead a.add') 80 | if (overheadLinks.length == 0) { 81 | // No link, add a blank form 82 | addNewDeliverableOverheadItem(); 83 | } else { 84 | overheadLinks.hide().last().show(); 85 | } 86 | var fixedLinks = $('#deliverable-fixed .fixed-budget-form .add-fixed a.add') 87 | if (fixedLinks.length == 0) { 88 | // No link, add a blank form 89 | addNewDeliverableFixedItem(); 90 | } else { 91 | fixedLinks.hide().last().show(); 92 | } 93 | }, 94 | 95 | addNewDeliverableLaborItem = function() { 96 | addNewDeliverableFinance('#labor-budget-template', 97 | '#deliverable-labor tbody', 98 | $("tr.labor-budget-form").size(), 99 | ''); 100 | }, 101 | 102 | addNewDeliverableOverheadItem = function() { 103 | addNewDeliverableFinance('#overhead-budget-template', 104 | '#deliverable-overhead tbody', 105 | $("tr.overhead-budget-form").size(), 106 | ''); 107 | }, 108 | 109 | addNewDeliverableFixedItem = function() { 110 | addNewDeliverableFinance('#fixed-budget-template', 111 | '#deliverable-fixed.fixed-item-form', 112 | $("div.fixed-budget-form").size(), 113 | '
    '); 114 | }, 115 | 116 | addNewDeliverableFinance = function(templateSelector, appendTemplateTo, countOfExisting, wrapperElement) { 117 | var t = $(templateSelector).tmpl({}); 118 | if (t.length > 0) { 119 | var recordLocation = countOfExisting + 1; // increments the Rails [n] placeholder 120 | var newContent = t.html().replace(/\[0\]/g, "[" + recordLocation + "]"); 121 | // New ids for textareas for the jsToolBar to attach to 122 | newContent = newContent.replace(/fixed-description\d*/g, "fixed-description" + Math.floor(Math.random() * 100000000)) 123 | var newItem = $(wrapperElement).html(newContent) 124 | 125 | newItem.appendTo(appendTemplateTo); 126 | newItem.find("textarea.wiki-edit").each(function () { 127 | attachWikiToolbar(this.id); 128 | }); 129 | showDeliverableAddButtons(); 130 | } 131 | }, 132 | 133 | // Set the deleted flag for Rails and move it out of the row into 134 | // a hidden table 135 | deleteDeliverableFinance = function(deleteLink) { 136 | if (confirm(i18nAreYouSure)) { 137 | $(deleteLink).parent().find('.delete-flag').val('1') 138 | if ($('#deleted-finances').length == 0) { 139 | $(deleteLink). 140 | closest("form"). 141 | append($("")); 142 | } 143 | $('#deleted-finances').append( 144 | $(deleteLink). // 145 | parent(). // 146 | parent().hide() 147 | ); // 148 | showDeliverableAddButtons(); 149 | } 150 | }, 151 | 152 | showDeliverableAddButtons(); 153 | toggleSpecificDeliverableFields($('form.deliverable')); 154 | 155 | $('select#deliverable_type').change(function() { 156 | toggleSpecificDeliverableFields($('form.deliverable')); 157 | }); 158 | 159 | $('form.deliverable').submit(function() { 160 | var deliverableType = $('form.deliverable').find('.type').val(); 161 | 162 | if (deliverableType == 'RetainerDeliverable') { 163 | if ($('form.deliverable .start-date[value!=""]').length == 0) { 164 | return confirm(i18nStartDateEmpty); 165 | } 166 | if ($('form.deliverable .end-date[value!=""]').length == 0) { 167 | return confirm(i18nEndDateEmpty); 168 | } 169 | 170 | if ($('form.deliverable #deliverable_stored_id').val() != '') { 171 | if ($('form.deliverable .start-date').val() != $('#deliverable_stored_start_date').val()) { 172 | return confirm(i18nChangedPeriodMessage); 173 | } 174 | if ($('form.deliverable .end-date').val() != $('#deliverable_stored_end_date').val()) { 175 | return confirm(i18nChangedPeriodMessage); 176 | } 177 | } 178 | 179 | } 180 | }); 181 | 182 | $('select.retainer_period_change').live('change', function() { 183 | var deliverable_url = $(this).closest('form').attr('action'); 184 | $(this).closest('tr').load(deliverable_url, this.form.serialize()); 185 | }); 186 | 187 | // Add a div for jquery UI windows. Need to check because other plugins 188 | // use the same element id for the same purpose. 189 | // 190 | // TODO: add dialog-window to ChiliProject core 191 | if ($('#dialog-window').length == 0) { 192 | $("
    ").appendTo('body'); 193 | } 194 | 195 | $('.deliverable-lightbox').live('click', function() { 196 | var deliverableId = $(this).data('deliverable-id'); 197 | 198 | $('#dialog-window'). 199 | hide(). 200 | html(''). 201 | load($(this).attr('href') + ".js"). 202 | dialog({ 203 | title: "", 204 | minWidth: 400, 205 | width: 850, 206 | minHeight: 400, 207 | height: 500, 208 | buttons: { 209 | "Close": function() { 210 | $(this).dialog("close"); 211 | } 212 | } 213 | }); 214 | 215 | return false; 216 | }); 217 | }); 218 | 219 | /* Jquery Table Expander Plugin */ 220 | (function($){ 221 | $.fn.jExpand = function(){ 222 | var element = this; 223 | $(element).find("tr.ign").hide(); 224 | 225 | $(element).find("tr.click").click(function() { 226 | $(this).toggleClass("noborder"); 227 | $(this).next("tr").toggle(); 228 | $(this).find('.arrow').toggleClass("alt"); 229 | 230 | var box_height = $(this).next().find('.expanded').height(); 231 | var table_height = $(this).next().find('.finance table').height(); 232 | 233 | if(box_height-table_height > 0){ 234 | $(this).next().find('.finance table .fill td').css("padding-top", box_height-table_height+2); 235 | } 236 | 237 | }); 238 | 239 | } 240 | })(jQuery); 241 | 242 | // Global functions outside of jQuery scoping 243 | function attachWikiToolbar(id) { 244 | var jsToolBarInstance = new jsToolBar($(id)); 245 | jsToolBarInstance.draw(); 246 | } 247 | -------------------------------------------------------------------------------- /assets/javascripts/jquery.tmpl.min.js: -------------------------------------------------------------------------------- 1 | (function(a){var r=a.fn.domManip,d="_tmplitem",q=/^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,b={},f={},e,p={key:0,data:{}},h=0,c=0,l=[];function g(e,d,g,i){var c={data:i||(d?d.data:{}),_wrap:d?d._wrap:null,tmpl:null,parent:d||null,nodes:[],calls:u,nest:w,wrap:x,html:v,update:t};e&&a.extend(c,e,{nodes:[],parent:d});if(g){c.tmpl=g;c._ctnt=c._ctnt||c.tmpl(a,c);c.key=++h;(l.length?f:b)[h]=c}return c}a.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(f,d){a.fn[f]=function(n){var g=[],i=a(n),k,h,m,l,j=this.length===1&&this[0].parentNode;e=b||{};if(j&&j.nodeType===11&&j.childNodes.length===1&&i.length===1){i[d](this[0]);g=this}else{for(h=0,m=i.length;h0?this.clone(true):this).get();a.fn[d].apply(a(i[h]),k);g=g.concat(k)}c=0;g=this.pushStack(g,f,i.selector)}l=e;e=null;a.tmpl.complete(l);return g}});a.fn.extend({tmpl:function(d,c,b){return a.tmpl(this[0],d,c,b)},tmplItem:function(){return a.tmplItem(this[0])},template:function(b){return a.template(b,this[0])},domManip:function(d,l,j){if(d[0]&&d[0].nodeType){var f=a.makeArray(arguments),g=d.length,i=0,h;while(i1)f[0]=[a.makeArray(d)];if(h&&c)f[2]=function(b){a.tmpl.afterManip(this,b,j)};r.apply(this,f)}else r.apply(this,arguments);c=0;!e&&a.tmpl.complete(b);return this}});a.extend({tmpl:function(d,h,e,c){var j,k=!c;if(k){c=p;d=a.template[d]||a.template(null,d);f={}}else if(!d){d=c.tmpl;b[c.key]=c;c.nodes=[];c.wrapped&&n(c,c.wrapped);return a(i(c,null,c.tmpl(a,c)))}if(!d)return[];if(typeof h==="function")h=h.call(c||{});e&&e.wrapped&&n(e,e.wrapped);j=a.isArray(h)?a.map(h,function(a){return a?g(e,c,d,a):null}):[g(e,c,d,h)];return k?a(i(c,null,j)):j},tmplItem:function(b){var c;if(b instanceof a)b=b[0];while(b&&b.nodeType===1&&!(c=a.data(b,"tmplItem"))&&(b=b.parentNode));return c||p},template:function(c,b){if(b){if(typeof b==="string")b=o(b);else if(b instanceof a)b=b[0]||{};if(b.nodeType)b=a.data(b,"tmpl")||a.data(b,"tmpl",o(b.innerHTML));return typeof c==="string"?(a.template[c]=b):b}return c?typeof c!=="string"?a.template(null,c):a.template[c]||a.template(null,q.test(c)?c:a(c)):null},encode:function(a){return(""+a).split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'")}});a.extend(a.tmpl,{tag:{tmpl:{_default:{$2:"null"},open:"if($notnull_1){_=_.concat($item.nest($1,$2));}"},wrap:{_default:{$2:"null"},open:"$item.calls(_,$1,$2);_=[];",close:"call=$item.calls();_=call._.concat($item.wrap(call,_));"},each:{_default:{$2:"$index, $value"},open:"if($notnull_1){$.each($1a,function($2){with(this){",close:"}});}"},"if":{open:"if(($notnull_1) && $1a){",close:"}"},"else":{_default:{$1:"true"},open:"}else if(($notnull_1) && $1a){"},html:{open:"if($notnull_1){_.push($1a);}"},"=":{_default:{$1:"$data"},open:"if($notnull_1){_.push($.encode($1a));}"},"!":{open:""}},complete:function(){b={}},afterManip:function(f,b,d){var e=b.nodeType===11?a.makeArray(b.childNodes):b.nodeType===1?[b]:[];d.call(f,b);m(e);c++}});function i(e,g,f){var b,c=f?a.map(f,function(a){return typeof a==="string"?e.key?a.replace(/(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g,"$1 "+d+'="'+e.key+'" $2'):a:i(a,e,a._ctnt)}):e;if(g)return c;c=c.join("");c.replace(/^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/,function(f,c,e,d){b=a(e).get();m(b);if(c)b=j(c).concat(b);if(d)b=b.concat(j(d))});return b?b:j(c)}function j(c){var b=document.createElement("div");b.innerHTML=c;return a.makeArray(b.childNodes)}function o(b){return new Function("jQuery","$item","var $=jQuery,call,_=[],$data=$item.data;with($data){_.push('"+a.trim(b).replace(/([\\'])/g,"\\$1").replace(/[\r\t\n]/g," ").replace(/\$\{([^\}]*)\}/g,"{{= $1}}").replace(/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,function(m,l,j,d,b,c,e){var i=a.tmpl.tag[j],h,f,g;if(!i)throw"Template command not found: "+j;h=i._default||[];if(c&&!/\w$/.test(b)){b+=c;c=""}if(b){b=k(b);e=e?","+k(e)+")":c?")":"";f=c?b.indexOf(".")>-1?b+c:"("+b+").call($item"+e:b;g=c?f:"(typeof("+b+")==='function'?("+b+").call($item):("+b+"))"}else g=f=h.$1||"null";d=k(d);return"');"+i[l?"close":"open"].split("$notnull_1").join(b?"typeof("+b+")!=='undefined' && ("+b+")!=null":"true").split("$1a").join(g).split("$1").join(f).split("$2").join(d?d.replace(/\s*([^\(]+)\s*(\((.*?)\))?/g,function(d,c,b,a){a=a?","+a+")":b?")":"";return a?"("+c+").call($item"+a:d}):h.$2||"")+"_.push('"})+"');}return _;")}function n(c,b){c._wrap=i(c,true,a.isArray(b)?b:[q.test(b)?b:a(b).html()]).join("")}function k(a){return a?a.replace(/\\'/g,"'").replace(/\\\\/g,"\\"):null}function s(b){var a=document.createElement("div");a.appendChild(b.cloneNode(true));return a.innerHTML}function m(o){var n="_"+c,k,j,l={},e,p,i;for(e=0,p=o.length;e=0;i--)m(j[i]);m(k)}function m(j){var p,i=j,k,e,m;if(m=j.getAttribute(d)){while(i.parentNode&&(i=i.parentNode).nodeType===1&&!(p=i.getAttribute(d)));if(p!==m){i=i.parentNode?i.nodeType===11?0:i.getAttribute(d)||0:0;if(!(e=b[m])){e=f[m];e=g(e,b[i]||f[i],null,true);e.key=++h;b[h]=e}c&&o(m)}j.removeAttribute(d)}else if(c&&(e=a.data(j,"tmplItem"))){o(e.key);b[e.key]=e;i=a.data(j.parentNode,"tmplItem");i=i?i.key:0}if(e){k=e;while(k&&k.key!=i){k.nodes.push(j);k=k.parent}delete e._ctnt;delete e._wrap;a.data(j,"tmplItem",e)}function o(a){a=a+n;e=l[a]=l[a]||g(e,b[e.parent.key+n]||e.parent,null,true)}}}function u(a,d,c,b){if(!a)return l.pop();l.push({_:a,tmpl:d,item:this,data:c,options:b})}function w(d,c,b){return a.tmpl(a.template(d),c,b,this)}function x(b,d){var c=b.options||{};c.wrapped=d;return a.tmpl(a.template(b.tmpl),b.data,c,b.item)}function v(d,c){var b=this._wrap;return a.map(a(a.isArray(b)?b.join(""):b).filter(d||"*"),function(a){return c?a.innerText||a.textContent:a.outerHTML||s(a)})}function t(){var b=this.nodes;a.tmpl(null,null,null,this).insertBefore(b[0]);a(b).remove()}})(jQuery) -------------------------------------------------------------------------------- /assets/stylesheets/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/stylesheets/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png -------------------------------------------------------------------------------- /assets/stylesheets/smoothness/images/ui-bg_flat_75_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/stylesheets/smoothness/images/ui-bg_flat_75_ffffff_40x100.png -------------------------------------------------------------------------------- /assets/stylesheets/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/stylesheets/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png -------------------------------------------------------------------------------- /assets/stylesheets/smoothness/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/stylesheets/smoothness/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /assets/stylesheets/smoothness/images/ui-bg_glass_75_dadada_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/stylesheets/smoothness/images/ui-bg_glass_75_dadada_1x400.png -------------------------------------------------------------------------------- /assets/stylesheets/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/stylesheets/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png -------------------------------------------------------------------------------- /assets/stylesheets/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/stylesheets/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png -------------------------------------------------------------------------------- /assets/stylesheets/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/stylesheets/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png -------------------------------------------------------------------------------- /assets/stylesheets/smoothness/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/stylesheets/smoothness/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /assets/stylesheets/smoothness/images/ui-icons_2e83ff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/stylesheets/smoothness/images/ui-icons_2e83ff_256x240.png -------------------------------------------------------------------------------- /assets/stylesheets/smoothness/images/ui-icons_454545_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/stylesheets/smoothness/images/ui-icons_454545_256x240.png -------------------------------------------------------------------------------- /assets/stylesheets/smoothness/images/ui-icons_888888_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/stylesheets/smoothness/images/ui-icons_888888_256x240.png -------------------------------------------------------------------------------- /assets/stylesheets/smoothness/images/ui-icons_cd0a0a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_contracts/30e7142c508666f5f11b046d2bc63d4e42265589/assets/stylesheets/smoothness/images/ui-icons_cd0a0a_256x240.png -------------------------------------------------------------------------------- /autotest/discover.rb: -------------------------------------------------------------------------------- 1 | Autotest.add_discovery do 2 | "rails" 3 | end 4 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | activerecord: 3 | errors: 4 | messages: 5 | cant_create_time_on_object: "Can't create a time entry on a %{reason} %{thing}" 6 | cant_assign_to_closed_deliverable: "Can't assign issue to a closed deliverable" 7 | cant_assign_to_locked_deliverable: "Can't assign issue to a locked deliverable" 8 | cant_assign_to_closed_contract: "Can't assign issue to a closed contract" 9 | cant_assign_to_locked_contract: "Can't assign issue to a locked contract" 10 | cant_update_locked_deliverable: "Can't update a locked deliverable" 11 | cant_update_closed_deliverable: "Can't update a closed deliverable" 12 | cant_update_locked_contract: "Can't update a locked contract" 13 | cant_update_closed_contract: "Can't update a closed contract" 14 | cant_create_deliverable_on_locked_contract: "Can't create a deliverable on a locked contract" 15 | cant_create_deliverable_on_closed_contract: "Can't create a deliverable on a closed contract" 16 | 17 | field_end_date: End Date 18 | field_executed: Executed 19 | text_contracts: Contracts 20 | field_id: ID 21 | field_account_executive: Account Executive 22 | field_account_executive_short: "Acct. Mgr." 23 | field_end_date: "End Date" 24 | text_new_contract: "New Contract" 25 | text_edit_contract_name: "Edit %{name}" 26 | field_billable_rate: "Billable Rate" 27 | field_billable_rate_hint: "$" 28 | field_discount: "Discount" 29 | field_discount_hint: "$, %" 30 | field_discount_note: "Discounts Notes" 31 | field_payment_term: "Payment Terms" 32 | field_client_ap_contact_information: "AP Contact Info" 33 | field_po_number: "PO Number" 34 | field_details: "Details" 35 | button_add_new: Add New 36 | text_new_deliverable: New Deliverable 37 | text_edit_deliverable_title: "Edit %{title}" 38 | field_manager: Manager 39 | field_labor: Labor 40 | field_overhead: Overhead 41 | field_fixed: Fixed 42 | field_total: Total 43 | field_feature_sign_off: Feature Sign Off 44 | field_warranty_sign_off: Warranty Sign Off 45 | text_deliverable_finances: Deliverable Finances 46 | text_deliverable_finances_date: "Deliverable Finances - %{date}" 47 | text_short_hours: hrs 48 | text_dollar_sign: '$' 49 | field_client_point_of_contact: "Point of Contact" 50 | field_discount_budget: "Discount" 51 | field_discount_spent: "Discount" 52 | field_estimated_hour_budget: "Total Hours" 53 | field_estimated_hour_spent: "Total Hours" 54 | field_labor_hour_spent: "Labor Hours" 55 | field_overhead_hour_spent: "Overhead Hours" 56 | field_total_hours: "Total Hours" 57 | field_fixed_budget: "Fixed" 58 | field_fixed_spent: "Fixed" 59 | field_labor_budget: "Labor" 60 | field_labor_spent: "Labor" 61 | field_fixed_markup_budget: "Markup" 62 | field_fixed_markup_spent: "Markup" 63 | field_overhead_budget: "Overhead" 64 | field_overhead_spent: "Overhead" 65 | field_profit_budget: "Profit" 66 | field_profit_spent: "Profit" 67 | field_total_budget: "Total Budget" 68 | field_total_spent: "Contract Total" 69 | field_contract_type: "Type" 70 | field_deliverable: "Deliverable" 71 | field_deliverable_plural: "Deliverables" 72 | text_terms_and_financial_overview: "Contract Terms and Financial Overview" 73 | text_general_legend: "General" 74 | text_budget_legend: "Budget" 75 | text_account_legend: "Account Management" 76 | text_deliverable_details_legend: "Deliverable Details" 77 | text_save_contract: "Save Contract" 78 | enumeration_payment_term: "Payment Terms" 79 | field_deliverable_title: "Deliverable" 80 | field_contract_name: "Contract" 81 | field_contract: "Contract" 82 | text_start_date_empty: "The start date is empty. If this form is submitted, no budget items will be created." 83 | text_end_date_empty: "The end date is empty. If this form is submitted, no budget items will be created." 84 | 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." 85 | text_changed_period_message: "The period for this deliverable has been changed. Would you like to expand/shrink the Deliverable Finances?" 86 | field_current_period: "Current period" 87 | text_retainer_monthly_message: "Enter budget for a representative month. Any overrides to individual months can be done via the editor after saving." 88 | text_flash_deliverable_created: "Deliverable: %{name} was successfully created." 89 | text_flash_deliverable_updated: "Deliverable: %{name} was successfully updated." 90 | text_flash_deliverable_deleted: "Deliverable: %{name} was successfully deleted." 91 | field_budget: Budget 92 | field_markup: Markup 93 | field_paid: Paid 94 | field_spent: Spent 95 | field_profit: Profit 96 | text_error_message_orphaned_time: "There is %{amount} worth of time clocked to issues that are not assigned to any deliverables." 97 | text_error_message_update_orphaned_time: "Please update the orphaned issues." 98 | field_estimated: Estimated 99 | text_deliverable_locked_warning: "This deliverable is locked and cannot be saved without changing it's status to Open." 100 | text_deliverable_closed_warning: "This deliverable is closed and cannot be saved without changing it's status to Open." 101 | text_contract_locked_warning: "This contract is locked and cannot be saved without changing it's status to Open." 102 | text_contract_closed_warning: "This contract is closed and cannot be saved without changing it's status to Open." 103 | field_time_entry_activity: "Activity" 104 | text_deliverable_spending_summary: "You've spent %{spent} / %{total} and %{hours} Billable Hours" 105 | field_cost: "Cost" 106 | field_spent_estimate: "Spent / Est." 107 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | ActionController::Routing::Routes.draw do |map| 2 | map.resources :contracts, :path_prefix => '/projects/:project_id' do |contracts| 3 | contracts.resources :deliverables, :member => {:finances => :get} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/001_create_contracts.rb: -------------------------------------------------------------------------------- 1 | class CreateContracts < ActiveRecord::Migration 2 | def self.up 3 | create_table :contracts do |t| 4 | t.string :name 5 | t.integer :account_executive_id # User 6 | t.references :project 7 | t.date :start_date 8 | t.date :end_date 9 | t.boolean :executed 10 | t.decimal :billable_rate, :precision => 15, :scale => 2 11 | t.string :discount 12 | t.string :discount_type # $ or % 13 | t.text :discount_note 14 | t.string :payment_terms 15 | t.text :client_ap_contact_information 16 | t.string :po_number 17 | t.text :details 18 | t.timestamps 19 | end 20 | 21 | add_index :contracts, :name 22 | add_index :contracts, :account_executive_id 23 | add_index :contracts, :project_id 24 | add_index :contracts, :start_date 25 | add_index :contracts, :end_date 26 | end 27 | 28 | def self.down 29 | drop_table :contracts 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /db/migrate/002_create_deliverables.rb: -------------------------------------------------------------------------------- 1 | class CreateDeliverables < ActiveRecord::Migration 2 | def self.up 3 | create_table :deliverables do |t| 4 | t.string :title 5 | t.string :type 6 | t.date :start_date 7 | t.date :end_date 8 | t.text :notes 9 | t.boolean :feature_sign_off 10 | t.boolean :warranty_sign_off 11 | t.integer :manager_id # User 12 | t.references :contract 13 | end 14 | 15 | add_index :deliverables, :title 16 | add_index :deliverables, :type 17 | add_index :deliverables, :start_date 18 | add_index :deliverables, :end_date 19 | add_index :deliverables, :contract_id 20 | end 21 | 22 | def self.down 23 | drop_table :deliverables 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /db/migrate/003_add_total_to_deliverables.rb: -------------------------------------------------------------------------------- 1 | class AddTotalToDeliverables < ActiveRecord::Migration 2 | def self.up 3 | add_column :deliverables, :total, :decimal, :precision => 15, :scale => 2 4 | 5 | add_index :deliverables, :total 6 | end 7 | 8 | def self.down 9 | remove_column :deliverables, :total 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/004_create_labor_budgets.rb: -------------------------------------------------------------------------------- 1 | class CreateLaborBudgets < ActiveRecord::Migration 2 | def self.up 3 | create_table :labor_budgets do |t| 4 | t.decimal :hours, :precision => 15, :scale => 4 5 | t.decimal :budget, :precision => 15, :scale => 4 6 | t.references :deliverable 7 | t.timestamps 8 | end 9 | 10 | add_index :labor_budgets, :deliverable_id 11 | end 12 | 13 | def self.down 14 | drop_table :labor_budgets 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/005_create_overhead_budgets.rb: -------------------------------------------------------------------------------- 1 | class CreateOverheadBudgets < ActiveRecord::Migration 2 | def self.up 3 | create_table :overhead_budgets do |t| 4 | t.decimal :hours, :precision => 15, :scale => 4 5 | t.decimal :budget, :precision => 15, :scale => 4 6 | t.references :deliverable 7 | t.timestamps 8 | end 9 | 10 | add_index :overhead_budgets, :deliverable_id 11 | end 12 | 13 | def self.down 14 | drop_table :overhead_budgets 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/006_add_deliverable_id_to_issues.rb: -------------------------------------------------------------------------------- 1 | class AddDeliverableIdToIssues < ActiveRecord::Migration 2 | def self.up 3 | # Skip adding the column if it exists from the Budget plugin 4 | unless Issue.column_names.include?('deliverable_id') 5 | add_column :issues, :deliverable_id, :integer 6 | end 7 | end 8 | 9 | def self.down 10 | remove_column :issues, :deliverable_id 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/007_add_client_point_of_contact_to_contracts.rb: -------------------------------------------------------------------------------- 1 | class AddClientPointOfContactToContracts < ActiveRecord::Migration 2 | def self.up 3 | add_column :contracts, :client_point_of_contact, :text 4 | end 5 | 6 | def self.down 7 | remove_column :contracts, :client_point_of_contact 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/008_add_payment_term_id_to_contracts.rb: -------------------------------------------------------------------------------- 1 | class AddPaymentTermIdToContracts < ActiveRecord::Migration 2 | def self.up 3 | add_column :contracts, :payment_term_id, :integer 4 | end 5 | 6 | def self.down 7 | remove_column :contracts, :payment_term_id 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/009_remove_payment_terms_from_contracts.rb: -------------------------------------------------------------------------------- 1 | class RemovePaymentTermsFromContracts < ActiveRecord::Migration 2 | def self.up 3 | remove_column :contracts, :payment_terms 4 | end 5 | 6 | def self.down 7 | add_column :contracts, :payment_terms, :string 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/010_populate_payment_terms.rb: -------------------------------------------------------------------------------- 1 | class PopulatePaymentTerms < ActiveRecord::Migration 2 | def self.up 3 | [0, 15, 30, 45, 60, 75, 90].each_with_index do |days, index| 4 | name = "Net #{days}" 5 | unless PaymentTerm.find_by_name(name) 6 | PaymentTerm.create!(:name => name, :position => index + 1) 7 | end 8 | end 9 | end 10 | 11 | def self.down 12 | # No-op 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/011_add_frequency_to_deliverables.rb: -------------------------------------------------------------------------------- 1 | class AddFrequencyToDeliverables < ActiveRecord::Migration 2 | def self.up 3 | add_column :deliverables, :frequency, :string 4 | end 5 | 6 | def self.down 7 | remove_column :deliverables, :frequency 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/012_add_year_and_month_to_labor_budgets.rb: -------------------------------------------------------------------------------- 1 | class AddYearAndMonthToLaborBudgets < ActiveRecord::Migration 2 | def self.up 3 | add_column :labor_budgets, :year, :integer 4 | add_index :labor_budgets, :year 5 | 6 | add_column :labor_budgets, :month, :integer 7 | add_index :labor_budgets, :month 8 | end 9 | 10 | def self.down 11 | remove_column :labor_budgets, :year 12 | remove_column :labor_budgets, :month 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/013_add_year_and_month_to_overhead_budgets.rb: -------------------------------------------------------------------------------- 1 | class AddYearAndMonthToOverheadBudgets < ActiveRecord::Migration 2 | def self.up 3 | add_column :overhead_budgets, :year, :integer 4 | add_index :overhead_budgets, :year 5 | 6 | add_column :overhead_budgets, :month, :integer 7 | add_index :overhead_budgets, :month 8 | end 9 | 10 | def self.down 11 | remove_column :overhead_budgets, :year 12 | remove_column :overhead_budgets, :month 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/014_remove_frequency_from_deliverables.rb: -------------------------------------------------------------------------------- 1 | class RemoveFrequencyFromDeliverables < ActiveRecord::Migration 2 | def self.up 3 | remove_column :deliverables, :frequency 4 | end 5 | 6 | def self.down 7 | add_column :deliverables, :frequency, :string 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/015_create_fixed_budgets.rb: -------------------------------------------------------------------------------- 1 | class CreateFixedBudgets < ActiveRecord::Migration 2 | def self.up 3 | create_table :fixed_budgets do |t| 4 | t.string :title 5 | t.decimal :budget, :precision => 15, :scale => 4 6 | t.string :markup 7 | t.text :description 8 | t.references :deliverable 9 | 10 | t.timestamps 11 | end 12 | 13 | add_index :fixed_budgets, :deliverable_id 14 | end 15 | 16 | def self.down 17 | drop_table :fixed_budgets 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /db/migrate/016_add_year_and_month_to_fixed_budgets.rb: -------------------------------------------------------------------------------- 1 | class AddYearAndMonthToFixedBudgets < ActiveRecord::Migration 2 | def self.up 3 | add_column :fixed_budgets, :year, :integer 4 | add_index :fixed_budgets, :year 5 | 6 | add_column :fixed_budgets, :month, :integer 7 | add_index :fixed_budgets, :month 8 | end 9 | 10 | def self.down 11 | remove_column :fixed_budgets, :year 12 | remove_column :fixed_budgets, :month 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/017_add_paid_to_fixed_budgets.rb: -------------------------------------------------------------------------------- 1 | class AddPaidToFixedBudgets < ActiveRecord::Migration 2 | def self.up 3 | add_column :fixed_budgets, :paid, :boolean 4 | add_index :fixed_budgets, :paid 5 | end 6 | 7 | def self.down 8 | remove_column :fixed_budgets, :paid 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/018_add_status_to_contracts.rb: -------------------------------------------------------------------------------- 1 | class AddStatusToContracts < ActiveRecord::Migration 2 | def self.up 3 | add_column :contracts, :status, :string 4 | add_index :contracts, :status 5 | end 6 | 7 | def self.down 8 | remove_column :contracts, :status 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/019_add_status_to_deliverables.rb: -------------------------------------------------------------------------------- 1 | class AddStatusToDeliverables < ActiveRecord::Migration 2 | def self.up 3 | add_column :deliverables, :status, :string 4 | add_index :deliverables, :status 5 | end 6 | 7 | def self.down 8 | remove_column :deliverables, :status 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/020_add_time_entry_activity_id_to_labor_budgets.rb: -------------------------------------------------------------------------------- 1 | class AddTimeEntryActivityIdToLaborBudgets < ActiveRecord::Migration 2 | def self.up 3 | add_column :labor_budgets, :time_entry_activity_id, :integer 4 | add_index :labor_budgets, :time_entry_activity_id 5 | end 6 | 7 | def self.down 8 | remove_column :labor_budgets, :time_entry_activity_id 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/021_add_time_entry_activity_id_to_overhead_budgets.rb: -------------------------------------------------------------------------------- 1 | class AddTimeEntryActivityIdToOverheadBudgets < ActiveRecord::Migration 2 | def self.up 3 | add_column :overhead_budgets, :time_entry_activity_id, :integer 4 | add_index :overhead_budgets, :time_entry_activity_id 5 | end 6 | 7 | def self.down 8 | remove_column :overhead_budgets, :time_entry_activity_id 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | config.gem 'formtastic', :version => '0.9.10' 2 | 3 | require 'redmine' 4 | 5 | Redmine::Plugin.register :redmine_contracts do 6 | name 'Redmine Contracts plugin' 7 | author 'Eric Davis' 8 | description 'A system to manage the execution of a client contract by separating it into deliverables and milestones.' 9 | url 'https://projects.littlestreamsoftware.com/projects/redmine-contracts' 10 | author_url 'http://www.littlestreamsoftware.com' 11 | version '0.1.0' 12 | 13 | requires_redmine :version_or_higher => '0.9.0' 14 | requires_redmine_plugin :redmine_rate, :version_or_higher => '0.1.0' 15 | requires_redmine_plugin :redmine_overhead, :version_or_higher => '0.1.0' 16 | 17 | project_module :contracts do 18 | permission(:manage_budget, { 19 | :contracts => [:index, :new, :create, :show, :edit, :update, :destroy], 20 | :deliverables => [:index, :new, :create, :show, :edit, :update, :destroy, :finances] 21 | }) 22 | end 23 | 24 | project_module :issue_tracking do 25 | permission(:assign_deliverable_to_issue, {}) 26 | end 27 | 28 | contract_list_submenu_items = Proc.new {|project| 29 | if project && project.module_enabled?(:contracts) 30 | 31 | project.contracts.inject([]) do |menu_items, contract| 32 | 33 | menu_items << ::Redmine::MenuManager::MenuItem.new("contract-#{contract.id}", 34 | { :controller => 'contracts', :action => 'show', :id => contract.id, :project_id => project}, 35 | # TODO: http://www.redmine.org/issues/6426 36 | # contract_path(project, contract), 37 | { 38 | :caption => contract.name, # h-escaped in Redmine 39 | :param => :project_id, 40 | :parent => :contracts 41 | }) 42 | end 43 | 44 | end 45 | } 46 | 47 | menu(:project_menu, 48 | :contracts, 49 | {:controller => 'contracts', :action => 'index'}, 50 | :caption => :text_contracts, 51 | :param => :project_id, 52 | :children => contract_list_submenu_items) 53 | 54 | menu(:project_menu, 55 | :new_contract, 56 | {:controller => 'contracts', :action => 'new'}, 57 | :caption => :text_new_contract, 58 | :param => :project_id, 59 | :parent => :contracts) 60 | 61 | end 62 | 63 | require 'dispatcher' 64 | Dispatcher.to_prepare :redmine_contracts do 65 | 66 | require_dependency 'time_entry' 67 | TimeEntry.send(:include, RedmineContracts::Patches::TimeEntryPatch) 68 | gem 'inherited_resources', :version => '1.0.6' 69 | require_dependency 'inherited_resources' 70 | require_dependency 'inherited_resources/base' 71 | 72 | # Load and bootstrap formtastic 73 | gem 'formtastic', :version => '0.9.10' 74 | require_dependency 'formtastic' 75 | require_dependency 'formtastic/layout_helper' 76 | ActionView::Base.send :include, Formtastic::SemanticFormHelper 77 | ActionView::Base.send :include, Formtastic::LayoutHelper 78 | 79 | Formtastic::SemanticFormBuilder.all_fields_required_by_default = false 80 | Formtastic::SemanticFormBuilder.required_string = " *" 81 | Formtastic::SemanticFormBuilder.inline_errors = :none 82 | 83 | require_dependency 'payment_term' # Load so Enumeration will pick up the subclass in dev 84 | 85 | require_dependency 'project' 86 | Project.send(:include, RedmineContracts::Patches::ProjectPatch) 87 | require_dependency 'issue' 88 | Issue.send(:include, RedmineContracts::Patches::IssuePatch) 89 | require_dependency 'query' 90 | unless Query.included_modules.include? RedmineContracts::Patches::QueryPatch 91 | Query.send(:include, RedmineContracts::Patches::QueryPatch) 92 | end 93 | 94 | unless Query.available_columns.collect(&:name).include?(:deliverable) 95 | Query.add_available_column(QueryColumn.new(:deliverable, :sortable => "#{Deliverable.table_name}.title", :groupable => 'deliverable')) 96 | end 97 | 98 | # Hack in order to get the associated contract to be grouped by name 99 | # * Proxy method Issue#contract_name 100 | # * Naming Query column contract_name 101 | # * Grouping by 'contracts.name' 102 | unless Query.available_columns.collect(&:name).include?(:contract_name) 103 | Query.add_available_column(QueryColumn.new(:contract_name, :sortable => "#{Contract.table_name}.name", :groupable => 'contracts.name')) 104 | end 105 | 106 | require_dependency 'application_controller' 107 | ApplicationController.send(:helper, :contracts) 108 | end 109 | 110 | require 'redmine_contracts/hooks/view_layouts_base_html_head_hook' 111 | require 'redmine_contracts/hooks/view_issues_show_details_bottom_hook' 112 | require 'redmine_contracts/hooks/view_issues_form_details_bottom_hook' 113 | require 'redmine_contracts/hooks/controller_issues_edit_before_save_hook' 114 | require 'redmine_contracts/hooks/view_issues_bulk_edit_details_bottom_hook' 115 | require 'redmine_contracts/hooks/controller_issues_bulk_edit_before_save_hook' 116 | require 'redmine_contracts/hooks/helper_issues_show_detail_after_setting_hook' 117 | require 'redmine_contracts/hooks/controller_timelog_available_criterias_hook' 118 | -------------------------------------------------------------------------------- /lang/en.yml: -------------------------------------------------------------------------------- 1 | # English strings go here 2 | my_label: "My label" 3 | -------------------------------------------------------------------------------- /lib/dollarized_attribute.rb: -------------------------------------------------------------------------------- 1 | # Shared module to allow seting an attribute using: 2 | # * Dollar amount - $1,000.00 3 | # * Number - 100.00 4 | module DollarizedAttribute 5 | module ClassMethods 6 | 7 | # dollarized_attribute(:budget) will create a budget=(value) method 8 | def dollarized_attribute(attribute) 9 | define_method(attribute.to_s + '=') {|value| 10 | if value.is_a? String 11 | write_attribute(attribute, value.gsub(/[$ ,]/, '')) 12 | else 13 | write_attribute(attribute, value) 14 | end 15 | } 16 | end 17 | 18 | end 19 | 20 | def self.included(base) 21 | base.extend ClassMethods 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/redmine_contracts/hooks/controller_issues_bulk_edit_before_save_hook.rb: -------------------------------------------------------------------------------- 1 | module RedmineContracts 2 | module Hooks 3 | class ControllerIssuesBulkEditBeforeSaveHook < Redmine::Hook::ViewListener 4 | # Context: 5 | # * :issue => Issue being saved 6 | # * :params => HTML parameters 7 | # 8 | def controller_issues_bulk_edit_before_save(context={}) 9 | return '' unless User.current.allowed_to?(:assign_deliverable_to_issue, context[:issue].project) 10 | 11 | case 12 | when context[:params][:deliverable_id].blank? 13 | # Do nothing 14 | when context[:params][:deliverable_id] == 'none' 15 | # Unassign deliverable 16 | context[:issue].deliverable = nil 17 | else 18 | context[:issue].deliverable = Deliverable.find(context[:params][:deliverable_id]) 19 | end 20 | 21 | return '' 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/redmine_contracts/hooks/controller_issues_edit_before_save_hook.rb: -------------------------------------------------------------------------------- 1 | module RedmineContracts 2 | module Hooks 3 | class ControllerIssuesEditBeforeSaveHook < Redmine::Hook::ViewListener 4 | def controller_issues_edit_before_save(context={}) 5 | 6 | if context[:params] && context[:params][:issue] 7 | if User.current.allowed_to?(:assign_deliverable_to_issue, context[:issue].project) 8 | if context[:params][:issue][:deliverable_id].present? 9 | deliverable = Deliverable.find_by_id(context[:params][:issue][:deliverable_id]) 10 | if deliverable.contract.project == context[:issue].project 11 | context[:issue].deliverable = deliverable 12 | end 13 | 14 | else 15 | context[:issue].deliverable = nil 16 | end 17 | end 18 | 19 | end 20 | 21 | return '' 22 | end 23 | 24 | alias_method :controller_issues_new_before_save, :controller_issues_edit_before_save 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/redmine_contracts/hooks/controller_timelog_available_criterias_hook.rb: -------------------------------------------------------------------------------- 1 | module RedmineContracts 2 | module Hooks 3 | class ControllerTimelogAvailableCriteriasHook < Redmine::Hook::ViewListener 4 | def controller_timelog_available_criterias(context={}) 5 | context[:available_criterias]["deliverable_id"] = { 6 | :sql => "#{Issue.table_name}.deliverable_id", 7 | :klass => Deliverable, 8 | :label => :field_deliverable 9 | } 10 | context[:available_criterias]["contract_id"] = { 11 | :sql => "(SELECT deliverable.contract_id FROM #{Deliverable.table_name} deliverable WHERE deliverable.id = issues.deliverable_id)", 12 | :klass => Contract, 13 | :label => :field_contract 14 | } 15 | return '' 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/redmine_contracts/hooks/helper_issues_show_detail_after_setting_hook.rb: -------------------------------------------------------------------------------- 1 | module RedmineContracts 2 | module Hooks 3 | class HelperIssuesShowDetailAfterSettingHook < Redmine::Hook::ViewListener 4 | # Deliverable changes for the journal use the Deliverable subject 5 | # instead of the id 6 | # 7 | # Context: 8 | # * :detail => Detail about the journal change 9 | # 10 | def helper_issues_show_detail_after_setting(context = { }) 11 | # This will be skipped in ChiliProject 2.x because 12 | # acts_as_journalized overrides the prop_key with the label 13 | # 'deliverable_id' becomes 'Deliverable' (i18n) 14 | # 15 | # register_on_journal_formatter is used for ChiliProject 2.x support 16 | # TODO Later: Overwritting the caller is bad juju 17 | if context[:detail].prop_key == 'deliverable_id' 18 | context[:detail].reload 19 | 20 | d = Deliverable.find_by_id(context[:detail].value) 21 | context[:detail].value = d.title if d.present? && d.title.present? 22 | 23 | d = Deliverable.find_by_id(context[:detail].old_value) 24 | context[:detail].old_value = d.title if d.present? && d.title.present? 25 | end 26 | '' 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/redmine_contracts/hooks/view_issues_bulk_edit_details_bottom_hook.rb: -------------------------------------------------------------------------------- 1 | module RedmineContracts 2 | module Hooks 3 | class ViewIssuesBulkEditDetailsBottomHook < Redmine::Hook::ViewListener 4 | 5 | render_on(:view_issues_bulk_edit_details_bottom, :partial => 'issues/bulk_edit_deliverable', :layout => false) 6 | 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/redmine_contracts/hooks/view_issues_form_details_bottom_hook.rb: -------------------------------------------------------------------------------- 1 | module RedmineContracts 2 | module Hooks 3 | class ViewIssuesFormDetailsBottomHook < Redmine::Hook::ViewListener 4 | render_on(:view_issues_form_details_bottom, :partial => 'issues/edit_deliverable', :layout => false) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/redmine_contracts/hooks/view_issues_show_details_bottom_hook.rb: -------------------------------------------------------------------------------- 1 | module RedmineContracts 2 | module Hooks 3 | class ViewIssuesShowDetailsBottomHook < Redmine::Hook::ViewListener 4 | include Redmine::I18n 5 | 6 | render_on(:view_issues_show_details_bottom, :partial => 'issues/show_deliverable', :layout => false) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/redmine_contracts/hooks/view_layouts_base_html_head_hook.rb: -------------------------------------------------------------------------------- 1 | module RedmineContracts 2 | module Hooks 3 | class ViewLayoutsBaseHtmlHeadHook < Redmine::Hook::ViewListener 4 | def view_layouts_base_html_head(context={}) 5 | if context[:controller] && ( 6 | context[:controller].is_a?(ContractsController) || 7 | context[:controller].is_a?(DeliverablesController) 8 | ) 9 | tags = [stylesheet_link_tag("redmine_contracts", :plugin => "redmine_contracts", :media => "screen")] 10 | tags << stylesheet_link_tag('smoothness/jquery-ui-1.8.15.custom.css', :plugin => "redmine_contracts") 11 | 12 | jquery_included = begin 13 | ChiliProject::Compatibility && ChiliProject::Compatibility.using_jquery? 14 | rescue NameError 15 | # No compatibilty test 16 | false 17 | end 18 | unless jquery_included 19 | tags << javascript_include_tag('jquery-1.4.4.min.js', :plugin => 'redmine_contracts') 20 | tags << javascript_tag('jQuery.noConflict();') 21 | end 22 | 23 | tags << javascript_include_tag('jquery.tmpl.min.js', :plugin => 'redmine_contracts') 24 | tags << javascript_include_tag('jquery-ui-1.8.15.custom.min.js', :plugin => "redmine_contracts") 25 | tags << javascript_include_tag('contracts.js', :plugin => 'redmine_contracts') 26 | 27 | return tags.join(' ') 28 | else 29 | return '' 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/redmine_contracts/patches/issue_patch.rb: -------------------------------------------------------------------------------- 1 | module RedmineContracts 2 | module Patches 3 | module IssuePatch 4 | def self.included(base) 5 | base.extend(ClassMethods) 6 | 7 | base.send(:include, InstanceMethods) 8 | base.class_eval do 9 | unloadable 10 | belongs_to :deliverable 11 | 12 | delegate :title, :to => :deliverable, :prefix => true, :allow_nil => true 13 | delegate :contract, :to => :deliverable, :allow_nil => true 14 | 15 | # ChiliProject 2.x support for acts_as_journalized. 16 | # Used to format the journal details on the Issue page 17 | # 18 | # See RedmineContracts::Hooks::HelperIssuesShowDetailAfterSettingHook 19 | # for <2.x and Redmine version 20 | # 21 | # TODO: Will not support permissions or custom code in the formatter. 22 | if Issue.respond_to?(:register_on_journal_formatter) 23 | register_on_journal_formatter(:named_association, 'deliverable_id') 24 | end 25 | 26 | def contract_name 27 | contract.try(:name) 28 | end 29 | 30 | validate :validate_deliverable_status 31 | validate :validate_contract_status 32 | 33 | def validate_deliverable_status 34 | if deliverable.present? && changes["deliverable_id"].present? 35 | errors.add_to_base(:cant_assign_to_closed_deliverable) if deliverable.closed? 36 | errors.add_to_base(:cant_assign_to_locked_deliverable) if deliverable.locked? 37 | end 38 | end 39 | 40 | def validate_contract_status 41 | if deliverable.present? && changes["deliverable_id"].present? && contract.present? 42 | errors.add_to_base(:cant_assign_to_closed_contract) if contract.closed? 43 | errors.add_to_base(:cant_assign_to_locked_contract) if contract.locked? 44 | end 45 | end 46 | 47 | end 48 | end 49 | 50 | module ClassMethods 51 | end 52 | 53 | module InstanceMethods 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/redmine_contracts/patches/project_patch.rb: -------------------------------------------------------------------------------- 1 | module RedmineContracts 2 | module Patches 3 | module ProjectPatch 4 | def self.included(base) 5 | base.extend(ClassMethods) 6 | 7 | base.send(:include, InstanceMethods) 8 | base.class_eval do 9 | unloadable 10 | has_many :contracts 11 | has_many :deliverables, :through => :contracts 12 | end 13 | end 14 | 15 | module ClassMethods 16 | end 17 | 18 | module InstanceMethods 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/redmine_contracts/patches/query_patch.rb: -------------------------------------------------------------------------------- 1 | module RedmineContracts 2 | module Patches 3 | module QueryPatch 4 | def self.included(base) 5 | base.extend(ClassMethods) 6 | 7 | base.send(:include, InstanceMethods) 8 | base.class_eval do 9 | unloadable 10 | 11 | alias_method_chain :available_filters, :deliverable 12 | alias_method_chain :available_filters, :contract 13 | 14 | alias_method_chain :sql_for_field, :contract 15 | 16 | alias_method_chain :issues, :deliverable 17 | alias_method_chain :issues, :contract 18 | 19 | # Override Query#count_by_group to allow adding include options like 20 | # Query#issues 21 | # TODO: core bug: Query#issue_count_by_group doesn't allow setting 22 | # options like Query#issue does. 23 | def issue_count_by_group(options={}) 24 | includes = ([:status, :project] + (options[:include] || [])).uniq 25 | 26 | r = nil 27 | if grouped? 28 | begin 29 | # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value 30 | r = Issue.count(:group => group_by_statement, :include => includes, :conditions => statement) 31 | rescue ActiveRecord::RecordNotFound 32 | r = {nil => issue_count} 33 | end 34 | c = group_by_column 35 | if c.is_a?(QueryCustomFieldColumn) 36 | r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h} 37 | end 38 | end 39 | r 40 | rescue ::ActiveRecord::StatementInvalid => e 41 | raise ::Query::StatementInvalid.new(e.message) 42 | end 43 | 44 | alias_method_chain :issue_count_by_group, :contract 45 | end 46 | end 47 | 48 | module ClassMethods 49 | end 50 | 51 | module InstanceMethods 52 | # TODO: Should have an API on the Redmine core for this 53 | def available_filters_with_deliverable 54 | @available_filters = available_filters_without_deliverable 55 | 56 | if project 57 | deliverable_filters = { 58 | "deliverable_id" => { 59 | :type => :list_optional, 60 | :order => 15, 61 | :values => project.deliverables.by_title.collect { |d| [d.title, d.id.to_s] } 62 | } 63 | } 64 | return @available_filters.merge(deliverable_filters) 65 | else 66 | return @available_filters 67 | end 68 | 69 | end 70 | 71 | # TODO: Should have an API on the Redmine core for this 72 | def available_filters_with_contract 73 | @available_filters = available_filters_without_contract 74 | 75 | if project 76 | contract_filters = { 77 | "contract_id" => { 78 | :type => :list_optional, 79 | :order => 16, 80 | :values => project.contracts.by_name.collect { |d| [d.name, d.id.to_s] } 81 | } 82 | } 83 | return @available_filters.merge(contract_filters) 84 | else 85 | return @available_filters 86 | end 87 | 88 | end 89 | 90 | def sql_for_field_with_contract(field, operator, value, db_table, db_field, is_custom_filter=false) 91 | if field != "contract_id" 92 | return sql_for_field_without_contract(field, operator, value, db_table, db_field, is_custom_filter) 93 | else 94 | # Contracts > Deliverables > Issue 95 | case operator 96 | when "=" 97 | contracts = value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") 98 | inner_select = "(SELECT id from deliverables where deliverables.contract_id IN (#{contracts}))" 99 | sql = "#{Issue.table_name}.deliverable_id IN (#{inner_select})" 100 | when "!" 101 | contracts = value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") 102 | inner_select = "(SELECT id from deliverables where deliverables.contract_id IN (#{contracts}))" 103 | sql = "(#{Issue.table_name}.deliverable_id IS NULL OR #{Issue.table_name}.deliverable_id NOT IN (#{inner_select}))" 104 | when "!*" 105 | # If it doesn't have a deliverable, it can't have a contract 106 | sql = "#{Issue.table_name}.deliverable_id IS NULL" 107 | when "*" 108 | # If it has a deliverable, it must have a contract 109 | sql = "#{Issue.table_name}.deliverable_id IS NOT NULL" 110 | end 111 | 112 | return sql 113 | end 114 | end 115 | 116 | # Add the deliverables into the includes 117 | # 118 | # Used with grouping 119 | def issues_with_deliverable(options={}) 120 | options[:include] ||= [] 121 | options[:include] << :deliverable 122 | 123 | issues_without_deliverable(options) 124 | end 125 | 126 | # Add the contracts into the includes 127 | # 128 | # Used with grouping 129 | def issues_with_contract(options={}) 130 | options[:include] ||= [] 131 | options[:include] << {:deliverable => :contract} 132 | 133 | issues_without_contract(options) 134 | end 135 | 136 | # Add the contracts into the includes 137 | # 138 | # Used with grouping 139 | def issue_count_by_group_with_contract(options={}) 140 | options[:include] ||= [] 141 | options[:include] << {:deliverable => :contract} 142 | 143 | issue_count_by_group_without_contract(options) 144 | end 145 | 146 | 147 | end 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /lib/redmine_contracts/patches/time_entry_patch.rb: -------------------------------------------------------------------------------- 1 | module RedmineContracts 2 | module Patches 3 | module TimeEntryPatch 4 | def self.included(base) 5 | base.extend(ClassMethods) 6 | 7 | base.send(:include, InstanceMethods) 8 | base.class_eval do 9 | unloadable 10 | 11 | validate :validate_deliverable_status 12 | validate :validate_contract_status 13 | 14 | def validate_deliverable_status 15 | if issue.present? && issue.deliverable.present? 16 | errors.add_to_base("#{l(:"activerecord.errors.messages.cant_create_time_on_object", :reason => 'locked', :thing => 'deliverable')}") if issue.deliverable.locked? 17 | errors.add_to_base("#{l(:"activerecord.errors.messages.cant_create_time_on_object", :reason => 'closed', :thing => 'deliverable')}") if issue.deliverable.closed? 18 | end 19 | end 20 | 21 | def validate_contract_status 22 | if issue.present? && issue.deliverable.present? && issue.deliverable.contract.present? 23 | errors.add_to_base("#{l(:"activerecord.errors.messages.cant_create_time_on_object", :reason => 'locked', :thing => 'contract')}") if issue.deliverable.contract.locked? 24 | errors.add_to_base("#{l(:"activerecord.errors.messages.cant_create_time_on_object", :reason => 'closed', :thing => 'contract')}") if issue.deliverable.contract.closed? 25 | end 26 | end 27 | 28 | end 29 | end 30 | 31 | module ClassMethods 32 | end 33 | 34 | module InstanceMethods 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/tasks/budget_plugin_migration.rake: -------------------------------------------------------------------------------- 1 | namespace :redmine_contracts do 2 | desc "Migrate data from the budget_plugin to redmine_contracts" 3 | task :budget_migration => :environment do 4 | options = {} 5 | options[:contract_rate] = ENV['contract_rate'] 6 | options[:account_executive] = ENV['account_executive'] 7 | options[:deliverable_manager] = ENV['deliverable_manager'] 8 | options[:append_object_notes] = ENV['append_object_notes'] 9 | options[:overhead_rate] = ENV['overhead_rate'] 10 | 11 | RedmineContracts::BudgetPluginMigration.check_for_installed_budget_plugin 12 | data = RedmineContracts::BudgetPluginMigration.export_data 13 | RedmineContracts::BudgetPluginMigration.rename_old_tables 14 | RedmineContracts::BudgetPluginMigration.migrate_contracts 15 | RedmineContracts::BudgetPluginMigration.migrate(data, options) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/fixtures/budget_plugin_migration/budget.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - total_hours: 100.0 3 | overhead: 200.0 4 | profit_percent: 5 | project_id: 1 6 | fixed_cost: 7 | cost_per_hour: 50.0 8 | subject: Deliverable One 9 | materials: 200.0 10 | id: 1 11 | type: HourlyDeliverable 12 | project_manager_signoff: false 13 | profit: 200.0 14 | overhead_percent: 15 | description: "" 16 | materials_percent: 17 | due: 2008-06-30 18 | budget: 5600.0 19 | client_signoff: false 20 | - total_hours: 12.0 21 | overhead: 22 | profit_percent: 50 23 | project_id: 2 24 | fixed_cost: 25 | cost_per_hour: 25.0 26 | subject: Deliverable 2 27 | materials: 0.0 28 | id: 2 29 | type: HourlyDeliverable 30 | project_manager_signoff: false 31 | profit: 32 | overhead_percent: 150 33 | description: "" 34 | materials_percent: 10 35 | due: 2008-06-18 36 | budget: 900.0 37 | client_signoff: false 38 | - total_hours: 80.0 39 | overhead: 40 | profit_percent: 150 41 | project_id: 2 42 | fixed_cost: 30000.0 43 | cost_per_hour: 85.0 44 | subject: Version 1.0 45 | materials: 0.0 46 | id: 4 47 | type: FixedDeliverable 48 | project_manager_signoff: true 49 | profit: 50 | overhead_percent: 60 51 | description: Some description here 52 | materials_percent: 53 | due: 2008-05-01 54 | budget: 93000.0 55 | client_signoff: true 56 | -------------------------------------------------------------------------------- /test/functional/contracts_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class ContractsControllerTest < ActionController::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/integration/contracts_delete_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ContractsDeleteTest < ActionController::IntegrationTest 4 | include Redmine::I18n 5 | 6 | def setup 7 | @project = Project.generate!(:identifier => 'main') 8 | @contract = Contract.generate!(:project => @project, :name => 'A Contract') 9 | @user = User.generate_user_with_permission_to_manage_budget(:project => @project) 10 | 11 | login_as(@user.login, 'contracts') 12 | end 13 | 14 | should "allow admins to delete the contract" do 15 | @user = User.generate!(:login => 'admin', :password => 'existing', :password_confirmation => 'existing', :admin => true) 16 | login_as('admin', 'existing') 17 | 18 | visit_contracts_for_project(@project) 19 | click_link @contract.id 20 | assert_response :success 21 | 22 | click_link 'Update' 23 | assert_response :success 24 | assert_template 'contracts/edit' 25 | 26 | assert_select "a[href=?]", contract_path(@project, @contract), :text => /Delete/ 27 | click_link 'Delete' 28 | assert_response :success 29 | assert_template 'contracts/index' 30 | 31 | assert_nil Contract.find_by_id(@contract.id), "Contract not deleted" 32 | end 33 | 34 | should "not allow non-admins to delete the contract" do 35 | visit_contracts_for_project(@project) 36 | click_link @contract.id 37 | assert_response :success 38 | 39 | click_link 'Update' 40 | assert_response :success 41 | assert_template 'contracts/edit' 42 | 43 | assert_select "a", :text => /Delete/, :count => 0 44 | delete contract_path(@project, @contract) 45 | assert_forbidden 46 | 47 | assert Contract.find_by_id(@contract.id), "Contract deleted" 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/integration/contracts_edit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ContractsEditTest < ActionController::IntegrationTest 4 | include Redmine::I18n 5 | 6 | def setup 7 | @project = Project.generate!(:identifier => 'main') 8 | @account_executive = User.generate! 9 | @role = Role.generate! 10 | User.add_to_project(@account_executive, @project, @role) 11 | @contract = Contract.generate!(:project => @project, :name => 'A Contract', :account_executive => @account_executive) 12 | @user = User.generate_user_with_permission_to_manage_budget(:project => @project) 13 | 14 | login_as(@user.login, 'contracts') 15 | end 16 | 17 | should "block anonymous users from editing the contract" do 18 | logout 19 | visit "/projects/#{@project.identifier}/contracts/#{@contract.id}/edit" 20 | 21 | assert_requires_login 22 | end 23 | 24 | should "block unauthorized users from editing the contract" do 25 | logout 26 | 27 | @user = User.generate!(:password => 'test', :password_confirmation => 'test') 28 | login_as(@user.login, 'test') 29 | 30 | visit "/projects/#{@project.identifier}/contracts/#{@contract.id}/edit" 31 | 32 | assert_forbidden 33 | end 34 | 35 | should "allow authorized users to edit the contract" do 36 | visit_contracts_for_project(@project) 37 | click_link @contract.id 38 | assert_response :success 39 | 40 | click_link 'Update' 41 | assert_response :success 42 | assert_template 'contracts/edit' 43 | 44 | assert_select "h2", :text => /#{@contract.name}/ 45 | assert_select "form#edit_contract_#{@contract.id}.contract" do 46 | assert_select "input[value=?]", /#{@contract.name}/ 47 | assert_select "select#contract_payment_term_id" 48 | end 49 | 50 | fill_in "Name", :with => 'An updated name' 51 | click_button "Save Contract" 52 | 53 | assert_response :success 54 | assert_template 'contracts/show' 55 | 56 | assert_equal "An updated name", @contract.reload.name 57 | end 58 | 59 | context "locked contract" do 60 | setup do 61 | assert @contract.lock! 62 | end 63 | 64 | should "block edits" do 65 | visit_contract_page(@contract) 66 | click_link 'Update' 67 | assert_response :success 68 | 69 | fill_in "Name", :with => 'An updated name' 70 | click_button 'Save Contract' 71 | 72 | assert_response :success 73 | assert_template 'contracts/edit' 74 | 75 | assert_not_equal "An updated name", @contract.reload.name 76 | end 77 | 78 | should "block edits even when the status is changed to closed" do 79 | visit_contract_page(@contract) 80 | click_link 'Update' 81 | assert_response :success 82 | 83 | fill_in "Name", :with => 'An updated name' 84 | select "Closed", :from => "Status" 85 | click_button 'Save Contract' 86 | 87 | assert_response :success 88 | assert_template 'contracts/edit' 89 | 90 | assert_not_equal "An updated name", @contract.reload.name 91 | assert @contract.reload.locked? 92 | end 93 | 94 | should "be allowed to change the status from locked to open" do 95 | visit_contract_page(@contract) 96 | click_link 'Update' 97 | assert_response :success 98 | 99 | select "Open", :from => "Status" 100 | click_button 'Save Contract' 101 | 102 | assert_response :success 103 | assert_template 'contracts/show' 104 | 105 | assert @contract.reload.open? 106 | end 107 | 108 | should "be allowed to change the status from locked to closed" do 109 | visit_contract_page(@contract) 110 | click_link 'Update' 111 | assert_response :success 112 | 113 | select "Closed", :from => "Status" 114 | click_button 'Save Contract' 115 | 116 | assert_response :success 117 | assert_template 'contracts/show' 118 | 119 | assert @contract.reload.closed? 120 | end 121 | end 122 | 123 | context "closed contract" do 124 | setup do 125 | assert @contract.close! 126 | end 127 | 128 | should "block edits" do 129 | visit_contract_page(@contract) 130 | click_link 'Update' 131 | assert_response :success 132 | 133 | fill_in "Name", :with => 'An updated name' 134 | click_button 'Save Contract' 135 | 136 | assert_response :success 137 | assert_template 'contracts/edit' 138 | 139 | assert_not_equal "An updated name", @contract.reload.name 140 | end 141 | 142 | should "block edits weven when the status is changed to locked" do 143 | visit_contract_page(@contract) 144 | click_link 'Update' 145 | assert_response :success 146 | 147 | fill_in "Name", :with => 'An updated name' 148 | select "Locked", :from => "Status" 149 | click_button 'Save Contract' 150 | 151 | assert_response :success 152 | assert_template 'contracts/edit' 153 | 154 | assert_not_equal "An updated name", @contract.reload.name 155 | assert @contract.reload.closed? 156 | end 157 | 158 | should "be allowed to change the status from closed to open" do 159 | visit_contract_page(@contract) 160 | click_link 'Update' 161 | assert_response :success 162 | 163 | select "Open", :from => "Status" 164 | click_button 'Save Contract' 165 | 166 | assert_response :success 167 | assert_template 'contracts/show' 168 | 169 | assert @contract.reload.open? 170 | end 171 | 172 | should "be allowed to change the status from closed to locked" do 173 | visit_contract_page(@contract) 174 | click_link 'Update' 175 | assert_response :success 176 | 177 | select "Locked", :from => "Status" 178 | click_button 'Save Contract' 179 | 180 | assert_response :success 181 | assert_template 'contracts/show' 182 | 183 | assert @contract.reload.locked? 184 | end 185 | end 186 | end 187 | -------------------------------------------------------------------------------- /test/integration/contracts_list_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ContractsListTest < ActionController::IntegrationTest 4 | include Redmine::I18n 5 | 6 | def setup 7 | @project = Project.generate!(:identifier => 'main') 8 | @contract = Contract.generate!(:project => @project, :name => 'Contract1').reload 9 | @contract2 = Contract.generate!(:project => @project, :name => 'Contract2').reload 10 | @contract_locked = Contract.generate!(:project => @project, :status => 'locked', :name => 'LockedContract').reload 11 | @contract_closed = Contract.generate!(:project => @project, :status => 'closed', :name => 'ClosedContract').reload 12 | 13 | @other_project = Project.generate!(:identifier => 'other') 14 | @other_contract = Contract.generate!(:project => @other_project) 15 | [@project, 16 | @other_project, 17 | @contract, 18 | @contract2, 19 | @other_contract 20 | ].map {|c| c.reload } 21 | 22 | @user = User.generate_user_with_permission_to_manage_budget(:project => @project) 23 | 24 | login_as(@user.login, 'contracts') 25 | end 26 | 27 | should "block anonymous users from listing the contracts" do 28 | logout 29 | visit "/projects/#{@project.identifier}/contracts" 30 | 31 | assert_requires_login 32 | end 33 | 34 | should "block unauthorized users from listing contracts" do 35 | logout 36 | 37 | @user = User.generate!(:password => 'test', :password_confirmation => 'test') 38 | login_as(@user.login, 'test') 39 | 40 | visit "/projects/#{@project.identifier}/contracts" 41 | 42 | assert_forbidden 43 | end 44 | 45 | should "allow authorized users to list the contracts on a project" do 46 | visit_contracts_for_project(@project) 47 | end 48 | 49 | should "list all contracts for the project grouped by status" do 50 | visit_contracts_for_project(@project) 51 | 52 | assert_select "table#contracts.open" do 53 | [@contract, @contract2].each do |contract| 54 | assert_select "td.id", :text => /#{contract.id}/ 55 | assert_select "td.name", :text => /#{contract.name}/ 56 | assert_select "td.account-executive", :text => /#{contract.account_executive.name}/ 57 | assert_select "td.end-date", :text => /#{format_date(contract.end_date)}/ 58 | assert_select "td.total-budget" 59 | end 60 | end 61 | 62 | assert_select "table#contracts.locked" do 63 | assert_select "td.id", :text => /#{@contract_locked.id}/ 64 | assert_select "td.name", :text => /#{@contract_locked.name}/ 65 | assert_select "td.account-executive", :text => /#{@contract_locked.account_executive.name}/ 66 | assert_select "td.end-date", :text => /#{format_date(@contract_locked.end_date)}/ 67 | assert_select "td.total-budget" 68 | end 69 | 70 | assert_select "table#contracts.closed" do 71 | assert_select "td.id", :text => /#{@contract_closed.id}/ 72 | assert_select "td.name", :text => /#{@contract_closed.name}/ 73 | assert_select "td.account-executive", :text => /#{@contract_closed.account_executive.name}/ 74 | assert_select "td.end-date", :text => /#{format_date(@contract_closed.end_date)}/ 75 | assert_select "td.total-budget" 76 | end 77 | 78 | end 79 | 80 | should "not list contracts from other projects" do 81 | visit_contracts_for_project(@project) 82 | 83 | assert_select "table#contracts" do 84 | assert_select "td", :text => /#{@other_contract.name}/, :count => 0 85 | end 86 | 87 | end 88 | 89 | end 90 | -------------------------------------------------------------------------------- /test/integration/contracts_new_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ContractsNewTest < ActionController::IntegrationTest 4 | include Redmine::I18n 5 | 6 | def setup 7 | @project = Project.generate!(:identifier => 'main') 8 | PaymentTerm.generate!(:type => 'PaymentTerm', :name => 'Net 15') 9 | PaymentTerm.generate!(:type => 'PaymentTerm', :name => 'Net 30') 10 | @user = User.generate_user_with_permission_to_manage_budget(:project => @project) 11 | 12 | login_as(@user.login, 'contracts') 13 | end 14 | 15 | should "block anonymous users from opening the new contract form" do 16 | logout 17 | visit "/projects/#{@project.identifier}/contracts/new" 18 | 19 | assert_requires_login 20 | end 21 | 22 | should "block unauthorized users from opening the new contract form" do 23 | logout 24 | 25 | @user = User.generate!(:password => 'test', :password_confirmation => 'test') 26 | login_as(@user.login, 'test') 27 | 28 | visit "/projects/#{@project.identifier}/contracts/new" 29 | 30 | assert_forbidden 31 | end 32 | 33 | should "allow authorized users to open the new contracts form" do 34 | visit_contracts_for_project(@project) 35 | click_link 'New Contract' 36 | assert_response :success 37 | 38 | assert_select "form#new_contract" 39 | end 40 | 41 | should "create a new contract" do 42 | @account_executive = User.generate! 43 | @role = Role.generate! 44 | User.add_to_project(@account_executive, @project, @role) 45 | 46 | visit_contracts_for_project(@project) 47 | click_link 'New Contract' 48 | assert_response :success 49 | 50 | fill_in "Name", :with => 'A New Contract' 51 | select @account_executive.name, :from => "Account Executive" 52 | fill_in "Start", :with => '2010-01-01' 53 | fill_in "End Date", :with => '2010-12-31' 54 | select "Net 30", :from => "Payment Terms" 55 | select "Locked", :from => "Status" 56 | 57 | click_button "Save Contract" 58 | 59 | assert_response :success 60 | assert_template 'contracts/show' 61 | 62 | @contract = Contract.last 63 | assert_equal "A New Contract", @contract.name 64 | assert_equal @account_executive, @contract.account_executive 65 | assert_equal '2010-01-01', @contract.start_date.to_s 66 | assert_equal '2010-12-31', @contract.end_date.to_s 67 | assert_equal 'Net 30', @contract.payment_term.name 68 | assert_equal "locked", @contract.status 69 | 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /test/integration/deliverable_details_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class DeliverableDetailsShowTest < ActionController::IntegrationTest 4 | include Redmine::I18n 5 | 6 | def setup 7 | @project = Project.generate!(:identifier => 'main').reload 8 | @contract = Contract.generate!(:project => @project, :billable_rate => 10) 9 | @manager = User.generate! 10 | @deliverable1 = RetainerDeliverable.spawn(:contract => @contract, :manager => @manager, :title => "Retainer", :start_date => '2010-01-01', :end_date => '2010-03-31') 11 | @deliverable1.labor_budgets << LaborBudget.spawn(:budget => 100, :hours => 10) 12 | @deliverable1.overhead_budgets << OverheadBudget.spawn(:budget => 200, :hours => 10) 13 | 14 | @deliverable1.save! 15 | @user = User.generate_user_with_permission_to_manage_budget(:project => @project) 16 | 17 | login_as(@user.login, 'contracts') 18 | end 19 | 20 | context "for an anonymous JS request" do 21 | should "require login" do 22 | logout 23 | 24 | visit "/projects/#{@project.id}/contracts/#{@contract.id}/deliverables/#{@deliverable1.id}", :get, {:format => 'js', :as => 'deliverable_details_row'} 25 | 26 | assert_response :unauthorized 27 | end 28 | 29 | end 30 | 31 | context "for an unauthorized JS request" do 32 | should "be forbidden" do 33 | logout 34 | 35 | @user = User.generate!(:password => 'test', :password_confirmation => 'test') 36 | login_as(@user.login, 'test') 37 | 38 | visit "/projects/#{@project.id}/contracts/#{@contract.id}/deliverables/#{@deliverable1.id}", :get, {:format => 'js', :as => 'deliverable_details_row'} 39 | 40 | assert_response :forbidden 41 | end 42 | 43 | end 44 | 45 | 46 | context "for an authorized JS request" do 47 | should "render the details for the deliverable" do 48 | visit "/projects/#{@project.id}/contracts/#{@contract.id}/deliverables/#{@deliverable1.id}", :get, {:format => 'js', :as => 'deliverable_details_row'} 49 | 50 | assert_response :success 51 | assert_select ".deliverable_details_outer_wrapper_#{@deliverable1.id}" 52 | assert_select "table#deliverables", :count => 0 # Not the full table 53 | assert_select "tr#deliverable_details_#{@deliverable1.id}", :count => 0 # Not the wrapper tr 54 | 55 | end 56 | 57 | should "filter the details based on the period" do 58 | assert_equal 300, @deliverable1.labor_budget_total 59 | assert_equal 600, @deliverable1.overhead_budget_total 60 | assert_equal 300, @deliverable1.total # Contract rate * 30 hours (labor) 61 | 62 | visit "/projects/#{@project.id}/contracts/#{@contract.id}/deliverables/#{@deliverable1.id}", :get, {:format => 'js', :as => 'deliverable_details_row', :period => '2010-02'} 63 | 64 | assert_response :success 65 | assert_select ".deliverable_details_outer_wrapper_#{@deliverable1.id}" do 66 | assert_select "td.labor_budget_total", '100' 67 | assert_select "td.overhead_budget_total", '200' 68 | assert_select "td.total", '100' 69 | 70 | assert_select "select.retainer_period_change" do 71 | assert_select "option[selected=selected]", "February 2010" 72 | end 73 | end 74 | 75 | end 76 | 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/integration/deliverable_finances_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class DeliverableFinancesShowTest < ActionController::IntegrationTest 4 | include Redmine::I18n 5 | 6 | def setup 7 | configure_overhead_plugin 8 | @project = Project.generate!(:identifier => 'main').reload 9 | @contract = Contract.generate!(:project => @project, :billable_rate => 10) 10 | @manager = User.generate!.reload 11 | @deliverable1 = RetainerDeliverable.spawn(:contract => @contract, :manager => @manager, :title => "Retainer Title", :start_date => '2010-01-01', :end_date => '2010-03-31') 12 | @deliverable1.labor_budgets << LaborBudget.spawn(:budget => 100, :hours => 10, :time_entry_activity => @billable_activity) 13 | @deliverable1.overhead_budgets << OverheadBudget.spawn(:budget => 200, :hours => 10) 14 | 15 | @deliverable1.save! 16 | @user = User.generate_user_with_permission_to_manage_budget(:project => @project) 17 | @category_on_billable = IssueCategory.generate!(:project => @project).reload 18 | @category_on_non_billable = IssueCategory.generate!(:project => @project).reload 19 | # 2 hours of $100 billable work 20 | create_issue_with_time_for_deliverable(@deliverable1, { 21 | :activity => @billable_activity, 22 | :user => @manager, 23 | :hours => 2, 24 | :amount => 100, 25 | :issue_category => @category_on_billable 26 | }) 27 | # 1 hour of $100 billable work with no category 28 | create_issue_with_time_for_deliverable(@deliverable1, { 29 | :activity => @billable_activity, 30 | :user => @manager, 31 | :hours => 1, 32 | :amount => 100, 33 | :issue_category => nil 34 | }) 35 | # 5 hours of $100 nonbillable work 36 | create_issue_with_time_for_deliverable(@deliverable1, { 37 | :activity => @non_billable_activity, 38 | :user => @manager, 39 | :hours => 5, 40 | :amount => 100, 41 | :issue_category => @category_on_non_billable 42 | }) 43 | 44 | @user.reload 45 | login_as(@user.login, 'contracts') 46 | end 47 | 48 | context "for an anonymous request" do 49 | should "require login" do 50 | logout 51 | 52 | visit "/projects/#{@project.id}/contracts/#{@contract.id}/deliverables/#{@deliverable1.id}/finances" 53 | 54 | assert_response :success 55 | assert_template 'account/login' 56 | end 57 | 58 | end 59 | 60 | context "for an unauthorized request" do 61 | should "be forbidden" do 62 | logout 63 | 64 | @user = User.generate!(:password => 'test', :password_confirmation => 'test') 65 | login_as(@user.login, 'test') 66 | 67 | visit "/projects/#{@project.id}/contracts/#{@contract.id}/deliverables/#{@deliverable1.id}/finances" 68 | 69 | assert_response :forbidden 70 | end 71 | 72 | end 73 | 74 | 75 | context "for an authorized request" do 76 | setup do 77 | visit "/projects/#{@project.id}/contracts/#{@contract.id}/deliverables/#{@deliverable1.id}/finances" 78 | 79 | assert_response :success 80 | end 81 | 82 | should "render the finance report title section for the deliverable" do 83 | assert_select "h2", :text => /#{@deliverable1.title}/ 84 | 85 | assert_select "div#finance-summary" do 86 | assert_select "span.spent", :text => /\$300/ # ($100 * 2) + ($100 * 1) 87 | assert_select "span.total", :text => /\$300/ # $100 * 3 88 | assert_select "span.hours", :text => /3/ 89 | end 90 | end 91 | 92 | should "render the labor activities table for the deliverable" do 93 | assert_select "table#deliverable-labor-activities" do 94 | assert_select "tr.labor" do 95 | assert_select "td", :text => /#{@billable_activity.name}/ 96 | assert_select "td.spent-amount", :text => /\$300/ 97 | assert_select "td.total-amount", :text => /\$300/ 98 | assert_select "td.spent-hours", :text => /3/ 99 | assert_select "td.total-deliverable-hours", :text => /30/ # 3 month retainer * 10 100 | end 101 | 102 | assert_select "tr.summary-row.labor" do 103 | assert_select "td", :text => /Totals/ 104 | assert_select "td.spent-amount", :text => /\$300/ 105 | assert_select "td.total-amount", :text => /\$300/ 106 | assert_select "td.spent-hours", :text => /3/ 107 | assert_select "td.total-deliverable-hours", :text => /30/ 108 | end 109 | 110 | end 111 | end 112 | 113 | should "render the overhead activities table for the deliverable" do 114 | assert_select "table#deliverable-overhead-activities" do 115 | assert_select "tr.overhead" do 116 | assert_select "td", :text => /#{@non_billable_activity.name}/ 117 | assert_select "td.spent-amount", :text => /\$500/ 118 | assert_select "td.total-amount", :text => /\$600/ 119 | assert_select "td.spent-hours", :text => /5/ 120 | assert_select "td.total-deliverable-hours", :text => /30/ # 3 month retainer * 10 121 | end 122 | 123 | assert_select "tr.summary-row.overhead" do 124 | assert_select "td", :text => /Totals/ 125 | assert_select "td.spent-amount", :text => /\$500/ 126 | assert_select "td.total-amount", :text => /\$600/ 127 | assert_select "td.spent-hours", :text => /5/ 128 | assert_select "td.total-deliverable-hours", :text => /30/ 129 | end 130 | 131 | end 132 | end 133 | 134 | should "render the labor finances for each user for the deliverable" do 135 | assert_select "table#deliverable-labor-users" do 136 | assert_select "tr.labor" do 137 | assert_select "td", :text => /#{@manager.name}/ 138 | assert_select "td.amount-cost", :text => /\$300/ 139 | assert_select "td.time-cost", :text => /3/ 140 | end 141 | 142 | assert_select "tr.summary-row" do 143 | assert_select "td", :text => /Totals/ 144 | assert_select "td.amount-cost", :text => /\$300/ 145 | assert_select "td.time-cost", :text => /3/ 146 | end 147 | 148 | end 149 | end 150 | 151 | should "render the overhead finances for each user for the deliverable" do 152 | assert_select "table#deliverable-overhead-users" do 153 | assert_select "tr.overhead" do 154 | assert_select "td", :text => /#{@manager.name}/ 155 | assert_select "td.amount-cost", :text => /\$500/ 156 | assert_select "td.time-cost", :text => /5/ 157 | end 158 | 159 | assert_select "tr.summary-row.overhead" do 160 | assert_select "td", :text => /Totals/ 161 | assert_select "td.amount-cost", :text => /\$500/ 162 | assert_select "td.time-cost", :text => /5/ 163 | end 164 | 165 | end 166 | end 167 | 168 | should "render the labor finances for each Issue Category for the deliverable" do 169 | assert_select "table#deliverable-labor-issue-categories" do 170 | assert_select "tr.labor" do 171 | assert_select "td", :text => /#{@category_on_billable.name}/ 172 | assert_select "td.amount-cost", :text => /\$200/ 173 | assert_select "td.time-cost", :text => /2/ 174 | end 175 | 176 | assert_select "tr.labor" do 177 | assert_select "td", :text => /none/ 178 | assert_select "td.amount-cost", :text => /\$100/ 179 | assert_select "td.time-cost", :text => /1/ 180 | end 181 | 182 | assert_select "tr.summary-row" do 183 | assert_select "td", :text => /Totals/ 184 | assert_select "td.amount-cost", :text => /\$300/ 185 | assert_select "td.time-cost", :text => /3/ 186 | end 187 | 188 | end 189 | end 190 | 191 | should "render the overhead finances for each Issue Category for the deliverable" do 192 | assert_select "table#deliverable-overhead-issue-categories" do 193 | assert_select "tr.overhead" do 194 | assert_select "td", :text => /#{@category_on_non_billable.name}/ 195 | assert_select "td.amount-cost", :text => /\$500/ 196 | assert_select "td.time-cost", :text => /5/ 197 | end 198 | 199 | assert_select "tr.summary-row.overhead" do 200 | assert_select "td", :text => /Totals/ 201 | assert_select "td.amount-cost", :text => /\$500/ 202 | assert_select "td.time-cost", :text => /5/ 203 | end 204 | 205 | end 206 | end 207 | end 208 | end 209 | -------------------------------------------------------------------------------- /test/integration/deliverables_delete_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class DeliverablesDeleteTest < ActionController::IntegrationTest 4 | include Redmine::I18n 5 | 6 | def setup 7 | @project = Project.generate!(:identifier => 'main') 8 | @contract = Contract.generate!(:project => @project, :name => 'A Contract') 9 | @manager = User.generate! 10 | @deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager) 11 | @user = User.generate_user_with_permission_to_manage_budget(:project => @project) 12 | 13 | login_as(@user.login, 'contracts') 14 | end 15 | 16 | should "block anonymous users from deleting the deliverable" do 17 | logout 18 | delete "/projects/#{@project.identifier}/contracts/#{@contract.id}/deliverables/#{@deliverable.id}" 19 | follow_redirect! 20 | 21 | assert_requires_login 22 | end 23 | 24 | should "block unauthorized users from deleting the deliverable" do 25 | logout 26 | 27 | @user = User.generate!(:password => 'test', :password_confirmation => 'test') 28 | login_as(@user.login, 'test') 29 | 30 | delete "/projects/#{@project.identifier}/contracts/#{@contract.id}/deliverables/#{@deliverable.id}" 31 | 32 | assert_forbidden 33 | end 34 | 35 | should "allow authorized users to delete the deliverable" do 36 | visit_contract_page(@contract) 37 | 38 | click_link_within "#deliverable_details_#{@deliverable.id}", 'Delete' 39 | assert_response :success 40 | assert_template 'contracts/show' 41 | 42 | assert_select '.flash.notice', /successfully deleted/ 43 | 44 | 45 | assert_nil Deliverable.find_by_id(@deliverable.id), "Deliverable not deleted" 46 | end 47 | 48 | should "unassign issues from the deliverable when deleting" do 49 | issues = [ 50 | Issue.generate_for_project!(@project, :deliverable => @deliverable), 51 | Issue.generate_for_project!(@project, :deliverable => @deliverable), 52 | Issue.generate_for_project!(@project, :deliverable => @deliverable) 53 | ] 54 | assert_equal 3, @deliverable.issues.count 55 | 56 | @project.reload 57 | 58 | visit_contract_page(@contract) 59 | 60 | click_link_within "#deliverable_details_#{@deliverable.id}", 'Delete' 61 | assert_response :success 62 | assert_template 'contracts/show' 63 | 64 | assert_nil Deliverable.find_by_id(@deliverable.id), "Deliverable not deleted" 65 | assert_equal 0, @deliverable.issues.count 66 | assert issues.all? {|issue| 67 | issue.reload && issue.deliverable_id.nil? 68 | }, "Issues' deliverable was not removed #{issues.collect(&:deliverable_id).join(', ')}" 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/integration/deliverables_list_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class DeliverablesListTest < ActionController::IntegrationTest 4 | include Redmine::I18n 5 | 6 | def setup 7 | @project = Project.generate!(:identifier => 'main') 8 | @contract = Contract.generate!(:project => @project) 9 | @manager = User.generate! 10 | @deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager) 11 | @user = User.generate_user_with_permission_to_manage_budget(:project => @project) 12 | 13 | login_as(@user.login, 'contracts') 14 | end 15 | 16 | should "redirect to the contract page" do 17 | visit "/projects/#{@project.identifier}/contracts/#{@contract.id}/deliverables/" 18 | assert_response :success 19 | assert_template 'contracts/show' 20 | 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/integration/deliverables_show_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class DeliverablesShowTest < ActionController::IntegrationTest 4 | include Redmine::I18n 5 | 6 | def setup 7 | @project = Project.generate!(:identifier => 'main') 8 | @contract = Contract.generate!(:project => @project) 9 | @manager = User.generate! 10 | @deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager) 11 | @user = User.generate_user_with_permission_to_manage_budget(:project => @project) 12 | 13 | login_as(@user.login, 'contracts') 14 | end 15 | 16 | should "redirect to the contract page" do 17 | visit "/projects/#{@project.identifier}/contracts/#{@contract.id}/deliverables/#{@deliverable.id}" 18 | assert_response :success 19 | assert_template 'contracts/show' 20 | 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/integration/disabled_contracts_module_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class DisabledContractsModuleTest < ActionController::IntegrationTest 4 | context "on a project with the Contracts module disabled" do 5 | setup do 6 | @project = Project.generate! 7 | @project.enabled_modules.find_by_name('contracts').destroy 8 | @project.reload 9 | assert !@project.module_enabled?(:contracts), "Contracts enabled on project" 10 | 11 | @user = User.generate_user_with_permission_to_manage_budget(:project => @project) 12 | login_as(@user.login, 'contracts') 13 | end 14 | 15 | should "not show the menu item" do 16 | visit_project(@project) 17 | 18 | assert_select "#main-menu" do 19 | assert_select 'a', :text => /contracts/i, :count => 0 20 | end 21 | end 22 | 23 | should "block access to list" do 24 | visit "/projects/#{@project.identifier}/contracts" 25 | assert_forbidden 26 | end 27 | 28 | should "block access to new" do 29 | visit "/projects/#{@project.identifier}/contracts/new" 30 | assert_forbidden 31 | end 32 | 33 | should "block access to show" do 34 | visit "/projects/#{@project.identifier}/contracts/1" 35 | assert_forbidden 36 | end 37 | 38 | should "block access to edit" do 39 | visit "/projects/#{@project.identifier}/contracts/1/edit" 40 | assert_forbidden 41 | end 42 | 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /test/integration/issue_filtering_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class IssueFilteringTest < ActionController::IntegrationTest 4 | include Redmine::I18n 5 | 6 | def setup 7 | @project = Project.generate!(:identifier => 'main') 8 | @contract = Contract.generate!(:project => @project) 9 | @manager = User.generate! 10 | @deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager) 11 | @user = User.generate_user_with_permission_to_manage_budget(:project => @project).reload 12 | @user.admin = true # Getting odd permissions issues 13 | @user.save 14 | @issue1 = Issue.generate_for_project!(@project) 15 | @issue2 = Issue.generate_for_project!(@project, :deliverable => @deliverable) 16 | assert_equal @deliverable, @issue2.deliverable 17 | 18 | login_as(@user.login, 'contracts') 19 | end 20 | 21 | should "allow grouping issues by deliverable" do 22 | visit_project(@project) 23 | click_link "Issues" 24 | 25 | assert_select '#group_by' do 26 | assert_select 'option', "Deliverable" 27 | end 28 | 29 | select "Deliverable", :from => 'group_by' 30 | 31 | # Apply link is behind a JavaScript form 32 | visit "/projects/#{@project.identifier}/issues/?set_filter&group_by=deliverable" 33 | assert_response :success 34 | 35 | assert_select "tr.group" do 36 | assert_select "td", :text => /None/ do 37 | assert_select "span.count", "(1)" 38 | end 39 | assert_select "td", :text => Regexp.new(@deliverable.title) do 40 | assert_select "span.count", "(1)" 41 | end 42 | end 43 | 44 | end 45 | 46 | should "allow grouping issues by contract" do 47 | visit_project(@project) 48 | click_link "Issues" 49 | 50 | assert_select '#group_by' do 51 | assert_select 'option', "Contract" 52 | end 53 | 54 | select "Contract", :from => 'group_by' 55 | 56 | # Apply link is behind a JavaScript form 57 | visit "/projects/#{@project.identifier}/issues/?set_filter&group_by=contract_name" 58 | assert_response :success 59 | 60 | assert_select "tr.group" do 61 | assert_select "td", :text => /None/ do 62 | assert_select "span.count", "(1)" 63 | end 64 | assert_select "td", :text => Regexp.new(@contract.name) do 65 | assert_select "span.count", "(1)" 66 | end 67 | end 68 | 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /test/integration/overhead_plugin_integration_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class OverheadPluginIntegrationTest < ActionController::IntegrationTest 4 | def setup 5 | @project = Project.generate!(:identifier => 'main') 6 | @contract = Contract.generate!(:project => @project, :name => 'A Contract') 7 | @manager = User.generate! 8 | @role = Role.generate! 9 | User.add_to_project(@manager, @project, @role) 10 | @fixed_deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'The Title') 11 | @hourly_deliverable = HourlyDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'An Hourly') 12 | 13 | configure_overhead_plugin 14 | end 15 | 16 | context "Patches to Deliverable" do 17 | context "#overhead_spent" do 18 | should "return the total cost for all of the time on the issues for non-billable activities" do 19 | @issue1 = Issue.generate_for_project!(@project) 20 | @time_entry1 = TimeEntry.generate!(:issue => @issue1, 21 | :project => @project, 22 | :activity => @billable_activity, 23 | :spent_on => Date.today, 24 | :hours => 10, 25 | :user => @manager) 26 | @time_entry2 = TimeEntry.generate!(:issue => @issue1, 27 | :project => @project, 28 | :activity => @non_billable_activity, 29 | :spent_on => Date.today, 30 | :hours => 20, 31 | :user => @manager) 32 | 33 | @rate = Rate.generate!(:project => @project, 34 | :user => @manager, 35 | :date_in_effect => Date.yesterday, 36 | :amount => 100) 37 | 38 | @hourly_deliverable.issues << @issue1 39 | 40 | assert_equal 1, @hourly_deliverable.issues.count 41 | 42 | assert_equal 20 * 100, @hourly_deliverable.overhead_spent 43 | 44 | end 45 | end 46 | end 47 | 48 | context "Patches to HourlyDeliverable" do 49 | context "#labor_budget_spent" do 50 | should "return 0 if there are no issues assigned" do 51 | assert_equal 0, @hourly_deliverable.issues.count 52 | 53 | assert_equal 0, @hourly_deliverable.labor_budget_spent 54 | end 55 | 56 | should "return the total cost for all of the time on the issues for billable activities" do 57 | 58 | @issue1 = Issue.generate_for_project!(@project) 59 | @time_entry1 = TimeEntry.generate!(:issue => @issue1, 60 | :project => @project, 61 | :activity => @billable_activity, 62 | :spent_on => Date.today, 63 | :hours => 10, 64 | :user => @manager) 65 | @time_entry2 = TimeEntry.generate!(:issue => @issue1, 66 | :project => @project, 67 | :activity => @non_billable_activity, 68 | :spent_on => Date.today, 69 | :hours => 20, 70 | :user => @manager) 71 | 72 | @rate = Rate.generate!(:project => @project, 73 | :user => @manager, 74 | :date_in_effect => Date.yesterday, 75 | :amount => 100) 76 | 77 | @hourly_deliverable.issues << @issue1 78 | 79 | assert_equal 1, @hourly_deliverable.issues.count 80 | 81 | assert_equal 10 * 100, @hourly_deliverable.labor_budget_spent 82 | 83 | end 84 | end 85 | end 86 | 87 | context "Patches to FixedDeliverable" do 88 | context "#labor_budget_spent" do 89 | should "return 0 if there are no issues assigned" do 90 | assert_equal 0, @fixed_deliverable.issues.count 91 | 92 | assert_equal 0, @fixed_deliverable.labor_budget_spent 93 | end 94 | 95 | should "return the total cost for all of the time on the issues for billable activities" do 96 | @issue1 = Issue.generate_for_project!(@project) 97 | @time_entry1 = TimeEntry.generate!(:issue => @issue1, 98 | :project => @project, 99 | :activity => @billable_activity, 100 | :spent_on => Date.today, 101 | :hours => 10, 102 | :user => @manager) 103 | @time_entry2 = TimeEntry.generate!(:issue => @issue1, 104 | :project => @project, 105 | :activity => @non_billable_activity, 106 | :spent_on => Date.today, 107 | :hours => 20, 108 | :user => @manager) 109 | 110 | @rate = Rate.generate!(:project => @project, 111 | :user => @manager, 112 | :date_in_effect => Date.yesterday, 113 | :amount => 100) 114 | 115 | @fixed_deliverable.issues << @issue1 116 | 117 | assert_equal 1, @fixed_deliverable.issues.count 118 | 119 | assert_equal 10 * 100, @fixed_deliverable.labor_budget_spent 120 | 121 | end 122 | end 123 | end 124 | 125 | end 126 | -------------------------------------------------------------------------------- /test/integration/redmine_contracts/hooks/controller_issues_bulk_edit_before_save_hook_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../../test_helper' 2 | 3 | class RedmineContracts::Hooks::ControllerIssuesBulkEditBeforeSaveHookTest < ActionController::IntegrationTest 4 | include Redmine::Hook::Helper 5 | 6 | context "#view_issues_bulk_edit_details_bottom" do 7 | setup do 8 | @project = Project.generate! 9 | @issue = Issue.generate_for_project!(@project) 10 | @issue2 = Issue.generate_for_project!(@project) 11 | @issue3 = Issue.generate_for_project!(@project) 12 | @issues = [@issue, @issue2, @issue3] 13 | @contract1 = Contract.generate!(:project => @project) 14 | @contract2 = Contract.generate!(:project => @project) 15 | 16 | @manager = User.generate!(:login => 'manager', :password => 'existing', :password_confirmation => 'existing') 17 | @role = Role.generate!(:permissions => [:view_issues, :edit_issues]) 18 | User.add_to_project(@manager, @project, @role) 19 | @deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'The Title 1') 20 | @deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'The Title 2') 21 | @issue.deliverable = @deliverable1 22 | @issue.save 23 | 24 | login_as('manager', 'existing') 25 | end 26 | 27 | context "when saving multiple issues" do 28 | 29 | context "with permission to Assign Deliverable to Issue" do 30 | setup do 31 | @role.permissions << :assign_deliverable_to_issue 32 | @role.save! 33 | visit_issue_bulk_edit_page(@issues) 34 | end 35 | 36 | should "allow clearing all of the deliverables" do 37 | select "none", :from => "Deliverable" 38 | click_button "Submit" 39 | 40 | assert_response :success 41 | 42 | @issues.each do |issue| 43 | assert_equal nil, issue.reload.deliverable 44 | end 45 | end 46 | 47 | should "allow assigning a deliverable" do 48 | select @deliverable2.title, :from => "Deliverable" 49 | click_button "Submit" 50 | 51 | assert_response :success 52 | 53 | @issues.each do |issue| 54 | assert_equal @deliverable2, issue.reload.deliverable 55 | end 56 | end 57 | 58 | end 59 | 60 | context "with no permission to Assign Deliverable to Issue" do 61 | setup do 62 | @role.permissions.delete(:assign_deliverable_to_issue) 63 | @role.save! 64 | visit_issue_bulk_edit_page(@issues) 65 | end 66 | 67 | should "not allow clearing deliverables" do 68 | # Simulate form post since the field is hidden 69 | post "/issues/bulk_edit", :ids => @issues.collect(&:id), :deliverable_id => 'none' 70 | 71 | assert_response :redirect 72 | 73 | assert_equal @deliverable1, @issue.reload.deliverable 74 | end 75 | 76 | should "not allow assigning a deliverable" do 77 | # Simulate form post since the field is hidden 78 | post "/issues/bulk_edit", :ids => @issues.collect(&:id), :deliverable_id => @deliverable2.id 79 | 80 | assert_response :redirect 81 | 82 | @issues.each do |issue| 83 | assert_not_equal @deliverable2, issue.reload.deliverable 84 | end 85 | end 86 | 87 | end 88 | 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /test/integration/redmine_contracts/hooks/controller_issues_edit_before_save.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../../test_helper' 2 | 3 | class RedmineContracts::Hooks::ControllerIssuesEditBeforeSaveTest < ActionController::IntegrationTest 4 | include Redmine::Hook::Helper 5 | 6 | context "#controller_issues_edit_before_save" do 7 | setup do 8 | @project = Project.generate! 9 | IssueStatus.generate!(:is_default => true) 10 | @issue = Issue.generate_for_project!(@project) 11 | @contract1 = Contract.generate!(:project => @project) 12 | @contract2 = Contract.generate!(:project => @project) 13 | 14 | @manager = User.generate!(:login => 'manager', :password => 'existing', :password_confirmation => 'existing', :admin => false) 15 | @role = Role.generate!(:permissions => [:view_issues, :add_issues, :edit_issues, :assign_deliverable_to_issue]) 16 | User.add_to_project(@manager, @project, @role) 17 | @deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'The Title for 1') 18 | @deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'The Title for 2') 19 | 20 | login_as('manager', 'existing') 21 | @project.reload 22 | end 23 | 24 | context "for a new issue" do 25 | setup do 26 | visit_project(@project) 27 | end 28 | 29 | should "set the issue's deliverable" do 30 | click_link "New issue" 31 | fill_in "Subject", :with => 'Hook test' 32 | select @deliverable2.title, :from => "Deliverable" 33 | click_button "Create" 34 | 35 | assert_response :success 36 | 37 | assert_equal @deliverable2, Issue.last.deliverable 38 | 39 | end 40 | 41 | should "not allow setting a locked Deliverable" do 42 | assert @deliverable2.lock! 43 | click_link "New issue" 44 | 45 | fill_in "Subject", :with => 'Hook test' 46 | select @deliverable2.title, :from => "Deliverable" 47 | assert_no_difference("Issue.count") do 48 | click_button "Create" 49 | 50 | assert_response :success 51 | end 52 | 53 | assert_equal nil, Issue.last.deliverable 54 | 55 | end 56 | 57 | should "not allow setting a closed Deliverable" do 58 | assert @deliverable2.close! 59 | click_link "New issue" 60 | 61 | fill_in "Subject", :with => 'Hook test' 62 | select @deliverable2.title, :from => "Deliverable" 63 | assert_no_difference("Issue.count") do 64 | click_button "Create" 65 | 66 | assert_response :success 67 | end 68 | 69 | assert_equal nil, Issue.last.deliverable 70 | 71 | end 72 | 73 | should "not allow setting a Deliverable on a locked Contract" do 74 | assert @contract2.lock! 75 | click_link "New issue" 76 | 77 | fill_in "Subject", :with => 'Hook test' 78 | select @deliverable2.title, :from => "Deliverable" 79 | assert_no_difference("Issue.count") do 80 | click_button "Create" 81 | 82 | assert_response :success 83 | end 84 | 85 | assert_equal nil, Issue.last.deliverable 86 | 87 | end 88 | 89 | should "not allow setting a Deliverable on a closed Contract" do 90 | assert @contract2.close! 91 | click_link "New issue" 92 | 93 | fill_in "Subject", :with => 'Hook test' 94 | select @deliverable2.title, :from => "Deliverable" 95 | assert_no_difference("Issue.count") do 96 | click_button "Create" 97 | 98 | assert_response :success 99 | end 100 | 101 | assert_equal nil, Issue.last.deliverable 102 | 103 | end 104 | 105 | context "with no permission to Assign Deliverable" do 106 | should "not allow setting the Deliverable (force HTTP request)" do 107 | @role.permissions.delete(:assign_deliverable_to_issue) 108 | @role.save! 109 | 110 | assert_difference('Issue.count', 1) do 111 | post "/projects/#{@project.identifier}/issues", :issue => {:subject => 'Force', :deliverable_id => @deliverable1.id, :priority_id => IssuePriority.first.id} 112 | end 113 | 114 | assert_equal nil, Issue.last.deliverable 115 | end 116 | end 117 | 118 | end 119 | 120 | context "for an existing issue" do 121 | setup do 122 | visit_issue_page(@issue) 123 | end 124 | 125 | should "update the issue's deliverable" do 126 | select @deliverable2.title, :from => "Deliverable" 127 | click_button "Submit" 128 | 129 | assert_response :success 130 | 131 | @issue.reload 132 | assert_equal @deliverable2, @issue.deliverable 133 | 134 | end 135 | 136 | should "not allow updating to a locked deliverable" do 137 | assert @deliverable2.lock! 138 | select @deliverable2.title, :from => "Deliverable" 139 | click_button "Submit" 140 | 141 | assert_response :success 142 | 143 | @issue.reload 144 | assert_equal nil, @issue.deliverable 145 | 146 | end 147 | 148 | should "not allow updating to a closed deliverable" do 149 | assert @deliverable2.close! 150 | select @deliverable2.title, :from => "Deliverable" 151 | click_button "Submit" 152 | 153 | assert_response :success 154 | 155 | @issue.reload 156 | assert_equal nil, @issue.deliverable 157 | 158 | end 159 | 160 | should "not allow updating to a deliverable on a locked contract" do 161 | assert @contract2.lock! 162 | select @deliverable2.title, :from => "Deliverable" 163 | click_button "Submit" 164 | 165 | assert_response :success 166 | 167 | @issue.reload 168 | assert_equal nil, @issue.deliverable 169 | 170 | end 171 | 172 | should "not allow updating to a deliverable on a closed contract" do 173 | assert @contract2.close! 174 | select @deliverable2.title, :from => "Deliverable" 175 | click_button "Submit" 176 | 177 | assert_response :success 178 | 179 | @issue.reload 180 | assert_equal nil, @issue.deliverable 181 | 182 | end 183 | 184 | should "allow updating an issue, even if the deliverable is locked as long as the deliverable isn't changed" do 185 | select @deliverable2.title, :from => "Deliverable" 186 | click_button "Submit" 187 | 188 | assert_response :success 189 | 190 | @issue.reload 191 | assert_equal @deliverable2, @issue.deliverable 192 | 193 | # Now normal update after locking 194 | assert @deliverable2.lock! 195 | fill_in "Subject", :with => 'Change subject' 196 | click_button "Submit" 197 | 198 | assert_response :success 199 | @issue.reload 200 | assert_equal "Change subject", @issue.subject 201 | assert_equal @deliverable2, @issue.deliverable 202 | 203 | end 204 | 205 | context "with no permission to Assign Deliverable" do 206 | should "not allow setting the Deliverable (force HTTP request)" do 207 | @role.permissions.delete(:assign_deliverable_to_issue) 208 | @role.save! 209 | 210 | assert_difference('Journal.count', 1) do 211 | put "/issues/#{@issue.id}", :issue => {:subject => 'Force', :deliverable_id => @deliverable1.id} 212 | end 213 | 214 | assert_equal nil, @issue.reload.deliverable 215 | end 216 | end 217 | 218 | end 219 | end 220 | end 221 | -------------------------------------------------------------------------------- /test/integration/redmine_contracts/hooks/helper_issues_show_detail_after_setting_hook_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../../test_helper' 2 | 3 | class RedmineContracts::Hooks::HelperIssuesShowDetailAfterSettingHookTest < ActionController::IntegrationTest 4 | include Redmine::Hook::Helper 5 | 6 | context "#helper_issues_show_detail_after_setting" do 7 | setup do 8 | @project = Project.generate! 9 | @issue = Issue.generate_for_project!(@project) 10 | 11 | @contract1 = Contract.generate!(:project => @project) 12 | @contract2 = Contract.generate!(:project => @project) 13 | 14 | @manager = User.generate!(:login => 'manager', :password => 'existing', :password_confirmation => 'existing') 15 | @role = Role.generate!(:permissions => [:view_issues, :edit_issues]) 16 | User.add_to_project(@manager, @project, @role) 17 | @deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'The Title 1') 18 | @deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'The Title 2') 19 | # Set first 20 | @issue.init_journal(@manager) 21 | @issue.deliverable = @deliverable1 22 | @issue.save! && @issue.reload 23 | # Change 24 | @issue.init_journal(@manager) 25 | @issue.deliverable = @deliverable2 26 | @issue.save! && @issue.reload 27 | # Unset 28 | @issue.init_journal(@manager) 29 | @issue.deliverable = nil 30 | @issue.save! && @issue.reload 31 | 32 | login_as('manager', 'existing') 33 | 34 | visit_issue_page(@issue) 35 | assert_response :success 36 | end 37 | 38 | should "show when a deliverable is set" do 39 | assert_select ".details" do 40 | assert_select "li", :text => /Deliverable set to #{@deliverable1.title}/ 41 | end 42 | end 43 | 44 | should "show when a deliverable is changed" do 45 | assert_select ".details" do 46 | assert_select "li", :text => /Deliverable changed from #{@deliverable1.title} to #{@deliverable2.title}/ 47 | end 48 | 49 | end 50 | 51 | should "show when a deliverable is removed" do 52 | assert_select ".details" do 53 | assert_select "li", :text => /Deliverable deleted .*#{@deliverable2.title}/ 54 | end 55 | 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/integration/redmine_contracts/hooks/view_issues_bulk_edit_details_bottom_hook_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../../test_helper' 2 | 3 | class RedmineContracts::Hooks::ViewIssuesBulkEditDetailsBottomHookTest < ActionController::IntegrationTest 4 | include Redmine::Hook::Helper 5 | 6 | context "#view_issues_bulk_edit_details_bottom" do 7 | setup do 8 | @project = Project.generate! 9 | @issue = Issue.generate_for_project!(@project) 10 | @issue2 = Issue.generate_for_project!(@project) 11 | @issue3 = Issue.generate_for_project!(@project) 12 | @contract1 = Contract.generate!(:project => @project) 13 | @contract2 = Contract.generate!(:project => @project) 14 | @locked_contract = Contract.generate!(:project => @project) 15 | @closed_contract = Contract.generate!(:project => @project) 16 | 17 | @manager = User.generate!(:login => 'manager', :password => 'existing', :password_confirmation => 'existing') 18 | @role = Role.generate!(:permissions => [:view_issues, :edit_issues]) 19 | User.add_to_project(@manager, @project, @role) 20 | @deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'The Title') 21 | @deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'The Title') 22 | @locked_deliverable = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Locked Deliverable', :status => 'locked') 23 | @closed_deliverable = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Closed Deliverable', :status => 'closed') 24 | @deliverable1_on_locked_contract = FixedDeliverable.generate!(:contract => @locked_contract, :manager => @manager, :title => 'Deliverable 1 on locked contract') 25 | @deliverable2_on_locked_contract = FixedDeliverable.generate!(:contract => @locked_contract, :manager => @manager, :title => 'Deliverable 2 on locked contract') 26 | @deliverable_on_closed_contract = FixedDeliverable.generate!(:contract => @closed_contract, :manager => @manager, :title => 'Deliverable on closed contract') 27 | @issue.deliverable = @deliverable1 28 | 29 | # Set contract statuses now that all deliverables are created 30 | assert @locked_contract.lock! 31 | assert @closed_contract.close! 32 | 33 | login_as('manager', 'existing') 34 | end 35 | 36 | context "with Contracts Enabled" do 37 | context "with permission to Assign Deliverable" do 38 | setup do 39 | @role.permissions << :assign_deliverable_to_issue 40 | @role.save! 41 | visit_issue_bulk_edit_page([@issue, @issue2, @issue3]) 42 | end 43 | 44 | should "render the a select field for the deliverables with all of the deliverables grouped by contract" do 45 | 46 | assert_select "select#deliverable_id" do 47 | assert_select "optgroup[label=?]", @contract1.name do 48 | assert_select "option", :text => /#{@deliverable1.title}/ 49 | end 50 | 51 | assert_select "optgroup[label=?]", @contract2.name do 52 | assert_select "option", :text => /#{@deliverable2.title}/ 53 | end 54 | end 55 | end 56 | 57 | should "disable all locked deliverables" do 58 | assert_select "select#deliverable_id" do 59 | assert_select "option[disabled=disabled]", :text => /#{@locked_deliverable.title}/ 60 | end 61 | end 62 | 63 | should "disable all deliverables on locked contracts" do 64 | assert_select "select#deliverable_id" do 65 | assert_select "optgroup[label=?]", @locked_contract.name do 66 | assert_select "option[disabled=disabled]", :text => /#{@deliverable1_on_locked_contract.title}/ 67 | assert_select "option[disabled=disabled]", :text => /#{@deliverable2_on_locked_contract.title}/ 68 | end 69 | end 70 | end 71 | 72 | should "not show closed deliverables" do 73 | assert_select "select#deliverable_id" do 74 | assert_select "option", :text => /#{@closed_deliverable.title}/, :count => 0 75 | end 76 | end 77 | 78 | should "not show deliverables on closed contracts" do 79 | assert_select "select#deliverable_id" do 80 | assert_select "optgroup[label=?]", @closed_contract.name, :count => 0 81 | assert_select "option", :text => /#{@deliverable_on_closed_contract.title}/, :count => 0 82 | end 83 | end 84 | 85 | end 86 | 87 | context "with no permission to Assign Deliverable" do 88 | setup do 89 | @role.permissions.delete(:assign_deliverable_to_issue) 90 | @role.save! 91 | visit_issue_bulk_edit_page([@issue, @issue2, @issue3]) 92 | end 93 | 94 | should "not render the deliverable select field" do 95 | assert_select 'select#deliverable_id', :count => 0 96 | end 97 | end 98 | 99 | end 100 | 101 | context "with Contracts Disabled" do 102 | setup do 103 | @project.enabled_modules.collect {|m| m.destroy if m.name == 'contracts' } 104 | visit_issue_bulk_edit_page([@issue, @issue2, @issue3]) 105 | end 106 | 107 | should "not render the deliverable select field" do 108 | assert_select 'select#deliverable_id', :count => 0 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /test/integration/redmine_contracts/hooks/view_issues_form_details_bottom_hook_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../../test_helper' 2 | 3 | class RedmineContracts::Hooks::ViewIssuesFormDetailsBottomTest < ActionController::IntegrationTest 4 | include Redmine::Hook::Helper 5 | 6 | context "#view_issues_form_details_bottom" do 7 | setup do 8 | @project = Project.generate! 9 | @issue = Issue.generate_for_project!(@project) 10 | @contract1 = Contract.generate!(:project => @project) 11 | @contract2 = Contract.generate!(:project => @project) 12 | @locked_contract = Contract.generate!(:project => @project) 13 | @closed_contract = Contract.generate!(:project => @project) 14 | 15 | @manager = User.generate!(:login => 'manager', :password => 'existing', :password_confirmation => 'existing') 16 | @role = Role.generate!(:permissions => [:view_issues, :edit_issues]) 17 | User.add_to_project(@manager, @project, @role) 18 | @deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Deliverable1') 19 | @deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'Deliverable2') 20 | @locked_deliverable = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Locked Deliverable', :status => 'locked') 21 | @closed_deliverable = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Closed Deliverable', :status => 'closed') 22 | @deliverable1_on_locked_contract = FixedDeliverable.generate!(:contract => @locked_contract, :manager => @manager, :title => 'Deliverable 1 on locked contract') 23 | @deliverable2_on_locked_contract = FixedDeliverable.generate!(:contract => @locked_contract, :manager => @manager, :title => 'Deliverable 2 on locked contract') 24 | @deliverable_on_closed_contract = FixedDeliverable.generate!(:contract => @closed_contract, :manager => @manager, :title => 'Deliverable on closed contract') 25 | @issue.deliverable = @deliverable1 26 | assert @issue.save 27 | 28 | # Set contract statuses now that all deliverables are created 29 | assert @locked_contract.lock! 30 | assert @closed_contract.close! 31 | 32 | login_as('manager', 'existing') 33 | end 34 | 35 | context "with Contracts Enabled" do 36 | context "with permission to Assign Deliverable" do 37 | setup do 38 | @role.permissions << :assign_deliverable_to_issue 39 | @role.save! 40 | visit_issue_page(@issue) 41 | end 42 | 43 | should "render the a select field for the deliverables with all of the deliverables grouped by contract" do 44 | assert_select "select#issue_deliverable_id" do 45 | assert_select "optgroup[label=?]", @contract1.name do 46 | assert_select "option", :text => /#{@deliverable1.title}/ 47 | end 48 | 49 | assert_select "optgroup[label=?]", @contract2.name do 50 | assert_select "option", :text => /#{@deliverable2.title}/ 51 | end 52 | end 53 | end 54 | 55 | should "disable all locked deliverables" do 56 | assert_select "select#issue_deliverable_id" do 57 | assert_select "option[disabled=disabled]", :text => /#{@locked_deliverable.title}/ 58 | end 59 | end 60 | 61 | should "disable all deliverables on locked contracts" do 62 | assert_select "select#issue_deliverable_id" do 63 | assert_select "optgroup[label=?]", @locked_contract.name do 64 | assert_select "option[disabled=disabled]", :text => /#{@deliverable1_on_locked_contract.title}/ 65 | assert_select "option[disabled=disabled]", :text => /#{@deliverable2_on_locked_contract.title}/ 66 | end 67 | end 68 | end 69 | 70 | should "not show closed deliverables" do 71 | assert_select "select#issue_deliverable_id" do 72 | assert_select "option", :text => /#{@closed_deliverable.title}/, :count => 0 73 | end 74 | end 75 | 76 | should "not show deliverables on closed contracts" do 77 | assert_select "select#issue_deliverable_id" do 78 | assert_select "optgroup[label=?]", @closed_contract.name, :count => 0 79 | assert_select "option", :text => /#{@deliverable_on_closed_contract.title}/, :count => 0 80 | end 81 | end 82 | 83 | should "show the assigned deliverable as an option, even if it's locked" do 84 | @deliverable1.lock! 85 | visit_issue_page(@issue) 86 | 87 | assert_select "select#issue_deliverable_id" do 88 | assert_select "option[disabled=disabled]", :text => /#{@deliverable1.title}/, :count => 0 # Not disabled 89 | assert_select "option", :text => /#{@deliverable1.title}/, :count => 1 # Present 90 | end 91 | 92 | end 93 | 94 | should "show the assigned deliverable as an option, even if it's closed" do 95 | @deliverable1.close! 96 | visit_issue_page(@issue) 97 | 98 | assert_select "select#issue_deliverable_id" do 99 | assert_select "option[disabled=disabled]", :text => /#{@deliverable1.title}/, :count => 0 # Not disabled 100 | assert_select "option", :text => /#{@deliverable1.title}/, :count => 1 # Present 101 | end 102 | 103 | end 104 | 105 | should "show the assigned deliverable as an option, even if it's contract is locked" do 106 | @contract1.lock! 107 | visit_issue_page(@issue) 108 | 109 | assert_select "select#issue_deliverable_id" do 110 | assert_select "option[disabled=disabled]", :text => /#{@deliverable1.title}/, :count => 0 # Not disabled 111 | assert_select "option", :text => /#{@deliverable1.title}/, :count => 1 # Present 112 | end 113 | 114 | end 115 | 116 | should "show the assigned deliverable as an option, even if it's contract is closed" do 117 | @contract1.close! 118 | visit_issue_page(@issue) 119 | 120 | assert_select "select#issue_deliverable_id" do 121 | assert_select "option[disabled=disabled]", :text => /#{@deliverable1.title}/, :count => 0 # Not disabled 122 | assert_select "option", :text => /#{@deliverable1.title}/, :count => 1 # Present 123 | end 124 | 125 | end 126 | end 127 | 128 | context "with no permission to Assign Deliverable" do 129 | setup do 130 | @role.permissions.delete(:assign_deliverable_to_issue) 131 | @role.save! 132 | visit_issue_page(@issue) 133 | end 134 | 135 | should "not render the deliverable select field" do 136 | assert_select 'select#issue_deliverable_id', :count => 0 137 | end 138 | end 139 | end 140 | 141 | context "with Contracts Disabled" do 142 | setup do 143 | @project.enabled_modules.collect {|m| m.destroy if m.name == 'contracts' } 144 | visit_issue_page(@issue) 145 | end 146 | 147 | should "not render the deliverable select field" do 148 | assert_select 'select#issue_deliverable_id', :count => 0 149 | end 150 | end 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /test/integration/routing_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RoutingTest < ActionController::IntegrationTest 4 | context "contracts" do 5 | should_route :get, "/projects/world_domination/contracts", :controller => 'contracts', :action => 'index', :project_id => 'world_domination' 6 | should_route :get, "/projects/world_domination/contracts/new", :controller => 'contracts', :action => 'new', :project_id => 'world_domination' 7 | should_route :get, "/projects/world_domination/contracts/1", :controller => 'contracts', :action => 'show', :id => '1', :project_id => 'world_domination' 8 | should_route :get, "/projects/world_domination/contracts/1/edit", :controller => 'contracts', :action => 'edit', :id => '1', :project_id => 'world_domination' 9 | 10 | should_route :post, "/projects/world_domination/contracts", :controller => 'contracts', :action => 'create', :project_id => 'world_domination' 11 | 12 | should_route :put, "/projects/world_domination/contracts/1", :controller => 'contracts', :action => 'update', :id => '1', :project_id => 'world_domination' 13 | 14 | should_route :delete, "/projects/world_domination/contracts/1", :controller => 'contracts', :action => 'destroy', :id => '1', :project_id => 'world_domination' 15 | end 16 | 17 | context "deliverables" do 18 | should_route :get, "/projects/world_domination/contracts/1/deliverables", :controller => 'deliverables', :action => 'index', :project_id => 'world_domination', :contract_id => '1' 19 | should_route :get, "/projects/world_domination/contracts/1/deliverables/new", :controller => 'deliverables', :action => 'new', :project_id => 'world_domination', :contract_id => '1' 20 | should_route :get, "/projects/world_domination/contracts/1/deliverables/10", :controller => 'deliverables', :action => 'show', :id => '10', :project_id => 'world_domination', :contract_id => '1' 21 | should_route :get, "/projects/world_domination/contracts/1/deliverables/10/edit", :controller => 'deliverables', :action => 'edit', :id => '10', :project_id => 'world_domination', :contract_id => '1' 22 | 23 | should_route :post, "/projects/world_domination/contracts/1/deliverables", :controller => 'deliverables', :action => 'create', :project_id => 'world_domination', :contract_id => '1' 24 | 25 | should_route :put, "/projects/world_domination/contracts/1/deliverables/10", :controller => 'deliverables', :action => 'update', :id => '10', :project_id => 'world_domination', :contract_id => '1' 26 | 27 | should_route :delete, "/projects/world_domination/contracts/1/deliverables/10", :controller => 'deliverables', :action => 'destroy', :id => '10', :project_id => 'world_domination', :contract_id => '1' 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/performance/contract_show_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'performance_test_help' 3 | 4 | # Performance logs 5 | # 6 | class ContractShowTest < ActionController::PerformanceTest 7 | def setup 8 | @project = Project.generate!(:identifier => 'main').reload 9 | @contract = Contract.generate!(:project => @project) 10 | @manager = User.generate_user_with_permission_to_manage_budget(:project => @project).reload 11 | @fixed_deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'The Title') 12 | @hourly_deliverable = HourlyDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'An Hourly') 13 | 14 | @rate = Rate.generate!(:project => @project, :user => @manager, :date_in_effect => Date.today, :amount => 100) 15 | 16 | configure_overhead_plugin 17 | 100.times do 18 | generate_issues_and_time_entries_for_deliverable(@hourly_deliverable, @project) 19 | generate_issues_and_time_entries_for_deliverable(@fixed_deliverable, @project) 20 | end 21 | 22 | # Load the app 23 | login_as @manager.login, 'contracts' 24 | visit_contracts_for_project(@project) 25 | end 26 | 27 | def test_contract_show 28 | click_link @contract.id 29 | end 30 | 31 | private 32 | 33 | def generate_issues_and_time_entries_for_deliverable(deliverable, project) 34 | @issue1 = Issue.generate_for_project!(project) 35 | @time_entry1 = TimeEntry.generate!(:issue => @issue1, 36 | :project => project, 37 | :activity => @billable_activity, 38 | :spent_on => Date.today, 39 | :hours => 10, 40 | :user => @manager) 41 | @time_entry2 = TimeEntry.generate!(:issue => @issue1, 42 | :project => project, 43 | :activity => @non_billable_activity, 44 | :spent_on => Date.today, 45 | :hours => 20, 46 | :user => @manager) 47 | 48 | deliverable.issues << @issue1 49 | 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Load the normal Rails helper 2 | require File.expand_path(File.dirname(__FILE__) + '/../../../../test/test_helper') 3 | 4 | # Ensure that we are using the temporary fixture path 5 | Engines::Testing.set_fixture_path 6 | 7 | require "webrat" 8 | 9 | Webrat.configure do |config| 10 | config.mode = :rails 11 | end 12 | 13 | def User.add_to_project(user, project, role) 14 | Member.generate!(:principal => user, :project => project, :roles => [role]) 15 | end 16 | 17 | def User.generate_user_with_permission_to_manage_budget(options={}) 18 | project = options[:project] 19 | 20 | user = User.generate!(:password => 'contracts', :password_confirmation => 'contracts') 21 | role = Role.generate!(:permissions => [:view_issues, :edit_issues, :add_issues, :manage_budget]) 22 | User.add_to_project(user, project, role) 23 | user 24 | end 25 | 26 | 27 | module IntegrationTestHelper 28 | def login_as(user="existing", password="existing") 29 | visit "/login" 30 | fill_in 'Login', :with => user 31 | fill_in 'Password', :with => password 32 | click_button 'login' 33 | assert_response :success 34 | assert User.current.logged? 35 | end 36 | 37 | def logout 38 | visit '/logout' 39 | assert_response :success 40 | assert !User.current.logged? 41 | end 42 | 43 | def visit_project(project) 44 | visit '/' 45 | assert_response :success 46 | 47 | click_link 'Projects' 48 | assert_response :success 49 | 50 | click_link project.name 51 | assert_response :success 52 | end 53 | 54 | def visit_contracts_for_project(project) 55 | visit_project(project) 56 | click_link "Contracts" 57 | 58 | assert_response :success 59 | assert_template 'contracts/index' 60 | end 61 | 62 | def visit_contract_page(contract) 63 | visit_contracts_for_project(contract.project) 64 | click_link @contract.id 65 | 66 | assert_response :success 67 | assert_template 'contracts/show' 68 | end 69 | 70 | def visit_issue_page(issue) 71 | visit '/issues/' + issue.id.to_s 72 | end 73 | 74 | def visit_issue_bulk_edit_page(issues) 75 | visit url_for(:controller => 'issues', :action => 'bulk_edit', :ids => issues.collect(&:id)) 76 | end 77 | 78 | def assert_forbidden 79 | assert_response :forbidden 80 | assert_template 'common/error' 81 | end 82 | 83 | def assert_requires_login 84 | assert_response :success 85 | assert_template 'account/login' 86 | end 87 | 88 | end 89 | 90 | class ActionController::IntegrationTest 91 | include IntegrationTestHelper 92 | end 93 | 94 | class ActiveSupport::TestCase 95 | begin 96 | require 'ruby_gc_test_patch' 97 | include RubyGcTestPatch 98 | rescue LoadError 99 | end 100 | 101 | def configure_overhead_plugin 102 | @custom_field = TimeEntryActivityCustomField.generate! 103 | Setting['plugin_redmine_overhead'] = { 104 | 'custom_field' => @custom_field.id.to_s, 105 | 'billable_value' => "true", 106 | 'overhead_value' => "false" 107 | } 108 | 109 | @billable_activity = TimeEntryActivity.generate!.reload 110 | @billable_activity.custom_field_values = { 111 | @custom_field.id => 'true' 112 | } 113 | assert @billable_activity.save 114 | 115 | assert @billable_activity.billable? 116 | 117 | @non_billable_activity = TimeEntryActivity.generate!.reload 118 | @non_billable_activity.custom_field_values = { 119 | @custom_field.id => 'false' 120 | } 121 | assert @non_billable_activity.save 122 | 123 | assert !@non_billable_activity.billable? 124 | 125 | end 126 | 127 | def create_issue_with_time_for_deliverable(deliverable, options) 128 | project = deliverable.project 129 | user = options[:user] 130 | activity = options[:activity] 131 | amount = options[:amount] || 100 132 | hours = options[:hours] || 2 133 | issue_category = options[:issue_category] 134 | 135 | issue = Issue.generate_for_project!(project, :category_id => issue_category.try(:id)) 136 | time_entry = TimeEntry.generate!(:issue => issue, 137 | :project => project, 138 | :activity => activity, 139 | :spent_on => Date.today, 140 | :hours => hours, 141 | :user => user) 142 | rate = Rate.generate!(:project => project, 143 | :user => user, 144 | :date_in_effect => Date.yesterday, 145 | :amount => amount) 146 | deliverable.issues << issue 147 | issue 148 | end 149 | 150 | def create_contract_and_deliverable 151 | @project = Project.generate!(:identifier => 'main').reload 152 | @contract = Contract.generate!(:project => @project, :billable_rate => 10) 153 | @manager = User.generate! 154 | @deliverable = RetainerDeliverable.generate!(:contract => @contract, :manager => @manager, :title => "Retainer Title", :start_date => '2010-01-01', :end_date => '2010-03-31') 155 | 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /test/unit/fixed_budget_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class FixedBudgetTest < ActiveSupport::TestCase 4 | should_belong_to :deliverable 5 | 6 | context "#markup_value" do 7 | setup do 8 | @fixed_budget = FixedBudget.new(:budget => 1000) 9 | end 10 | 11 | context "with no markup" do 12 | should "be 0" do 13 | assert_equal nil, @fixed_budget.markup 14 | assert_equal 0, @fixed_budget.markup_value 15 | end 16 | end 17 | 18 | context "with a % markup" do 19 | should "equal the budget times the %" do 20 | @fixed_budget.markup = '50%' 21 | assert_equal 500, @fixed_budget.markup_value 22 | end 23 | end 24 | 25 | context "with a $ markup" do 26 | should "equal the $ markup (straight markup)" do 27 | @fixed_budget.markup = '$4,000.57' 28 | assert_equal 4000.57, @fixed_budget.markup_value 29 | end 30 | 31 | should "work without the $ sign" do 32 | @fixed_budget.markup = '4,000.57' 33 | assert_equal 4000.57, @fixed_budget.markup_value 34 | end 35 | 36 | end 37 | 38 | context "with a straight amount of markup" do 39 | should "equal the markup" do 40 | @fixed_budget.markup = 4000.57 41 | assert_equal 4000.57, @fixed_budget.markup_value 42 | end 43 | end 44 | 45 | end 46 | 47 | context "#budget=" do 48 | setup do 49 | @fixed_budget = FixedBudget.new 50 | end 51 | 52 | should "allow a $ string" do 53 | @fixed_budget.budget = '$1,000.00' 54 | assert_equal 1000.00, @fixed_budget.budget 55 | end 56 | 57 | should "allow a plain string" do 58 | @fixed_budget.budget = '1,000.00' 59 | assert_equal 1000.00, @fixed_budget.budget 60 | end 61 | 62 | should "allow a numeric value" do 63 | @fixed_budget.budget = 1000.00 64 | assert_equal 1000.00, @fixed_budget.budget 65 | end 66 | 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /test/unit/fixed_deliverable_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class FixedDeliverableTest < ActiveSupport::TestCase 4 | context "#profit_budget" do 5 | context "with no labor budget, no overhead budget" do 6 | should "equal the total" do 7 | assert_equal 1000, FixedDeliverable.generate(:total => 1_000).profit_budget 8 | end 9 | end 10 | 11 | should "be the total minus the sum of all of the budgets" do 12 | deliverable = FixedDeliverable.generate(:total => 1_000) 13 | LaborBudget.generate!(:deliverable => deliverable, :budget => 200) 14 | LaborBudget.generate!(:deliverable => deliverable, :budget => 200) 15 | OverheadBudget.generate!(:deliverable => deliverable, :budget => 200) 16 | FixedBudget.generate!(:deliverable => deliverable, :budget => '$100', :markup => '50%') # $50 markup 17 | 18 | assert_equal 400 - 150, deliverable.profit_budget 19 | end 20 | 21 | should "be 0 if there is no total" do 22 | assert_equal 0, FixedDeliverable.generate(:total => nil).profit_budget 23 | end 24 | end 25 | 26 | context "#total_spent" do 27 | should "equal the budgeted total" do 28 | assert_equal 1000, FixedDeliverable.generate(:total => 1_000).total_spent 29 | end 30 | end 31 | 32 | context "#profit_left" do 33 | should "be the total_spent minus the labor budget spent minus the overhead budget spent" do 34 | configure_overhead_plugin 35 | 36 | @project = Project.generate! 37 | @developer = User.generate! 38 | @manager = User.generate! 39 | @role = Role.generate! 40 | User.add_to_project(@developer, @project, @role) 41 | User.add_to_project(@manager, @project, @role) 42 | @rate = Rate.generate!(:project => @project, 43 | :user => @developer, 44 | :date_in_effect => Date.yesterday, 45 | :amount => 55) 46 | @rate = Rate.generate!(:project => @project, 47 | :user => @manager, 48 | :date_in_effect => Date.yesterday, 49 | :amount => 75) 50 | 51 | @deliverable_1 = FixedDeliverable.generate!(:total => 2000) 52 | @deliverable_1.issues << @issue1 = Issue.generate_for_project!(@project) 53 | TimeEntry.generate!(:hours => 15, :issue => @issue1, :project => @project, 54 | :activity => @billable_activity, 55 | :user => @developer) 56 | TimeEntry.generate!(:hours => 4, :issue => @issue1, :project => @project, 57 | :activity => @non_billable_activity, 58 | :user => @manager) 59 | 60 | # Check intermediate values 61 | assert_equal 825, @deliverable_1.labor_budget_spent 62 | assert_equal 300, @deliverable_1.overhead_spent 63 | 64 | assert_equal 875, @deliverable_1.profit_left 65 | 66 | end 67 | end 68 | 69 | context "#fixed_markup_budget_total_spent" do 70 | should "be the total markup from fixed budgets because FixedDeliverables are considered 100% paid" do 71 | @deliverable = FixedDeliverable.generate! 72 | FixedBudget.generate!(:deliverable => @deliverable, :budget => '$1,000', :markup => '$100', :paid => true) 73 | FixedBudget.generate!(:deliverable => @deliverable, :budget => '$1,000', :markup => '$100', :paid => false) 74 | 75 | assert_equal 200, @deliverable.fixed_markup_budget_total_spent 76 | 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /test/unit/helpers/contracts_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ContractsHelperTest < ActionView::TestCase 4 | context "#validate_period" do 5 | should "with a HourlyDeliverable should return nil" do 6 | assert_equal nil, validate_period(HourlyDeliverable.new, '2010-01') 7 | end 8 | 9 | should "with a FixedDeliverable should return nil" do 10 | assert_equal nil, validate_period(FixedDeliverable.new, '2010-01') 11 | end 12 | 13 | context "with a RetainerDeliverable" do 14 | should "return nil when there period is not within the Deliverable's date range" do 15 | retainer = RetainerDeliverable.new(:start_date => Date.new(2011,1,1), 16 | :end_date => Date.new(2012,1,1)) 17 | 18 | assert_equal nil, validate_period(retainer, '2010-01') 19 | end 20 | 21 | should "return the period when it's within the Deliverable's date range" do 22 | retainer = RetainerDeliverable.new(:start_date => Date.new(2001,1,1), 23 | :end_date => Date.new(2003,1,1)) 24 | 25 | assert_equal '2001-02', validate_period(retainer, '2001-02') 26 | end 27 | 28 | end 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /test/unit/hourly_deliverable_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class HourlyDeliverableTest < ActiveSupport::TestCase 4 | context "#total" do 5 | should "be 0 when not assigned to a contract" do 6 | assert_equal 0, HourlyDeliverable.new.total 7 | end 8 | 9 | should "be 0 with no billable rate set on the Contract" do 10 | contract = Contract.generate!(:billable_rate => nil) 11 | d = HourlyDeliverable.generate!(:contract => contract) 12 | 13 | assert_equal 0, d.total 14 | end 15 | 16 | should "be 0 with no budgets" do 17 | contract = Contract.generate!(:billable_rate => 100.0) 18 | d = HourlyDeliverable.generate!(:contract => contract) 19 | 20 | assert_equal 0, d.total 21 | end 22 | 23 | should "multiply the total number of labor budget hours by the contract billable rate and add the fixed budget and markup" do 24 | contract = Contract.generate!(:billable_rate => 100.0) 25 | d = HourlyDeliverable.generate!(:contract => contract) 26 | d.labor_budgets << LaborBudget.generate!(:hours => 10) 27 | d.overhead_budgets << OverheadBudget.generate!(:hours => 20) 28 | d.fixed_budgets << FixedBudget.generate!(:budget => '$100', :markup => '50%') # $50 markup 29 | 30 | assert_equal (100.0 * 10) + (100 + 50), d.total 31 | end 32 | end 33 | 34 | context "#total_spent" do 35 | should "be equal to the number of hours used multipled by the contract rate and adding the fixed budget and markup spent" do 36 | configure_overhead_plugin 37 | 38 | contract = Contract.generate!(:billable_rate => 150.0) 39 | @project = Project.generate! 40 | @developer = User.generate! 41 | @role = Role.generate! 42 | User.add_to_project(@developer, @project, @role) 43 | 44 | d = HourlyDeliverable.generate!(:contract => contract) 45 | d.issues << @issue1 = Issue.generate_for_project!(@project) 46 | TimeEntry.generate!(:hours => 15, :issue => @issue1, :project => @project, 47 | :activity => @billable_activity, 48 | :user => @developer) 49 | # Only paid fixed budgets counted 50 | d.fixed_budgets << FixedBudget.generate!(:budget => '$100', :markup => '50%') # $50 markup 51 | d.fixed_budgets << FixedBudget.generate!(:budget => '$100', :markup => '50%', :paid => true) # $50 markup 52 | 53 | assert_equal 2250 + 150, d.total_spent 54 | end 55 | end 56 | 57 | context "#total=" do 58 | should "not write any attributes" do 59 | d = HourlyDeliverable.new 60 | d.total = '$100.00' 61 | 62 | assert_equal nil, d.read_attribute(:total) 63 | end 64 | end 65 | 66 | context "clear_total" do 67 | should "clear any total attributes" do 68 | d = HourlyDeliverable.new 69 | d.write_attribute(:total, 100.00) 70 | d.clear_total 71 | 72 | assert_equal nil, d.read_attribute(:total) 73 | 74 | end 75 | end 76 | 77 | context "#profit_budget" do 78 | setup do 79 | @contract = Contract.generate!(:billable_rate => 150.0) 80 | @deliverable = HourlyDeliverable.generate!(:contract => @contract) 81 | end 82 | 83 | context "with no labor budget, no overhead budget" do 84 | should "be 0 (no hours available to bill)" do 85 | assert_equal 0, @deliverable.profit_budget 86 | end 87 | end 88 | 89 | should "be the total minus the sum of all of the budgets' amounts" do 90 | LaborBudget.generate!(:deliverable => @deliverable, :hours => 5, :budget => 250) 91 | LaborBudget.generate!(:deliverable => @deliverable, :hours => 5, :budget => 250) 92 | OverheadBudget.generate!(:deliverable => @deliverable, :hours => 3, :budget => 225) 93 | FixedBudget.generate!(:deliverable => @deliverable, :budget => '$100', :markup => '50%') # $50 markup 94 | 95 | assert_equal 1650, @deliverable.total # has the FixedBudget items added to the total also 96 | assert_equal 1650 - (225 + 250 + 250 + 100 + 50), @deliverable.profit_budget 97 | end 98 | end 99 | 100 | context "#profit_left" do 101 | should "be equal to the total to bill (total_spent) minus the labor budget spent minus the overhead spent" do 102 | configure_overhead_plugin 103 | 104 | contract = Contract.generate!(:billable_rate => 150.0) 105 | @project = Project.generate! 106 | @developer = User.generate! 107 | @manager = User.generate! 108 | @role = Role.generate! 109 | User.add_to_project(@developer, @project, @role) 110 | User.add_to_project(@manager, @project, @role) 111 | @rate = Rate.generate!(:project => @project, 112 | :user => @developer, 113 | :date_in_effect => Date.yesterday, 114 | :amount => 55) 115 | @rate = Rate.generate!(:project => @project, 116 | :user => @manager, 117 | :date_in_effect => Date.yesterday, 118 | :amount => 75) 119 | 120 | @deliverable_1 = HourlyDeliverable.generate!(:contract => contract) 121 | @deliverable_1.issues << @issue1 = Issue.generate_for_project!(@project) 122 | TimeEntry.generate!(:hours => 15, :issue => @issue1, :project => @project, 123 | :activity => @billable_activity, 124 | :user => @developer) 125 | TimeEntry.generate!(:hours => 4, :issue => @issue1, :project => @project, 126 | :activity => @non_billable_activity, 127 | :user => @manager) 128 | 129 | # Check intermediate values 130 | assert_equal 825, @deliverable_1.labor_budget_spent 131 | assert_equal 300, @deliverable_1.overhead_spent 132 | 133 | assert_equal 1125, @deliverable_1.profit_left 134 | end 135 | end 136 | 137 | end 138 | -------------------------------------------------------------------------------- /test/unit/labor_budget_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class LaborBudgetTest < ActiveSupport::TestCase 4 | should_belong_to :deliverable 5 | should_belong_to :time_entry_activity 6 | 7 | should_validate_presence_of :time_entry_activity_id 8 | 9 | context "#budget=" do 10 | should "strip dollar signs when writing" do 11 | e = LaborBudget.new 12 | e.budget = '$100.00' 13 | 14 | assert_equal 100.00, e.budget.to_f 15 | end 16 | 17 | should "strip commas when writing" do 18 | e = LaborBudget.new 19 | e.budget = '20,100.00' 20 | 21 | assert_equal 20100.00, e.budget.to_f 22 | end 23 | 24 | should "strip spaces when writing" do 25 | e = LaborBudget.new 26 | e.budget = '20 100.00' 27 | 28 | assert_equal 20100.00, e.budget.to_f 29 | end 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /test/unit/lib/redmine_contracts/hooks/controller_timelog_available_criterias_hook_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../../../test_helper' 2 | 3 | class RedmineContracts::Hooks::ControllerTimelogAvailableCriteriasTest < ActionController::TestCase 4 | include Redmine::Hook::Helper 5 | 6 | def controller 7 | @controller ||= ApplicationController.new 8 | @controller.response ||= ActionController::TestResponse.new 9 | @controller 10 | end 11 | 12 | def request 13 | @request ||= ActionController::TestRequest.new 14 | end 15 | 16 | def hook(args={}) 17 | call_hook :controller_timelog_available_criterias, args 18 | end 19 | 20 | def context 21 | @context ||= { 22 | :available_criterias => {"existing" => {:label => 'existing'}} 23 | } 24 | end 25 | 26 | context "#controller_timelog_available_criterias" do 27 | should "return an empty string" do 28 | @response.body = hook(context) 29 | assert @response.body.blank? 30 | end 31 | 32 | context "Deliverables" do 33 | should "add a deliverable_id to the available criterias" do 34 | @response.body = hook(context) 35 | assert context[:available_criterias]['deliverable_id'] 36 | end 37 | 38 | should "add the deliverable sql to the available criterias" do 39 | @response.body = hook(context) 40 | assert "issues.deliverable_id", context[:available_criterias]['deliverable_id'][:sql] 41 | end 42 | 43 | should "add the deliverable Class to the available criterias" do 44 | @response.body = hook(context) 45 | assert Deliverable, context[:available_criterias]['deliverable_id'][:klass] 46 | end 47 | 48 | should "add the deliverable label to the available criterias" do 49 | @response.body = hook(context) 50 | assert :field_deliverable, context[:available_criterias]['deliverable_id'][:label] 51 | end 52 | end 53 | 54 | context "Contracts" do 55 | should "add a contract_id to the available criterias" do 56 | @response.body = hook(context) 57 | assert context[:available_criterias]['contract_id'] 58 | end 59 | 60 | should "add the contact sql to the available criterias" do 61 | @response.body = hook(context) 62 | assert "issues.deliverable_id", context[:available_criterias]['contract_id'][:sql] 63 | end 64 | 65 | should "add the deliverable Class to the available criterias" do 66 | @response.body = hook(context) 67 | assert Contract, context[:available_criterias]['contract_id'][:klass] 68 | end 69 | 70 | should "add the deliverable label to the available criterias" do 71 | @response.body = hook(context) 72 | assert :field_contract, context[:available_criterias]['contract_id'][:label] 73 | end 74 | end 75 | 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/unit/lib/redmine_contracts/hooks/view_issues_show_details_bottom_hook_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../../../test_helper' 2 | 3 | class RedmineContracts::Hooks::ViewIssuesShowDetailsBottomTest < ActionController::TestCase 4 | include Redmine::Hook::Helper 5 | 6 | # Bloody bloody hack to work around Rails coupling of _VC. 7 | def template 8 | t = ActionView::Base.new(ActionController::Base.view_paths, {}, @controller) 9 | def t.template_format 10 | "html" 11 | end 12 | 13 | # Rendered views aren't getting access to the controller's I18n module 14 | t.send(:extend, Redmine::I18n) 15 | t 16 | end 17 | 18 | def controller 19 | @controller ||= ApplicationController.new 20 | @controller.class.send(:include, ::Redmine::I18n) 21 | @controller.response ||= ActionController::TestResponse.new 22 | def @controller.api_request? 23 | false 24 | end 25 | # Hack to support render_on 26 | @controller.instance_variable_set('@template', template) 27 | @controller.response = response 28 | @controller 29 | end 30 | 31 | def request 32 | @request ||= ActionController::TestRequest.new 33 | end 34 | 35 | # Hack to support render_on 36 | def response 37 | @response.template ||= template 38 | @response 39 | end 40 | 41 | def hook(args={}) 42 | call_hook :view_issues_show_details_bottom, args 43 | end 44 | 45 | context "#view_issues_show_details_bottom" do 46 | setup do 47 | @project = Project.generate! 48 | @issue = Issue.generate_for_project!(@project) 49 | @contract = Contract.generate!(:project => @project) 50 | 51 | @manager = User.generate! 52 | @role = Role.generate! 53 | User.add_to_project(@manager, @project, @role) 54 | @deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'The Title') 55 | @issue.deliverable = @deliverable 56 | end 57 | 58 | context "with Contracts Enabled" do 59 | should "render the deliverable's name" do 60 | @response.body = hook(:project => @project, :issue => @issue, :controller => controller) 61 | 62 | assert_select "tr" do 63 | assert_select "td", :text => /#{@deliverable.title}/ 64 | end 65 | end 66 | end 67 | 68 | context "with Contracts Disabled" do 69 | setup do 70 | @project.enabled_modules.destroy_all 71 | end 72 | 73 | should "not render the deliverable's name" do 74 | @response.body = hook(:project => @project, :issue => @issue, :controller => controller) 75 | 76 | assert_no_match /#{@deliverable.title}/, @response.body 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/unit/lib/redmine_contracts/hooks/view_layouts_base_html_head_hook_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../../../test_helper' 2 | 3 | class RedmineContracts::Hooks::ViewLayoutsBaseHtmlHeadTest < ActionController::TestCase 4 | include Redmine::Hook::Helper 5 | 6 | def controller 7 | @controller ||= ApplicationController.new 8 | @controller.response ||= ActionController::TestResponse.new 9 | @controller 10 | end 11 | 12 | def request 13 | @request ||= ActionController::TestRequest.new 14 | end 15 | 16 | def hook(args={}) 17 | call_hook :view_layouts_base_html_head, args 18 | end 19 | 20 | context "#view_layouts_base_html_head" do 21 | context "for any non-contracts plugin controller" do 22 | should "return an empty string" do 23 | @response.body = hook 24 | assert @response.body.blank? 25 | end 26 | end 27 | 28 | context "for Contracts Controller" do 29 | setup do 30 | @controller = ContractsController.new 31 | @controller.response = ActionController::TestResponse.new 32 | end 33 | 34 | should "load the redmine_contracts.css stylesheet" do 35 | @response.body = hook 36 | assert_select "link[href*=?]", "redmine_contracts.css" 37 | end 38 | 39 | should "load jquery" do 40 | @response.body = hook 41 | assert_select "script[src*=?]", "jquery-1.4.4.min.js" 42 | end 43 | 44 | should "load the contracts.js JavaScript" do 45 | @response.body = hook 46 | assert_select "script[src*=?]", "contracts.js" 47 | end 48 | end 49 | 50 | context "for Deliverables Controller" do 51 | setup do 52 | @controller = DeliverablesController.new 53 | @controller.response = ActionController::TestResponse.new 54 | end 55 | 56 | should "load the redmine_contracts.css stylesheet" do 57 | @response.body = hook 58 | assert_select "link[href*=?]", "redmine_contracts.css" 59 | end 60 | 61 | should "load jquery" do 62 | @response.body = hook 63 | assert_select "script[src*=?]", "jquery-1.4.4.min.js" 64 | end 65 | 66 | should "load the contracts.js JavaScript" do 67 | @response.body = hook 68 | assert_select "script[src*=?]", "contracts.js" 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/unit/lib/redmine_contracts/patches/issue_patch_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../../../test_helper' 2 | 3 | class RedmineContracts::Patches::IssueTest < ActionController::TestCase 4 | 5 | context "Issue" do 6 | subject { Issue.new } 7 | should_belong_to :deliverable 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/unit/lib/redmine_contracts/patches/project_patch_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../../../test_helper' 2 | 3 | class RedmineContracts::Patches::ProjectTest < ActionController::TestCase 4 | 5 | context "Project" do 6 | subject { Project.new } 7 | should_have_many :contracts 8 | should_have_many :deliverables 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/unit/lib/redmine_contracts/patches/query_patch_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../../../test_helper' 2 | 3 | class RedmineContracts::Patches::QueryTest < ActionController::TestCase 4 | 5 | context "Query" do 6 | subject {Query.new} 7 | 8 | context "#available_filters with project" do 9 | setup do 10 | @query = Query.new 11 | @query.project = @project = Project.generate! 12 | @contract = Contract.generate!(:project => @project, :name => 'A Contract') 13 | @manager = User.generate! 14 | @deliverable1 = FixedDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'One') 15 | @deliverable2 = FixedDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'Two') 16 | end 17 | 18 | should "add a deliverable_id filter" do 19 | filters = @query.available_filters 20 | 21 | assert filters.keys.include?("deliverable_id") 22 | 23 | deliverable_filter = filters["deliverable_id"] 24 | assert_equal :list_optional, deliverable_filter[:type] 25 | assert_equal [ 26 | ["One", @deliverable1.id.to_s], 27 | ["Two", @deliverable2.id.to_s] 28 | ], deliverable_filter[:values] 29 | end 30 | 31 | should "add a contract_id filter" do 32 | filters = @query.available_filters 33 | 34 | assert filters.keys.include?("contract_id") 35 | 36 | contract_filter = filters["contract_id"] 37 | assert_equal :list_optional, contract_filter[:type] 38 | assert_equal [["A Contract", @contract.id.to_s]], contract_filter[:values] 39 | end 40 | end 41 | 42 | # TODO: Dragons in this test 43 | context "#sql_for_field_with_contract" do 44 | context "for contract_id fields" do 45 | setup do 46 | @query = Query.new 47 | end 48 | 49 | context "with the equal operator" do 50 | should "return the SQL snippet for checking for deliverables on the specific contracts" do 51 | sql = @query.send(:sql_for_field, 'contract_id', '=', ['1','2'], '', '') 52 | assert_equal "issues.deliverable_id IN ((SELECT id from deliverables where deliverables.contract_id IN ('1','2')))", sql 53 | end 54 | end 55 | 56 | context "with is not operator" do 57 | should "return the SQL snippet for checking for null deliverables or deliverables no on the specific contracts" do 58 | sql = @query.send(:sql_for_field, 'contract_id', '!', ['1','2'], '', '') 59 | 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 60 | end 61 | end 62 | 63 | context "with none operator" do 64 | should "return the SQL snippet for checking for null deliverables" do 65 | sql = @query.send(:sql_for_field, 'contract_id', '!*', '', '', '') 66 | assert_equal "issues.deliverable_id IS NULL", sql 67 | end 68 | end 69 | 70 | context "with all operator" do 71 | should "return the SQL snippet for checking for not null deliverables" do 72 | sql = @query.send(:sql_for_field, 'contract_id', '*', '', '', '') 73 | assert_equal "issues.deliverable_id IS NOT NULL", sql 74 | end 75 | end 76 | 77 | 78 | end 79 | end 80 | 81 | end 82 | 83 | end 84 | -------------------------------------------------------------------------------- /test/unit/lib/redmine_contracts/patches/time_entry_patch_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../../../test_helper' 2 | 3 | class RedmineContracts::Patches::TimeEntryTest < ActionController::TestCase 4 | 5 | def setup 6 | @project = Project.generate! 7 | @contract = Contract.generate!(:project => @project, :status => 'open') 8 | @deliverable = FixedDeliverable.generate!(:contract => @contract, :status => 'open').reload 9 | @issue = Issue.generate_for_project!(@project, :deliverable => @deliverable).reload 10 | assert_equal @deliverable, @issue.deliverable 11 | @user = User.generate! 12 | @role = Role.generate! 13 | User.add_to_project(@user, @project, @role) 14 | @activity = TimeEntryActivity.generate! 15 | end 16 | 17 | def create_time_entry 18 | @issue.reload 19 | @time_entry = TimeEntry.create(:issue => @issue, 20 | :project => @project, 21 | :spent_on => Date.today, 22 | :activity => @activity, 23 | :hours => 10, 24 | :user => @user) 25 | end 26 | 27 | def assert_error_about_locked_deliverable(time_entry) 28 | assert_equal "Can't create a time entry on a locked deliverable", time_entry.errors.on_base 29 | end 30 | 31 | def assert_error_about_locked_contract(time_entry) 32 | assert_equal "Can't create a time entry on a locked contract", time_entry.errors.on_base 33 | end 34 | 35 | def assert_error_about_closed_deliverable(time_entry) 36 | assert_equal "Can't create a time entry on a closed deliverable", time_entry.errors.on_base 37 | end 38 | 39 | def assert_error_about_closed_contract(time_entry) 40 | assert_equal "Can't create a time entry on a closed contract", time_entry.errors.on_base 41 | end 42 | 43 | should "allow logging time to an issue on an open deliverable, open contract" do 44 | assert_difference("TimeEntry.count") { create_time_entry } 45 | end 46 | 47 | should "block logging time to an issue on a locked deliverable, open contract" do 48 | assert @deliverable.lock! 49 | assert @deliverable.locked? 50 | 51 | assert_no_difference("TimeEntry.count") { create_time_entry } 52 | assert_error_about_locked_deliverable(@time_entry) 53 | end 54 | 55 | should "block logging time to an issue on an open deliverable, locked contract" do 56 | assert @contract.lock! 57 | assert @contract.locked? 58 | 59 | assert_no_difference("TimeEntry.count") { create_time_entry } 60 | assert_error_about_locked_contract(@time_entry) 61 | end 62 | 63 | should "block logging time to an issue on a locked deliverable, locked contract" do 64 | assert @deliverable.lock! 65 | assert @deliverable.locked? 66 | assert @contract.lock! 67 | assert @contract.locked? 68 | 69 | assert_no_difference("TimeEntry.count") { create_time_entry } 70 | assert @time_entry.errors.on_base.include?("Can't create a time entry on a locked deliverable") 71 | assert @time_entry.errors.on_base.include?("Can't create a time entry on a locked contract") 72 | end 73 | 74 | should "block logging time to an issue on a closed deliverable, open contract" do 75 | assert @deliverable.close! 76 | assert @deliverable.closed? 77 | 78 | assert_no_difference("TimeEntry.count") { create_time_entry } 79 | assert_error_about_closed_deliverable(@time_entry) 80 | end 81 | 82 | should "block logging time to an issue on a closed deliverable, locked contract" do 83 | assert @deliverable.close! 84 | assert @deliverable.closed? 85 | assert @contract.lock! 86 | assert @contract.locked? 87 | 88 | assert_no_difference("TimeEntry.count") { create_time_entry } 89 | assert @time_entry.errors.on_base.include?("Can't create a time entry on a closed deliverable") 90 | assert @time_entry.errors.on_base.include?("Can't create a time entry on a locked contract") 91 | end 92 | 93 | should "block logging time to an issue on an open deliverable, closed contract" do 94 | assert @contract.close! 95 | assert @contract.closed? 96 | 97 | assert_no_difference("TimeEntry.count") { create_time_entry } 98 | assert_error_about_closed_contract(@time_entry) 99 | end 100 | 101 | should "block logging time to an issue on a locked deliverable, closed contract" do 102 | assert @deliverable.lock! 103 | assert @deliverable.locked? 104 | assert @contract.close! 105 | assert @contract.closed? 106 | 107 | assert_no_difference("TimeEntry.count") { create_time_entry } 108 | assert @time_entry.errors.on_base.include?("Can't create a time entry on a locked deliverable") 109 | assert @time_entry.errors.on_base.include?("Can't create a time entry on a closed contract") 110 | end 111 | 112 | should "block logging time to an issue on a closed deliverable, closed contract" do 113 | assert @deliverable.close! 114 | assert @deliverable.closed? 115 | assert @contract.close! 116 | assert @contract.closed? 117 | 118 | assert_no_difference("TimeEntry.count") { create_time_entry } 119 | assert @time_entry.errors.on_base.include?("Can't create a time entry on a closed deliverable") 120 | assert @time_entry.errors.on_base.include?("Can't create a time entry on a closed contract") 121 | end 122 | 123 | end 124 | -------------------------------------------------------------------------------- /test/unit/overhead_budget_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class OverheadBudgetTest < ActiveSupport::TestCase 4 | should_belong_to :deliverable 5 | should_belong_to :time_entry_activity 6 | 7 | should_validate_presence_of :time_entry_activity_id 8 | 9 | context "#budget=" do 10 | should "strip dollar signs when writing" do 11 | e = OverheadBudget.new 12 | e.budget = '$100.00' 13 | 14 | assert_equal 100.00, e.budget.to_f 15 | end 16 | 17 | should "strip commas when writing" do 18 | e = OverheadBudget.new 19 | e.budget = '20,100.00' 20 | 21 | assert_equal 20100.00, e.budget.to_f 22 | end 23 | 24 | should "strip spaces when writing" do 25 | e = OverheadBudget.new 26 | e.budget = '20 100.00' 27 | 28 | assert_equal 20100.00, e.budget.to_f 29 | end 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /test/unit/payment_term_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class PaymentTermTest < ActiveSupport::TestCase 4 | include Redmine::I18n 5 | 6 | should_have_many(:contracts) 7 | 8 | should "be a subclass of Enumeration" do 9 | assert_equal Enumeration, PaymentTerm.superclass 10 | end 11 | 12 | context "#option_name" do 13 | should "be Payment Terms" do 14 | assert_equal "Payment Terms", l(PaymentTerm.new.option_name) 15 | end 16 | end 17 | 18 | context "#objects_count" do 19 | should "count the number of contracts with this payment term" do 20 | @payment_term = PaymentTerm.generate!(:type => 'PaymentTerm') 21 | Contract.generate!(:payment_term => @payment_term) 22 | Contract.generate!(:payment_term => @payment_term) 23 | 24 | assert_equal 2, @payment_term.objects_count 25 | end 26 | end 27 | 28 | context "#transfer_relations" do 29 | should "update all contracts to use a new PaymentTerm" do 30 | @old_payment_term = PaymentTerm.generate!(:type => 'PaymentTerm') 31 | @new_payment_term = PaymentTerm.generate!(:type => 'PaymentTerm') 32 | @contract1 = Contract.generate!(:payment_term => @old_payment_term) 33 | @contract2 = Contract.generate!(:payment_term => @old_payment_term) 34 | 35 | @old_payment_term.transfer_relations(@new_payment_term) 36 | assert_equal @new_payment_term, @contract1.reload.payment_term 37 | assert_equal @new_payment_term, @contract2.reload.payment_term 38 | end 39 | end 40 | end 41 | --------------------------------------------------------------------------------