Module: Models::Profitable

Extended by:
ActiveSupport::Concern
Includes:
Memery
Included in:
Delivery, Invoice, Order, Quote
Defined in:
app/concerns/models/profitable.rb

Overview

Concern mixed into Order / Invoice / Quote / CreditMemo that
computes profit-margin / markup figures from the document's line
items vs. their cost-of-goods-sold. See profitable_* methods for
the public surface.

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#min_profit_markupObject (readonly)



14
# File 'app/concerns/models/profitable.rb', line 14

validates :min_profit_markup, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :validate_min_profit_markup?

Instance Method Details

#default_sales_markupInteger

Markup percentage threshold (30%) below which an order /
quote moves from :warning to :alert. Override on the
including class to set a different per-business floor.

Returns:

  • (Integer)


28
29
30
# File 'app/concerns/models/profitable.rb', line 28

def default_sales_markup
  30
end

#profit_margins_met?Boolean

Returns:

  • (Boolean)


132
133
134
135
136
137
138
# File 'app/concerns/models/profitable.rb', line 132

def profit_margins_met?
  return true unless track_profit?
  return true if min_profit_markup.zero?

  pm = (profitable_total_profit_markup * 100).to_i
  pm >= min_profit_markup
end

#profitable_line_itemsArray<LineItem>

Goods line items relevant to gross-profit calculation. Always
includes taxable goods; includes priced shipping rows only
when we're under-recovering shipping cost (so under-water
freight bleeds into the margin).

Returns:



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'app/concerns/models/profitable.rb', line 38

def profitable_line_items
  items = line_items.where("line_items.tax_class = 'g' OR (line_items.tax_class = 'shp' and line_items.price > 0)")
                    .parents_only
                    .includes({ catalog_item: :store_item }, :item, :shipping_cost, :line_discounts)

  # Exclude shipping line items where charged shipping >= actual shipping cost
  # Only include shipping in profit calculation when we're losing money on it
  items.reject do |li|
    next false unless li.is_shipping?

    actual_cost = li.shipping_cost&.rate_data&.dig('actual_cost').to_f
    charged = li.discounted_price.to_f
    actual_cost > 0 && charged >= actual_cost
  end
end

#profitable_statusSymbol

:ok / :warning / :alert traffic-light for the order's
margin: :alert when min-profit-markup isn't met, :ok when
markup ≥ 60%, :warning between #default_sales_markup and
60%, :alert below.

Returns:

  • (Symbol)

    :ok, :warning, or :alert



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'app/concerns/models/profitable.rb', line 111

def profitable_status
  # If there's nothing to calculate 0 /0 then its ok
  return :ok if profitable_total_profit.zero? && profitable_total_discounted.zero?
  # Shortcut, if we don't meet the profit margins, its always an alert
  return :alert unless profit_margins_met?

  tm = profitable_total_profit_markup
  if tm >= 0.60
    :ok
  elsif (tm * 100).to_i > default_sales_markup
    :warning
  else
    :alert
  end
end

#profitable_total_discountedBigDecimal

Sum of discounted_total over #profitable_line_items
what the customer actually pays for in-scope rows.

Returns:

  • (BigDecimal)


57
58
59
# File 'app/concerns/models/profitable.rb', line 57

def profitable_total_discounted
  profitable_line_items.map(&:discounted_total).compact.sum
end

#profitable_total_estimated_costBigDecimal

Sum of estimated_line_cost (COGS) over the same in-scope
rows.

Returns:

  • (BigDecimal)


65
66
67
# File 'app/concerns/models/profitable.rb', line 65

def profitable_total_estimated_cost
  profitable_line_items.map(&:estimated_line_cost).compact.sum
end

#profitable_total_estimated_line_costBigDecimal

Alias of #profitable_total_estimated_cost kept for use in
markup vs. cost denominators where the "estimated_line_cost"
name is more readable.

Returns:

  • (BigDecimal)


79
80
81
# File 'app/concerns/models/profitable.rb', line 79

def profitable_total_estimated_line_cost
  profitable_line_items.map(&:estimated_line_cost).compact.sum
end

#profitable_total_profitObject

AKA Gross Profit



70
71
72
# File 'app/concerns/models/profitable.rb', line 70

def profitable_total_profit # AKA Gross Profit
  profitable_line_items.map(&:current_line_profit).compact.sum
end

#profitable_total_profit_marginObject

To find the margin, divide gross profit by the revenue.



85
86
87
88
89
90
91
92
# File 'app/concerns/models/profitable.rb', line 85

def profitable_total_profit_margin
  gross_profit = profitable_total_profit
  revenue = profitable_total_discounted
  return 0.0 if revenue.zero? && gross_profit == 0
  return -1.0 if revenue == 0

  (gross_profit / revenue).round(4)
end

#profitable_total_profit_markupObject

To write the markup as a percentage, divide the gross profit by the COGS.



96
97
98
99
100
101
102
# File 'app/concerns/models/profitable.rb', line 96

def profitable_total_profit_markup
  gross_profit = profitable_total_profit
  cogs = profitable_total_estimated_line_cost
  return 0.0 if cogs.zero? # Should not happen item never cost nothing but you never know

  (gross_profit / cogs).round(4)
end

#track_profit?Boolean

Returns:

  • (Boolean)


128
129
130
# File 'app/concerns/models/profitable.rb', line 128

def track_profit?
  has_attribute?(:min_profit_markup) && profitable_line_items.present? && (is_a?(Order) || is_a?(Quote))
end

#validate_min_profit_markup?Boolean

Returns:

  • (Boolean)


17
18
19
20
21
22
# File 'app/concerns/models/profitable.rb', line 17

def validate_min_profit_markup?
  return false unless has_attribute?(:min_profit_markup)
  return false if try(:invoiced?)

  true
end