Files
redmine_contracts/app/models/retainer_deliverable.rb
2010-10-13 15:18:52 -07:00

490 lines
15 KiB
Ruby

# A RetainerDeliverable is an HourlyDeliverable that is renewed at
# regular calendar periods. The Company bills a regular number of
# hours for a hourly rate whereby the budgets are reset over a
# regular cyclical period (monthly).
class RetainerDeliverable < HourlyDeliverable
unloadable
# Associations
# Validations
# Accessors
# Callbacks
before_update :check_for_extended_period
before_update :check_for_shrunk_period
def short_type
'R'
end
def current_date
Date.today
end
def current_period
current_date.strftime("%B %Y")
end
def beginning_date
start_date && start_date.beginning_of_month.to_date
end
def ending_date
end_date && end_date.end_of_month.to_date
end
def date_range
if beginning_date && ending_date && beginning_date <= ending_date
(beginning_date..ending_date)
else
[]
end
end
def within_date_range?(date)
date_range.include?(date)
end
# period in the format of "%Y-%m" or "%B %Y"
def within_period_range?(period)
begin
# both valid formats work by adding a day to the end like -01
date = Date.parse(period.to_s + "-01")
within_date_range?(date)
rescue ArgumentError
return false
end
end
def months
month_acc = []
current_date = beginning_date
return [] if current_date.nil? || ending_date.nil?
while current_date < ending_date do
month_acc << current_date
current_date = current_date.advance(:months => 1)
end
month_acc
end
# Returns the months used by the Deliverable that are before date
def months_before_date(date)
months.select {|m| m < date }
end
# Returns the months used by the Deliverable that are after date
def months_after_date(date)
months.select {|m| m > date }
end
def labor_budgets_for_date(date)
budgets = labor_budgets.all(:conditions => {:year => date.year, :month => date.month})
budgets = [labor_budgets.build(:year => date.year, :month => date.month)] if budgets.empty?
budgets
end
def overhead_budgets_for_date(date)
budgets = overhead_budgets.all(:conditions => {:year => date.year, :month => date.month})
budgets = [overhead_budgets.build(:year => date.year, :month => date.month)] if budgets.empty?
budgets
end
def fixed_budgets_for_date(date)
budgets = fixed_budgets.all(:conditions => {:year => date.year, :month => date.month})
budgets = [fixed_budgets.build(:year => date.year, :month => date.month)] if budgets.empty?
budgets
end
def labor_budget_total(date=nil)
case scope_date_status(date)
when :in
memoize_by_date("@labor_budget_total", date) do
labor_budgets.sum(:budget, :conditions => {:year => date.year, :month => date.month})
end
when :out
0
else
super
end
end
def overhead_budget_total(date=nil)
case scope_date_status(date)
when :in
memoize_by_date("@overhead_budget_total", date) do
overhead_budgets.sum(:budget, :conditions => {:year => date.year, :month => date.month})
end
when :out
0
else
super
end
end
def labor_budget_hours(date=nil)
case scope_date_status(date)
when :in
memoize_by_date("@labor_budget_hours", date) do
labor_budgets.sum(:hours, :conditions => {:year => date.year, :month => date.month})
end
when :out
0
else
super
end
end
def labor_hours_spent_total(date=nil)
case scope_date_status(date)
when :in
memoize_by_date("@labor_hours_spent_total", date) do
time_entries = issues.collect {|issue| issue.time_entries.all(:conditions => {:tyear => date.year, :tmonth => date.month}) }.flatten
billable_hours_on_time_entries(time_entries)
end
when :out
0
else
super
end
end
def overhead_hours_spent_total(date=nil)
case scope_date_status(date)
when :in
memoize_by_date("@overhead_hours_spent_total", date) do
time_entries = issues.collect {|issue| issue.time_entries.all(:conditions => {:tyear => date.year, :tmonth => date.month}) }.flatten
nonbillable_hours_on_time_entries(time_entries)
end
when :out
0
else
super
end
end
def hours_spent_total(date=nil)
case scope_date_status(date)
when :in
return 0 if issues.empty?
TimeEntry.sum(:hours, :conditions => {
:issue_id => issues.collect(&:id),
:tyear => date.year,
:tmonth => date.month
})
when :out
0
else
super
end
end
def fixed_budget_total(date=nil)
case scope_date_status(date)
when :in
memoize_by_date("@fixed_budget_total", date) do
fixed_budgets.sum(:budget, :conditions => {:year => date.year, :month => date.month})
end
when :out
0
else
super
end
end
def fixed_budget_total_spent(date=nil)
case scope_date_status(date)
when :in
memoize_by_date("@fixed_budget_total_spent", date) do
fixed_budgets.paid.sum(:budget, :conditions => {:year => date.year, :month => date.month})
end
when :out
0
else
super
end
end
def fixed_markup_budget_total(date=nil)
case scope_date_status(date)
when :in
memoize_by_date("@fixed_markup_budget_total", date) do
fixed_budgets.
all(:conditions => {:year => date.year, :month => date.month}).
inject(0) {|total, fixed_budget| total += fixed_budget.markup_value }
end
when :out
0
else
super
end
end
def fixed_markup_budget_total_spent(date=nil)
case scope_date_status(date)
when :in
memoize_by_date("@fixed_markup_budget_total_spent", date) do
fixed_budgets.
paid.
all(:conditions => {:year => date.year, :month => date.month}).
inject(0) {|total, fixed_budget| total += fixed_budget.markup_value }
end
when :out
0
else
super
end
end
def total_spent(date=nil)
case scope_date_status(date)
when :in
# TODO: duplicated on HourlyDeliverable#total_spent
memoize_by_date("@total_spent", date) do
return 0 if contract.nil?
return 0 if contract.billable_rate.blank?
return 0 unless self.issues.count > 0
issue_ids = self.issues.collect(&:id)
time_logs = time_entries_for_date_and_issue_ids(date, issue_ids)
hours = billable_hours_on_time_entries(time_logs)
fixed_budget_amount = fixed_budget_total_spent(date) + fixed_markup_budget_total_spent(date)
return (hours * contract.billable_rate) + fixed_budget_amount
end
when :out
0
else
super
end
end
# TODO: stolen directly from redmine_overhead but with a block option
def labor_budget_spent_with_filter(&block)
return 0.0 unless self.issues.size > 0
total = 0.0
# Get all timelogs assigned
if block_given?
time_logs = block.call
else
time_logs = self.issues.collect(&:time_entries).flatten
end
return time_logs.collect {|time_log|
if time_log.billable?
time_log.cost
else
0.0
end
}.sum
end
# TODO: stolen directly from redmine_overhead but with a block option
def overhead_spent_with_filter(&block)
if block_given?
time_logs = block.call
else
time_logs = issues.collect(&:time_entries).flatten
end
return time_logs.collect {|time_entry|
if time_entry.billable?
0
else
time_entry.cost
end
}.sum
end
def labor_budget_spent(date=nil)
case scope_date_status(date)
when :in
memoize_by_date("@labor_budget_spent", date) do
labor_budget_spent_with_filter do
issue_ids = self.issues.collect(&:id)
time_entries_for_date_and_issue_ids(date, issue_ids)
end
end
when :out
0
else
labor_budget_spent_with_filter
end
end
def overhead_spent(date=nil)
case scope_date_status(date)
when :in
memoize_by_date("@overhead_spent", date) do
overhead_spent_with_filter do
issue_ids = self.issues.collect(&:id)
time_entries_for_date_and_issue_ids(date, issue_ids)
end
end
when :out
0
else
overhead_spent_with_filter
end
end
def create_budgets_for_periods
# For each month in the time span
months.each do |month|
# Iterate over all un-dated budgets, created dated versions
undated_labor_budgets = labor_budgets.all(:conditions => ["#{LaborBudget.table_name}.year IS NULL AND #{LaborBudget.table_name}.month IS NULL"])
undated_labor_budgets.each do |template_budget|
labor_budgets.create(template_budget.attributes.merge(:year => month.year, :month => month.month))
end
undated_overhead_budgets = overhead_budgets.all(:conditions => ["#{OverheadBudget.table_name}.year IS NULL AND #{OverheadBudget.table_name}.month IS NULL"])
undated_overhead_budgets.each do |template_budget|
overhead_budgets.create(template_budget.attributes.merge(:year => month.year, :month => month.month))
end
undated_fixed_budgets = fixed_budgets.all(:conditions => ["#{FixedBudget.table_name}.year IS NULL AND #{FixedBudget.table_name}.month IS NULL"])
undated_fixed_budgets.each do |template_budget|
fixed_budgets.create(template_budget.attributes.merge(:year => month.year, :month => month.month))
end
end
# Destroy origional un-dated budgets
labor_budgets.all(:conditions => ["#{LaborBudget.table_name}.year IS NULL AND #{LaborBudget.table_name}.month IS NULL"]).collect(&:destroy)
overhead_budgets.all(:conditions => ["#{OverheadBudget.table_name}.year IS NULL AND #{OverheadBudget.table_name}.month IS NULL"]).collect(&:destroy)
fixed_budgets.all(:conditions => ["#{FixedBudget.table_name}.year IS NULL AND #{FixedBudget.table_name}.month IS NULL"]).collect(&:destroy)
end
def check_for_extended_period
# TODO: brute force. Alternative would be to check end_date_changes to see if the period actually shifted
if end_date_changed?
extend_period_to_new_end_date
end
# TODO: brute force. Alternative would be to check start_date_changes to see if the period actually shifted
if start_date_changed?
extend_period_to_new_start_date
end
end
def check_for_shrunk_period
if end_date_changed? || start_date_changed?
shrink_budgets_to_new_period
end
end
private
def shrink_budgets_to_new_period
return if beginning_date.nil? || ending_date.nil?
shrink_budgets(labor_budgets.all)
shrink_budgets(overhead_budgets.all)
shrink_budgets(fixed_budgets.all)
true
end
def shrink_budgets(budget_items)
budget_items.each do |budget_item|
# Purge un-dated budgets, should not be saved at all
budget_item.destroy unless budget_item.year.present?
budget_item.destroy unless budget_item.month.present?
# Purge budgets outside the new beginning/ending range
unless (beginning_date..ending_date).to_a.include?(Date.new(budget_item.year, budget_item.month, 1))
budget_item.destroy
end
end
end
def extend_period_to_new_end_date
return if end_date_change[0].nil? # No previous end date, so it will not have budgets
old_end_date = end_date_change[0]
last_labor_budgets = labor_budgets.all(:conditions => {:year => old_end_date.year, :month => old_end_date.month})
last_overhead_budgets = overhead_budgets.all(:conditions => {:year => old_end_date.year, :month => old_end_date.month})
last_fixed_budgets = fixed_budgets.all(:conditions => {:year => old_end_date.year, :month => old_end_date.month})
months_after_date(old_end_date.end_of_month.to_date).each do |new_period|
create_budgets_for_new_period(new_period, last_labor_budgets, last_overhead_budgets, last_fixed_budgets)
end
end
def extend_period_to_new_start_date
return if start_date_change[0].nil? # No previous start date, so it will not have budgets
old_start_date = start_date_change[0]
first_labor_budgets = labor_budgets.all(:conditions => {:year => old_start_date.year, :month => old_start_date.month})
first_overhead_budgets = overhead_budgets.all(:conditions => {:year => old_start_date.year, :month => old_start_date.month})
first_fixed_budgets = fixed_budgets.all(:conditions => {:year => old_start_date.year, :month => old_start_date.month})
months_before_date(old_start_date.beginning_of_month.to_date).each do |new_period|
create_budgets_for_new_period(new_period, first_labor_budgets, first_overhead_budgets, first_fixed_budgets)
end
end
def create_budgets_for_new_period(new_period, labor_budgets_to_copy, overhead_budgets_to_copy, fixed_budgets_to_copy)
labor_budgets_to_copy.each do |labor_budget_to_copy|
create_new_labor_budget_based_on_existing_budget(labor_budget_to_copy, 'year' => new_period.year, 'month' => new_period.month)
end
overhead_budgets_to_copy.each do |overhead_budget_to_copy|
create_new_overhead_budget_based_on_existing_budget(overhead_budget_to_copy, 'year' => new_period.year, 'month' => new_period.month)
end
fixed_budgets_to_copy.each do |fixed_budget_to_copy|
create_new_fixed_budget_based_on_existing_budget(fixed_budget_to_copy, 'year' => new_period.year, 'month' => new_period.month)
end
end
def create_new_labor_budget_based_on_existing_budget(existing_labor_budget, attributes={})
labor_budgets.create(existing_labor_budget.attributes.except('id').merge(attributes))
end
def create_new_overhead_budget_based_on_existing_budget(existing_overhead_budget, attributes={})
overhead_budgets.create(existing_overhead_budget.attributes.except('id').merge(attributes))
end
def create_new_fixed_budget_based_on_existing_budget(existing_fixed_budget, attributes={})
fixed_budgets.create(existing_fixed_budget.attributes.except('id').merge(attributes))
end
def scope_date_status(date)
if date
if within_date_range?(date)
status = :in
else
status = :out # outside of range
end
else
status = :no_date
end
status
end
def time_entries_for_date_and_issue_ids(date, issue_ids)
if issue_ids.present?
TimeEntry.all(:conditions => ["#{Issue.table_name}.id IN (:issue_ids) AND tyear = (:year) AND tmonth = (:month)",
{:issue_ids => issue_ids,
:year => date.year,
:month => date.month}
],
:include => :issue)
else
[]
end
end
end