class Deliverable < ActiveRecord::Base unloadable ViewPrecision = 2 # Associations belongs_to :contract belongs_to :manager, :class_name => 'User', :foreign_key => 'manager_id' has_many :labor_budgets has_many :overhead_budgets has_many :fixed_budgets has_many :issues, :dependent => :nullify accepts_nested_attributes_for :labor_budgets accepts_nested_attributes_for :overhead_budgets accepts_nested_attributes_for :fixed_budgets # Validations validates_presence_of :title validates_presence_of :type validates_presence_of :manager validates_inclusion_of :status, :in => ["open","locked","closed"], :allow_blank => true, :allow_nil => true validate_on_update :validate_status_changes validate :validate_contract_status # Accessors include DollarizedAttribute dollarized_attribute :total delegate :name, :to => :contract, :prefix => true, :allow_nil => true delegate "open?", :to => :contract, :prefix => true, :allow_nil => true delegate "closed?", :to => :contract, :prefix => true, :allow_nil => true delegate "locked?", :to => :contract, :prefix => true, :allow_nil => true delegate :project, :to => :contract, :allow_nil => true # Callbacks before_destroy :block_on_locked_contracts before_destroy :block_on_closed_contracts def after_initialize self.status = "open" unless self.status.present? end # Register callbacks here, on new records the class isn't set so class-specific # callbacks don't fire. def after_save if type == "RetainerDeliverable" self.becomes(self.type.constantize).create_budgets_for_periods end end named_scope :by_title, {:order => "#{Deliverable.table_name}.title ASC"} named_scope :with_status, lambda {|statuses| { :conditions => ["#{Deliverable.table_name}.status IN (?)", statuses] } } def short_type '' end def humanize_type type.to_s.sub('Deliverable','') end # Deliverable's aren't dated. Subclasses may override this for period behavior. def current_date nil end def lock! update_attribute(:status, "locked") end def close! update_attribute(:status, "closed") end def open? self.status == "open" end def locked? self.status == "locked" end def closed? self.status == "closed" end def editable? (new_record? || open?) end def valid_status_change? change_to_status_only? || changing_to_the_open_status? || changing_from_the_open_status? end def change_to_status_only? ["status"] == changes.keys end def changing_to_the_open_status? changes["status"].present? && "open" == changes["status"].second end def changing_from_the_open_status? changes["status"].present? && "open" == changes["status"].first end # TODO: duplicated on Contract, refactor after one more duplication def validate_status_changes return if valid_status_change? errors.add_to_base(:cant_update_locked_deliverable) if locked? errors.add_to_base(:cant_update_closed_deliverable) if closed? end def validate_contract_status return if contract_open? return if change_to_status_only? if contract_locked? if new_record? errors.add_to_base(:cant_create_deliverable_on_locked_contract) else errors.add_to_base(:cant_update_locked_contract) end end if contract_closed? if new_record? errors.add_to_base(:cant_create_deliverable_on_closed_contract) else errors.add_to_base(:cant_update_closed_contract) end end end # No operation method, useful to clean up logic with an optional message # for documentation def noop(message="") end def block_on_locked_contracts !contract_locked? end def block_on_closed_contracts !contract_closed? end def to_s title end def to_underscore self.class.to_s.underscore end def labor_budget_total(date=nil) memoize_by_date("@labor_budget_total", date) do labor_budgets.sum(:budget) end end def overhead_budget_total(date=nil) memoize_by_date("@overhead_budget_total", date) do overhead_budgets.sum(:budget) end end # The amount of profit that is budgeted for this deliverable. # Profit = Total - ( Labor + Overhead + Fixed + Markup ) def profit_budget(date=nil) memoize_by_date("@profit_budget", date) do budgets = labor_budget_total(date) + overhead_budget_total(date) + fixed_budget_total(date) + fixed_markup_budget_total(date) (total(date) || 0.0) - budgets end end # The amount of money remaining after expenses have been taken out # Profit left = Total - Labor spent - Overhead spent - Fixed - Markup def profit_left(date=nil) memoize_by_date("@profit_left", date) do total_spent(date) - labor_budget_spent(date) - overhead_spent(date) - fixed_budget_total_spent(date) - fixed_markup_budget_total_spent(date) end end def labor_budget_hours(date=nil) memoize_by_date("@labor_budget_hours", date) do labor_budgets.sum(:hours) end end def overhead_budget_hours(date=nil) memoize_by_date("@overhead_budget_hours", date) do overhead_budgets.sum(:hours) end end # Total number of hours estimated in the Deliverable's budgets def estimated_hour_budget_total(date=nil) memoize_by_date("@estimated_hour_budget_total", date) do labor_budget_hours(date) + overhead_budget_hours(date) end end # OPTIMIZE: N+1 def labor_hours_spent_total(date=nil) memoize_by_date("@labor_hours_spent_total", date) do issues.inject(0) {|total, issue| total += issue.billable_time_spent } # From redmine_overhead end end # OPTIMIZE: N+1 def overhead_hours_spent_total(date=nil) memoize_by_date("@overhead_hours_spent_total", date) do issues.inject(0) {|total, issue| total += issue.overhead_time_spent } # From redmine_overhead end end def hours_spent_total(date=nil) return 0 if issues.empty? # Don't count subissues TimeEntry.sum(:hours, :conditions => { :issue_id => issues.collect(&:id) }) end def fixed_budget_total(date=nil) memoize_by_date("@fixed_budget_total", date) do fixed_budgets.sum(:budget) end end def fixed_budget_total_spent(date=nil) memoize_by_date("@fixed_budget_total_spent", date) do fixed_budgets.paid.sum(:budget) end end # OPTIMIZE: N+1 def fixed_markup_budget_total(date=nil) memoize_by_date("@fixed_markup_budget_total", date) do fixed_budgets.inject(0) {|total, fixed_budget| total += fixed_budget.markup_value } end end # OPTIMIZE: N+1 def fixed_markup_budget_total_spent(date=nil) memoize_by_date("@fixed_markup_budget_total_spent", date) do fixed_budgets.paid.inject(0) {|total, fixed_budget| total += fixed_budget.markup_value } end end def filter_by_date(date=nil, &block) block.call end def issues_by_status issues.inject({}) {|grouped, issue| grouped[issue.status] ||= [] grouped[issue.status] << issue grouped } end def retainer? type == "RetainerDeliverable" end def self.valid_types ['FixedDeliverable','HourlyDeliverable','RetainerDeliverable'] end def self.valid_types_to_select valid_types.inject([]) do |types, type| types << [type.gsub(/Deliverable/i,''), type] types end end # Required attribute for AAJ's JournalFormatter def name title end # Accessors from the budget plugin that need to be wrapped def subject warn "[DEPRECATION] Deliverable#subject is deprecated. Please use Deliverable#title instead." title end def due warn "[DEPRECATION] Deliverable#due is deprecated. Please use Deliverable#end_date instead." end_date end def hours_used warn "[DEPRECATION] Deliverable#hours_used is deprecated. Please use Deliverable#hours_spent_total instead." hours_spent_total end def spent warn "[DEPRECATION] Deliverable#spent is deprecated. Please use Deliverable#total_spent instead." total_spent end def total_hours warn "[DEPRECATION] Deliverable#total_hours is deprecated. Please use Deliverable#estimated_hour_budget_total instead." estimated_hour_budget_total end def labor_budget warn "[DEPRECATION] Deliverable#labor_budget is deprecated. Please use Deliverable#labor_budget_total instead." labor_budget_total end if Rails.env.test? generator_for :title, :method => :next_title generator_for :status, 'open' def self.next_title @last_title ||= 'Deliverable 0000' @last_title.succ! end end private def memoize_by_date(ivar, date, &block) cache_hash = instance_variable_get(ivar) cache_hash ||= {} if date if date.is_a?(Date) cache_key = "#{date.year}-#{date.month}" else cache_key = :invalid end else cache_key = :all end cache_hash[cache_key] ||= block.call instance_variable_set(ivar, cache_hash) cache_hash[cache_key] end end