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 ⇒ Integer
Minimum markup percentage required for the document.
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
Whether the document's markup meets its
min_profit_markupfloor (always true when profit isn't tracked or the floor is zero). -
#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 ⇒ BigDecimal
Sum of
current_line_profitover the in-scope rows — gross profit (AKA Gross Profit). -
#profitable_total_profit_margin ⇒ Float
To find the margin, divide gross profit by the revenue.
-
#profitable_total_profit_markup ⇒ Float
To write the markup as a percentage, divide the gross profit by the COGS.
- #track_profit? ⇒ Boolean
-
#validate_min_profit_markup? ⇒ Boolean
Whether the minimum-profit-markup validation should run — only when the column exists and the document isn't already invoiced.
Instance Attribute Details
#min_profit_markup ⇒ Integer
Returns minimum markup percentage required for the document.
16 |
# File 'app/concerns/models/profitable.rb', line 16 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.
34 35 36 |
# File 'app/concerns/models/profitable.rb', line 34 def default_sales_markup 30 end |
#profit_margins_met? ⇒ Boolean
Whether the document's markup meets its min_profit_markup floor (always
true when profit isn't tracked or the floor is zero).
154 155 156 157 158 159 160 |
# File 'app/concerns/models/profitable.rb', line 154 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).
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'app/concerns/models/profitable.rb', line 44 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.
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'app/concerns/models/profitable.rb', line 125 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.
63 64 65 |
# File 'app/concerns/models/profitable.rb', line 63 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.
71 72 73 |
# File 'app/concerns/models/profitable.rb', line 71 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.
89 90 91 |
# File 'app/concerns/models/profitable.rb', line 89 def profitable_total_estimated_line_cost profitable_line_items.map(&:estimated_line_cost).compact.sum end |
#profitable_total_profit ⇒ BigDecimal
Sum of current_line_profit over the in-scope rows — gross profit
(AKA Gross Profit).
80 81 82 |
# File 'app/concerns/models/profitable.rb', line 80 def profitable_total_profit profitable_line_items.map(&:current_line_profit).compact.sum end |
#profitable_total_profit_margin ⇒ Float
To find the margin, divide gross profit by the revenue.
97 98 99 100 101 102 103 104 |
# File 'app/concerns/models/profitable.rb', line 97 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 ⇒ Float
To write the markup as a percentage, divide the gross profit by the COGS.
110 111 112 113 114 115 116 |
# File 'app/concerns/models/profitable.rb', line 110 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
146 147 148 |
# File 'app/concerns/models/profitable.rb', line 146 def track_profit? has_attribute?(:min_profit_markup) && profitable_line_items.present? && (is_a?(Order) || is_a?(Quote)) end |
#validate_min_profit_markup? ⇒ Boolean
Whether the minimum-profit-markup validation should run — only when the
column exists and the document isn't already invoiced.
23 24 25 26 27 28 |
# File 'app/concerns/models/profitable.rb', line 23 def validate_min_profit_markup? return false unless has_attribute?(:min_profit_markup) return false if try(:invoiced?) true end |