app/models/chargeback.rb
class Chargeback < ActsAsArModel
set_columns_hash( # Fields common to any chargeback type
:start_date => :datetime,
:end_date => :datetime,
:interval_name => :string,
:display_range => :string,
:report_interval_range => :string,
:report_generation_date => :datetime,
:chargeback_rates => :string,
:entity => :binary,
:tag_name => :string,
:label_name => :string,
:fixed_compute_metric => :integer
)
ALLOWED_FIELD_SUFFIXES = %w[
_rate
_cost
-owner_name
_metric
-report_interval_range
-report_generation_date
-provider_name
-provider_uid
-project_uid
-archived
-chargeback_rates
-vm_guid
-vm_uid
].freeze
def self.dynamic_rate_columns
@chargeable_fields = {}
@chargeable_fields[self.class] ||=
begin
ChargeableField.all.each_with_object({}) do |chargeable_field, result|
next unless report_col_options.keys.include?("#{chargeable_field.rate_name}_cost")
result["#{chargeable_field.rate_name}_rate"] = :string
end
end
end
def self.refresh_dynamic_metric_columns
set_columns_hash(dynamic_rate_columns)
end
def self.build_results_for_report_chargeback(options)
_log.info("Calculating chargeback costs...")
@options = options = ReportOptions.new_from_h(options)
data = {}
rates = RatesCache.new(options)
_log.debug("With report options: #{options.inspect}")
MiqRegion.all.each do |region|
_log.debug("For region #{region.region}")
ConsumptionHistory.for_report(self, options, region.region) do |consumption|
rates_to_apply = rates.get(consumption)
Array(report_row_key(consumption)).each do |result_key|
key = result_key[:key]
_log.debug("Report row key #{key}")
consumption.tag_filter_for_rollup_records(result_key[:key_object].tag) if result_key[:key_object]&.tag
data[key] ||= new(options, consumption, region.region, result_key)
chargeback_rates = data[key]["chargeback_rates"].split(', ') + rates_to_apply.collect(&:description)
data[key]["chargeback_rates"] = chargeback_rates.uniq.join(', ')
# we are getting hash with metrics and costs for metrics defined for chargeback
if Settings[:new_chargeback]
data[key].new_chargeback_calculate_costs(consumption, rates_to_apply)
else
data[key].calculate_costs(consumption, rates_to_apply)
end
end
end
end
_log.info("Calculating chargeback costs...Complete")
[data.values]
end
def self.report_row_key(consumption)
ts_key = @options.start_of_report_step(consumption.timestamp)
if @options[:groupby_tag].present?
classifications = @options.classification_for(consumption)
if classifications.present?
Array(classifications).map do |x|
{:key => "#{x.id}_#{ts_key}", :key_object => x}
end
else
[{:key => "none"}]
end
elsif @options[:groupby_label].present?
[{:key => "#{groupby_label_value(consumption, @options[:groupby_label])}_#{ts_key}"}]
elsif @options.group_by_tenant?
tenant = @options.tenant_for(consumption)
[{:key => "#{tenant ? tenant.id : 'none'}_#{ts_key}"}]
elsif @options.group_by_date_only?
[{:key => ts_key.to_s}]
elsif @options.group_by_date_first?
[{:key => "#{ts_key}_#{consumption.resource_id}"}]
else
[{:key => default_key(consumption, ts_key)}]
end
end
def self.default_key(consumption, ts_key)
"#{consumption.resource_id}_#{ts_key}"
end
def self.groupby_label_value(_consumption, _groupby_label)
nil
end
def initialize(options, consumption, region, result_key)
@options = options
super()
if @options[:groupby_tag].present?
self.tag_name = result_key[:key_object] ? result_key[:key_object].description : _('<Empty>')
elsif @options[:groupby_label].present?
label_value = self.class.groupby_label_value(consumption, options[:groupby_label])
self.label_name = (label_value.presence || _('<Empty>'))
else
init_extra_fields(consumption, region)
end
self.start_date, self.end_date, self.display_range = options.report_step_range(consumption.timestamp)
self.interval_name = options.interval
self.chargeback_rates = ''
self.entity ||= consumption.resource
self.tenant_name = consumption.resource.try(:tenant).try(:name) if options.group_by_tenant?
end
def showback_category
case self
when ChargebackVm
'Vm'
when ChargebackContainerProject
'Container'
when ChargebackContainerImage
'ContainerImage'
end
end
def new_chargeback_calculate_costs(consumption, rates)
self.fixed_compute_metric = consumption.chargeback_fields_present if consumption.chargeback_fields_present
rates.each do |rate|
plan = ManageIQ::Showback::PricePlan.find_or_create_by(:description => rate.description,
:name => rate.description,
:resource => MiqEnterprise.first)
data = {}
rate.rate_details_relevant_to(relevant_fields, self.class.attribute_names).each do |r|
r.populate_showback_rate(plan, r, showback_category)
measure = r.chargeable_field.showback_measure
dimension, _, _ = r.chargeable_field.showback_dimension
value = r.chargeable_field.measure(consumption, @options)
data[measure] ||= {}
data[measure][dimension] = [value, r.showback_unit(ChargeableField::UNITS[r.chargeable_field.metric])]
end
# TODO: duration_of_report_step is 30.days for price plans but for consumption history,
# it's used for date ranges and needs to be 1.month with rails 5.1
duration = @options.interval == "monthly" ? 30.days : @options.duration_of_report_step
results = plan.calculate_list_of_costs_input(resource_type: showback_category,
data: data,
start_time: consumption.instance_variable_get(:@start_time),
end_time: consumption.instance_variable_get(:@end_time),
cycle_duration: duration)
results.each do |cost_value, sb_rate|
r = ChargebackRateDetail.find(sb_rate.concept)
metric = r.chargeable_field.metric
metric_index = ChargeableField::VIRTUAL_COL_USES.invert[metric] || metric
metric_value = data[r.chargeable_field.group][metric_index]
metric_field = [r.chargeable_field.group, r.chargeable_field.source, "metric"].join("_")
cost_field = [r.chargeable_field.group, r.chargeable_field.source, "cost"].join("_")
_, total_metric_field, total_field = r.chargeable_field.cost_keys
self[total_field] = (self[total_field].to_f || 0) + cost_value.to_f
self[total_metric_field] = (self[total_metric_field].to_f || 0) + cost_value.to_f
self[cost_field] = cost_value.to_f
self[metric_field] = metric_value.first.to_f
end
end
end
def calculate_fixed_compute_metric(consumption)
return unless consumption.chargeback_fields_present
if @options.group_by_date_only?
self.fixed_compute_metric ||= 0
self.fixed_compute_metric += consumption.chargeback_fields_present
else
self.fixed_compute_metric = consumption.chargeback_fields_present
end
end
def calculate_costs(consumption, rates)
calculate_fixed_compute_metric(consumption)
self.class.try(:refresh_dynamic_metric_columns)
self.report_interval_range = "#{consumption.report_interval_start.strftime('%m/%d/%Y')} - #{consumption.report_interval_end.strftime('%m/%d/%Y')}"
self.report_generation_date = Time.current
_log.debug("Consumption Type: #{consumption.class}")
rates.each do |rate|
_log.debug("Calculation with rate: #{rate.id} #{rate.description}(#{rate.rate_type})")
rate.rate_details_relevant_to(relevant_fields, self.class.attribute_names).each do |r|
_log.debug("Metric: #{r.chargeable_field.metric} Group: #{r.chargeable_field.group} Source: #{r.chargeable_field.source}")
r.chargeback_tiers.each do |tier|
_log.debug("Start: #{tier.start} Finish: #{tier.finish} Fixed Rate: #{tier.fixed_rate} Variable Rate: #{tier.variable_rate}")
end
r.charge(consumption, @options).each do |field, value|
next if @options.skip_field_accumulation?(field, self[field])
_log.debug("Calculation with field: #{field} and with value: #{value}")
(self[field] = self[field].kind_of?(Numeric) ? (self[field] || 0) + value : value)
_log.debug("Accumulated value: #{self[field]}")
end
end
end
end
def self.report_cb_model(model)
model.gsub(/^(Chargeback|Metering)/, "")
end
def self.db_is_chargeback?(db)
db && db.present? && db.safe_constantize < Chargeback
end
def self.report_tag_field
"tag_name"
end
def self.report_label_field
"label_name"
end
def self.set_chargeback_report_options(rpt, group_by, header_for_tag, groupby_label, tz)
static_cols = case group_by
when "project" then ["project_name"]
when "date-only" then []
when "tag" then [report_tag_field]
when "label" then [report_label_field]
when "tenant" then ["tenant_name"]
else report_static_cols
end
rpt.cols = %w[start_date display_range] + static_cols
if group_by == "date-first"
rpt.col_order = ["display_range"] + static_cols
rpt.sortby = (["start_date"] + static_cols)
else
rpt.col_order = static_cols + ["display_range"]
rpt.sortby = (static_cols + ["start_date"])
end
rpt.col_order.each do |c|
header_column = if c == report_tag_field && header_for_tag
header_for_tag
elsif c == report_label_field && groupby_label
groupby_label
else
c
end
rpt.headers.push(Dictionary.gettext(header_column, :type => :column, :notfound => :titleize))
rpt.col_formats.push(nil) # No formatting needed on the static cols
end
rpt.col_options = report_col_options
rpt.order = "Ascending"
rpt.group = "y"
rpt.tz = tz
rpt
end
def tags
entity.try(:tags).to_a
end
def self.load_custom_attributes_for(cols)
chargeback_klass = report_cb_model(to_s).safe_constantize
chargeback_klass.load_custom_attributes_for(cols)
cols.each do |x|
next unless x.include?(CustomAttributeMixin::CUSTOM_ATTRIBUTES_PREFIX)
load_custom_attribute(x)
end
end
def self.load_custom_attribute(custom_attribute)
virtual_column(custom_attribute.to_sym, :type => :string)
define_method(custom_attribute.to_sym) do
entity.send(custom_attribute)
end
end
def self.default_column_for_format(col)
if col.start_with?('storage_allocated')
col.ends_with?('cost') ? 'storage_allocated_cost' : 'storage_allocated_metric'
else
col
end
end
def self.rate_column?(col)
col.ends_with?("_rate")
end
private
def relevant_fields
@relevant_fields ||= (@options.report_cols || self.class.report_col_options.keys).to_set
end
end # class Chargeback