├── README.md ├── app ├── controllers │ ├── contracts_controller.rb │ └── contracts_expenses_controller.rb ├── helpers │ ├── contracts_expenses_helper.rb │ └── contracts_helper.rb ├── models │ ├── contract.rb │ ├── contract_category.rb │ ├── contracts_expense.rb │ ├── user_contract_rate.rb │ └── user_project_rate.rb └── views │ ├── contracts │ ├── _contract_summary.html.erb │ ├── _contracts_list.html.erb │ ├── _contracts_summary.html.erb │ ├── _expenses_list.html.erb │ ├── _fixed_price_list.html.erb │ ├── _form.html.erb │ ├── _time_entries_list.html.erb │ ├── add_time_entries.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── new.html.erb │ ├── show.html.erb │ └── tooltips.html.erb │ ├── contracts_expenses │ ├── _form.html.erb │ ├── edit.html.erb │ └── new.html.erb │ └── settings │ └── _contract_settings.html.erb ├── assets ├── images │ └── true.png └── stylesheets │ └── contracts.css ├── config ├── locales │ ├── bg.yml │ ├── cs.yml │ ├── de.yml │ ├── en.yml │ ├── es.yml │ ├── nl.yml │ ├── no.yml │ ├── pt-BR.yml │ ├── ru.yml │ └── zh.yml └── routes.rb ├── db └── migrate │ ├── 001_create_contracts.rb │ ├── 002_add_contract_id_to_time_entries.rb │ ├── 003_add_precision_to_hourly_rate.rb │ ├── 004_create_user_project_rates.rb │ ├── 005_create_user_contract_rates.rb │ ├── 006_create_expenses.rb │ ├── 007_lock.rb │ ├── 008_project_contract_id.rb │ ├── 009_contract_category_enumeration.rb │ ├── 010_rename_expenses.rb │ ├── 011_add_fixed_price_contract.rb │ └── 012_add_recurring_contracts.rb ├── docs └── screenshots │ ├── edit_contract.png │ ├── multiple_contracts.png │ ├── permissions.png │ └── single_contract.png ├── init.rb ├── lib └── contracts │ ├── hooks │ └── hooks.rb │ ├── patches │ ├── project_patch.rb │ ├── time_entry_patch.rb │ ├── timelog_controller_patch.rb │ └── user_patch.rb │ └── validators │ ├── is_after_agreement_date_validator.rb │ └── is_after_start_date_validator.rb └── test ├── fixtures ├── contracts.yml ├── user_contract_rates.yml └── user_project_rates.yml ├── functional ├── contracts_controller_test.rb ├── contracts_expenses_controller_test.rb └── timelog_controller_test.rb ├── test_helper.rb └── unit ├── contract_test.rb ├── contracts_expense_test.rb ├── project_test.rb ├── time_entry_test.rb ├── user_contract_rate_test.rb ├── user_project_rate_test.rb └── user_test.rb /README.md: -------------------------------------------------------------------------------- 1 | A Redmine plugin for managing the time/money being spent on client contracts. 2 | 3 | This plugin allows you to: 4 | 5 | - Create and store client contracts 6 | - Visualize how much time/money has been spent on a particular contract 7 | - Associate time entries with specific contracts 8 | 9 | ### Special thanks to [UpgradeYa](http://www.upgradeya.com) for funding this project. 10 | 11 | Installation 12 | ------------ 13 | Option 1 - Download zip 14 | 15 | 1. Download the zip (for Redmine 2 you will need to download the v1.3.1 zip from the github releases page) 16 | 1. Unzip the redmine-contracts-with-time-tracking-plugin-master folder, rename it to contracts, and place it in the redmine plugins folder. 17 | 1. run 'rake redmine:plugins:migrate RAILS_ENV=production' from your redmine root directory 18 | 19 | Option 2 - Git clone 20 | 21 | 1. Run 'git clone https://github.com/upgradeya/redmine-contracts-with-time-tracking-plugin.git plugins/contracts' from your redmine root directory 22 | * Note : use 'git submodule add' instead of 'git clone' if your install folder is part of a git project. 23 | 1. This step is only for Redmine 2 - After you run the git command above, cd into the contracts directory and run 'git checkout tags/v1.3.1' 24 | 1. run 'rake redmine:plugins:migrate RAILS_ENV=production' from your redmine root directory 25 | 26 | Screenshots 27 | ----------- 28 | 29 | ### View all contracts for a project: 30 |  31 | 32 | ### View contract details: 33 |  34 | 35 | ### Create and edit contracts: 36 |  37 | 38 | ### Set permisisons: 39 |  40 | 41 | Changelog 42 | --------- 43 | Contracts v2.2 2017-3-6 44 | ----------------------- 45 | - Added a recurring contract option so you that can have fixed contracts created automatically each month or year. 46 | 47 | Contracts v2.2 2017-2-7 48 | ----------------------- 49 | - Added a 'Fixed Price' contract type that calculates your effective rate and hides hourly information from your client (in permissions uncheck 'View spent time '). 50 | - Added a summary tab that shows the cumulative time spent on each issue within that contract. 51 | - Fixed the no confirmation for deletion bug. 52 | 53 | Contracts v2.1 2016-3-5 54 | ----------------------- 55 | - Renamed the expenses database table name to prevent conflicts with other redmine plugins 56 | 57 | Contracts v2.0 2016-1-9 58 | ----------------------- 59 | - Contracts plugin is now Redmine 3 compatible 60 | 61 | Contracts v1.3.1, 2015-12-27 62 | ---------------------------- 63 | - Implemented new feature to lock contracts. This can be used to prevent old contracts and their time entries from accidentally being edited. 64 | - Locked contracts are hidden from new time entry dropdowns 65 | - Implementing caching on locked contracts to decrease load time on the contract pages. 66 | 67 | Contracts v1.2.0, 2015-12-14 68 | ---------------------------- 69 | - On contract form the fields are now inline and date fields use calendar widget. Required fields are now marked. Any validations will re-populate the screen with previous data. 70 | - Adding a time entry selects last created contract. Used to use start and end date. For sub-projects it selects the last created contract within the sub-project if a contract exists. Also fixed for expenses. Currently there is no way to add expense in sub-project to the parent contract if there are no sub-project contracts. 71 | - New Agreement Pending - (basically just not marking that field as required) Agreed on date shows agreement pending on contract list and detail page. Date range is not shown when agreement is pending. 72 | - If they have auto-contract creation enabled, a time entry that exceeds the remaining contract will auto-create a new contract and submit a time entry to the new contract with the remaining time. 73 | - Discussion on the title. Fixed title format. Auto-increments based on all the projects IDs. Need to add a per project identifier so the auto-increment is project based and not entire redmine based. 74 | -------------------------------------------------------------------------------- /app/controllers/contracts_controller.rb: -------------------------------------------------------------------------------- 1 | class ContractsController < ApplicationController 2 | before_filter :find_project, :authorize, :only => [:index, :show, :new, :create, :edit, :update, :destroy, 3 | :add_time_entries, :assoc_time_entries_with_contract, :series] 4 | 5 | def index 6 | fixed_contracts = Contract.order("start_date ASC").where(:project_id => @project.id, :is_fixed_price => true) 7 | hourly_contracts = Contract.order("start_date ASC").where(:project_id => @project.id, :is_fixed_price => false) 8 | 9 | # Show the tabs only if there are hourly and fixed contracts within the same project. 10 | if fixed_contracts.size > 0 && hourly_contracts.size > 0 11 | @show_tabs = true 12 | end 13 | 14 | # Show fixed contracts if the fixed tab is selected or if there aren't any hourly contracts. 15 | @show_fixed_contracts = (fixed_contracts.size > 0 && hourly_contracts.size == 0) || params[:fixed_tab_active] == 'true' 16 | 17 | # Set @contracts to the fixed our hourly array of contracts to be displayed. 18 | if @show_fixed_contracts 19 | @contracts = fixed_contracts 20 | else 21 | @contracts = hourly_contracts 22 | end 23 | 24 | # Calculate metrics for display. 25 | @total_purchased_dollars = @project.total_amount_purchased 26 | @total_purchased_fixed = fixed_contracts.map(&:purchase_amount).inject(0, &:+) 27 | @total_purchased_hourly = hourly_contracts.map(&:purchase_amount).inject(0, &:+) 28 | @total_purchased_hourly_hours = hourly_contracts.map(&:hours_purchased).inject(0, &:+) 29 | @total_amount_remaining_hourly = hourly_contracts.map(&:amount_remaining).inject(0, &:+) 30 | @total_remaining_hours = hourly_contracts.map(&:hours_remaining).inject(0, &:+) 31 | 32 | set_contract_visibility 33 | 34 | end 35 | 36 | def all 37 | user = User.current 38 | projects = user.projects.select { |project| user.allowed_to?(:view_all_contracts_for_project, project) } 39 | 40 | fixed_contracts = projects.collect { |project| project.contracts.order("start_date ASC").where(:is_fixed_price => '1') } 41 | fixed_contracts.flatten! 42 | hourly_contracts = projects.collect { |project| project.contracts.order("start_date ASC").where(:is_fixed_price => '0') } 43 | hourly_contracts.flatten! 44 | all_contracts = projects.collect { |project| project.contracts.order("start_date ASC") } 45 | all_contracts.flatten! 46 | 47 | # Show the tabs only if there are hourly and fixed contracts within the same project. 48 | if fixed_contracts.size > 0 && hourly_contracts.size > 0 49 | @show_tabs = true 50 | end 51 | 52 | # Show fixed contracts if the fixed tab is selected or if there aren't any hourly contracts. 53 | @show_fixed_contracts = (fixed_contracts.size > 0 && hourly_contracts.size == 0) || params[:fixed_tab_active] == 'true' 54 | 55 | if @show_fixed_contracts 56 | @contracts = fixed_contracts 57 | else 58 | @contracts = hourly_contracts 59 | end 60 | 61 | @total_purchased_dollars = all_contracts.map(&:purchase_amount).inject(0, &:+) 62 | @total_purchased_fixed = fixed_contracts.map(&:purchase_amount).inject(0, &:+) 63 | @total_purchased_hourly = hourly_contracts.map(&:purchase_amount).inject(0, &:+) 64 | @total_purchased_hourly_hours = hourly_contracts.map(&:hours_purchased).inject(0, &:+) 65 | @total_amount_remaining_hourly = hourly_contracts.map(&:amount_remaining).inject(0, &:+) 66 | @total_remaining_hours = hourly_contracts.map(&:hours_remaining).inject(0, &:+) 67 | 68 | set_contract_visibility 69 | 70 | render "index" 71 | end 72 | 73 | def new 74 | @contract = Contract.new 75 | @project = Project.find(params[:project_id]) 76 | load_contractors_and_rates 77 | end 78 | 79 | def create 80 | if contract_params[:contract_type] != 'recurring' 81 | params[:contract][:recurring_frequency] = :not_recurring 82 | end 83 | 84 | @contract = Contract.new(contract_params) 85 | 86 | if !rates_are_valid(params[:rates]) 87 | flash[:error] = l(:text_invalid_rate) 88 | redirect_to :action => "new", :id => @contract.id 89 | return 90 | end 91 | 92 | if contract_params[:contract_type] != 'recurring' 93 | params[:contract][:recurring_frequency] = :not_recurring 94 | end 95 | 96 | @contract.rates = params[:rates] 97 | @contract.project_contract_id = @project.contracts.empty? ? 1 : @project.contracts.last.project_contract_id + 1 98 | 99 | # Set the series ID to the project_contract_id if its a new recurring contract. 100 | @contract.series_id = @contract.project_contract_id if contract_params[:contract_type] == 'recurring' 101 | 102 | if @contract.save 103 | if contract_params[:contract_type] == 'recurring' 104 | if @contract.monthly? 105 | @contract.update_attribute(:end_date, @contract.start_date + 1.month) 106 | elsif @contract.yearly? 107 | @contract.update_attribute(:end_date, @contract.start_date + 1.year) 108 | end 109 | end 110 | 111 | flash[:notice] = l(:text_contract_saved) 112 | redirect_to :action => "show", :id => @contract.id 113 | else 114 | flash[:error] = "* " + @contract.errors.full_messages.join("* ") 115 | load_contractors_and_rates 116 | render :new 117 | end 118 | end 119 | 120 | def show 121 | @contract = Contract.find(params[:id]) 122 | @time_entries = @contract.time_entries.order("spent_on DESC") 123 | @members = [] 124 | @time_entries.each { |entry| @members.append(entry.user) unless @members.include?(entry.user) } 125 | @expenses_tab = (params[:contracts_expenses] == 'true') 126 | @summary_tab = (params[:contract_summary] == 'true') 127 | if @expenses_tab 128 | @expenses = @contract.contracts_expenses 129 | end 130 | if @summary_tab 131 | @issues = [] 132 | @time_entries.each { |entry| @issues.append(entry.issue) unless @issues.include?(entry.issue) } 133 | @issues.sort! { |a,b| @contract.amount_spent_on_issue(b) <=> @contract.amount_spent_on_issue(a)} 134 | end 135 | 136 | end 137 | 138 | def edit 139 | @contract = Contract.find(params[:id]) 140 | @projects = Project.all 141 | load_contractors_and_rates 142 | end 143 | 144 | def update 145 | @contract = Contract.find(params[:id]) 146 | 147 | if !rates_are_valid(params[:rates]) 148 | flash[:error] = l(:text_invalid_rate) 149 | redirect_to :action => "edit", :id => @contract.id 150 | return 151 | end 152 | 153 | # Set the end date to null so that the start_date end_date validation passes 154 | # if the start date is changed to after the end date. 155 | if @contract.contract_type == 'recurring' 156 | params[:contract][:end_date] = nil 157 | @contract.end_date = nil 158 | end 159 | 160 | if @contract.update_attributes(contract_params) 161 | @contract.update_attribute(:rates, params[:rates]) 162 | if @contract.contract_type == 'recurring' 163 | if @contract.monthly? 164 | @contract.update_attribute(:end_date, @contract.start_date + 1.month) 165 | elsif @contract.yearly? 166 | @contract.update_attribute(:end_date, @contract.start_date + 1.year) 167 | end 168 | end 169 | flash[:notice] = l(:text_contract_updated) 170 | redirect_to :action => "show", :id => @contract.id 171 | else 172 | flash[:error] = "* " + @contract.errors.full_messages.join("* ") 173 | redirect_to :action => "edit", :id => @contract.id 174 | end 175 | end 176 | 177 | def series 178 | @contracts = Contract.order("start_date ASC").where(:project_id => @project.id, :series_id => params[:id]) 179 | @show_fixed_contracts = true 180 | 181 | # Calculate metrics for display. 182 | @total_purchased_fixed = @contracts.map(&:purchase_amount).inject(0, &:+) 183 | 184 | set_contract_visibility 185 | 186 | render "index" 187 | end 188 | 189 | def cancel_recurring 190 | @contract = Contract.find(params[:id]) 191 | @contract.completed! 192 | 193 | if @contract.save 194 | flash[:notice] = l(:text_contract_updated) 195 | redirect_to :action => "show", :id => @contract.id 196 | else 197 | flash[:error] = "* " + @contract.errors.full_messages.join("* ") 198 | redirect_to :action => "edit", :id => @contract.id 199 | end 200 | end 201 | 202 | def destroy 203 | @contract = Contract.find(params[:id]) 204 | if @contract.destroy 205 | flash[:notice] = l(:text_contract_deleted) 206 | if !params[:project_id].nil? 207 | redirect_to :action => "index", :project_id => params[:project_id] 208 | else 209 | redirect_to :action => "all" 210 | end 211 | else 212 | redirect_to(:back) 213 | end 214 | end 215 | 216 | def add_time_entries 217 | @contract = Contract.find(params[:id]) 218 | @project = @contract.project 219 | @time_entries = @contract.project.time_entries_for_all_descendant_projects.sort_by! { |entry| entry.spent_on } 220 | end 221 | 222 | def assoc_time_entries_with_contract 223 | @contract = Contract.find(params[:id]) 224 | @project = @contract.project 225 | time_entries = params[:time_entries] 226 | if time_entries != nil 227 | time_entries.each do |time_entry| 228 | updated_time_entry = TimeEntry.find(time_entry.first) 229 | updated_time_entry.contract = @contract 230 | updated_time_entry.save 231 | end 232 | end 233 | unless @contract.hours_remaining >= 0 234 | flash[:error] = l(:text_hours_over_contract, :hours_over => l_hours(-1 * @contract.hours_remaining)) 235 | end 236 | redirect_to "/projects/#{@contract.project.id}/contracts/#{@contract.id}" 237 | end 238 | 239 | def lock 240 | @contract = Contract.find(params[:id]) 241 | @lock = (params[:lock] == 'true') 242 | if @lock 243 | @contract.update_attribute(:is_locked, @lock) 244 | flash[:notice] = l(:text_contract_locked) 245 | else 246 | @contract.is_locked = false 247 | @contract.hours_worked = nil 248 | @contract.billable_amount_total = nil 249 | @contract.save! 250 | flash[:notice] = l(:text_contract_unlocked) 251 | end 252 | 253 | if params[:view] == 'index' 254 | redirect_to :action => "index", :project_id => params[:project_id] 255 | else 256 | redirect_to url_for({ :controller => 'contracts', :action => 'show', :project_id => @contract.project.identifier, :id => @contract.id }) 257 | end 258 | end 259 | 260 | def tooltips 261 | @id = params[:id] 262 | end 263 | 264 | 265 | private 266 | 267 | def rates_are_valid(rates) 268 | return false if rates.nil? 269 | rates.each_pair do |user_id, rate| 270 | if !is_number?(rate) or rate.to_f < 0 271 | return false 272 | end 273 | end 274 | return true 275 | end 276 | 277 | def load_contractors_and_rates 278 | @contractors = Contract.users_for_project_and_sub_projects(@project) 279 | @contractor_rates = {} 280 | @contractors.each do |contractor| 281 | if @contract.new_record? 282 | rate = @project.rate_for_user(contractor) 283 | else 284 | rate = @contract.user_contract_rate_or_default(contractor) 285 | end 286 | @contractor_rates[contractor.id] = rate 287 | end 288 | end 289 | 290 | def find_project 291 | #@project variable must be set before calling the authorize filter 292 | @project = Project.find(params[:project_id]) 293 | end 294 | 295 | def contract_params 296 | params.require(:contract).permit(:description, :agreement_date, :start_date, :end_date, :contract_url, 297 | :invoice_url, :project_id, :purchase_amount, :hourly_rate, :category_id, :is_fixed_price, :title, 298 | :contract_type, :recurring_frequency) 299 | end 300 | 301 | # Allows the user to hide or show locked contracts on contract list pages 302 | def set_contract_visibility 303 | # set session variable to the boolean true and false instead of using the string parameter 304 | if params[:contract_list].present? 305 | if params[:contract_list][:show_locked_contracts] == "true" 306 | session[:show_locked_contracts] = true 307 | else 308 | session[:show_locked_contracts] = false 309 | end 310 | if params[:contract_list][:show_only_active_recurring] == "true" 311 | session[:show_only_active_recurring] = true 312 | else 313 | session[:show_only_active_recurring] = false 314 | end 315 | elsif session[:show_locked_contracts].nil? 316 | # set session variable for first time guests 317 | session[:show_locked_contracts] = false 318 | session[:show_only_active_recurring] = false 319 | end 320 | end 321 | 322 | # Helper method for determining if a string is numeric. 323 | def is_number? string 324 | true if Float(string) rescue false 325 | end 326 | 327 | end 328 | -------------------------------------------------------------------------------- /app/controllers/contracts_expenses_controller.rb: -------------------------------------------------------------------------------- 1 | class ContractsExpensesController < ApplicationController 2 | before_filter :set_project, :authorize, :only => [:new, :edit, :update, :create, :destroy] 3 | before_filter :set_expense, :only => [:edit, :update, :destroy] 4 | 5 | def new 6 | @contracts_expense = ContractsExpense.new 7 | load_contracts 8 | end 9 | 10 | def edit 11 | load_contracts 12 | end 13 | 14 | def create 15 | @contracts_expense = ContractsExpense.new(expense_params) 16 | 17 | respond_to do |format| 18 | if @contracts_expense.save 19 | format.html { redirect_to contract_urlpath(@contracts_expense), notice: l(:text_expense_created) } 20 | else 21 | load_contracts 22 | format.html { render action: 'new' } 23 | end 24 | end 25 | end 26 | 27 | def update 28 | respond_to do |format| 29 | if @contracts_expense.update_attributes(expense_params) 30 | format.html { redirect_to contract_urlpath(@contracts_expense), notice: l(:text_expense_updated) } 31 | else 32 | load_contracts 33 | format.html { render action: 'edit' } 34 | end 35 | end 36 | end 37 | 38 | def destroy 39 | back_to = contract_urlpath(@contracts_expense) 40 | @contracts_expense.destroy 41 | flash[:notice] = l(:text_expense_deleted) 42 | respond_to do |format| 43 | format.html { redirect_to back_to } 44 | end 45 | end 46 | 47 | private 48 | 49 | def contract_urlpath(expense) 50 | url_for({ :controller => 'contracts', :action => 'show', :project_id => expense.contract.project.identifier, :id => expense.contract.id, :contracts_expenses => 'true'}) 51 | end 52 | 53 | def set_expense 54 | @contracts_expense = ContractsExpense.find(params[:id]) 55 | if @contracts_expense.contract.is_locked 56 | flash[:error] = l(:text_expenses_uneditable) 57 | redirect_to contract_urlpath(@contracts_expense) 58 | end 59 | end 60 | 61 | def set_project 62 | @project = Project.find(params[:project_id]) 63 | end 64 | 65 | def load_contracts 66 | @contracts = Contract.order("start_date ASC").where(:project_id => @project.id).where(:is_locked => false) 67 | end 68 | 69 | private 70 | 71 | def expense_params 72 | params.require(:contracts_expense).permit(:name, :expense_date, :amount, :contract_id, :issue_id, :description) 73 | end 74 | 75 | end 76 | -------------------------------------------------------------------------------- /app/helpers/contracts_expenses_helper.rb: -------------------------------------------------------------------------------- 1 | module ContractsExpensesHelper 2 | 3 | def expense_edit_urlpath(contract, expense) 4 | "/projects/#{contract.project.identifier}/expenses/#{expense.id}/edit" 5 | end 6 | 7 | def span_required 8 | raw ' *' 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/helpers/contracts_helper.rb: -------------------------------------------------------------------------------- 1 | module ContractsHelper 2 | 3 | def expense_edit_urlpath(contract, expense) 4 | { :controller => 'contracts_expenses', :action => 'edit', :project_id => contract.project.identifier, :id => expense.id } 5 | end 6 | 7 | def format_hours(hours) 8 | format("%#.2f", hours) 9 | end 10 | 11 | def tab_selected 12 | raw 'class="selected"' 13 | end 14 | 15 | def span_required 16 | raw ' *' 17 | end 18 | 19 | # Returns a collection of categories for a select field. contract 20 | # is optional and will be used to check if the selected ContractCategory 21 | # is active. 22 | def contract_category_collection_for_select_options(contract=nil) 23 | categories = ContractCategory.shared.active 24 | 25 | collection = [] 26 | if contract && contract.category && !contract.category.active? 27 | collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] 28 | else 29 | collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless categories.detect(&:is_default) 30 | end 31 | categories.each { |a| collection << [a.name, a.id] } 32 | collection 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /app/models/contract.rb: -------------------------------------------------------------------------------- 1 | class Contract < ActiveRecord::Base 2 | belongs_to :project 3 | has_many :time_entries 4 | has_many :user_contract_rates 5 | has_many :contracts_expenses 6 | belongs_to :category, :class_name => 'ContractCategory' 7 | 8 | validates_presence_of :start_date, :purchase_amount, :hourly_rate, :project_id 9 | validates_uniqueness_of :project_contract_id, :scope => :project_id 10 | validates :project_contract_id, :numericality => { :greater_than_or_equal_to => 1, :less_than_or_equal_to => 999 } 11 | validates :purchase_amount, :numericality => { :greater_than_or_equal_to => 0 } 12 | validates :hourly_rate, :numericality => { :greater_than_or_equal_to => 0 } 13 | validates :end_date, :is_after_start_date => true 14 | before_destroy { |contract| contract.time_entries.clear } 15 | after_save :apply_rates 16 | attr_accessor :rates 17 | 18 | enum recurring_frequency: { 19 | not_recurring: 0, 20 | monthly: 1, 21 | yearly: 2, 22 | completed: 3 23 | } 24 | 25 | # The values have been made lower-case to match the conventions of Rails I18n 26 | HOURLY = "hourly" 27 | FIXED = "fixed" 28 | RECURRING = "recurring" 29 | 30 | CONTRACT_TYPES = [HOURLY, FIXED, RECURRING] 31 | DROPDOWN_RECURRING_FREQUENCIES = [ 32 | "monthly", 33 | "yearly" 34 | ] 35 | 36 | def hours_purchased 37 | self.purchase_amount / self.hourly_rate 38 | end 39 | 40 | def reset_cache! 41 | update_attributes(:hours_worked => nil, :billable_amount_total => nil) 42 | end 43 | 44 | def smart_hours_spent 45 | if self.is_locked 46 | if self.hours_worked.nil? 47 | self.hours_worked = hours_spent 48 | save! 49 | end 50 | return self.hours_worked 51 | end 52 | hours_spent 53 | end 54 | 55 | def hours_spent 56 | self.time_entries.map(&:hours).inject(0, &:+) 57 | end 58 | 59 | def hours_spent_by_user(user) 60 | self.time_entries.select { |entry| entry.user == user }.map(&:hours).inject(0, &:+) 61 | end 62 | 63 | def hours_spent_on_issue(issue) 64 | self.time_entries.select { |entry| entry.issue == issue }.map(&:hours).inject(0, &:+) 65 | end 66 | 67 | def amount_spent_on_issue(issue) 68 | time_entries = self.time_entries.select { |entry| entry.issue == issue } 69 | total_amount = 0 70 | time_entries.each do |entry| 71 | total_amount += entry.hours * self.user_contract_rate_or_default(entry.user) 72 | end 73 | return total_amount 74 | end 75 | 76 | def effective_rate 77 | if self.expenses_total >= self.purchase_amount 78 | 0 79 | elsif self.smart_hours_spent >= 1 80 | (self.purchase_amount - self.expenses_total) / self.smart_hours_spent 81 | else 82 | self.purchase_amount - self.expenses_total 83 | end 84 | end 85 | 86 | def billable_amount_for_user(user) 87 | member_hours = self.time_entries.select { |entry| entry.user == user }.map(&:hours).inject(0, &:+) 88 | member_rate = self.user_contract_rate_or_default(user) 89 | member_hours * member_rate 90 | end 91 | 92 | # IF the contract is locked 93 | # - check to see if the billable amount total is pre-calculcated, if so, return it 94 | # - if not, calculate and save the billable amount total, and return it 95 | # ELSE return the calculated billable amount total 96 | def smart_billable_amount_total 97 | if self.is_locked 98 | if self.billable_amount_total.nil? 99 | self.billable_amount_total = calculate_billable_amount_total 100 | save! 101 | end 102 | return self.billable_amount_total 103 | end 104 | calculate_billable_amount_total 105 | end 106 | 107 | def calculate_billable_amount_total 108 | members = members_with_entries 109 | return 0 if members.empty? 110 | total_billable_amount = 0 111 | members.each do |member| 112 | member_hours = self.time_entries.select { |entry| entry.user_id == member.id }.map(&:hours).inject(0, &:+) 113 | member_rate = self.user_contract_rate_or_default(member) 114 | billable_amount = member_hours * member_rate 115 | total_billable_amount += billable_amount 116 | end 117 | total_billable_amount 118 | end 119 | 120 | def amount_remaining 121 | self.purchase_amount - self.smart_billable_amount_total - self.expenses_total 122 | end 123 | 124 | def hours_remaining 125 | self.amount_remaining / self.hourly_rate 126 | end 127 | 128 | def user_contract_rate_by_user(user) 129 | self.user_contract_rates.select { |ucr| ucr.user_id == user.id}.first 130 | end 131 | 132 | def rate_for_user(user) 133 | ucr = self.user_contract_rate_by_user(user) 134 | ucr.nil? ? 0.0 : ucr.rate 135 | end 136 | 137 | def set_user_contract_rate(user, rate) 138 | ucr = self.user_contract_rate_by_user(user) 139 | if ucr.nil? 140 | self.user_contract_rates.create!(:user_id => user.id, :rate => rate) 141 | else 142 | ucr.update_attribute(:rate, rate) 143 | end 144 | end 145 | 146 | def user_contract_rate_or_default(user) 147 | ucr = self.user_contract_rate_by_user(user) 148 | ucr.nil? ? self.hourly_rate : ucr.rate 149 | end 150 | 151 | # Usage: 152 | # contract.rates = {"3"=>"27.00", "1"=>"35.00"} 153 | # (where the hash keys are user_id's and the values are the rates) 154 | def rates=(rates) 155 | @rates = rates 156 | end 157 | 158 | # Getter method for contract_type (virtual attribute) 159 | def contract_type 160 | if self.is_fixed_price? 161 | if self.not_recurring? 162 | return FIXED 163 | else 164 | return RECURRING 165 | end 166 | else 167 | return HOURLY 168 | end 169 | end 170 | 171 | # Setter method for contract_type (virtual attribute) 172 | def contract_type=(contract_type) 173 | if contract_type == HOURLY 174 | self.is_fixed_price = false 175 | elsif contract_type == FIXED 176 | self.is_fixed_price = true 177 | elsif contract_type == RECURRING 178 | self.is_fixed_price = true 179 | end 180 | end 181 | 182 | def user_project_rate_or_default(user) 183 | upr = self.project.user_project_rate_by_user(user) 184 | upr.nil? ? self.hourly_rate : upr.rate 185 | end 186 | 187 | def members_with_entries 188 | return [] if self.time_entries.empty? 189 | uniq_user_ids = self.time_entries.collect { |entry| entry.user_id }.uniq 190 | return [] if uniq_user_ids.nil? 191 | User.find(uniq_user_ids) 192 | end 193 | 194 | def self.users_for_project_and_sub_projects(project) 195 | users = [] 196 | users += project.users 197 | users += Contract.users_for_sub_projects(project) 198 | users.flatten! 199 | users.uniq 200 | end 201 | 202 | def self.users_for_sub_projects(project) 203 | users = [] 204 | sub_projects = Project.where(:parent_id => project.id) 205 | sub_projects.each do |sub_project| 206 | subs = Project.where(:parent_id => sub_project.id) 207 | if !subs.empty? 208 | users << Contract.users_for_sub_projects(sub_project) 209 | end 210 | users << sub_project.users 211 | end 212 | users.uniq 213 | end 214 | 215 | def expenses_total 216 | expenses_sum = self.contracts_expenses.map(&:amount).inject(0, &:+) 217 | end 218 | 219 | def getDisplayTitle 220 | return self.title if self.title.present? 221 | if self.category_id.blank? 222 | category = 'Contract' 223 | else 224 | category = ContractCategory.find(self.category_id).name 225 | end 226 | Project.find(self.project_id).identifier + "_" + category + "#" + ("%03d" % (self.project_contract_id)) 227 | end 228 | 229 | def copy(contract, project = nil) 230 | if project.nil? 231 | project = Project.find(contract.project_id) 232 | end 233 | self.project_contract_id = project.contracts.last.project_contract_id + 1 234 | self.category_id = contract.category_id 235 | self.description = contract.description 236 | self.title = contract.title 237 | self.is_fixed_price = contract.is_fixed_price 238 | self.recurring_frequency = contract.recurring_frequency 239 | self.series_id = contract.series_id 240 | self.hourly_rate = contract.hourly_rate 241 | self.purchase_amount = contract.purchase_amount 242 | self.contract_url = "" 243 | self.invoice_url = "" 244 | self.project_id = contract.project_id 245 | if contract.contract_type == "recurring" 246 | if contract.monthly? 247 | self.start_date = contract.start_date + 1.month 248 | self.end_date = contract.start_date + 2.month 249 | elsif contract.yearly? 250 | self.start_date = contract.start_date + 1.year 251 | self.end_date = contract.start_date + 2.year 252 | end 253 | else 254 | self.start_date = Time.new 255 | end 256 | # add the contractors and rates 257 | contractors = Contract.users_for_project_and_sub_projects(project) 258 | contractor_rates = {} 259 | contractors.each do |contractor| 260 | if contract.new_record? 261 | rate = project.rate_for_user(contractor) 262 | else 263 | rate = contract.user_contract_rate_or_default(contractor) 264 | end 265 | contractor_rates[contractor.id] = rate 266 | end 267 | 268 | self.rates = contractor_rates 269 | self.save 270 | end 271 | 272 | private 273 | 274 | def apply_rates 275 | return unless @rates 276 | @rates.each_pair do |user_id, rate| 277 | user = User.find(user_id) 278 | self.project.set_user_rate(user, rate) 279 | self.set_user_contract_rate(user, rate) 280 | end 281 | end 282 | 283 | def remove_contract_id_from_associated_time_entries 284 | self.time_entries.each do |time_entry| 285 | time_entry.contract_id = nil 286 | time_entry.save 287 | end 288 | end 289 | end 290 | -------------------------------------------------------------------------------- /app/models/contract_category.rb: -------------------------------------------------------------------------------- 1 | class ContractCategory < Enumeration 2 | has_many :contracts, :foreign_key => 'category_id' 3 | 4 | OptionName = :enumeration_contract_categories 5 | 6 | def option_name 7 | OptionName 8 | end 9 | 10 | def objects_count 11 | contracts.count 12 | end 13 | 14 | def transfer_relations(to) 15 | contracts.update_all(:category_id => to.id) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/models/contracts_expense.rb: -------------------------------------------------------------------------------- 1 | class ContractsExpense < ActiveRecord::Base 2 | belongs_to :contract 3 | belongs_to :issue 4 | validates_presence_of :name, :expense_date, :amount, :contract_id 5 | validates :amount, :numericality => { :greater_than => 0 } 6 | 7 | validate :issue_exists 8 | 9 | def issue_exists 10 | return true if self.issue_id.blank? 11 | if self.issue.nil? 12 | errors.add(:issue_id, l(:text_invalid_issue_id)) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/models/user_contract_rate.rb: -------------------------------------------------------------------------------- 1 | class UserContractRate < ActiveRecord::Base 2 | belongs_to :contract 3 | belongs_to :user 4 | validates_uniqueness_of :user_id, :scope => :contract_id 5 | end 6 | -------------------------------------------------------------------------------- /app/models/user_project_rate.rb: -------------------------------------------------------------------------------- 1 | class UserProjectRate < ActiveRecord::Base 2 | belongs_to :project 3 | belongs_to :user 4 | 5 | validates_uniqueness_of :user_id, :scope => :project_id 6 | end 7 | -------------------------------------------------------------------------------- /app/views/contracts/_contract_summary.html.erb: -------------------------------------------------------------------------------- 1 |
<%= l(:label_issue) %> | 5 |<%= l(:label_hours) %> | 6 | <% if User.current.allowed_to?(:view_hourly_rate, @project) %> 7 |<%= l(:label_cost) %> | 8 | <% end %> 9 |10 | | 11 | |
---|---|---|---|---|
<%= link_to issue, issue_path(issue) if issue.present? %> | 16 |<%= format_hours(@contract.hours_spent_on_issue(issue)) %> | 17 | <% if User.current.allowed_to?(:view_hourly_rate, @project) %> 18 |<%= number_to_currency(@contract.amount_spent_on_issue(issue)) %> | 19 | <% end %> 20 |
<%= l(:label_name) %> | 11 |<%= l(:label_agreed_on) %> | 12 |<%= l(:label_date_period) %> | 13 |<%= l(:label_purchased) %> | 14 | <% if (@project != nil) && (User.current.allowed_to?(:view_hourly_rate, @project)) %> 15 |<%= l(:field_hourly_rate) %> | 16 | <% end %> 17 |<%= l(:label_expenses) %> | 18 |<%= l(:label_remaining) %> | 19 |<%= l(:label_hours_worked) %> | 20 |<%= l(:label_hours_left) %> | 21 |<%= l(:label_contract) %> | 22 |<%= l(:label_invoice) %> | 23 |24 | | 25 | | 26 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
33 | <%= link_to contract.getDisplayTitle, { :controller => 'contracts', :action => 'show', :project_id => contract.project.identifier, :id => contract.id } %> 34 | | 35 |36 | <% if contract.agreement_date != nil %> 37 | <%= format_date(contract.agreement_date) %> 38 | <% else %><%= l(:text_agreement_pending) %> 39 | <% end %> 40 | | 41 |42 | <% if contract.start_date != nil %> 43 | <%= format_date(contract.start_date) + " - " %> 44 | <% if contract.end_date != nil %> 45 | <%= format_date(contract.end_date) %> 46 | <% else %> 47 | <%= l(:text_no_end_date) %> 48 | <% end %> 49 | <% end %> 50 | | 51 |<%= number_to_currency(contract.purchase_amount) %> | 52 | <% if (@project != nil) && (User.current.allowed_to?(:view_hourly_rate, contract.project)) %> 53 |<%= number_to_currency(contract.hourly_rate) %> | 54 | <% end %> 55 |<%= number_to_currency(contract.expenses_total) %> | 56 |<%= number_to_currency(contract.amount_remaining) %> | 57 |<%= number_with_precision contract.smart_hours_spent, :precision => 2 %> | 58 |<%= number_with_precision contract.hours_remaining, :precision => 2 %> | 59 |60 | <% if !contract.contract_url.blank? %> 61 | <%= link_to image_tag("/images/files/text.png"), contract.contract_url %> 62 | <% end %> 63 | | 64 |65 | <% if !contract.invoice_url.blank? %> 66 | <%= link_to image_tag("/images/files/text.png"), contract.invoice_url %> 67 | <% end %> 68 | | 69 |70 | <% if (User.current.allowed_to?(:edit_contracts, contract.project)) %> 71 | <% if contract.is_locked %> 72 | <%= link_to image_tag("unlock.png"), { :controller => 'contracts', :action => 'lock', :project_id => contract.project.identifier, :id => contract.id, :lock => false, :view => 'index' }, :method => :put, :title => l(:label_unlock) %> 73 | <% else %> 74 | <%= link_to image_tag("locked.png"), { :controller => 'contracts', :action => 'lock', :project_id => contract.project.identifier, :id => contract.id, :lock => true, :view => 'index' }, :method => :put, :title => l(:label_lock) %> 75 | <% end %> 76 | <% end %> 77 | | 78 |79 | <% if !contract.is_locked && User.current.allowed_to?(:edit_contracts, contract.project) %> 80 | <%= link_to image_tag("edit.png"), { :controller => 'contracts', :action => 'edit', :project_id => contract.project.identifier, :id => contract.id }, :title => l(:label_edit) %> 81 | <% end %> 82 | | 83 |84 | <% if !contract.is_locked && (User.current.allowed_to?(:delete_contracts, contract.project)) %> 85 | <%= link_to image_tag("delete.png"), { :controller => 'contracts', :action => 'destroy', :project_id => contract.project.identifier, :id => contract.id }, :method => :delete, 86 | :title => l(:label_delete), :data => {:confirm => l(:text_are_you_sure_delete_title, contract.getDisplayTitle)} %> 87 | <% end %> 88 | | 89 |
6 | <%= l(:label_total_purchased) %>7 |<%= number_to_currency(@total_purchased_dollars) %> 8 | |
9 | <% end %>
10 |
11 | 12 | <%= controller.action_name == "series" ? l(:label_total_series) : l(:label_total_fixed) %> 13 |14 |<%= number_to_currency(@total_purchased_fixed) %> 15 | |
16 |
21 | <%= l(:label_total_hourly) %>22 |<%= number_to_currency(@total_purchased_hourly) %> 23 |<%= l(:label_or) %> 24 |~<%= l_hours @total_purchased_hourly_hours %> 25 | |
26 |
27 | <%= l(:label_remaining_hourly) %>28 |<%= number_to_currency(@total_amount_remaining_hourly) %> 29 |<%= l(:label_or) %> 30 |~<%= l_hours @total_remaining_hours %> 31 | |
32 |
<%= l(:label_date) %> | 5 |<%= l(:label_name) %> | 6 |<%= l(:label_amount) %> | 7 |<%= l(:label_contract) %> | 8 |<%= l(:label_issue) %> | 9 |<%= l(:label_description) %> | 10 |11 | | 12 | |
---|---|---|---|---|---|---|---|
<%= format_date(expense.expense_date) %> | 17 |<%= link_to expense.name, expense_edit_urlpath(@contract, expense) %> | 18 |<%= number_to_currency(expense.amount) %> | 19 |<%= link_to expense.contract.getDisplayTitle, { :controller => 'contracts', :action => 'show', :project_id => @contract.project.identifier, :id => @contract.id } %> | 20 |<%= expense.issue.nil? ? l(:text_na) : link_to("#{expense.issue.tracker.name} ##{expense.issue_id}: #{expense.issue.subject}", issue_path(expense.issue)) %> | 21 |<%= expense.description %> | 22 |23 | <% if !@contract.is_locked && User.current.allowed_to?(:edit_expenses, @project) %> 24 | <%= link_to image_tag("edit.png"), expense_edit_urlpath(@contract, expense), :title => l(:label_edit) %> 25 | <% end %> 26 | | 27 |28 | <% if !@contract.is_locked && User.current.allowed_to?(:delete_expenses, @project) %> 29 | <%= link_to image_tag("delete.png"), { :controller => 'contracts_expenses', :action => 'destroy', 30 | :project_id => @contract.project.identifier, :id => expense.id }, 31 | :title => l(:label_delete), :data => {:confirm => l(:text_are_you_sure_delete_expense)}, :method => :delete %> 32 | <% end %> 33 | | 34 | 35 |
<%= l(:label_name) %> | 17 |<%= l(:label_recurring) %> | 18 |<%= l(:label_agreed_on) %> | 19 |<%= l(:label_date_period) %> | 20 |<%= l(:label_purchased) %> | 21 | <% if (@project != nil) && (User.current.allowed_to?(:view_time_entries, @project)) && (User.current.allowed_to?(:view_hourly_rate, @project)) %> 22 |<%= l(:label_expenses) %> | 23 |<%= l(:label_hours_worked) %> | 24 |<%= l(:label_effective_rate) %> | 25 | <% end %> 26 |<%= l(:label_contract) %> | 27 |<%= l(:label_invoice) %> | 28 |29 | | 30 | | 31 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
42 | <%= link_to contract.getDisplayTitle, { :controller => 'contracts', :action => 'show', :project_id => contract.project.identifier, :id => contract.id } %> 43 | | 44 |45 | <% if contract.not_recurring? %> 46 | <%= I18n.t("recurring_frequencies.#{contract.recurring_frequency}") %> 47 | <% else %> 48 | <%= link_to I18n.t("recurring_frequencies.#{contract.recurring_frequency}"), { :controller => 'contracts', :action => 'series', :project_id => contract.project.identifier, :id => contract.series_id }, :method => :get, :class => 'blue' %> 49 | <% end %> 50 | | 51 |52 | <% if contract.agreement_date != nil %> 53 | <%= format_date(contract.agreement_date) %> 54 | <% else %><%= l(:text_agreement_pending) %> 55 | <% end %> 56 | | 57 |58 | <% if contract.start_date != nil %> 59 | <%= format_date(contract.start_date) + " - " %> 60 | <% if contract.end_date != nil %> 61 | <%= format_date(contract.end_date) %> 62 | <% else %> 63 | <%= l(:text_no_end_date) %> 64 | <% end %> 65 | <% end %> 66 | | 67 |<%= number_to_currency(contract.purchase_amount) %> | 68 | <% if (@project != nil) && (User.current.allowed_to?(:view_time_entries, @project)) && (User.current.allowed_to?(:view_hourly_rate, @project)) %> 69 |<%= number_to_currency(contract.expenses_total) %> | 70 |<%= number_with_precision contract.smart_hours_spent, :precision => 2 %> | 71 |<%= number_to_currency(contract.effective_rate) %><%= l(:text_hour) %> | 72 | <% end %> 73 |74 | <% if !contract.contract_url.blank? %> 75 | <%= link_to image_tag("/images/files/text.png"), contract.contract_url %> 76 | <% end %> 77 | | 78 |79 | <% if !contract.invoice_url.blank? %> 80 | <%= link_to image_tag("/images/files/text.png"), contract.invoice_url %> 81 | <% end %> 82 | | 83 |84 | <% if (User.current.allowed_to?(:edit_contracts, contract.project)) %> 85 | <% if contract.is_locked %> 86 | <%= link_to image_tag("unlock.png"), { :controller => 'contracts', :action => 'lock', :project_id => contract.project.identifier, :id => contract.id, :lock => false, :view => 'index' }, :method => :put, :title => l(:label_unlock) %> 87 | <% else %> 88 | <%= link_to image_tag("locked.png"), { :controller => 'contracts', :action => 'lock', :project_id => contract.project.identifier, :id => contract.id, :lock => true, :view => 'index' }, :method => :put, :title => l(:label_lock) %> 89 | <% end %> 90 | <% end %> 91 | | 92 |93 | <% if !contract.is_locked && User.current.allowed_to?(:edit_contracts, contract.project) %> 94 | <%= link_to image_tag("edit.png"), { :controller => 'contracts', :action => 'edit', :project_id => contract.project.identifier, :id => contract.id }, :title => l(:label_edit) %> 95 | <% end %> 96 | | 97 |98 | <% if !contract.is_locked && (User.current.allowed_to?(:delete_contracts, contract.project)) %> 99 | <%= link_to image_tag("delete.png"), { :controller => 'contracts', :action => 'destroy', :project_id => contract.project.identifier, :id => contract.id }, :method => :delete, 100 | :title => l(:label_delete), :data => {:confirm => l(:text_are_you_sure_delete_title, contract.getDisplayTitle)} %> 101 | <% end %> 102 | | 103 |
<%= f.select :contract_type, Contract::CONTRACT_TYPES.map { |s| [I18n.t("contract_types.#{s}"), s] }, {}, {:disabled => action == 'update'} %>
8 |
9 |
> 11 | <%= f.select :recurring_frequency, Contract::DROPDOWN_RECURRING_FREQUENCIES.map { |s| [I18n.t("recurring_frequencies.#{s}"), s] }, {}, {:disabled => action == 'update'} %> 12 | <% if action == "update" and @contract.contract_type == 'recurring' %> 13 | <%= link_to l(:label_view_series), { :controller => 'contracts', :action => 'series', :project_id => @project.identifier, :id => @contract.series_id }, :method => :get, :class => 'blue' %> 14 | <% if !@contract.completed? %> 15 | <%= link_to l(:label_cancel_recurring), { :controller => 'contracts', :action => 'cancel_recurring', :project_id => @project.identifier, :id => @contract.id }, :method => :put, :class => 'red', :data => {:confirm => l(:text_are_you_sure_stop_recurring, @contract.getDisplayTitle)} %> 16 | <% end %> 17 | <% end %> 18 |
19 |<%= f.text_field :start_date, :size => 12, :required => true, :disabled => @contract.completed? %><%= calendar_for('contract_start_date') %>
20 |> 21 | <%= f.text_field :end_date, :size => 12 %><%= calendar_for('contract_end_date') %>
22 |<%= f.text_field :agreement_date, :size => 12 %><%= calendar_for('contract_agreement_date') %>
23 |<%= f.text_field :purchase_amount, :size => 10, :required => true %>
24 |<%= f.text_field :hourly_rate, :size => 10, :required => true %> 25 | ><%= l(:text_hourly_rate_helper) %> 26 |
27 |<%= f.text_field :title, :size => 25, :required => false %> 28 | <%= l(:text_contract_title_helper) %>
29 |<%= f.select :category_id, contract_category_collection_for_select_options(@contract), :required => false %> 30 | <%= l(:text_contract_category_helper) %>
31 |<%= f.text_area :description, { :cols => 50, :rows => 3 } %>
32 |<%= f.text_field :contract_url, :size => 50 %>
33 |<%= f.text_field :invoice_url, :size => 50 %>
34 |<%= f.submit msg %>
49 |<%= l(:label_date) %> | 5 |<%= l(:label_member) %> | 6 |<%= l(:label_activity) %> | 7 |<%= l(:label_issue) %> | 8 |<%= l(:label_comments) %> | 9 | <% if User.current.allowed_to?(:view_time_entries, @project) || not(@contract.is_fixed_price) %> 10 |<%= l(:label_hours) %> | 11 | <% end %> 12 |13 | | 14 | |
---|---|---|---|---|---|---|---|
<%= format_date(time_entry.spent_on) %> | 19 |<%= link_to time_entry.user.name, user_path(time_entry.user) %> | 20 |<%= time_entry.activity.name %> | 21 |<%= link_to time_entry.issue, issue_path(time_entry.issue) unless !time_entry.issue %> | 22 |<%= time_entry.comments %> | 23 | <% if User.current.allowed_to?(:view_time_entries, @project) || not(@contract.is_fixed_price) %> 24 |<%= time_entry.hours %> | 25 | <% end %> 26 |27 | <% if !@contract.is_locked && User.current.allowed_to?(:edit_time_entries, @project) -%> 28 | <%= link_to image_tag("edit.png"), edit_time_entry_path(time_entry), :title => l(:label_edit) %> 29 | <% end %> 30 | | 31 |32 | <% if !@contract.is_locked && User.current.allowed_to?(:edit_time_entries, @project) -%> 33 | <%= link_to image_tag("delete.png"), time_entry_path(time_entry), 34 | :title => l(:label_delete), :data => {:confirm => l(:text_are_you_sure_delete_time_entry)}, :method => :delete %> 35 | <% end %> 36 | | 37 |
<%= l(:label_add_to_contract) %> | 8 |<%= l(:label_date) %> | 9 |<%= l(:label_current_contract) %> | 10 |<%= l(:label_member) %> | 11 |<%= l(:label_activity) %> | 12 |<%= l(:label_issue) %> | 13 |<%= l(:label_comments) %> | 14 |<%= l(:label_hours) %> | 15 |16 | | 17 | |
---|---|---|---|---|---|---|---|---|---|
<%= check_box_tag("time_entries[#{time_entry.id}]", "1", if_checked, :disabled => if_disabled) %> | 24 |<%= format_date(time_entry.spent_on) %> | 25 |<%= time_entry.contract.getDisplayTitle unless time_entry.contract == nil %> | 26 |<%= link_to time_entry.user.name, user_path(time_entry.user) %> | 27 |<%= time_entry.activity.name %> | 28 |<%= link_to time_entry.issue, issue_path(time_entry.issue) unless !time_entry.issue %> | 29 |<%= time_entry.comments %> | 30 |<%= time_entry.hours %> | 31 |<%= link_to image_tag("edit.png"), edit_time_entry_path(time_entry), :title => l(:label_edit) %> | 32 |33 | <%= link_to image_tag("delete.png"), time_entry_path(time_entry), 34 | :title => l(:label_delete), 35 | :data => {:confirm => l(:text_are_you_sure_delete_time_entry)}, 36 | :method => :delete %> 37 | | 38 |
<%= @contract.description %> |
30 | |||||||||||||||||||||||||||||
33 |
|
93 | <% if User.current.allowed_to?(:view_time_entries, @project) || not(@contract.is_fixed_price) %>
94 |
95 |
|
128 | <% end %>
129 |
<%= f.text_field :name, :required => true %>
25 |26 | <%= f.text_field :expense_date, :size => 12, :required => true %> 27 | <%= calendar_for('contracts_expense_expense_date') %> 28 |
29 |<%= f.text_field :amount, :size => 6, :required => true %>
30 |31 | 32 | <%-# If no default contract, then it indicates this is a new expense. Populate dropdown with last created contract -%> 33 | <% if @contracts_expense.contract_id == nil %> 34 | <% @contracts_expense.contract_id = @project.contracts.maximum(:id) %> 35 | <% end %> 36 | <% @contracts = @project.contracts_for_all_ancestor_projects %> 37 | <%= select("contracts_expense", "contract_id", @contracts.collect { |c| [ c.getDisplayTitle, c.id ] }, {:include_blank => l(:label_select_contract)}) %> 38 |
39 |<%= f.text_field :issue_id, :size => 5 %>
40 |<%= f.text_field :description, :size => 50 %>
41 |2 | <%= check_box_tag "settings[automatic_contract_creation]", true, @settings['automatic_contract_creation'] %> 3 | <%= label_tag('settings_automatic_contract_creation', "Automatically create new hourly contract when time log entry exceeds current contract: ") %> 4 |
5 | -------------------------------------------------------------------------------- /assets/images/true.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upgradeya/redmine-contracts-with-time-tracking-plugin/9421ed6e417aef9e917f9ce95294677ad1b62067/assets/images/true.png -------------------------------------------------------------------------------- /assets/stylesheets/contracts.css: -------------------------------------------------------------------------------- 1 | .contracts-summary td{ 2 | background: #EEE; 3 | padding: 10px; 4 | border: 1px solid #E4E4E4; 5 | text-align: center; 6 | } 7 | 8 | .contracts-summary h3{ 9 | font-size: 1.2em; 10 | } 11 | 12 | .contracts-list, .time-entries-for-contract-list { 13 | width: 100%; 14 | } 15 | 16 | .bigbold{ 17 | font-size:1.5em; 18 | font-weight:bold; 19 | } 20 | 21 | .green{ 22 | color:#009b00; 23 | } 24 | 25 | .blue{ 26 | color:#0072ca; 27 | } 28 | 29 | p .red{ 30 | color: red; 31 | text-decoration: underline; 32 | font-size: 85%; 33 | } 34 | 35 | p .blue{ 36 | text-decoration: underline; 37 | font-size: 85%; 38 | } 39 | 40 | .contract-summary-td { 41 | width: 60%; 42 | padding-right: 10px; 43 | } 44 | 45 | .hours-summary-td { 46 | width: 35%; 47 | } 48 | 49 | fieldset#contractor_rates { 50 | width: 500px; 51 | } 52 | 53 | /***** Flash & error messages ****/ 54 | div.flash.contract { 55 | background: url(../images/true.png) 8px 5px no-repeat; 56 | background-color: #dfffdf; 57 | border-color: #9fcf9f; 58 | color: #005f00; 59 | } 60 | -------------------------------------------------------------------------------- /config/locales/bg.yml: -------------------------------------------------------------------------------- 1 | bg: 2 | project_module_contracts: Договори 3 | permission_view_all_contracts_for_project: Достъп до всички договори по проекта 4 | permission_view_contract_details: Достъп до детайли на договор 5 | permission_edit_contracts: Достъп до редактиране на договор 6 | permission_create_contracts: Право за създаване на договор 7 | permission_delete_contracts: Право за изтриване на договор 8 | permission_view_hourly_rate: Достъпо до тарифи 9 | permission_create_expenses: Право за създаване на разход 10 | permission_edit_expenses: Право за редакция на разход 11 | permission_delete_expenses: Право за изтриване на разход 12 | permission_view_expenses: Достъпо до разходи 13 | 14 | contracts: Договори 15 | label_contracts: Договори 16 | label_contract: Договор 17 | label_new_contract: Нов договор 18 | label_editing_contract: Редакция на договор 19 | label_add_time_entries: Въвеждане на време 20 | label_add_time_entries: Въвеждане на време (масово) 21 | label_add_expense: Добавяне на разход 22 | label_log_time: Добави време 23 | label_edit: Редакция 24 | label_delete: Изтриване 25 | label_view_contract: Виж договор 26 | label_view_invoice: Виж фактура 27 | label_date_period: Срок на валидност 28 | label_amount_purchased: Стойност на договора 29 | label_invoice: Фактури 30 | label_members: Членове 31 | label_hours: Часове 32 | label_contractor_rate: Тарифи 33 | label_billable_amount: Платима сума 34 | label_time_entries: Вписвания на време 35 | label_add_to_contract: Добави към договор 36 | label_date: Дата 37 | label_current_contract: Текущ договор 38 | label_member: Изпълнител 39 | label_activity: Дейност 40 | label_issue: Задача 41 | label_comments: Коментари 42 | label_apply: Приложи 43 | label_name: Име 44 | label_agreed_on: Дата на договора 45 | label_purchased: Обща сума 46 | label_remaining: Оставащ 47 | label_hours_worked: Изработени часове 48 | label_hours_left: "~ Оставащи часове" 49 | label_total_purchased: Обща стойност на всички договори 50 | label_total_remaining: Общо оставащо за всички договори 51 | label_or: "-или-" 52 | label_create_contract: Създай договор 53 | label_update_contract: Промени договор 54 | label_contract_empty: Няма договор 55 | label_select_contract: "[Избери договор]" 56 | label_save_expense: Запази разход 57 | label_expenses: Разходи 58 | label_description: Описание 59 | label_amount: Количество 60 | label_edit_expense: Редактирай разход 61 | label_new_expense: Нов разход 62 | 63 | text_are_you_sure_delete_title: "Сигурни ли сте, че искате да изтриете %{value}?" 64 | text_are_you_sure_delete_time_entry: "Сигурни ли сре, че искате да изтриете това време?" 65 | text_are_you_sure_delete_expense: "Сигурни ли сте, че искате да изтриете този разход?" 66 | text_time_exceeded_time_remaining: "Въведеното време надвишава оставащото време в настоящия договор с %{hours_over} часа.\nЗа да може да въведете време, моля въведете стойност по-малка от %{hours_remaining}." 67 | text_must_come_after_agreement_date: Тряба да е след датата на споразумението 68 | text_must_come_after_start_date: Трябва да е след началната дата 69 | text_contract_saved: Договорът е успешно запазен! 70 | text_contract_updated: Договорът беше променен успешно! 71 | text_contract_deleted: Договорът беше изтрит успешно 72 | text_hours_over_contract: "В момента надвишаване договореният лимит с %{hours_over} часа." 73 | text_invalid_rate: "Тарифата трябва да е 0 или по-висока." 74 | text_invalid_issue_id: "Невалиден ID" 75 | text_expense_created: 'Разходът беше създаден успешно.' 76 | text_expense_updated: 'Разходът беше променен успешно.' 77 | text_na: 'N/A' 78 | 79 | field_contract: Договор 80 | field_title: Заглавие 81 | field_description: Описание 82 | field_agreement_date: Дата на договора 83 | field_start_date: Начална дата 84 | field_end_date: Крайна дата 85 | field_purchase_amount: Закупено количество 86 | field_hourly_rate: Цена на час 87 | field_contract_url: URL на договора 88 | field_invoice_url: URL на фактурата 89 | 90 | field_expense_name: Име 91 | field_expense_date: Дата на разхода 92 | field_amount: Сума 93 | field_expense_contract: Договор 94 | field_issue: Задача (ID) -------------------------------------------------------------------------------- /config/locales/cs.yml: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | cs: 3 | project_module_contracts: Objednávky 4 | permission_view_all_contracts_for_project: Zobrazit všechny objednávky pro projekt 5 | permission_view_contract_details: Zobrazit detail objednávky 6 | permission_edit_contracts: Updavit objednávky 7 | permission_create_contracts: Vytvořit objednávky 8 | permission_delete_contracts: Smazat objednávky 9 | permission_view_hourly_rate: Ukázat hodinovou sazbu 10 | permission_create_expenses: Vytvořit náklady 11 | permission_edit_expenses: Updavit náklady 12 | permission_delete_expenses: Smazat náklady 13 | permission_view_expenses: Ukázat náklady 14 | 15 | contracts: Objednávky 16 | label_contracts: Objednávky 17 | label_contract: Objednávky 18 | label_new_contract: Nová objednávka 19 | label_editing_contract: Editovat objednávku 20 | label_add_time_entries: Přidat časový záznam 21 | label_add_time_entries: Přidat časový záznam hromadně 22 | label_add_expense: Přidat náklad 23 | label_log_time: Přidat čas 24 | label_edit: Upravit 25 | label_delete: Smazat 26 | label_view_contract: Ukázat objednávku 27 | label_view_invoice: Ukázat fakturu 28 | label_date_period: Časový rozsah 29 | label_amount_purchased: Suma objednávky 30 | label_invoice: Faktura 31 | label_members: Členové 32 | label_hours: Hodiny 33 | label_contractor_rate: Sazba vývojáře 34 | label_contractors_rates: Sazby vývojářů 35 | label_billable_amount: Účtovatelná částka 36 | label_time_entries: Časové záznamy 37 | label_add_to_contract: Přidat ke smlouvě 38 | label_date: Datum 39 | label_current_contract: Aktuální objednávka 40 | label_member: Člen 41 | label_activity: Aktivita 42 | label_issue: Úkol 43 | label_comments: Komentáře 44 | label_apply: Aplikovat 45 | label_name: Název 46 | label_agreed_on: Odsouhlaseno 47 | label_purchased: Objednáno 48 | label_remaining: Zbývá 49 | label_hours_worked: hodin odpracováno 50 | label_hours_left: "~hodin zbývá" 51 | label_total_purchased: Celkem všechny objednávky 52 | label_total_remaining: Celkem zbývá u všech objednávek 53 | label_or: "-nebo-" 54 | label_create_contract: Vytvořit objednávku 55 | label_update_contract: Upravit objednávku 56 | label_contract_empty: Žádná objednávka 57 | label_select_contract: "[Vyberte objednávku]" 58 | label_save_expense: Uložit náklad 59 | label_expenses: Náklady 60 | label_description: Popis 61 | label_amount: Suma 62 | label_edit_expense: Upravit náklad 63 | label_new_expense: Nový náklad 64 | label_lock: Uzamknout 65 | label_unlock: Odemknout 66 | label_show_locked_contracts: Ukázat uzamčené obednávky 67 | 68 | text_contract_list: Seznam objednávek 69 | text_are_you_sure_delete_title: "Opravdu chcete smazat %{value}?" 70 | text_are_you_sure_delete_time_entry: "Opravdu chcete smazat časový záznam?" 71 | text_are_you_sure_delete_expense: "Opravdu chcete smazat tento náklad?" 72 | text_time_exceeded_time_remaining: "Tento časový záznam přesáhl čas přípustný pro objednávku: %{hours_over} hodin.\nAbyste se vlezli do objednávky, upravte čas, aby nepřesahoval více než %{hours_remaining} hodin." 73 | text_must_come_after_agreement_date: musí být až po datu odsouhlasení 74 | text_must_come_after_start_date: musí být až po datu zahájení 75 | text_contract_saved: Objednávka úspěšně uložena! 76 | text_contract_updated: Objednávka úspěšně aktualizována! 77 | text_contract_deleted: Objednávka úspěšně smazána! 78 | text_hours_over_contract: "Nyní jste %{hours_over} hodin přes limit objednávky." 79 | text_split_time_entry_saved: Váš zadaný čas byl rozdělený na 2 objednávky. Druhá část byla přiřazena nově založené čekající smlouvě. 80 | text_one_time_entry_saved: "Byla založena nová objednávka a váš čas byl přiřazen k ní, protože už se nevešel do původní dohody o času objednávky." 81 | text_hours_too_large: není platný. Hodnota převyšuje čas objednávky a nevleze se ani do nové objednávky. 82 | text_invalid_rate: "Hodinová sazba vývojáře musí být kladné číslo." 83 | text_invalid_issue_id: "není platné ID úkolu." 84 | text_invalid_hours: "není platné. Objednávka %{title} má jen %{hours} zbývajících hodin. Poproste správce o povolení automatického založení nové objednávky při přesáhnutí času." 85 | text_second_time_entry_failure: "něco se nepovedlo, druhý záznam po rozdělení času se nepovedlo zaspsat. %{error}" 86 | text_auto_contract_failure: "něco se nepovedlo. Nová objednávka po rozdělení času nebyla uložena. %{error}" 87 | text_expense_created: 'Náklad přidán.' 88 | text_expense_updated: 'Náklad aktualizován.' 89 | text_agreement_pending: 'očekává se schválení' 90 | text_no_end_date: 'nemá datum dokončení' 91 | text_na: 'N/A' 92 | text_contract_locked: Objednávka byla úspěšně uzamčena. 93 | text_contract_unlocked: Objednávka byla úspěšně odemčena. 94 | text_locked_contract_cache_updated: Tento záznam patří k uzamčené objednávce. Hodnoty byly aktualizovány a objednávka upravena. 95 | text_expenses_uneditable: "Nelze editovate náklady objednávky, ta je uzamčena." 96 | text_expense_deleted: Náklad smazán. 97 | text_apply: Uložit 98 | text_previous_id: "Poslední objednávka v tomto projektu měla číslo %{previous_id}" 99 | text_contract_category_helper: "[Volitelné] Používá se k přehlednější identifikaci objednávky. Spravuje se přes enumeraci." 100 | 101 | field_contract: Objednávka 102 | field_project_contract: ID Objednávky 103 | field_description: Popis 104 | field_agreement_date: Datum odsouhlasení 105 | field_start_date: Datum zahájení 106 | field_end_date: Datum ukončení 107 | field_purchase_amount: Suma objednávky 108 | field_hourly_rate: Hodinová sazba 109 | field_contract_url: URL smlouvy 110 | field_invoice_url: URL faktury 111 | 112 | field_expense_name: Název 113 | field_expense_date: Datum 114 | field_amount: Suma 115 | field_expense_contract: Objednávka 116 | field_issue: Úkol (ID) 117 | 118 | enumeration_contract_categories: Kategorie objednávek 119 | -------------------------------------------------------------------------------- /config/locales/de.yml: -------------------------------------------------------------------------------- 1 | # German strings go here for Rails i18n 2 | de: 3 | project_module_contracts: Verträge 4 | permission_view_all_contracts_for_project: Alle Verträge für Projekt anzeigen 5 | permission_view_contract_details: Vertragsdetails anzeigen 6 | permission_edit_contracts: Verträge bearbeitn 7 | permission_create_contracts: Verträge erstellen 8 | permission_delete_contracts: Verträge öschen 9 | permission_view_hourly_rate: Stundensatz 10 | permission_create_expenses: Ausgabe erstellen 11 | permission_edit_expenses: Ausgaben bearbeiten 12 | permission_delete_expenses: Ausgaben löschen 13 | permission_view_expenses: Ausgaben anzeigen 14 | 15 | 16 | contracts: Verträge 17 | label_contracts: Verträge 18 | label_contract: Vertrag 19 | label_new_contract: Neuer Vertrag 20 | label_editing_contract: Vertrag bearbeiten 21 | label_add_time_entries: Zeiten zuordnen 22 | label_add_expense: Ausgabe hinzufügen 23 | label_log_time: Zeit stoppen 24 | label_edit: bearbeiten 25 | label_delete: löschen 26 | label_view_contract: Vertrag anzeigen 27 | label_view_invoice: Rechnung anzeigen 28 | label_date_period: Vertragszeitraum 29 | label_amount_purchased: bezahlter Betrag 30 | label_invoice: Rechnung 31 | label_members: Mitglieder 32 | label_hours: Stunden 33 | label_contractor_rate: Preis pro Stunde 34 | label_billable_amount: Abrechenbarer Betrag 35 | label_time_entries: gebuchte Zeiten 36 | label_add_to_contract: dem Vertrag zuordnen 37 | label_date: Datum 38 | label_current_contract: aktueller Vertrag 39 | label_member: Mitglied 40 | label_activity: Aktivität 41 | label_issue: Ticket 42 | label_comments: Kommentare 43 | label_apply: Anwenden 44 | label_name: Name 45 | label_agreed_on: abgeschlossen am 46 | label_purchased: vereinbart 47 | label_remaining: verbleibend 48 | label_hours_worked: gearbeitete Stunden 49 | label_hours_left: "~verbleibende Stunden" 50 | label_total_purchased: vereinbart für alle Verträge 51 | label_total_remaining: verbleibend für alle Verträge 52 | label_or: "-oder-" 53 | label_create_contract: Vertrag anlegen 54 | label_update_contract: Vertrag berarbeiten 55 | label_contract_empty: kein Vertrag 56 | label_select_contract: "[Vertrag auswählen]" 57 | label_save_expense: Ausgabe speichern 58 | label_expenses: Ausgaben 59 | label_description: Beschreibung 60 | label_amount: Betrag 61 | label_edit_expense: Ausgabe bearbeiten 62 | label_new_expense: Neue Ausgabe 63 | 64 | text_are_you_sure_delete_title: "Sind sie sicher das Sie %{value} löschen möchten?" 65 | text_are_you_sure_delete_time_entry: "Sind Sie sicher, dass sie diesen Zeiteintrag löschen möchten?" 66 | text_are_you_sure_delete_expense: "Sind Sie sicher, dass sie diese Ausgabe löschen wollen?" 67 | text_time_exceeded_time_remaining: "Dieser Zeiteintrag überschreitet die verbeleibende Zeit im Vertrag um %{hours_over} Stunden.\nUm im Vertragsrahmen zu bleiben ändern Sie bitte den Eintrag auf nicht mehr als %{hours_remaining} Stunden." 68 | text_must_come_after_agreement_date: muss nach dem Vertragsdatum liegen 69 | text_must_come_after_start_date: muss nach dem Startdatum des Vertrages liegen 70 | text_contract_saved: Der Vertrag wurde erfolgreich gespeichert! 71 | text_contract_updated: Der Vertrag wurde erfolgreich aktualisiert! 72 | text_contract_deleted: Der Vertrag wurde erfolgreich gelöscht 73 | text_hours_over_contract: "Das vereinbarte Stundenkontingent wird um %{hours_over} Stunden überschritten" 74 | text_invalid_rate: "Preis pro Stunde muss größer oder gleich Null sein." 75 | text_invalid_issue_id: "ist keine gültige Ticket-ID" 76 | text_expense_created: "Ausgabe wurde erfolgreich erstellt." 77 | text_expense_updated: "Ausgabe wurde erfolgreich aktualisiert." 78 | text_na: "keine Angabe" 79 | 80 | field_contract: Vertrag 81 | field_title: Titel 82 | field_description: Beschreibung 83 | field_agreement_date: Vertragsdatum 84 | field_start_date: Beginn 85 | field_end_date: Ende 86 | field_purchase_amount: Summe 87 | field_hourly_rate: Stundensatz 88 | field_contract_url: Vertrags-URL 89 | field_invoice_url: Rechnungs-URL 90 | 91 | field_expense_name: Name 92 | field_expense_date: Datum für Ausgabe 93 | field_amount: Betrag 94 | field_expense_contract: Vertrag 95 | field_issue: Ticket (ID) 96 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # English strings go here for Rails i18n 2 | en: 3 | contract_types: 4 | hourly: "Hourly" 5 | fixed: "Fixed" 6 | recurring: "Fixed-Recurring" 7 | recurring_frequencies: 8 | monthly: "Monthly" 9 | yearly: "Yearly" 10 | completed: "Completed" 11 | not_recurring: "No" 12 | project_module_contracts: Contracts 13 | permission_view_all_contracts_for_project: View all contracts for project 14 | permission_view_contract_details: View contract details 15 | permission_edit_contracts: Edit contracts 16 | permission_create_contracts: Create contracts 17 | permission_delete_contracts: Delete contracts 18 | permission_view_hourly_rate: View hourly rate 19 | permission_create_expenses: Create expenses 20 | permission_edit_expenses: Edit expenses 21 | permission_delete_expenses: Delete expenses 22 | permission_view_expenses: View expenses 23 | 24 | contracts: Contracts 25 | label_contracts: Contracts 26 | label_series_title: "Recurring Contracts in Series #%{series_id}" 27 | label_contract: Contract 28 | label_contractors_rates: Contractor Rates 29 | label_new_contract: New Contract 30 | label_editing_contract: Editing Contract 31 | label_add_time_entries: Add Time Entries 32 | label_add_time_entries: Bulk Assign Time Entries 33 | label_add_expense: Add Expense 34 | label_log_time: Log time 35 | label_edit: Edit 36 | label_delete: Delete 37 | label_view_contract: View Contract 38 | label_view_invoice: View Invoice 39 | label_date_period: Date Range 40 | label_amount_purchased: Amount Purchased 41 | label_invoice: Invoice 42 | label_members: Members 43 | label_hours: Hours 44 | label_contractor_rate: Contractor Rate 45 | label_billable_amount: Billable Amount 46 | label_time_entries: Time Entries 47 | label_hourly_priced_contracts: Hourly Contracts 48 | label_effective_rate: Effective Rate 49 | label_fixed_priced_contracts: Fixed Price Contracts 50 | label_add_to_contract: Add to Contract 51 | label_date: Date 52 | label_current_contract: Current Contract 53 | label_member: Member 54 | label_activity: Activity 55 | label_issue: Issue 56 | label_cost: Cost 57 | label_comments: Comments 58 | label_apply: Apply 59 | label_name: Name 60 | label_agreed_on: Agreed On 61 | label_purchased: Purchased 62 | label_remaining: Remaining 63 | label_hours_worked: Hours Worked 64 | label_hours_left: "~Hours Left" 65 | label_total_purchased: Total Purchased for All Contracts 66 | label_total_remaining: Total Remaining for All Contracts 67 | label_total_fixed: Total Purchased for Fixed Contracts 68 | label_total_series: Total Purchased in Series 69 | label_total_hourly: Total Purchased for Hourly Contracts 70 | label_remaining_hourly: Total Remaining for Hourly Contracts 71 | label_or: "-or-" 72 | label_create_contract: Create Contract 73 | label_update_contract: Update Contract 74 | label_contract_empty: No Contract 75 | label_select_contract: "[Select a contract]" 76 | label_save_expense: Save Expense 77 | label_expenses: Expenses 78 | label_summary: Summary 79 | label_description: Description 80 | label_amount: Amount 81 | label_edit_expense: Edit Expense 82 | label_new_expense: New Expense 83 | label_lock: Lock 84 | label_unlock: Unlock 85 | label_show_locked_contracts: Show locked contracts 86 | label_show_only_active_recurring: "Show only \"Active\" recurring contracts" 87 | label_recurring: Recurring 88 | label_cancel_recurring: Stop Recurring 89 | label_view_series: View Series 90 | label_tooltips: Redmine Contract Help 91 | label_contract_types: Contract Types 92 | 93 | text_contract_list: Contract List 94 | text_are_you_sure_delete_title: "Are you sure you want to delete %{value}?" 95 | text_are_you_sure_stop_recurring: "Are you sure you want to stop this recurring contract?" 96 | text_are_you_sure_delete_time_entry: "Are you sure you want to delete this time entry?" 97 | text_are_you_sure_delete_expense: "Are you sure you want to delete this expense?" 98 | text_time_exceeded_time_remaining: "This time entry exceeded the time remaining for the contract by %{hours_over}.\nTo stay within the contract, please edit the time entry to be no more than %{hours_remaining}." 99 | text_must_come_after_agreement_date: must come on or after the agreement date 100 | text_must_come_after_start_date: must come after the start date 101 | text_contract_saved: Contract successfully saved! 102 | text_contract_updated: Contract successfully updated! 103 | text_contract_deleted: Contract successfully deleted 104 | text_hours_over_contract: "You are now %{hours_over} over the contract's limit." 105 | text_split_time_entry_saved: Your time entry has been split into two entries because it exceeded the remaining time on the selected contract. The second entry has been added to a new pending contract. 106 | text_one_time_entry_saved: A new pending contract was created and your time entry was saved there because the contract you selected did not have any remaining time. 107 | text_hours_too_large: is invalid. The amount exceeds the time remaining plus the size of a new contract. 108 | text_invalid_rate: "Contractor rate must be zero or greater." 109 | text_invalid_issue_id: "is not a valid issue ID" 110 | text_invalid_hours: "is invalid. The contract %{title} only has %{hours} remaining. Ask your administrator to enable auto contract creation in contract settings." 111 | text_second_time_entry_failure: "something went wrong. The 2nd entry for this split time entry could not be saved. %{error}" 112 | text_auto_contract_failure: "something went wrong. The new contract for this split time entry could not be saved. %{error}" 113 | text_expense_created: 'Expense was successfully created.' 114 | text_expense_updated: 'Expense was successfully updated.' 115 | text_agreement_pending: 'agreement pending' 116 | text_no_end_date: 'no end date' 117 | text_na: 'N/A' 118 | text_contract_locked: Contract was successfully locked. 119 | text_contract_unlocked: Contract was successfully unlocked. 120 | text_locked_contract_cache_updated: This entry is associated with a locked contract. The contract's cached values have been updated to reflect your changes. 121 | text_expenses_uneditable: Expenses for this contract are not editable (contract is locked). 122 | text_expense_deleted: Expense deleted. 123 | text_apply: Apply 124 | text_previous_id: "The last contract created for this project used ID %{previous_id}" 125 | text_contract_category_helper: "[Optional] If title is not provided, the default title will use this category. These are managed by enumerations." 126 | text_contract_title_helper: "[Optional] Used to override the default title." 127 | text_hourly_rate_helper: "Helpful in determining how much to pay a contractor. Has nothing to do with the effective hourly rate." 128 | text_hour: / hr 129 | text_fixed_contract_help: Fixed contracts are ideal when you are being paid a fixed price. Fixed Price contracts will display under the fixed contracts tab on the Contracts List page. In fixed contracts, time entries will determine your effective hourly rate and will not subtract from the Purchase Amount. The hourly rate field simply helps you determine what to pay potential contractors that are working on the project. 130 | text_recurring_contract_help: Recurring contracts are ideal for subscriptions. Recurring contracts will display under the fixed contracts tab on the Contracts List page. They are identified under the "Recurring" column. Fixed Price Recurring contracts are just like fixed contracts except they will automatically create new contracts based on recurring schedule. 131 | text_hourly_contract_help: Hourly contracts are ideal when you are being paid by the hour. Hourly contracts will display under the hourly contracts tab on the Contract List page. In hourly contracts, time entries (at the contractors hourly rate) will subtract from the Purchase Amount. If selected in the Contract Plugin settings, you can have new contracts automatically be created when time log entry exceeds the current Purchase Amount. 132 | 133 | field_contract: Contract 134 | field_project_contract: Contract ID 135 | field_description: Description 136 | field_agreement_date: Agreement Date 137 | field_start_date: Start Date 138 | field_end_date: End Date 139 | field_purchase_amount: Purchase Amount 140 | field_hourly_rate: Hourly Rate 141 | field_contract_url: Contract URL 142 | field_invoice_url: Invoice URL 143 | field_is_fixed_price: Fixed Price 144 | field_contract_type: Contract Type 145 | field_recurring_frequency: Frequency 146 | 147 | field_expense_name: Name 148 | field_expense_date: Expense Date 149 | field_amount: Amount 150 | field_expense_contract: Contract 151 | field_issue: Issue (ID) 152 | 153 | enumeration_contract_categories: Contract categories 154 | -------------------------------------------------------------------------------- /config/locales/es.yml: -------------------------------------------------------------------------------- 1 | es: 2 | project_module_contracts: Contratos 3 | permission_view_all_contracts_for_project: Ver Todos los contratos por proyecto 4 | permission_view_contract_details: Ver detalles del contrato 5 | permission_edit_contracts: Editar contratos 6 | permission_create_contracts: Crear contratos 7 | permission_delete_contracts: Eliminar contratos 8 | permission_view_hourly_rate: Ver tarifa por hora 9 | permission_create_expenses: Crear gastos 10 | permission_edit_expenses: Editar gastos 11 | permission_delete_expenses: Eliminar gastos 12 | permission_view_expenses: Ver gastos 13 | 14 | contracts: Contratos 15 | label_contracts: Contratos 16 | label_contract: Contrato 17 | label_new_contract: Nuevo Contrato 18 | label_editing_contract: Editar Contrato 19 | label_add_time_entries: Añadir registro de tiempo 20 | label_add_time_entries: Asignación masiva de registro de tiempo 21 | label_add_expense: Añadir Gasto 22 | label_edit: Editar 23 | label_delete: Eliminar 24 | label_view_contract: Ver Contrato 25 | label_view_invoice: Ver Factura 26 | label_date_period: Rango de fechas 27 | label_amount_purchased: Importe contratado 28 | label_invoice: Factura 29 | label_members: Miembros 30 | label_hours: Horas 31 | label_contractor_rate: Tarifa Contratada 32 | label_billable_amount: Cantidad Facturable 33 | label_time_entries: Registros de tiepo 34 | label_add_to_contract: Añadir al Contrato 35 | label_date: Fecha 36 | label_current_contract: Contrato Actual 37 | label_member: Miembro 38 | label_activity: Actividad 39 | label_issue: Cuestión 40 | label_comments: Comentarios 41 | label_apply: Aplicar 42 | label_name: Nombre 43 | label_agreed_on: Convenido 44 | label_purchased: Contratado 45 | label_remaining: Pendiente 46 | label_hours_worked: Horas Trabajadas 47 | label_hours_left: Horas Restantes 48 | label_total_purchased: Total Contratado para Todos los Contratos 49 | label_total_remaining: Total Pendiente para Todos los Contratos 50 | label_or: "-o-" 51 | label_create_contract: Crear Contrato 52 | label_update_contract: Actualizar Contrato 53 | label_contract_empty: Contrato Vacio 54 | label_select_contract: "[Elige un Contrato]" 55 | label_save_expense: Guardar Gasto 56 | label_expenses: Gastos 57 | label_description: Descripción 58 | label_amount: Cantidad 59 | label_edit_expense: Editar Gasto 60 | label_new_expense: Nuevo Gasto 61 | 62 | text_are_you_sure_delete_title: "¿Estás seguro que quieres eliminar %{value}?" 63 | text_are_you_sure_delete_time_entry: "¿Estás seguro que quieres eliminar este registro?" 64 | text_are_you_sure_delete_expense: "¿Estás seguro que quieres eliminar este gasto?" 65 | text_time_exceeded_time_remaining: "Este registro de tiempo super el máximo de tiempo en %{hours_over}.\Para mantenerte en el contrato, edita este registro para que quede en %{hours_remaining}." 66 | text_must_come_after_agreement_date: debe ser tras la fecha aceptada 67 | text_must_come_after_start_date: debe venir después de la fecha de comienzo 68 | text_contract_saved: Contrato guarado correctamente! 69 | text_contract_updated: Contrato actualizado correctamente! 70 | text_contract_deleted: Contrato eliminado correctamente 71 | text_hours_over_contract: "Estás %{hours_over} horas por encioma del límite del contrato" 72 | text_invalid_rate: "La tarifa del contrato debe ser superior a cero." 73 | text_invalid_issue_id: "no es un ID de cuestión básico" 74 | text_expense_created: 'Gasto creado correctamente.' 75 | text_expense_updated: 'Gasto actualizado correctamente.' 76 | text_na: 'N/A' 77 | 78 | field_contract: Contrato 79 | field_title: Título 80 | field_description: Descripción 81 | field_agreement_date: Fecha del acuerdo 82 | field_start_date: Fecha de inicio 83 | field_end_date: Fecha de fin 84 | field_purchase_amount: Cantidad comprada 85 | field_hourly_rate: Tarifa horaria 86 | field_contract_url: URL del Contrato 87 | field_invoice_url: URL de la Factura 88 | 89 | field_expense_name: Nombre 90 | field_expense_date: Fecha de gastos 91 | field_amount: Cantidad 92 | field_expense_contract: Contrato 93 | field_issue: Cuestión (ID) 94 | -------------------------------------------------------------------------------- /config/locales/nl.yml: -------------------------------------------------------------------------------- 1 | # Dutch strings go here for Rails i18n 2 | nl: 3 | project_module_contracts: Contracten 4 | permission_view_all_contracts_for_project: Alle contracten voor project bekijken 5 | permission_view_contract_details: Contractdetails bekijken 6 | permission_edit_contracts: Contracten bewerken 7 | permission_create_contracts: Contracten aanmaken 8 | permission_delete_contracts: Contracten verwijderen 9 | permission_view_hourly_rate: Bekijk uurtarief 10 | permission_create_expenses: Kostenposten aanmaken 11 | permission_edit_expenses: Kostenposten bewerken 12 | permission_delete_expenses: Kostenposten verwijderen 13 | permission_view_expenses: Kostenposten bekijken 14 | 15 | contracts: Contracten 16 | label_contracts: Contracten 17 | label_contract: Contracten 18 | label_contractors_rates: Uurtarieven 19 | label_new_contract: Nieuw Contract 20 | label_editing_contract: Contract bewerken 21 | label_add_time_entries: Tijd registreren 22 | label_add_time_entries: Tijdregistraties in bulk toewijzen 23 | label_add_expense: Kostenpost toevoegen 24 | label_log_time: Tijd registreren 25 | label_edit: Bewerken 26 | label_delete: Verwijderen 27 | label_view_contract: Contract bekijken 28 | label_view_invoice: Factuur bekijken 29 | label_date_period: Datumbereik 30 | label_amount_purchased: Aantal aangeschaft 31 | label_invoice: Factuur 32 | label_members: Leden 33 | label_hours: Uren 34 | label_contractor_rate: Uurtarief 35 | label_billable_amount: Factureerbaar aantal 36 | label_time_entries: Tijdregistraties 37 | label_hourly_priced_contracts: Uurcontracten 38 | label_effective_rate: Effectieve uurtarieven 39 | label_fixed_priced_contracts: Vaste prijs contracten 40 | label_add_to_contract: Aan contract toevoegen 41 | label_date: Datum 42 | label_current_contract: Huidig contract 43 | label_member: Lid 44 | label_activity: Activiteit 45 | label_issue: Incident 46 | label_cost: Kosten 47 | label_comments: Commentaar 48 | label_apply: Toepassen 49 | label_name: Naam 50 | label_agreed_on: Akkoord op 51 | label_purchased: Gekocht 52 | label_remaining: Resterend 53 | label_hours_worked: Uren gewerkt 54 | label_hours_left: "~Uren resterend" 55 | label_total_purchased: Totaal gekocht voor alle contracten 56 | label_total_remaining: Totaal resterend voor alle contracten 57 | label_total_fixed: Totaal gekocht voor vaste prijs contracten 58 | label_total_hourly: Totaal gekocht voor uurcontracten 59 | label_remaining_hourly: Totaal resterend voor uurcontracten 60 | label_or: "-of-" 61 | label_create_contract: Contract aanmaken 62 | label_update_contract: Contract bijwerken 63 | label_contract_empty: Geen contract 64 | label_select_contract: "[Selecteer een contract]" 65 | label_save_expense: Kostenpost opslaan 66 | label_expenses: Kostenposten 67 | label_summary: Samenvatting 68 | label_description: Omschrijving 69 | label_amount: Aantal 70 | label_edit_expense: Kostenpost bewerken 71 | label_new_expense: Nieuwe kostenpost 72 | label_lock: Sluiten 73 | label_unlock: Openen 74 | label_show_locked_contracts: Toon gesloten contracten 75 | 76 | text_contract_list: Contractenoverzicht 77 | text_are_you_sure_delete_title: "Weet je zeker dat je %{value} wilt verwijderen?" 78 | text_are_you_sure_delete_time_entry: "Weet je zeker dat je deze tijdregistratie wilt verwijderen?" 79 | text_are_you_sure_delete_expense: "Weet je zeker dat je deze kostenpost wilt verwijderen?" 80 | text_time_exceeded_time_remaining: "Deze tijdregistatie overschrijdt de resterende tijd op het contract met %{hours_over} uren.\nPas de tijdregistratie aan zodat deze niet meer dan %{hours_remaining} is om binnen het contract te blijven." 81 | text_must_come_after_agreement_date: moet op of na de datum van akkoord zijn 82 | text_must_come_after_start_date: moet na de startdatum zijn 83 | text_contract_saved: Contract succesvol opgeslagen! 84 | text_contract_updated: Contract succesvol bijgewerkt! 85 | text_contract_deleted: Contract succesvol verwijderd 86 | text_hours_over_contract: "Je bent nu %{hours_over} uren over de limiet van het contract heen." 87 | text_split_time_entry_saved: Je tijdregistratie is gesplitst naar twee tijdregistraties, omdat het de resterende tijd op het geselecteerde contract overschreed. De tweede tijdregistratie is toegevoegd aan een nieuw contract in afwachting van akkoord. 88 | text_one_time_entry_saved: Een nieuw contract in afwachting van akkoord is aangemaakt en je tijdregistratie is opgeslagen, omdat het contract dat je hebt geselecteerd geen resterende tijd meer bevat. 89 | text_hours_too_large: is invalid. The amount exceeds the time remaining plus the size of a new contract. 90 | text_invalid_rate: "Uurtarief moet 0 of hoger zijn." 91 | text_invalid_issue_id: "is een ongeldig incident ID" 92 | text_invalid_hours: "is ongeldig. Het contract %{title} heeft slechts %{hours} resterende uren. Vraag je beheerder om het automatisch aanmaken van contracten in te schakelen in de contract instellingen." 93 | text_second_time_entry_failure: "er is iets fout gegaan. De tweede tijdregistratie voor deze gesplitste tijdregistratie kon niet worden opgeslagen. %{error}" 94 | text_auto_contract_failure: "er is iets fout gegaan. Het nieuwe contract voor deze gesplitste tijdregistratie kon niet worden opgeslagen. %{error}" 95 | text_expense_created: 'Kostenpost is succesvol aangemaakt.' 96 | text_expense_updated: 'Kostenpost is succesvol bijgewerkt.' 97 | text_agreement_pending: 'wacht op akkoord' 98 | text_no_end_date: 'geen einddatum' 99 | text_na: 'n.v.t.' 100 | text_contract_locked: Contract is succesvolg gesloten. 101 | text_contract_unlocked: Contract is succesvol geopend. 102 | text_locked_contract_cache_updated: Deze tijdregistratie is gekoppeld met een gesloten contract. De gecachte waarden van het contract zijn bijgewerkt om je wijzigingen zichtbaar te maken. 103 | text_expenses_uneditable: Kostenposten voor dit contract zijn niet te bewerken (contract is gesloten). 104 | text_expense_deleted: Kostenpost verwijderd. 105 | text_apply: Toepassen 106 | text_previous_id: "Het laatste contract dat is aangemaakt voor dit project heeft ID %{previous_id}" 107 | text_contract_category_helper: "[Optioneel] Gebruikt om contracten makkelijker te identificeren door de titel te varieren. Deze worden beheerd als enumeraties." 108 | text_contract_title_helper: "[Optioneel] Gebruikt om de standaardtitel te overschrijven." 109 | text_hour: / u 110 | 111 | field_contract: Contract 112 | field_project_contract: Contract ID 113 | field_description: Omschrijving 114 | field_agreement_date: Datum akkoord 115 | field_start_date: Startdatum 116 | field_end_date: Einddatum 117 | field_purchase_amount: Gekocht aantal 118 | field_hourly_rate: Uurtarief 119 | field_contract_url: Contract URL 120 | field_invoice_url: Factuur URL 121 | field_is_fixed_price: Vaste prijs 122 | 123 | field_expense_name: Naam 124 | field_expense_date: Datum kostenpost 125 | field_amount: Aantal 126 | field_expense_contract: Contract 127 | field_issue: Incident (ID) 128 | 129 | enumeration_contract_categories: Contract categorieën 130 | -------------------------------------------------------------------------------- /config/locales/no.yml: -------------------------------------------------------------------------------- 1 | # Norwegian bokmål strings go here for Rails i18n 2 | "no": 3 | project_module_contracts: Kontrakter 4 | permission_view_all_contracts_for_project: Vis alle kontrakter for prosjektet 5 | permission_view_contract_details: Vis kontraktdetaljer 6 | permission_edit_contracts: Endre kontrakter 7 | permission_create_contracts: Lag kontrakter 8 | permission_delete_contracts: Slett kontrakter 9 | permission_view_hourly_rate: Vis timepris 10 | permission_create_expenses: Lag utgifter 11 | permission_edit_expenses: Endre utgifter 12 | permission_delete_expenses: Slett utgifter 13 | permission_view_expenses: Vis utgifter 14 | 15 | contracts: Kontrakter 16 | label_contracts: Kontrakter 17 | label_contract: Kontrakt 18 | label_new_contract: Ny kontrakt 19 | label_editing_contract: Endre kontrakt 20 | label_add_time_entries: Legg til tid brukt 21 | label_add_time_entries: Legg til flere tilfeller av tid brukt 22 | label_add_expense: Legg til utgift 23 | label_log_time: Logg tid 24 | label_edit: Endre 25 | label_delete: Slett 26 | label_view_contract: Vis kontrakt 27 | label_view_invoice: Vis faktura 28 | label_date_period: Dato-område 29 | label_amount_purchased: Beløp kjøpt 30 | label_invoice: Faktura 31 | label_members: Medlemmer 32 | label_hours: Timer 33 | label_contractor_rate: Kontraktørens timepris 34 | label_billable_amount: Fakturerbart beløp 35 | label_time_entries: Tid brukt 36 | label_add_to_contract: Legg til i kontrakt 37 | label_date: Dato 38 | label_current_contract: Gjeldende kontrakt 39 | label_member: Medlem 40 | label_activity: Aktivitet 41 | label_issue: Sak 42 | label_comments: Kommentarer 43 | label_apply: Bruk 44 | label_name: Navn 45 | label_agreed_on: Avtalt 46 | label_purchased: Kjøpt 47 | label_remaining: Gjenstår 48 | label_hours_worked: Timer arbeidet 49 | label_hours_left: "~Timer igjen" 50 | label_total_purchased: Totalt kjøpt for alle kontrakter 51 | label_total_remaining: Totalt gjenstående for alle kontrakter 52 | label_or: "-eller-" 53 | label_create_contract: Lag kontrakt 54 | label_update_contract: Oppdater kontrakt 55 | label_contract_empty: Ingen kontrakt 56 | label_select_contract: "[Velg kontrakt]" 57 | label_save_expense: Lagre utgift 58 | label_expenses: Utgifter 59 | label_description: Beskrivelse 60 | label_amount: Beløp 61 | label_edit_expense: Endre utgift 62 | label_new_expense: Ny utgift 63 | 64 | text_are_you_sure_delete_title: "Er du sikker på at du vil slette %{value}?" 65 | text_are_you_sure_delete_time_entry: "Er du sikker på at du vil slette denne tidsregistreringen?" 66 | text_are_you_sure_delete_expense: "Er du sikker på at du vil slette denne utgiften?" 67 | text_time_exceeded_time_remaining: "Denne tidsregistreringen overgår gjenstående tid for denne kontrakten med %{hours_over}.\nFor å være innenfor kontrakt, vennligst endre tidsregistreringen til ikke mer enn %{hours_remaining}." 68 | text_must_come_after_agreement_date: må komme på eller etter avtalt dato 69 | text_must_come_after_start_date: må komme etter startdatoen 70 | text_contract_saved: Kontrakten er lagret! 71 | text_contract_updated: Kontrakten er oppdatert! 72 | text_contract_deleted: Kontrakten er slettet 73 | text_hours_over_contract: "Du er nå %{hours_over} over kontraktens grense." 74 | text_invalid_rate: "Kontraktørens timepris må være null eller større." 75 | text_invalid_issue_id: "er ikke en gyldig saks-ID" 76 | text_expense_created: 'Utgiften er laget.' 77 | text_expense_updated: 'Utgiften er oppdatert.' 78 | text_na: 'Ikke tilgjengelig' 79 | 80 | field_contract: Kontrakt 81 | field_title: Tittel 82 | field_description: Beskrivelse 83 | field_agreement_date: Avtaledato 84 | field_start_date: Startdato 85 | field_end_date: Sluttdato 86 | field_purchase_amount: Kjøpsbeløp 87 | field_hourly_rate: Timepris 88 | field_contract_url: Kontrakt URL 89 | field_invoice_url: Faktura URL 90 | 91 | field_expense_name: Navn 92 | field_expense_date: Utgiftsdato 93 | field_amount: Beløp 94 | field_expense_contract: Kontrakt 95 | field_issue: Sak (ID) -------------------------------------------------------------------------------- /config/locales/pt-BR.yml: -------------------------------------------------------------------------------- 1 | pt-BR: 2 | project_module_contratos: Contratos 3 | permission_view_all_contratos_for_project: Visualizar todos contratos do projeto 4 | permission_view_contract_details: Exibir detalhes do contrato 5 | permission_edit_contratos: Editar contratos 6 | permission_create_contratos: Criar contratos 7 | permission_delete_contratos: Excluir contratos 8 | permission_view_hourly_rate: Visualizar taxa por hora 9 | permission_create_despesas: Inserir despesas 10 | permission_edit_despesas: Editar despesas 11 | permission_delete_despesas: Excluir despesas 12 | permission_view_despesas: Visualizar despesas 13 | 14 | contracts: Contratos 15 | label_contracts: Contratos 16 | label_contract: Contrato 17 | label_new_contract: Novo Contrato 18 | label_editing_contract: Editando Contrato 19 | label_add_time_entries: Inserir Registro de Tempo 20 | label_add_time_entries: Atribuir Registros de Tempo em Massa 21 | label_add_expense: Inserir Despesa 22 | label_log_time: Registro de Tempo 23 | label_edit: Editar 24 | label_delete: Excluir 25 | label_view_contract: Visualizar Contrato 26 | label_view_invoice: Visualizar Fatura 27 | label_date_period: Período 28 | label_amount_purchased: Valor Negociado 29 | label_invoice: Fatura 30 | label_members: Membros 31 | label_hours: Horas 32 | label_contractor_rate: Contratoor Taxa 33 | label_billable_amount: Valor Faturado 34 | label_time_entries: Registros de Tempo 35 | label_add_to_contract: Inserir to Contrato 36 | label_date: Data 37 | label_current_contract: Contrato Atual 38 | label_member: Membro 39 | label_activity: Atividade 40 | label_issue: Tarefa 41 | label_comments: Comentários 42 | label_apply: Aplicar 43 | label_name: Nome 44 | label_agreed_on: Aceite Em 45 | label_purchased: Vendido 46 | label_remaining: Disponível 47 | label_hours_worked: Horas Trabalhadas 48 | label_hours_left: "~Horas Disponíveis" 49 | label_total_purchased: Total Negociado para Todos Contratos 50 | label_total_remaining: Total Disponível para Todos Contratos 51 | label_or: "-ou-" 52 | label_create_contract: Inserir Contrato 53 | label_update_contract: Atualizar Contrato 54 | label_contract_empty: Sem Contrato 55 | label_select_contract: "[Selecione um contrato]" 56 | label_save_expense: Gravar Despesa 57 | label_expenses: Despesas 58 | label_description: Descrição 59 | label_amount: Valor 60 | label_edit_expense: Editar Despesa 61 | label_new_expense: Novo Despesa 62 | 63 | text_are_you_sure_delete_title: "Você tem certeza que deseja excluir %{value}?" 64 | text_are_you_sure_delete_time_entry: "Você tem certeza que deseja excluir este registro de tempo?" 65 | text_are_you_sure_delete_expense: "Você tem certeza que deseja excluir esta despesa?" 66 | text_time_exceeded_time_remaining: "Este registro de tempo excedeu o total de horas disponíveis para o contrato em %{hours_over} horas.\nPara existir uma coerência com o contrato, por favor edite o registro de tempo de forma que não seja superior a %{hours_remaining} horas." 67 | text_must_come_after_agreement_date: tem que ser igual ou posterior à Data de Aceite 68 | text_must_come_after_start_date: tem que ser posterior à Data de Início 69 | text_contract_saved: Contrato gravado com sucesso! 70 | text_contract_updated: Contrato atualizado com sucesso! 71 | text_contract_deleted: Contrato excluído com sucesso 72 | text_hours_over_contract: "Você está com um desvio de %{hours_over} horas acima do número de horas contradas." 73 | text_invalid_rate: "Taxa por Hora deve ser não pode ser negativa ou nula." 74 | text_invalid_issue_id: "não é um ID válido de Tarefa" 75 | text_expense_created: 'Despesa criada com sucesso.' 76 | text_expense_updated: 'Despesa atualizada com sucesso.' 77 | text_na: 'N/A' 78 | 79 | field_contract: Contrato 80 | field_title: Título 81 | field_description: Descrição 82 | field_agreement_date: Data Aceite Contrato 83 | field_start_date: Data Início 84 | field_end_date: Data Término 85 | field_purchase_amount: Valor Vendido 86 | field_hourly_rate: Taxa por Hora 87 | field_contract_url: Contrato URL 88 | field_invoice_url: Fatura URL 89 | 90 | field_expense_name: Nome 91 | field_expense_date: Data Despesa 92 | field_amount: Valor 93 | field_expense_contract: Contrato 94 | field_issue: Tarefa (ID) -------------------------------------------------------------------------------- /config/locales/ru.yml: -------------------------------------------------------------------------------- 1 | # Russian strings go here for Rails i18n 2 | ru: 3 | project_module_contracts: Контракты 4 | permission_view_all_contracts_for_project: Просмотр всех контрактов проекта 5 | permission_view_contract_details: Просмотр деталей контракта 6 | permission_edit_contracts: Редактирование контрактов 7 | permission_create_contracts: Создание контрактов 8 | permission_delete_contracts: Удаление контрактов 9 | permission_view_hourly_rate: Просмотр стоимости часа 10 | 11 | contracts: Контракты 12 | label_contracts: Контракты 13 | label_contract: Контракт 14 | label_new_contract: Новый контракт 15 | label_editing_contract: Редактирование контракта 16 | label_add_time_entries: Добавить работы 17 | label_edit: Редактировать 18 | label_delete: Удалить 19 | label_view_contract: Просмотр контракта 20 | label_view_invoice: Просмотр счета 21 | label_date_period: Период действия 22 | label_amount_purchased: Оплачено 23 | label_invoice: Счет 24 | label_members: Участники 25 | label_hours: Часы 26 | label_time_entries: Список работ 27 | label_add_to_contract: Добавить 28 | label_date: Дата 29 | label_current_contract: Текущий контракт 30 | label_member: Участник 31 | label_activity: Действие 32 | label_issue: Задача 33 | label_comments: Комментарий 34 | label_apply: Применить 35 | label_name: Название 36 | label_agreed_on: Заключен 37 | label_purchased: Оплачено 38 | label_remaining: Осталось 39 | label_hours_worked: Часов потрачено 40 | label_hours_left: "~Часов осталось" 41 | label_total_purchased: Всего оплачено по контрактам 42 | label_total_remaining: Всего осталось по контрактам 43 | label_or: "или" 44 | label_create_contract: Создать контракт 45 | label_update_contract: Обновить контракт 46 | label_contract_empty: Без контракта 47 | 48 | text_are_you_sure_delete_title: "Вы действительно хотите удалить %{value}?" 49 | text_are_you_sure_delete_time_entry: "Вы действительно хотите удалить эту запись?" 50 | text_time_exceeded_time_remaining: "Эта работа превышает оставшееся время по контракту на %{hours_over}.\nДля работы в рамках контракта укажите время не более %{hours_remaining}." 51 | text_must_come_after_agreement_date: должна быть не ранее даты соглашения 52 | text_must_come_after_start_date: должна быть позже даты начала 53 | text_contract_saved: Контракт успешно создан! 54 | text_contract_updated: Контракт успешно обновлен! 55 | text_contract_deleted: Контракт успешно удален 56 | text_hours_over_contract: "Лимит по контракту превышен на %{hours_over}." 57 | 58 | field_contract: Контракт 59 | field_title: Заголовок 60 | field_description: Описание 61 | field_agreement_date: Дата соглашения 62 | field_start_date: Дата начала работ 63 | field_end_date: Дата завершения работ 64 | field_purchase_amount: Сумма к оплате 65 | field_hourly_rate: Часовой рейт 66 | field_contract_url: Ссылка на контракт 67 | field_invoice_url: Ссылка на счет 68 | -------------------------------------------------------------------------------- /config/locales/zh.yml: -------------------------------------------------------------------------------- 1 | # Simplified Chinese strings go here for Rails i18n 2 | zh: 3 | contract_types: 4 | hourly: "工时计价" 5 | fixed: "固定总价" 6 | recurring: "续生" 7 | recurring_frequencies: 8 | monthly: "按月" 9 | yearly: "按年" 10 | completed: "已完成" 11 | not_recurring: "非续生" 12 | project_module_contracts: 合同 13 | permission_view_all_contracts_for_project: 查看项目的所有合同 14 | permission_view_contract_details: 查看合同明细 15 | permission_edit_contracts: 编辑合同 16 | permission_create_contracts: 添加合同 17 | permission_delete_contracts: 删除合同 18 | permission_view_hourly_rate: 查看每小时费率 19 | permission_create_expenses: 添加费用 20 | permission_edit_expenses: 编辑费用 21 | permission_delete_expenses: 删除费用 22 | permission_view_expenses: 查看费用 23 | 24 | contracts: 合同管理 25 | label_contracts: 合同 26 | label_series_title: "#%{series_id} 系列的续生合同" 27 | label_contract: 合同 28 | label_contractors_rates: 承包商费用 29 | label_new_contract: 添加合同 30 | label_editing_contract: 编辑合同 31 | label_add_time_entries: 登记工时 32 | label_add_time_entries: 批量登记工时 33 | label_add_expense: 添加费用 34 | label_log_time: 登记时间 35 | label_edit: 编辑 36 | label_delete: 删除 37 | label_view_contract: 查看合同 38 | label_view_invoice: 查看票据 39 | label_date_period: 日期范围 40 | label_amount_purchased: 采购量 41 | label_invoice: 票据 42 | label_members: 成员 43 | label_hours: 小时 44 | label_contractor_rate: 承包商费率 45 | label_billable_amount: 计费金额 46 | label_time_entries: 登记工时 47 | label_hourly_priced_contracts: 工时计价合同 48 | label_effective_rate: 有效费率 49 | label_fixed_priced_contracts: 固定总价合同 50 | label_add_to_contract: 添加到合同 51 | label_date: 日期 52 | label_current_contract: 当前合同 53 | label_member: 成员 54 | label_activity: 活动 55 | label_issue: 问题 56 | label_cost: 成本 57 | label_comments: 批注 58 | label_apply: 应用 59 | label_name: 名称 60 | label_agreed_on: 签订于 61 | label_purchased: 购买总额 62 | label_remaining: 剩余 63 | label_hours_worked: 已用工时 64 | label_hours_left: "~剩余工时" 65 | label_total_purchased: 所有合同的购买总额 66 | label_total_remaining: 所有合同的剩余总额 67 | label_total_fixed: 固定总价合同的购买总额 68 | label_total_hourly: 工时计价合同的购买总额 69 | label_remaining_hourly: 工时计价合同的剩余总额 70 | label_remaining_hourly: 工时计价合同的剩余总额 71 | label_or: "-or-" 72 | label_create_contract: 添加合同 73 | label_update_contract: 更新合同 74 | label_contract_empty: 无合同信息 75 | label_select_contract: "[选择合同]" 76 | label_save_expense: 保存费用 77 | label_expenses: 费用 78 | label_summary: 汇总 79 | label_description: 描述 80 | label_amount: Amount 81 | label_edit_expense: 编辑费用 82 | label_new_expense: 添加费用 83 | label_lock: 锁定 84 | label_unlock: 解锁 85 | label_show_locked_contracts: 显示已锁定的合同 86 | label_show_only_active_recurring: "仅显示 \"活动中\" 的续生合同" 87 | label_recurring: 续生周期 88 | label_cancel_recurring: 结束续生 89 | label_view_series: 查看系列 90 | label_tooltips: Redmine Contract - 帮助说明 91 | label_contract_types: 合同类型 92 | 93 | text_contract_list: 合同列表 94 | text_are_you_sure_delete_title: "确认删除 %{value}?" 95 | text_are_you_sure_stop_recurring: "确认结束该续生合同?" 96 | text_are_you_sure_delete_time_entry: "确认删除该工时记录?" 97 | text_are_you_sure_delete_expense: "确认删除该费用记录?" 98 | text_time_exceeded_time_remaining: "登记工时已超过合同的剩余时间 %{hours_over} 小时。\n为保留在合同记录内,请修改您的工时记录不超过 %{hours_remaining} 小时。" 99 | text_must_come_after_agreement_date: 不能早于协议日期 100 | text_must_come_after_start_date: 不能早于开始日期 101 | text_contract_saved: 合同已添加。 102 | text_contract_updated: 合同已更新。 103 | text_contract_deleted: 合同已删除。 104 | text_hours_over_contract: "目前已超合同上限 %{hours_over} 工时。" 105 | text_split_time_entry_saved: 由于工时记录的数量已超过了当前合同的剩余时间,您的工时记录已自动拆分为两条。拆分的第二条工时记录已添加到新的待定合同中。 106 | text_one_time_entry_saved: 由于您选择的合同工时余额已耗尽,故创建一个新合同,并将工时记录保存至该合同。 107 | text_hours_too_large: 无效。填报工时已超剩余工时。 108 | text_invalid_rate: "承包商费率必须大于等于零。" 109 | text_invalid_issue_id: "不是有效的问题ID" 110 | text_invalid_hours: "无效。合同 %{title} 仅剩 %{hours} 小时。 请与管理联系确认开启自动创建工时计价合同的配置。" 111 | text_second_time_entry_failure: "系统故障。无法保存第2条工时记录。错误信息:%{error}" 112 | text_auto_contract_failure: "系统故障。无法保存该合同的拆分工时记录。错误信息:%{error}" 113 | text_expense_created: '费用已添加。' 114 | text_expense_updated: '费用已更新。' 115 | text_agreement_pending: '协议待定' 116 | text_no_end_date: '没有结束日期' 117 | text_na: 'N/A' 118 | text_contract_locked: 合同已锁定。 119 | text_contract_unlocked: 合同已解锁。 120 | text_locked_contract_cache_updated: 工时等级条目与锁定合同关联。合同的缓存值已更新以反映您的更改。 121 | text_expenses_uneditable: 该合同的费用不可编辑。(合同已锁定)。 122 | text_expense_deleted: 费用记录已删除。 123 | text_apply: 应用 124 | text_previous_id: "为该项目创建的最近一个合同使用的合同ID为 %{previous_id}" 125 | text_contract_category_helper: "[可选] 如果没有提供合同类别,默认将使用此类别。合同类别使用枚举值。" 126 | text_contract_title_helper: "[可选] 用于覆盖默认标题。" 127 | text_hourly_rate_helper: "有助于确定支付给承包商多少钱。与每工时有效费率无关。" 128 | text_hour: / hr 129 | text_fixed_contract_help: 在人工单价已确认的情况下,固定总价合同是最理想的选择。固定总价合同显示在合同列表页面的固定总价合同标签下。在固定总价合同中,工时记录将影响每工时的有效费率,且工时费用不会从采购金额中扣除。 每工时费率只是帮助您确定对项目工作的潜在承包商支付什么费用。 130 | text_recurring_contract_help: 续生合同是订阅的理想选择。续生合同显示在合同列表页面的固定总价合同标签下。续生合同以“续生”列标识。除了在续生周期内会自动创建新合同外,续生合同和固定总价合同无二。 131 | text_hourly_contract_help: 在按工时计费的情况下,工时计价合同是比较理想的选择。工时计价合同显示在合同列表页面上的工时计价合同标签下。工时计价合同中,工时记录的费用(承包商每小时的费率)将从采购金额中直接扣除。在合同插件的选项中,可以配置登记工时记录总量超过当前合同采购金额时,自动创建新的工时计价合同。 132 | 133 | field_contract: 合同 134 | field_project_contract: 合同编号 135 | field_description: 描述 136 | field_agreement_date: 协议日期 137 | field_start_date: 起始日期 138 | field_end_date: 结束日期 139 | field_purchase_amount: 购买金额 140 | field_hourly_rate: 费用/工时 141 | field_contract_url: 合同 URL 142 | field_invoice_url: 票据 URL 143 | field_is_fixed_price: 固定总价 144 | field_contract_type: 合同类型 145 | field_recurring_frequency: 续生频率 146 | 147 | field_expense_name: 费用名称 148 | field_expense_date: 费用发生日期 149 | field_amount: 数额 150 | field_expense_contract: 关联合同 151 | field_issue: 问题 (ID) 152 | 153 | enumeration_contract_categories: 合同类别 154 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # Plugin's routes 2 | # See: http://guides.rubyonrails.org/routing.html 3 | # 4 | 5 | match 'contracts/all' => 'contracts#all', :via => :get 6 | match 'contracts/:id' => 'contracts#destroy', :via => :delete 7 | match 'contracts/:id/edit' => 'contracts#edit', :via => :get 8 | match 'contracts/tooltips/:id' => 'contracts#tooltips',:via => :get 9 | match 'projects/:project_id/contracts' => 'contracts#index', :via => :get 10 | match 'projects/:project_id/contracts/new' => 'contracts#new', :via => :get 11 | match 'projects/:project_id/contracts/:id/edit' => 'contracts#edit', :via => :get 12 | match 'projects/:project_id/contracts/:id' => 'contracts#show', :via => :get 13 | match 'projects/:project_id/contracts' => 'contracts#create', :via => :post 14 | match 'projects/:project_id/contracts/:id' => 'contracts#update', :via => :put 15 | match 'projects/:project_id/contracts/:id' => 'contracts#destroy', :via => :delete 16 | match 'projects/:project_id/contracts/:id/lock' => 'contracts#lock', :via => :put 17 | match 'projects/:project_id/contracts/:id/add_time_entries' => 'contracts#add_time_entries', :via => :get 18 | match 'projects/:project_id/contracts/:id/assoc_time_entries_with_contract' => 19 | 'contracts#assoc_time_entries_with_contract', 20 | :via => :put 21 | match 'projects/:project_id/contracts/:id/cancel_recurring' => 'contracts#cancel_recurring', :via => :put 22 | match 'projects/:project_id/contracts/:id/series' => 'contracts#series', :via => :get 23 | 24 | # Expenses 25 | match 'projects/:project_id/expenses/new' => 'contracts_expenses#new', :via => :get 26 | match 'projects/:project_id/expenses/:id/edit' => 'contracts_expenses#edit', :via => :get 27 | match 'projects/:project_id/expenses/create' => 'contracts_expenses#create', :via => :post 28 | match 'projects/:project_id/expenses/update/:id' => 'contracts_expenses#update', :via => :put 29 | match 'projects/:project_id/expenses/destroy/:id' => 'contracts_expenses#destroy', :via => :delete 30 | -------------------------------------------------------------------------------- /db/migrate/001_create_contracts.rb: -------------------------------------------------------------------------------- 1 | class CreateContracts < ActiveRecord::Migration 2 | def change 3 | create_table :contracts do |t| 4 | t.string :title 5 | t.text :description 6 | t.datetime :start_date 7 | t.datetime :end_date 8 | t.datetime :agreement_date 9 | t.decimal :hourly_rate 10 | t.decimal :purchase_amount, :precision => 16, :scale => 2 11 | t.string :contract_url 12 | t.string :invoice_url 13 | t.integer :project_id 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/002_add_contract_id_to_time_entries.rb: -------------------------------------------------------------------------------- 1 | class AddContractIdToTimeEntries < ActiveRecord::Migration 2 | def change 3 | add_column :time_entries, :contract_id, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/003_add_precision_to_hourly_rate.rb: -------------------------------------------------------------------------------- 1 | class AddPrecisionToHourlyRate < ActiveRecord::Migration 2 | def up 3 | change_column :contracts, :hourly_rate, :decimal, :precision => 16, :scale => 2 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/004_create_user_project_rates.rb: -------------------------------------------------------------------------------- 1 | class CreateUserProjectRates < ActiveRecord::Migration 2 | def change 3 | create_table :user_project_rates do |t| 4 | t.integer :user_id 5 | t.integer :project_id 6 | t.float :rate, :length => 8, :decimals => 2 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/005_create_user_contract_rates.rb: -------------------------------------------------------------------------------- 1 | class CreateUserContractRates < ActiveRecord::Migration 2 | def change 3 | create_table :user_contract_rates do |t| 4 | t.integer :user_id 5 | t.integer :contract_id 6 | t.float :rate, :length => 8, :decimals => 2 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/006_create_expenses.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | This file used to create the expenses table 3 | but it was in conflict with several other 4 | plugins that also had an expense table. 5 | The create expenses table migration has 6 | moved to 010_rename_expenses.rb and the 7 | table name is now contracts_expenses. 8 | =end 9 | 10 | class CreateExpenses < ActiveRecord::Migration 11 | 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/007_lock.rb: -------------------------------------------------------------------------------- 1 | class Lock < ActiveRecord::Migration 2 | def change 3 | add_column :contracts, :is_locked, :boolean, :default => false 4 | add_column :contracts, :hours_worked, :float, :length => 8, :decimals => 2 5 | add_column :contracts, :billable_amount_total, :float, :length => 8, :decimals => 2 6 | Contract.update_all( :is_locked => false ) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/008_project_contract_id.rb: -------------------------------------------------------------------------------- 1 | class ProjectContractId < ActiveRecord::Migration 2 | def change 3 | add_column :contracts, :project_contract_id, :integer, :default => 1 4 | Contract.reset_column_information 5 | Project.all.each do |project| 6 | # loop thru each project assigning ids to each contract 7 | id = 1; 8 | project.contracts.each do |contract| 9 | contract.update_attributes(:project_contract_id => id) 10 | id += 1; 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/009_contract_category_enumeration.rb: -------------------------------------------------------------------------------- 1 | class ContractCategoryEnumeration < ActiveRecord::Migration 2 | def change 3 | add_column :contracts, :category_id, :integer 4 | 5 | # Add a few default categories here 6 | category1 = ContractCategory.new 7 | category1.name = "Dev" 8 | category1.position = 1 9 | category1.type = ContractCategory 10 | category1.save 11 | 12 | category2 = ContractCategory.new 13 | category2.name = "Maint" 14 | category2.position = 2 15 | category2.type = ContractCategory 16 | category2.save 17 | end 18 | end -------------------------------------------------------------------------------- /db/migrate/010_rename_expenses.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | This file is used to create the 3 | contracts_expenses table 4 | =end 5 | 6 | class RenameExpenses < ActiveRecord::Migration 7 | def change 8 | create_table :contracts_expenses do |t| 9 | t.string :name 10 | t.date :expense_date 11 | t.float :amount, :length => 8, :decimals => 2 12 | t.integer :contract_id 13 | t.integer :issue_id 14 | t.string :description 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/011_add_fixed_price_contract.rb: -------------------------------------------------------------------------------- 1 | class AddFixedPriceContract < ActiveRecord::Migration 2 | def change 3 | add_column :contracts, :is_fixed_price, :boolean, :default => false 4 | Contract.update_all( :is_fixed_price => false ) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/012_add_recurring_contracts.rb: -------------------------------------------------------------------------------- 1 | class AddRecurringContracts < ActiveRecord::Migration 2 | def change 3 | add_column :contracts, :recurring_frequency, :integer, :default => 0 4 | add_column :contracts, :series_id, :integer 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /docs/screenshots/edit_contract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upgradeya/redmine-contracts-with-time-tracking-plugin/9421ed6e417aef9e917f9ce95294677ad1b62067/docs/screenshots/edit_contract.png -------------------------------------------------------------------------------- /docs/screenshots/multiple_contracts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upgradeya/redmine-contracts-with-time-tracking-plugin/9421ed6e417aef9e917f9ce95294677ad1b62067/docs/screenshots/multiple_contracts.png -------------------------------------------------------------------------------- /docs/screenshots/permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upgradeya/redmine-contracts-with-time-tracking-plugin/9421ed6e417aef9e917f9ce95294677ad1b62067/docs/screenshots/permissions.png -------------------------------------------------------------------------------- /docs/screenshots/single_contract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upgradeya/redmine-contracts-with-time-tracking-plugin/9421ed6e417aef9e917f9ce95294677ad1b62067/docs/screenshots/single_contract.png -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'contracts/hooks/hooks' 2 | require_dependency 'contracts/patches/time_entry_patch' 3 | require_dependency 'contracts/patches/timelog_controller_patch' 4 | require_dependency 'contracts/patches/user_patch' 5 | require_dependency 'contracts/patches/project_patch' 6 | require_dependency 'contracts/validators/is_after_agreement_date_validator' 7 | require_dependency 'contracts/validators/is_after_start_date_validator' 8 | 9 | 10 | Redmine::Plugin.register :contracts do 11 | name 'Redmine Contracts With Time Tracking' 12 | author 'Ben Syzek, Shanti Braford, Wesley Jones' 13 | description 'A Redmine plugin that allows you to manage contracts and associate time-entries with those contracts.' 14 | version '2.4' 15 | url 'https://github.com/upgradeya/redmine-contracts-with-time-tracking-plugin.git' 16 | 17 | requires_redmine :version_or_higher => '3.0' 18 | 19 | menu :application_menu, :contracts, { :controller => :contracts, :action => :all }, :caption => :label_contracts, :if => Proc.new { User.current.logged? && User.current.allowed_to?(:view_all_contracts_for_project, nil, :global => true) } 20 | menu :project_menu, :contracts, { :controller => :contracts, :action => :index }, :caption => :label_contracts, :param => :project_id 21 | 22 | settings :default => {'empty' => true}, :partial => 'settings/contract_settings' 23 | 24 | project_module :contracts do 25 | permission :view_all_contracts_for_project, :contracts => [:index, :series] 26 | permission :view_contract_details, :contracts => :show 27 | permission :edit_contracts, :contracts => [:edit, :update, :add_time_entries, :assoc_time_entries_with_contract, :lock] 28 | permission :create_contracts, :contracts => [:new, :create] 29 | permission :delete_contracts, :contracts => :destroy 30 | permission :view_hourly_rate, :contracts => :view_hourly_rate #view_hourly_rate is a fake action! 31 | permission :create_expenses, :contracts_expenses => [:new, :create] 32 | permission :edit_expenses, :contracts_expenses => [:edit, :update] 33 | permission :delete_expenses, :contracts_expenses => :destroy 34 | permission :view_expenses, :contracts_expenses => :show 35 | end 36 | end 37 | 38 | # Load your patches from contracts/lib/contracts/patches/ 39 | ActionDispatch::Callbacks.to_prepare do 40 | Project.send(:include, Contracts::ProjectPatch) 41 | TimeEntry.send(:include, Contracts::TimeEntryPatch) 42 | TimelogController.send(:include, Contracts::TimelogControllerPatch) 43 | User.send(:include, Contracts::UserPatch) 44 | require_dependency 'contract_category' 45 | end 46 | -------------------------------------------------------------------------------- /lib/contracts/hooks/hooks.rb: -------------------------------------------------------------------------------- 1 | module Contracts 2 | class ContractsHookListener < Redmine::Hook::ViewListener 3 | 4 | def view_timelog_edit_form_bottom(context={}) 5 | if context[:time_entry].project_id != nil 6 | @current_project = Project.find(context[:time_entry].project_id) 7 | @contracts = @current_project.contracts_for_all_ancestor_projects 8 | 9 | if !@contracts.empty? 10 | if context[:time_entry].contract_id != nil 11 | selected_contract = context[:time_entry].contract_id 12 | elsif !(@current_project.contracts.empty?) 13 | selected_contract = @current_project.contracts.maximum(:id) 14 | elsif !(@contracts.empty?) 15 | selected_contract = @contracts.max_by(&:id).id 16 | else 17 | selected_contract = '' 18 | end 19 | contract_unselectable = false 20 | if !selected_contract.blank? 21 | # There is a selected contract. Check to see if it has been locked 22 | selected_contract_obj = Contract.find(selected_contract) 23 | if selected_contract_obj.is_locked 24 | # Contract has been locked. Only list that contract in the drop-down 25 | @contracts = [selected_contract_obj] 26 | contract_unselectable = true 27 | else 28 | # Only show NON-locked contracts in the drop-down 29 | @contracts = @current_project.unlocked_contracts_for_all_ancestor_projects 30 | end 31 | else 32 | # There is NO selected contract. Only show NON-locked contracts in the drop-down 33 | @contracts = @current_project.unlocked_contracts_for_all_ancestor_projects 34 | end 35 | db_options = options_from_collection_for_select(@contracts, :id, :getDisplayTitle, selected_contract) 36 | no_contract_option = "\n".html_safe 37 | if !contract_unselectable 38 | all_options = no_contract_option << db_options 39 | else 40 | # Contract selected has already been locked. Do not show the [Select Contract] label. 41 | all_options = db_options 42 | end 43 | select = context[:form].select :contract_id, all_options 44 | return "#{select}
" 45 | end 46 | else 47 | "This page will not work due to the contracts plugin. You must log time entries from within a project." 48 | end 49 | end 50 | 51 | # Poor Man's Cron 52 | def controller_account_success_authentication_after(context={}) 53 | # check to see if cron has ran today or if its null 54 | last_run = Setting.plugin_contracts[:last_cron_run] 55 | if last_run.nil? || last_run < Date.today 56 | # Get all monthly recurring contracts 57 | monthly_contracts = Contract.monthly 58 | # Loop thru the contracts and check if any have passed their recurring date 59 | monthly_contracts.each do |contract| 60 | if Date.today > (contract.start_date + 1.month) 61 | # Create new contract and expire the old one 62 | new_contract = Contract.new 63 | if new_contract.copy(contract) 64 | expire_contract(contract) 65 | end 66 | end 67 | end 68 | 69 | # Get all yearly recurring contracts 70 | yearly_contracts = Contract.yearly 71 | # Loop thru the contracts and check if any have passed their recurring date 72 | yearly_contracts.each do |contract| 73 | if Date.today > (contract.start_date + 1.year) 74 | # Create new contract and expire the old one 75 | new_contract = Contract.new 76 | if new_contract.copy(contract) 77 | expire_contract(contract) 78 | end 79 | end 80 | end 81 | end 82 | 83 | Setting.plugin_contracts.update({last_cron_run: Date.today}) 84 | end 85 | 86 | def expire_contract(contract) 87 | contract.completed! 88 | contract.save 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/contracts/patches/project_patch.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'project' 2 | 3 | module Contracts 4 | module ProjectPatch 5 | def self.included(base) 6 | base.send(:include, InstanceMethods) 7 | base.class_eval do 8 | has_many :contracts 9 | has_many :user_project_rates 10 | end 11 | end 12 | 13 | module InstanceMethods 14 | 15 | def unlocked_contracts 16 | contracts.select { |contract| !contract.is_locked } 17 | end 18 | 19 | def user_project_rate_by_user(user) 20 | self.user_project_rates.select { |upr| upr.user_id == user.id}.first 21 | end 22 | 23 | def rate_for_user(user) 24 | upr = self.user_project_rate_by_user(user) 25 | upr.nil? ? 0.0 : upr.rate 26 | end 27 | 28 | def set_user_rate(user, rate) 29 | upr = user_project_rate_by_user(user) 30 | if upr.nil? 31 | self.user_project_rates.create!(:user_id => user.id, :rate => rate) 32 | else 33 | upr.update_attribute(:rate, rate) 34 | upr 35 | end 36 | end 37 | 38 | def total_amount_purchased 39 | self.contracts.map(&:purchase_amount).inject(0, &:+) 40 | end 41 | 42 | def total_hours_purchased 43 | self.contracts.map(&:hours_purchased).inject(0, &:+) 44 | end 45 | 46 | def total_amount_remaining 47 | self.contracts.map(&:amount_remaining).inject(0, &:+) 48 | end 49 | 50 | def total_hours_remaining 51 | self.contracts.map(&:hours_remaining).inject(0, &:+) 52 | end 53 | 54 | def contracts_for_all_ancestor_projects(contracts=self.contracts) 55 | if self.parent != nil 56 | parent = self.parent 57 | contracts += parent.contracts_for_all_ancestor_projects 58 | end 59 | return contracts 60 | end 61 | 62 | def unlocked_contracts_for_all_ancestor_projects(contracts = self.unlocked_contracts) 63 | if self.parent != nil 64 | parent = self.parent 65 | contracts += parent.unlocked_contracts_for_all_ancestor_projects 66 | end 67 | return contracts 68 | end 69 | 70 | def time_entries_for_all_descendant_projects(time_entries=self.time_entries) 71 | if self.children != nil 72 | self.children.each { |child| time_entries += child.time_entries_for_all_descendant_projects } 73 | end 74 | return time_entries 75 | end 76 | end 77 | end 78 | end 79 | 80 | -------------------------------------------------------------------------------- /lib/contracts/patches/time_entry_patch.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'time_entry' 2 | 3 | module Contracts 4 | 5 | module TimeEntryPatch 6 | def self.included(base) 7 | base.send(:include, InstanceMethods) 8 | base.class_eval do 9 | belongs_to :contract 10 | safe_attributes 'contract_id' 11 | after_update :refresh_contract 12 | after_destroy :refresh_contract 13 | after_create :refresh_contract 14 | 15 | attr_accessor :flash_time_entry_success, :flash_only_one_time_entry 16 | 17 | validate :time_not_exceed_contract 18 | before_save :create_next_contract 19 | 20 | # Validate the "hours" input field for hourly contracts. 21 | # 22 | # Validate that the hours entered do not exceed the hours remaining on a contract. 23 | # If "auto create new contract" settings option is enabled, use this validation to 24 | # ensure the hours entered does not exceed the hours remaining plus the size of a 25 | # new contract. 26 | protected 27 | def time_not_exceed_contract 28 | return if hours.blank? 29 | return if contract.is_fixed_price 30 | previous_hours = (hours_was != nil) ? hours_was : 0 31 | 32 | if contract_id != nil 33 | if Setting.plugin_contracts['automatic_contract_creation'] 34 | if hours > (contract.hours_remaining + contract.hours_purchased + previous_hours) 35 | errors.add :hours, l(:text_hours_too_large) 36 | end 37 | else 38 | if hours > (contract.hours_remaining + previous_hours) 39 | errors.add :hours, l(:text_invalid_hours, :title => contract.getDisplayTitle, :hours => l_hours(contract.hours_remaining + previous_hours)) 40 | end 41 | end 42 | end 43 | end 44 | 45 | 46 | # Create new contract if it is an hourly contract, and the settings configuration 47 | # is enabled and the hours exceed the current contract. 48 | private 49 | def create_next_contract 50 | return if contract.is_fixed_price 51 | previous_hours = (hours_was != nil) ? hours_was : 0 52 | if Setting.plugin_contracts['automatic_contract_creation'] && hours > (contract.hours_remaining + previous_hours) 53 | new_contract = Contract.new 54 | if new_contract.copy(contract, project) 55 | 56 | # split the time entry and save new entry in the new contract 57 | new_time_entry = TimeEntry.new 58 | new_time_entry.project_id = project_id 59 | new_time_entry.issue_id = issue_id 60 | new_time_entry.user_id = user.id 61 | new_time_entry.hours = hours - (contract.hours_remaining + previous_hours) 62 | new_time_entry.comments = comments 63 | new_time_entry.activity_id = activity_id 64 | new_time_entry.spent_on = spent_on 65 | new_time_entry.contract_id = new_contract.id 66 | 67 | if new_time_entry.save 68 | self.flash_time_entry_success = true 69 | self.hours = contract.hours_remaining + previous_hours 70 | 71 | # @TODO This is not working. Its supposed to create a different error message 72 | # if there are zero remaining hours in the current contract 73 | if self.hours <= 0.1 && self.hours >= -0.1 74 | self.flash_only_one_time_entry 75 | end 76 | else 77 | logger.error "Split time entry ran into errors" 78 | logger.error new_time_entry.errors.full_messages.join("\n") 79 | errors.add :contract_id, l(:text_second_time_entry_failure, :error => new_time_entry.errors.full_messages.join("\n")) 80 | return false 81 | end 82 | else 83 | logger.error "New auto created contract ran into errors" 84 | logger.error new_contract.errors.full_messages.join("\n") 85 | errors.add :contract_id, l(:text_auto_contract_failure, :error => new_contract.errors.full_messages.join("\n")) 86 | return false 87 | end 88 | else 89 | # Do nothing. Configuration is off or contract hours not exceeded. 90 | end 91 | end 92 | end 93 | end 94 | 95 | module InstanceMethods 96 | 97 | def refresh_contract 98 | return if self.contract_id.nil? 99 | the_contract = Contract.find(self.contract_id) 100 | the_contract.reset_cache! 101 | end 102 | end 103 | end 104 | end 105 | 106 | -------------------------------------------------------------------------------- /lib/contracts/patches/timelog_controller_patch.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'timelog_controller' 2 | 3 | module Contracts 4 | 5 | module TimelogControllerPatch 6 | def self.included(base) 7 | base.class_eval do 8 | after_filter :check_flash_messages, :only => [:create, :update] 9 | 10 | def check_flash_messages 11 | if @time_entry.flash_only_one_time_entry 12 | flash[:contract] = l(:text_one_time_entry_saved) 13 | elsif @time_entry.flash_time_entry_success 14 | flash[:contract] = l(:text_split_time_entry_saved) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /lib/contracts/patches/user_patch.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'user' 2 | 3 | module Contracts 4 | 5 | module UserPatch 6 | def self.included(base) 7 | base.class_eval do 8 | has_many :user_project_rates 9 | has_many :user_contract_rates 10 | end 11 | end 12 | end 13 | end 14 | 15 | -------------------------------------------------------------------------------- /lib/contracts/validators/is_after_agreement_date_validator.rb: -------------------------------------------------------------------------------- 1 | class IsAfterAgreementDateValidator < ActiveModel::EachValidator 2 | include Redmine::I18n 3 | 4 | def validate_each(record, attribute, value) 5 | if (value != nil) and (record.agreement_date != nil) 6 | unless value >= record.agreement_date 7 | record.errors[attribute] << l(:text_must_come_after_agreement_date) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/contracts/validators/is_after_start_date_validator.rb: -------------------------------------------------------------------------------- 1 | class IsAfterStartDateValidator < ActiveModel::EachValidator 2 | include Redmine::I18n 3 | 4 | def validate_each(record, attribute, value) 5 | if (value != nil) and (record.start_date != nil) 6 | unless value > record.start_date 7 | record.errors[attribute] << l(:text_must_come_after_start_date) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/fixtures/contracts.yml: -------------------------------------------------------------------------------- 1 | contract_one: 2 | title: Contract One 3 | project_contract_id: 1 4 | description: Description for contract one 5 | start_date: 2006-07-19 19:13:59 +02:00 6 | hourly_rate: 25.00 7 | purchase_amount: 625.00 8 | contract_url: 9 | invoice_url: 10 | project_id: 1 11 | contract_two: 12 | title: Contract Two 13 | project_contract_id: 2 14 | description: Description for contract two 15 | start_date: 2006-07-19 19:13:59 +02:00 16 | hourly_rate: 25.00 17 | purchase_amount: 625.00 18 | contract_url: 19 | invoice_url: 20 | project_id: 6 21 | contract_three: 22 | title: Contract Three 23 | project_contract_id: 2 24 | description: Description for contract three 25 | start_date: 2006-07-19 19:13:59 +02:00 26 | hourly_rate: 25.00 27 | purchase_amount: 625.00 28 | contract_url: 29 | invoice_url: 30 | project_id: 1 -------------------------------------------------------------------------------- /test/fixtures/user_contract_rates.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upgradeya/redmine-contracts-with-time-tracking-plugin/9421ed6e417aef9e917f9ce95294677ad1b62067/test/fixtures/user_contract_rates.yml -------------------------------------------------------------------------------- /test/fixtures/user_project_rates.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upgradeya/redmine-contracts-with-time-tracking-plugin/9421ed6e417aef9e917f9ce95294677ad1b62067/test/fixtures/user_project_rates.yml -------------------------------------------------------------------------------- /test/functional/contracts_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class ContractsControllerTest < ActionController::TestCase 4 | include Redmine::I18n 5 | 6 | fixtures :issues, :projects, :users, :time_entries, 7 | :members, :roles, :member_roles, 8 | :trackers, :issue_statuses, 9 | :journals, :journal_details, 10 | :issue_categories, :enumerations, 11 | :groups_users, 12 | :enabled_modules, 13 | :workflows, 14 | :contracts, :user_contract_rates, :user_project_rates 15 | 16 | def setup 17 | Setting.plugin_contracts = { 18 | 'automatic_contract_creation' => false 19 | } 20 | @contract = contracts(:contract_one) 21 | @project = projects(:projects_001) 22 | @user = users(:users_004) 23 | @time_entry = time_entries(:time_entries_002) 24 | @contract.project_id = @project.id 25 | @request.session[:user_id] = @user.id 26 | @project.enabled_module_names = [:contracts] 27 | end 28 | 29 | test "should get index with permission" do 30 | Role.find(4).add_permission! :view_all_contracts_for_project 31 | get :index, :project_id => @project.id 32 | assert_response :success 33 | assert_not_nil assigns(:contracts) 34 | assert_not_nil assigns(:total_purchased_dollars) 35 | assert_not_nil assigns(:total_purchased_hours) 36 | assert_not_nil assigns(:total_remaining_dollars) 37 | assert_not_nil assigns(:total_remaining_hours) 38 | end 39 | 40 | test "should not get index without permission" do 41 | assert !@user.roles_for_project(@project).first.permissions.include?(:view_all_contracts_for_project) 42 | get :index, :project_id => @project.id 43 | assert_response 403 44 | end 45 | 46 | test "should get new with permission" do 47 | Role.find(4).add_permission! :create_contracts 48 | get :new, :project_id => @project.id 49 | assert_response :success 50 | assert_not_nil assigns(:contract) 51 | assert_not_nil assigns(:contractors) 52 | end 53 | 54 | test "should not get new without permission" do 55 | assert !@user.roles_for_project(@project).first.permissions.include?(:create_contracts) 56 | get :new, :project_id => @project.id 57 | assert_response 403 58 | end 59 | 60 | test "should create new contract with permission" do 61 | Role.find(4).add_permission! :create_contracts 62 | assert_difference('Contract.count') do 63 | post :create, :project_id => @project.identifier, 64 | :contract => { :title => "New Title", 65 | :description => @contract.description, 66 | :start_date => @contract.start_date, 67 | :purchase_amount => @contract.purchase_amount, 68 | :hourly_rate => @contract.hourly_rate, 69 | :project_id => @project.id, 70 | :project_contract_id => @contract.project_contract_id + 12 71 | } 72 | end 73 | assert_not_nil assigns(:contract) 74 | assert_redirected_to :action => "show", :project_id => @project.identifier, :id => assigns(:contract) 75 | end 76 | 77 | test "should not create new contract without permission" do 78 | assert !@user.roles_for_project(@project).first.permissions.include?(:create_contracts) 79 | assert_no_difference('Contract.count') do 80 | post :create, :project_id => @project.identifier, 81 | :contract => { :title => "New Title", 82 | :description => @contract.description, 83 | :agreement_date => @contract.agreement_date, 84 | :start_date => @contract.start_date, 85 | :end_date => @contract.end_date, 86 | :purchase_amount => @contract.purchase_amount, 87 | :hourly_rate => @contract.hourly_rate, 88 | :project_id => @project.id 89 | } 90 | end 91 | end 92 | 93 | test "should get show with permission" do 94 | Role.find(4).add_permission! :view_contract_details 95 | get :show, :project_id => @project.id, :id => @contract.id 96 | assert_response :success 97 | assert_not_nil assigns(:contract) 98 | assert_not_nil assigns(:time_entries) 99 | assert_not_nil assigns(:members) 100 | end 101 | 102 | test "should get show and assign all user who've logged time to contributers" do 103 | Role.find(4).add_permission! :view_contract_details 104 | @time_entry.contract_id = @contract.id 105 | @time_entry.save 106 | get :show, :project_id => @project.id, :id => @contract.id 107 | assert assigns(:members).include?(@time_entry.user) 108 | end 109 | 110 | test "should not get show without permission" do 111 | assert !@user.roles_for_project(@project).first.permissions.include?(:view_contract_details) 112 | get :show, :project_id => @project.id, :id => @contract.id 113 | assert_response 403 114 | end 115 | 116 | test "should get edit with permission" do 117 | Role.find(4).add_permission! :edit_contracts 118 | get :edit, :project_id => @project.id, :id => @contract.id 119 | assert_response :success 120 | assert_not_nil assigns(:contract) 121 | assert_not_nil assigns(:projects) 122 | end 123 | 124 | test "should not get edit without permission" do 125 | assert !@user.roles_for_project(@project).first.permissions.include?(:edit_contracts) 126 | get :edit, :project_id => @project.id, :id => @contract.id 127 | assert_response 403 128 | end 129 | 130 | test "should update contract with permission" do 131 | Role.find(4).add_permission! :edit_contracts 132 | @contract.save 133 | assert_no_difference('Contract.count') do 134 | put :update, :project_id => @project.id, :id => @contract.id, 135 | :contract => { :title => @contract.title, 136 | :description => @contract.description, 137 | :agreement_date => @contract.agreement_date, 138 | :start_date => @contract.start_date, 139 | :end_date => @contract.end_date, 140 | :purchase_amount => @contract.purchase_amount, 141 | :hourly_rate => @contract.hourly_rate, 142 | :project_id => @contract.project_id 143 | }, 144 | :rates => { @user.id => '37.50'} 145 | end 146 | assert_redirected_to :action => "show", :project_id => @project.id, :id => assigns(:contract).id 147 | assert_equal 37.5, assigns(:contract).project.rate_for_user(@user).to_f 148 | end 149 | 150 | test "should not update contract without permission" do 151 | assert !@user.roles_for_project(@project).first.permissions.include?(:edit_contracts) 152 | put :update, :project_id => @project.id, :id => @contract.id, 153 | :contract => { :title => @contract.title, 154 | :description => @contract.description, 155 | :agreement_date => @contract.agreement_date, 156 | :start_date => @contract.start_date, 157 | :end_date => @contract.end_date, 158 | :purchase_amount => @contract.purchase_amount, 159 | :hourly_rate => @contract.hourly_rate, 160 | :project_id => @contract.project_id 161 | } 162 | assert_response 403 163 | end 164 | 165 | test "should get all contracts" do 166 | get :all 167 | assert_response :success 168 | assert_not_nil assigns(:contracts) 169 | assert_not_nil assigns(:total_purchased_dollars) 170 | assert_not_nil assigns(:total_purchased_hours) 171 | assert_not_nil assigns(:total_remaining_dollars) 172 | assert_not_nil assigns(:total_remaining_hours) 173 | end 174 | 175 | test "should destroy contract with permission" do 176 | Role.find(4).add_permission! :delete_contracts 177 | assert_difference('Contract.count', -1) do 178 | delete :destroy, :project_id => @project.id, :id => @contract.id 179 | end 180 | end 181 | 182 | test "should not destroy contract without permission" do 183 | assert !@user.roles_for_project(@project).first.permissions.include?(:delete_contracts) 184 | delete :destroy, :project_id => @project.id, :id => @contract.id 185 | assert_response 403 186 | end 187 | 188 | test "should get 'add time entries' with permission" do 189 | Role.find(4).add_permission! :edit_contracts 190 | get :add_time_entries, :project_id => @project.id, :id => @contract.id 191 | assert_response :success 192 | assert_not_nil assigns(:contract) 193 | assert_not_nil assigns(:project) 194 | assert_not_nil assigns(:time_entries) 195 | end 196 | 197 | test "should not get 'add time entries' without permission" do 198 | get :add_time_entries, :project_id => @project.id, :id => @contract.id 199 | assert_response 403 200 | end 201 | 202 | test "should be able to associate time entries with contracts with permission" do 203 | Role.find(4).add_permission! :edit_contracts 204 | put :assoc_time_entries_with_contract, :project_id => @contract.project_id, :id => @contract.id, 205 | :time_entries => [[@time_entry.id]] 206 | assert_redirected_to :action => "show", :project_id => @contract.project_id, :id => @contract.id 207 | end 208 | 209 | test "should not be able to associate time entries with contracts without permission" do 210 | put :assoc_time_entries_with_contract, :project_id => @project.id, :id => @contract.id, 211 | :time_entries => [[@time_entry.id]] 212 | assert_response 403 213 | end 214 | end 215 | -------------------------------------------------------------------------------- /test/functional/contracts_expenses_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class ContractsExpensesControllerTest < ActionController::TestCase 4 | include Redmine::I18n 5 | fixtures :contracts, :projects, :users 6 | 7 | def setup 8 | Setting.plugin_contracts = { 9 | 'automatic_contract_creation' => false 10 | } 11 | @contract = contracts(:contract_one) 12 | @project = projects(:projects_001) 13 | @user = users(:users_004) 14 | @contract.project_id = @project.id 15 | @request.session[:user_id] = @user.id 16 | @project.enabled_module_names = [:contracts] 17 | end 18 | 19 | test "should get new with permission" do 20 | Role.find(4).add_permission! :create_expenses 21 | get :new, :project_id => @project.id 22 | assert_response :success 23 | assert_not_nil assigns(:contracts_expense) 24 | assert_not_nil assigns(:project) 25 | assert_not_nil assigns(:contracts) 26 | end 27 | 28 | test "should create new expense with permission" do 29 | Role.find(4).add_permission! :create_expenses 30 | get :new, :project_id => @project.id, :contracts_expense => { :name => '' } 31 | assert_response :success 32 | assert_not_nil assigns(:contracts_expense) 33 | assert_not_nil assigns(:project) 34 | assert_not_nil assigns(:contracts) 35 | end 36 | 37 | test "should save valid expense" do 38 | Role.find(4).add_permission! :create_expenses 39 | post :create, :project_id => @project.id, 40 | :contracts_expense => { :name => 'Domain name registration', :expense_date => '2013-05-31', 41 | :amount => '10.25', :contract_id => @contract.id, :description => 'The description' } 42 | assert_response :redirect 43 | assert_equal 0, assigns(:contracts_expense).errors.size 44 | assert !assigns(:contracts_expense).new_record? 45 | end 46 | 47 | test "should not save expense with non-valid issue_id" do 48 | Role.find(4).add_permission! :create_expenses 49 | post :create, :project_id => @project.id, 50 | :contracts_expense => { :name => '', :issue_id => -1 } 51 | assert_response :success 52 | assert_not_nil assigns(:contracts_expense).errors.messages[:issue_id] 53 | end 54 | 55 | test "should update valid expense" do 56 | Role.find(4).add_permission! :edit_expenses 57 | expense = ContractsExpense.create!(:name => 'Foo', :expense_date => '2013-05-15', :amount => 1, :contract_id => @contract.id) 58 | put :update, :project_id => @project.id, :id => expense.id, 59 | :contracts_expense => { :name => 'Foo Updated', :expense_date => '2013-05-31', 60 | :amount => '42.42', :contract_id => @contract.id, :description => 'desc' } 61 | assert_response :redirect 62 | assert_equal 'Foo Updated', assigns(:contracts_expense).name 63 | assert_equal 42.42, assigns(:contracts_expense).amount 64 | assert_equal 'desc', assigns(:contracts_expense).description 65 | end 66 | 67 | test "should not update invalid expense" do 68 | Role.find(4).add_permission! :edit_expenses 69 | expense = ContractsExpense.create!(:name => 'Foo', :expense_date => '2013-05-15', :amount => 1, :contract_id => @contract.id) 70 | put :update, :project_id => @project.id, :id => expense.id, 71 | :contracts_expense => { :name => '', :expense_date => '2013-05-31', 72 | :amount => '42.42', :contract_id => @contract.id, :description => 'desc' } 73 | assert_response :success 74 | assert_not_nil assigns(:contracts_expense).errors.messages[:name] 75 | assert_equal 'Foo', expense.reload.name 76 | end 77 | 78 | test "should destroy an expense" do 79 | Role.find(4).add_permission! :delete_expenses 80 | expense = ContractsExpense.create!(:name => 'Foo', :expense_date => '2013-05-15', :amount => 1, :contract_id => @contract.id) 81 | delete :destroy, :project_id => @project.id, :id => expense.id 82 | assert_response :redirect 83 | assert_nil ContractsExpense.where(:id => expense.id).first 84 | end 85 | 86 | test "should get error notice on new without permission" do 87 | get :new, :project_id => @project.id 88 | assert_response 403 89 | assert_nil assigns(:contracts_expense) 90 | end 91 | 92 | test "should get error notice on create without permission" do 93 | post :create, :project_id => @project.id 94 | assert_response 403 95 | end 96 | 97 | test "should get error notice on edit without permission" do 98 | get :edit, :project_id => @project.id, :id => 1 99 | assert_response 403 100 | end 101 | 102 | test "should get error notice on update without permission" do 103 | put :update, :project_id => @project.id, :id => 1, :contracts_expense => { :name => 'foo' } 104 | assert_response 403 105 | end 106 | 107 | test "should get error notice on destroy without permission" do 108 | delete :destroy, :project_id => @project.id, :id => 1 109 | assert_response 403 110 | end 111 | 112 | 113 | end 114 | -------------------------------------------------------------------------------- /test/functional/timelog_controller_test.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Redmine - project management software 3 | # Copyright (C) 2006-2012 Jean-Philippe Lang 4 | # 5 | # This program is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either version 2 8 | # of the License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | 19 | require File.expand_path('../../test_helper', __FILE__) 20 | require 'timelog_controller' 21 | 22 | # Re-raise errors caught by the controller. 23 | class TimelogController; def rescue_action(e) raise e end; end 24 | 25 | class TimelogControllerTest < ActionController::TestCase 26 | fixtures :projects, :enabled_modules, :roles, :members, 27 | :member_roles, :issues, :time_entries, :users, 28 | :trackers, :enumerations, :issue_statuses, 29 | :custom_fields, :custom_values, :contracts 30 | 31 | include Redmine::I18n 32 | 33 | def setup 34 | @controller = TimelogController.new 35 | @request = ActionController::TestRequest.new 36 | @response = ActionController::TestResponse.new 37 | @contract = contracts(:contract_three) 38 | @contract2 = contracts(:contract_two) 39 | end 40 | 41 | test "should create time entry if hours is under amount remaining" do 42 | Setting.plugin_contracts = { 43 | 'automatic_contract_creation' => false 44 | } 45 | @request.session[:user_id] = 3 46 | post :create, :project_id => 1, 47 | :time_entry => {:comments => 'Some work on TimelogControllerTest', 48 | # Not the default activity 49 | :activity_id => '11', 50 | :spent_on => '2015-03-14', 51 | :issue_id => '1', 52 | :hours => 1, 53 | :contract_id => @contract.id} 54 | assert_response 302 55 | assert_equal "Successful creation.", flash[:notice] 56 | end 57 | 58 | test "should not create time entry if hours is over amount remaining" do 59 | Setting.plugin_contracts = { 60 | 'automatic_contract_creation' => false 61 | } 62 | entry_hours = @contract.hours_remaining + 1 63 | @request.session[:user_id] = 3 64 | post :create, :project_id => 1, 65 | :time_entry => {:comments => 'Some work on TimelogControllerTest', 66 | # Not the default activity 67 | :activity_id => '11', 68 | :spent_on => '2015-03-14', 69 | :issue_id => '1', 70 | :hours => entry_hours, 71 | :contract_id => @contract.id} 72 | assert_response 200 73 | assert_select("div#errorExplanation", /Hours is invalid. The contract/) 74 | end 75 | 76 | test "a new contract is created automatically" do 77 | Setting.plugin_contracts = { 78 | 'automatic_contract_creation' => true 79 | } 80 | @contract.project_contract_id = 10; 81 | @contract.save 82 | @request.session[:user_id] = 3 83 | entry_hours = @contract.hours_remaining + 1 84 | post :create, :project_id => 1, 85 | :time_entry => {:comments => 'Some work on TimelogControllerTest', 86 | # Not the default activity 87 | :activity_id => '11', 88 | :spent_on => '2015-03-14', 89 | :issue_id => '1', 90 | :hours => entry_hours, 91 | :contract_id => @contract.id} 92 | assert_response 302 93 | assert_equal "Successful creation.", flash[:notice] 94 | assert_match /Your time entry has been split into two entries/, flash[:contract] 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Load the normal Rails helper 2 | require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') 3 | ActiveRecord::FixtureSet.create_fixtures(File.dirname(__FILE__) + '/fixtures/', [:contracts, :user_contract_rates, :user_project_rates]) 4 | -------------------------------------------------------------------------------- /test/unit/contract_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class ContractTest < ActiveSupport::TestCase 4 | fixtures :contracts, :time_entries, :projects, :issues, 5 | :user_contract_rates, :user_project_rates, 6 | :users, :members, :member_roles 7 | 8 | def setup 9 | Setting.plugin_contracts = { 10 | 'automatic_contract_creation' => false 11 | } 12 | @contract = contracts(:contract_one) 13 | @contract2 = contracts(:contract_two) 14 | @project = projects(:projects_001) 15 | @issue = issues(:issues_001) 16 | @time_entry = time_entries(:time_entries_001) 17 | @time_entry.contract_id = @contract.id 18 | @time_entry.project_id = @project.id 19 | @time_entry.issue_id = @issue.id 20 | assert @time_entry.save 21 | @user = @project.users.first 22 | end 23 | 24 | test "should not save without project contract id" do 25 | @contract.project_contract_id = nil 26 | assert !@contract.save 27 | end 28 | 29 | test "project contract id should be unique" do 30 | @contract2.project_id = @contract.project_id 31 | @contract2.project_contract_id = @contract.project_contract_id 32 | assert !@contract2.save 33 | end 34 | 35 | test "project contract id should be more than 0" do 36 | @contract2.project_contract_id = -1 37 | assert !@contract2.save 38 | @contract2.project_contract_id = 0 39 | assert !@contract2.save 40 | end 41 | 42 | test "project contract id should be less than 1000" do 43 | @contract2.project_contract_id = 1000 44 | assert !@contract2.save 45 | end 46 | 47 | test "project contract id should be between 0 and 1000" do 48 | @contract2.project_contract_id = 444 49 | assert @contract2.save 50 | end 51 | 52 | test "should not save without start date" do 53 | @contract.start_date = nil 54 | assert !@contract.save 55 | end 56 | 57 | test "should save without end date" do 58 | @contract.end_date = nil 59 | assert @contract.save 60 | end 61 | 62 | test "should save without agreement date" do 63 | @contract.agreement_date = nil 64 | assert @contract.save 65 | end 66 | 67 | test "should not save without hourly rate" do 68 | @contract.hourly_rate = nil 69 | assert !@contract.save 70 | end 71 | 72 | test "should not save without purchase amount" do 73 | @contract.purchase_amount = nil 74 | assert !@contract.save 75 | end 76 | 77 | test "should not save without project id" do 78 | @contract.project_id = nil 79 | assert !@contract.save 80 | end 81 | 82 | test "agreement date can come after start date" do 83 | @contract.agreement_date = @contract.start_date + 7 84 | assert @contract.save 85 | end 86 | 87 | test "start date should come before end date" do 88 | @contract.end_date = @contract.start_date - 7 89 | assert !@contract.save 90 | end 91 | 92 | test "should have time entries" do 93 | assert_respond_to @contract, "time_entries" 94 | assert_equal @contract.time_entries.count, 1 95 | end 96 | 97 | test "should have members with time entries" do 98 | assert_equal @contract, @time_entry.reload.contract 99 | assert_equal 2, @time_entry.user.id 100 | assert_equal 1, @contract.time_entries.reload.size 101 | assert_equal [@time_entry.user], @contract.members_with_entries 102 | end 103 | 104 | test "should have a user project rate or default rate" do 105 | assert_equal @contract.hourly_rate.to_f, @contract.user_project_rate_or_default(@contract.project.users.first).to_f 106 | end 107 | 108 | test "should calculate total hours spent" do 109 | assert_equal @contract.hours_spent, @time_entry.hours 110 | end 111 | 112 | test "should calculate the billable amount for a contract based upon contractor-specific rates" do 113 | billable = @time_entry.hours * @contract.user_project_rate_or_default(@time_entry.user) 114 | @contract.billable_amount_total = @contract.calculate_billable_amount_total 115 | assert_equal billable, @contract.billable_amount_total 116 | end 117 | 118 | test "should calculate dollar amount remaining for contract" do 119 | @contract.billable_amount_total = @contract.calculate_billable_amount_total 120 | amount_remaining = @contract.purchase_amount - (@contract.billable_amount_total) 121 | assert_equal @contract.amount_remaining, amount_remaining 122 | end 123 | 124 | test "should set rates accessor" do 125 | rates = {"3"=>"27.00", "1"=>"35.00"} 126 | @contract.rates = rates 127 | assert_equal rates, @contract.rates 128 | end 129 | 130 | test "should apply rates to project's user project rates after save" do 131 | assert_equal 2, @project.users.size 132 | rate_hash = {} 133 | @project.users.each do |user| 134 | rate_hash[user.id.to_s] = '25.00' 135 | end 136 | @contract.rates = rate_hash 137 | @contract.project_id = @project.id 138 | assert @contract.save 139 | @project.users.each do |user| 140 | assert_equal 25.00, @project.rate_for_user(user) 141 | end 142 | end 143 | 144 | test "should have many user contract rates" do 145 | assert_respond_to @contract, :user_contract_rates 146 | assert_not_nil @user 147 | assert_equal 0, @contract.user_contract_rates.size 148 | ucr = @contract.user_contract_rates.create!(:user_id => @user.id, :rate => 37.50) 149 | assert_equal @user, ucr.user 150 | assert_equal 37.50, ucr.rate 151 | end 152 | 153 | test "should get a user project rate by user" do 154 | assert_not_nil @user 155 | ucr = @contract.user_contract_rates.create!(:user_id => @user.id, :rate => 48.00) 156 | assert_equal ucr, @contract.user_contract_rate_by_user(@user) 157 | end 158 | 159 | test "should get a rate for a user" do 160 | assert_not_nil @user 161 | @contract.user_contract_rates.create!(:user_id => @user.id, :rate => 25.00) 162 | assert_equal 25.00, @contract.rate_for_user(@user) 163 | end 164 | 165 | test "should set a user rate" do 166 | assert_not_nil @user 167 | assert_equal 0, @contract.user_contract_rates.size 168 | assert_nil @contract.user_contract_rate_by_user(@user) 169 | @contract.set_user_contract_rate(@user, 37.25) 170 | assert_equal 37.25, @contract.rate_for_user(@user) 171 | end 172 | 173 | test "should get a sum of contract expenses" do 174 | assert_equal 0, @contract.contracts_expenses.size 175 | assert_equal 0.0, @contract.expenses_total 176 | 2.times do 177 | ContractsExpense.create!(:name => 'Foo', :expense_date => '2013-05-15', :amount => 1.11, :contract_id => @contract.id) 178 | end 179 | assert_equal 2, @contract.contracts_expenses.reload.size 180 | assert_equal 2.22, @contract.expenses_total 181 | end 182 | 183 | end 184 | -------------------------------------------------------------------------------- /test/unit/contracts_expense_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class ContractsExpenseTest < ActiveSupport::TestCase 4 | fixtures :contracts, :issues 5 | 6 | def setup 7 | Setting.plugin_contracts = { 8 | 'automatic_contract_creation' => false 9 | } 10 | @contract = contracts(:contract_one) 11 | #@project = projects(:projects_001) 12 | @issue = issues(:issues_001) 13 | #@user = @project.users.first 14 | @expense = build_valid_expense 15 | end 16 | 17 | test "should save a valid expense" do 18 | assert @expense.save 19 | end 20 | 21 | test "should not save without name" do 22 | @expense.name = '' 23 | assert !@expense.save 24 | end 25 | 26 | test "should not save without expense date" do 27 | @expense.expense_date = nil 28 | assert !@expense.save 29 | end 30 | 31 | test "should not save without amount" do 32 | @expense.amount = nil 33 | assert !@expense.save 34 | end 35 | 36 | test "should not save without a contract" do 37 | @expense.contract_id = nil 38 | assert !@expense.save 39 | end 40 | 41 | test "should not save unless amount is greater than zero" do 42 | @expense.amount = -0.01 43 | assert !@expense.save 44 | @expense.amount = 0.01 45 | assert @expense.save 46 | end 47 | 48 | def build_valid_expense 49 | ContractsExpense.new(:name => 'Domain name purchase', :expense_date => Date.today, :amount => 12.98, :contract_id => @contract.id) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/unit/project_test.rb: -------------------------------------------------------------------------------- 1 | # Redmine - project management software 2 | # Copyright (C) 2006-2012 Jean-Philippe Lang 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | 18 | require File.expand_path('../../test_helper', __FILE__) 19 | 20 | class ProjectTest < ActiveSupport::TestCase 21 | fixtures :projects, :contracts, :time_entries, :user_project_rates, 22 | :user_contract_rates, :users, :members, :enabled_modules 23 | 24 | def setup 25 | Setting.plugin_contracts = { 26 | 'automatic_contract_creation' => false 27 | } 28 | @project = projects(:projects_001) 29 | @parent_project = projects(:projects_003) 30 | @sub_subproject = projects(:projects_004) 31 | @contract = contracts(:contract_one) 32 | @contract2 = contracts(:contract_two) 33 | @time_entry1 = time_entries(:time_entries_001) 34 | @time_entry2 = time_entries(:time_entries_004) 35 | @time_entry3 = time_entries(:time_entries_005) 36 | @contract.project_id = @project.id 37 | @contract2.project_id = @project.id 38 | @contract.save 39 | @contract2.save 40 | @sub_subproject.parent_id = @parent_project.id 41 | @sub_subproject.save 42 | @project.time_entries.clear 43 | @project.time_entries.append(@time_entry1) 44 | @project.save 45 | @time_entry3.project_id = @sub_subproject.id 46 | @time_entry3.save 47 | @user = @project.users.first 48 | end 49 | 50 | test "should have many contracts" do 51 | assert_respond_to @project, "contracts" 52 | end 53 | 54 | test "should calculate amount purchased across all contracts" do 55 | assert_equal @project.total_amount_purchased, @project.contracts.map(&:purchase_amount).inject(0, &:+) 56 | end 57 | 58 | test "should calculate approximate hours purchased across all contracts" do 59 | assert_equal @project.total_hours_purchased, @project.contracts.map(&:hours_purchased).inject(0, &:+) 60 | end 61 | 62 | test "should calculate amount remaining across all contracts" do 63 | assert_equal @project.total_amount_remaining, @project.contracts.map(&:amount_remaining).inject(0, &:+) 64 | end 65 | 66 | test "should calculate hours remaining across all contracts" do 67 | assert_equal @project.total_hours_remaining, @project.contracts.map(&:hours_remaining).inject(0, &:+) 68 | end 69 | 70 | test "should get contracts for all ancestor projects" do 71 | @contract2.project_id = @parent_project.id 72 | @contract2.save 73 | assert_equal 3, @sub_subproject.contracts_for_all_ancestor_projects.count 74 | end 75 | 76 | test "should get all time entries for current project and all descendent projects" do 77 | time_entries = @project.time_entries_for_all_descendant_projects 78 | assert_equal 3, time_entries.count 79 | assert time_entries.include?(@time_entry1) 80 | assert time_entries.include?(@time_entry2) 81 | assert time_entries.include?(@time_entry3) 82 | end 83 | 84 | test "should have many user project rates" do 85 | assert_not_nil @user 86 | @project.set_user_rate(@user, 25.00) 87 | assert_operator @project.user_project_rates.size, :>=, 1 88 | end 89 | 90 | test "should get a user project rate by user" do 91 | assert_not_nil @user 92 | upr = @project.set_user_rate(@user, 25.00) 93 | assert_equal upr, @project.user_project_rate_by_user(@user) 94 | end 95 | 96 | test "should get a rate for a user" do 97 | assert_not_nil @user 98 | @project.set_user_rate(@user, 25.00) 99 | assert_equal 25.00, @project.rate_for_user(@user) 100 | end 101 | 102 | test "should set a user rate" do 103 | assert_not_nil @user 104 | # check the value is not already set 105 | assert_not_equal 37.25, @project.rate_for_user(@user) 106 | @project.set_user_rate(@user, 37.25) 107 | assert_equal 37.25, @project.rate_for_user(@user) 108 | end 109 | 110 | end 111 | -------------------------------------------------------------------------------- /test/unit/time_entry_test.rb: -------------------------------------------------------------------------------- 1 | # Redmine - project management software 2 | # Copyright (C) 2006-2012 Jean-Philippe Lang 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | 18 | require File.expand_path('../../test_helper', __FILE__) 19 | 20 | class TimeEntryTest < ActiveSupport::TestCase 21 | fixtures :issues, :projects, :users, :time_entries, 22 | :members, :roles, :member_roles, 23 | :trackers, :issue_statuses, 24 | :journals, :journal_details, 25 | :issue_categories, :enumerations, 26 | :groups_users, 27 | :enabled_modules, 28 | :workflows, 29 | :contracts 30 | 31 | test "should have a contract attribute" do 32 | time_entry = TimeEntry.new 33 | assert_respond_to time_entry, "contract" 34 | end 35 | 36 | test "should not save if exceeds remaining contract time" do 37 | Setting.plugin_contracts = { 38 | 'automatic_contract_creation' => false 39 | } 40 | @project = projects(:projects_001) 41 | @user = users(:users_004) 42 | @contract = contracts(:contract_one) 43 | new_time_entry = TimeEntry.new 44 | new_time_entry.project_id = @project.id 45 | new_time_entry.user_id = @user.id 46 | new_time_entry.hours = @contract.hours_remaining + 5 47 | new_time_entry.contract_id = @contract.id 48 | assert !new_time_entry.save, "Saved the entry exceeding the remaining contract time" 49 | assert_match /is invalid. The contract #{@contract.title} only has #{"%.2f" % @contract.hours_remaining} hours remaining. Ask your administrator to enable auto contract creation in contract settings./, 50 | new_time_entry.errors.messages[:hours].to_s 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/unit/user_contract_rate_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class UserContractRateTest < ActiveSupport::TestCase 4 | fixtures :projects, :contracts, :time_entries, :user_contract_rates 5 | 6 | def setup 7 | Setting.plugin_contracts = { 8 | 'automatic_contract_creation' => false 9 | } 10 | @project = projects(:projects_001) 11 | @contract = contracts(:contract_one) 12 | @user = @project.users.first 13 | @user_contract_rate = UserContractRate.create!(:contract => @contract, :user => @user) 14 | end 15 | 16 | test "should belong to a user" do 17 | assert_respond_to @user_contract_rate, :user 18 | assert_equal @user, @user_contract_rate.user 19 | end 20 | 21 | test "should belong to a contract" do 22 | assert_respond_to @user_contract_rate, :contract 23 | assert_equal @contract, @user_contract_rate.contract 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/unit/user_project_rate_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class UserProjectRateTest < ActiveSupport::TestCase 4 | fixtures :projects, :contracts, :time_entries, :user_project_rates 5 | 6 | def setup 7 | Setting.plugin_contracts = { 8 | 'automatic_contract_creation' => false 9 | } 10 | @project = projects(:projects_001) 11 | @user = @project.users.first 12 | @user_project_rate = UserProjectRate.create!(:project => @project, :user => @user) 13 | end 14 | 15 | test "should belong to a user" do 16 | assert_respond_to @user_project_rate, :user 17 | assert_equal @user, @user_project_rate.user 18 | end 19 | 20 | test "should belong to a project" do 21 | assert_respond_to @user_project_rate, :project 22 | assert_equal @project, @user_project_rate.project 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /test/unit/user_test.rb: -------------------------------------------------------------------------------- 1 | # Redmine - project management software 2 | # Copyright (C) 2006-2012 Jean-Philippe Lang 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | 18 | require File.expand_path('../../test_helper', __FILE__) 19 | 20 | class UserTest < ActiveSupport::TestCase 21 | fixtures :projects, :contracts, :time_entries, :user_project_rates, :users 22 | 23 | def setup 24 | Setting.plugin_contracts = { 25 | 'automatic_contract_creation' => false 26 | } 27 | @project = projects(:projects_001) 28 | @contract = contracts(:contract_one) 29 | @contract.project_id = @project.id 30 | @contract.save 31 | @user = @project.users.first 32 | end 33 | 34 | test "should have many user project rates" do 35 | assert_respond_to @user, :user_project_rates 36 | end 37 | 38 | test "should have many user contract rates" do 39 | assert_respond_to @user, :user_contract_rates 40 | end 41 | 42 | test "should add a new user project rate" do 43 | assert_not_nil @user 44 | if upr = @project.user_project_rate_by_user(@user) 45 | upr.destroy 46 | end 47 | assert_nil @project.user_project_rate_by_user(@user) 48 | upr = @project.user_project_rates.create!(:user_id => @user.id, :rate => 25.00) 49 | assert_equal [upr], @project.user_project_rates 50 | assert_equal @user, upr.user 51 | assert_equal 25.00, upr.rate 52 | end 53 | 54 | end 55 | --------------------------------------------------------------------------------