338 lines
9.2 KiB
Ruby
338 lines
9.2 KiB
Ruby
class Contract < ActiveRecord::Base
|
|
unloadable
|
|
extend ActiveSupport::Memoizable
|
|
|
|
ViewPrecision = 0
|
|
|
|
# Associations
|
|
belongs_to :project
|
|
belongs_to :account_executive, :class_name => 'User', :foreign_key => 'account_executive_id'
|
|
belongs_to :payment_term, :class_name => "PaymentTerm", :foreign_key => "payment_term_id"
|
|
has_many :deliverables, :dependent => :destroy
|
|
|
|
# Validations
|
|
validates_presence_of :name
|
|
validates_presence_of :account_executive
|
|
validates_presence_of :project
|
|
validates_presence_of :start_date
|
|
validates_presence_of :end_date
|
|
validates_inclusion_of :discount_type, :in => %w($ %), :allow_blank => true, :allow_nil => true
|
|
validates_inclusion_of :status, :in => ["open","locked","closed"], :allow_blank => true, :allow_nil => true
|
|
validate :start_and_end_date_are_valid
|
|
validate_on_update :validate_status_changes
|
|
|
|
# Accessors
|
|
attr_accessible :name
|
|
attr_accessible :account_executive_id
|
|
attr_accessible :start_date
|
|
attr_accessible :end_date
|
|
attr_accessible :executed
|
|
attr_accessible :billable_rate
|
|
attr_accessible :discount
|
|
attr_accessible :discount_note
|
|
attr_accessible :payment_term_id
|
|
attr_accessible :client_ap_contact_information
|
|
attr_accessible :po_number
|
|
attr_accessible :client_point_of_contact
|
|
attr_accessible :details
|
|
attr_accessible :status
|
|
|
|
named_scope :by_name, {:order => "#{Contract.table_name}.name ASC"}
|
|
named_scope :with_status, lambda {|statuses|
|
|
{
|
|
:conditions => ["#{Contract.table_name}.status IN (?)", statuses]
|
|
}
|
|
}
|
|
|
|
[:contract_type,
|
|
:discount_spent, :discount_budget
|
|
].each do |mthd|
|
|
define_method(mthd) { "TODO in later release" }
|
|
end
|
|
|
|
def status
|
|
read_attribute(:status) || "open"
|
|
end
|
|
|
|
def lock!
|
|
update_attribute(:status, "locked")
|
|
end
|
|
|
|
def close!
|
|
update_attribute(:status, "closed")
|
|
end
|
|
|
|
def open?
|
|
self.status == "open"
|
|
end
|
|
|
|
def locked?
|
|
self.status == "locked"
|
|
end
|
|
|
|
def closed?
|
|
self.status == "closed"
|
|
end
|
|
|
|
def includes_deliverable_id?(deliverable_id)
|
|
deliverable_ids.include?(deliverable_id.to_i)
|
|
end
|
|
|
|
# ------------------------------------------------------------
|
|
# Labor Methods
|
|
# ------------------------------------------------------------
|
|
|
|
# Currency value that is budgeted for the contract for labor
|
|
# ie. estimated billable amount
|
|
#
|
|
# OPTIMIZE: N+1
|
|
def labor_budget
|
|
summarize_associated_values(deliverables, :labor_budget_total)
|
|
end
|
|
memoize :labor_budget
|
|
|
|
# Currency value that is spent for the contract on labor
|
|
# ie. actual billable cost
|
|
#
|
|
# OPTIMIZE: N+1
|
|
# OPTIMIZE: also hits redmine_overhead which is known to be slow
|
|
def labor_spent
|
|
summarize_associated_values(deliverables, :labor_budget_spent)
|
|
end
|
|
memoize :labor_spent
|
|
|
|
# Hours budgeted for the contract for labor
|
|
# ie. estimated billable hours
|
|
#
|
|
# OPTIMIZE: N+1
|
|
def labor_hour_budget
|
|
summarize_associated_values(deliverables, :labor_budget_hours)
|
|
end
|
|
memoize :labor_hour_budget
|
|
|
|
# Hours spent for the contract on labor
|
|
# ie. actual billable time worked
|
|
#
|
|
# OPTIMIZE: N+1
|
|
# OPTIMIZE: also hits redmine_overhead which is known to be slow
|
|
def labor_hour_spent
|
|
summarize_associated_values(deliverables, :labor_hours_spent_total)
|
|
end
|
|
memoize :labor_hour_spent
|
|
|
|
# ------------------------------------------------------------
|
|
# Overhead Methods
|
|
# ------------------------------------------------------------
|
|
|
|
# Currency value budgeted for the contract on overhead
|
|
# ie. estimated non-billable amount
|
|
#
|
|
# OPTIMIZE: N+1
|
|
def overhead_budget
|
|
summarize_associated_values(deliverables, :overhead_budget_total)
|
|
end
|
|
memoize :overhead_budget
|
|
|
|
# Currency value spent for the contract on overhead work
|
|
# ie. actual non-billable used
|
|
#
|
|
# OPTIMIZE: N+1
|
|
# OPTIMIZE: also hits redmine_overhead which is known to be slow
|
|
def overhead_spent
|
|
summarize_associated_values(deliverables, :overhead_spent)
|
|
end
|
|
memoize :overhead_spent
|
|
|
|
# Hours budgeted for the contract for overhead
|
|
# ie. estimated non-billable time
|
|
#
|
|
# OPTIMIZE: N+1
|
|
def overhead_hour_budget
|
|
summarize_associated_values(deliverables, :overhead_budget_hours)
|
|
end
|
|
memoize :overhead_hour_budget
|
|
|
|
# Hours used for the contract on overhead
|
|
# ie. actual time spent on non-billable work
|
|
#
|
|
# OPTIMIZE: N+1
|
|
# OPTIMIZE: also hits redmine_overhead which is known to be slow
|
|
def overhead_hour_spent
|
|
summarize_associated_values(deliverables, :overhead_hours_spent_total)
|
|
end
|
|
memoize :overhead_hour_spent
|
|
|
|
# ------------------------------------------------------------
|
|
# Total Methods (labor + overhead)
|
|
# ------------------------------------------------------------
|
|
|
|
# Total hours budgeted for the contract
|
|
# ie. total time estimated
|
|
#
|
|
# OPTIMIZE: N+1
|
|
def estimated_hour_budget
|
|
summarize_associated_values(deliverables, :estimated_hour_budget_total)
|
|
end
|
|
memoize :estimated_hour_budget
|
|
|
|
# Total hours spent on the contract
|
|
# ie. hours used
|
|
#
|
|
# OPTIMIZE: N+1
|
|
def estimated_hour_spent
|
|
summarize_associated_values(deliverables, :hours_spent_total)
|
|
end
|
|
memoize :estimated_hour_spent
|
|
|
|
# Currency amount budgeted for the contract
|
|
# ie. estimated budget
|
|
#
|
|
# OPTIMIZE: N+1
|
|
def total_budget
|
|
summarize_associated_values(deliverables, :total)
|
|
end
|
|
memoize :total_budget
|
|
|
|
# Currency amount spent on the contract
|
|
# ie. amount spent already
|
|
#
|
|
# OPTIMIZE: N+1
|
|
def total_spent
|
|
summarize_associated_values(deliverables, :total_spent)
|
|
end
|
|
memoize :total_spent
|
|
|
|
# ------------------------------------------------------------
|
|
# Profit Methods
|
|
# ------------------------------------------------------------
|
|
|
|
# Estimated currency amount of profit
|
|
# ie. profit estimate
|
|
#
|
|
# OPTIMIZE: N+1
|
|
def profit_budget
|
|
summarize_associated_values(deliverables, :profit_budget)
|
|
end
|
|
memoize :profit_budget
|
|
|
|
# Amount of the profit that is left in the contract
|
|
#
|
|
# OPTIMIZE: N+1
|
|
def profit_left
|
|
summarize_associated_values(deliverables, :profit_left)
|
|
end
|
|
alias_method :profit_spent, :profit_left
|
|
memoize :profit_left
|
|
|
|
# ------------------------------------------------------------
|
|
# Fixed Budget Methods
|
|
# ------------------------------------------------------------
|
|
|
|
# Currency amount of estimated fixed expenses
|
|
#
|
|
# OPTIMIZE: N+1
|
|
def fixed_budget
|
|
summarize_associated_values(deliverables, :fixed_budget_total)
|
|
end
|
|
memoize :fixed_budget
|
|
|
|
# Currency amount of spent fixed expenses
|
|
#
|
|
# OPTIMIZE: N+1
|
|
def fixed_spent
|
|
summarize_associated_values(deliverables, :fixed_budget_total_spent)
|
|
end
|
|
memoize :fixed_spent
|
|
|
|
# Currency amount of estimated fixed expense markups
|
|
# ie. estimated fixed expense markups
|
|
#
|
|
# OPTIMIZE: N+1
|
|
def fixed_markup_budget
|
|
summarize_associated_values(deliverables, :fixed_markup_budget_total)
|
|
end
|
|
memoize :fixed_markup_budget
|
|
|
|
# Currency amount of fixed expenses spent
|
|
#
|
|
# OPTIMIZE: N+1
|
|
def fixed_markup_spent
|
|
summarize_associated_values(deliverables, :fixed_markup_budget_total_spent)
|
|
end
|
|
memoize :fixed_markup_spent
|
|
|
|
# ------------------------------------------------------------
|
|
|
|
def after_initialize
|
|
self.executed = false unless self.executed.present?
|
|
self.status = "open" unless self.status.present?
|
|
end
|
|
|
|
# Are the start_date and end_date valid?
|
|
def start_and_end_date_are_valid
|
|
if start_date && end_date && end_date < start_date
|
|
errors.add :end_date, :greater_than_start_date
|
|
end
|
|
end
|
|
|
|
def valid_status_change?
|
|
change_to_status_only? || changing_to_the_open_status? || changing_from_the_open_status?
|
|
end
|
|
|
|
def change_to_status_only?
|
|
["status"] == changes.keys
|
|
end
|
|
|
|
def changing_to_the_open_status?
|
|
changes["status"].present? && "open" == changes["status"].second
|
|
end
|
|
|
|
def changing_from_the_open_status?
|
|
changes["status"].present? && "open" == changes["status"].first
|
|
end
|
|
|
|
# TODO: duplicated on Deliverable, refactor after one more duplication
|
|
def validate_status_changes
|
|
return if valid_status_change?
|
|
|
|
errors.add_to_base(:cant_update_locked_contract) if locked?
|
|
errors.add_to_base(:cant_update_closed_contract) if closed?
|
|
end
|
|
|
|
# Currency amount of time that is logged to the project or to issues
|
|
# that are not assigned to a Deliverable
|
|
def orphaned_time
|
|
@orphaned_time ||= project.time_entries.all(:include => [:issue => :deliverable],
|
|
:conditions => "#{Issue.table_name}.deliverable_id IS NULL OR #{TimeEntry.table_name}.issue_id IS NULL").inject(0) do |total, time_entry|
|
|
total += time_entry.cost
|
|
end
|
|
end
|
|
|
|
def to_s
|
|
name
|
|
end
|
|
|
|
if Rails.env.test?
|
|
generator_for :name, :start => "Contract 0000"
|
|
generator_for :executed => true
|
|
generator_for(:start_date) { Date.yesterday }
|
|
generator_for(:end_date) { Date.tomorrow }
|
|
generator_for :discount, ''
|
|
generator_for :details, ''
|
|
generator_for :discount_note, ''
|
|
generator_for :client_point_of_contact, ''
|
|
generator_for :client_ap_contact_information, ''
|
|
generator_for :po_number, ''
|
|
generator_for :status, 'open'
|
|
|
|
end
|
|
|
|
private
|
|
|
|
# This is a potential N+1 method since value_method might be calculated
|
|
def summarize_associated_values(records, value_method)
|
|
records.inject(0) {|total, record| total += record.send(value_method)}
|
|
end
|
|
|
|
end
|