Module: Models::Profitable
- Extended by:
- ActiveSupport::Concern
- Includes:
- Memery
- 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
- #min_profit_markup ⇒ Object readonly
Instance Method Summary collapse
-
#default_sales_markup ⇒ Integer
Markup percentage threshold (30%) below which an order / quote moves from
:warningto:alert. - #profit_margins_met? ⇒ Boolean
-
#profitable_line_items ⇒ Array<LineItem>
Goods line items relevant to gross-profit calculation.
-
#profitable_status ⇒ Symbol
:ok/:warning/:alerttraffic-light for the order's margin::alertwhen min-profit-markup isn't met,:okwhen markup ≥ 60%,:warningbetween #default_sales_markup and 60%,:alertbelow. -
#profitable_total_discounted ⇒ BigDecimal
Sum of
discounted_totalover #profitable_line_items — what the customer actually pays for in-scope rows. -
#profitable_total_estimated_cost ⇒ BigDecimal
Sum of
estimated_line_cost(COGS) over the same in-scope rows. -
#profitable_total_estimated_line_cost ⇒ BigDecimal
Alias of #profitable_total_estimated_cost kept for use in markup vs.
-
#profitable_total_profit ⇒ Object
AKA Gross Profit.
-
#profitable_total_profit_margin ⇒ Object
To find the margin, divide gross profit by the revenue.
-
#profitable_total_profit_markup ⇒ Object
To write the markup as a percentage, divide the gross profit by the COGS.
- #track_profit? ⇒ Boolean
- #validate_min_profit_markup? ⇒ Boolean
Instance Attribute Details
#min_profit_markup ⇒ Object (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_markup ⇒ Integer
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.
28 29 30 |
# File 'app/concerns/models/profitable.rb', line 28 def default_sales_markup 30 end |
#profit_margins_met? ⇒ 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_items ⇒ Array<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).
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_status ⇒ Symbol
: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.
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_discounted ⇒ BigDecimal
Sum of discounted_total over #profitable_line_items —
what the customer actually pays for in-scope rows.
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_cost ⇒ BigDecimal
Sum of estimated_line_cost (COGS) over the same in-scope
rows.
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_cost ⇒ BigDecimal
Alias of #profitable_total_estimated_cost kept for use in
markup vs. cost denominators where the "estimated_line_cost"
name is more readable.
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_profit ⇒ Object
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_margin ⇒ Object
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_markup ⇒ Object
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
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
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 |