Source code for metricengine.calculations.profitability

"""
Profitability and margin calculations.

This module contains calculations for profit margins, profitability ratios, and related metrics.
All calculations use the Collection namespace for proper organization.
"""

from __future__ import annotations

from decimal import Decimal

from ..exceptions import CalculationError
from ..policy import DEFAULT_POLICY, Policy
from ..policy_context import get_policy
from ..registry_collections import Collection
from ..units import Dimensionless, Money, Percent, Ratio
from ..value import FV
from .rules import skip_if_negative_sales

profitability = Collection("profitability")

# ── small local helpers ──────────────────────────────────────────────────────


def _resolve_policy(*fvs: FV | None) -> Policy:
    for fv in fvs:
        if isinstance(fv, FV) and fv.policy:
            return fv.policy
    return get_policy() or DEFAULT_POLICY


def _none_with_unit(unit, pol: Policy) -> FV:
    return FV(None, policy=pol, unit=unit)


def _ratio_with_policy(value: FV, pol: Policy) -> FV[Ratio]:
    """Create a ratio FinancialValue without polluting the policy with percent_style='ratio'."""
    return FV(value._value, policy=pol, unit=Ratio, _is_percentage=False)


def _is_zero(fv: FV) -> bool:
    d = fv.as_decimal()
    return (d is not None) and (d == 0)


# ── core profitability amounts ───────────────────────────────────────────────


[docs] @profitability.calc("gross_profit", depends_on=("sales", "cost")) @skip_if_negative_sales("sales") def gross_profit( sales: FV[Money], cost: FV[Money], ) -> FV[Money]: """Gross profit = sales - cost""" pol = _resolve_policy(sales, cost) if sales.is_none() or cost.is_none(): return _none_with_unit(Money, pol) return sales - cost
[docs] @profitability.calc("gross_profit_ex_tax", depends_on=("sales_ex_tax", "cost")) @skip_if_negative_sales("sales_ex_tax") def gross_profit_ex_tax( sales_ex_tax: FV[Money], cost: FV[Money], ) -> FV[Money]: """Gross profit (ex tax) = sales_ex_tax - cost""" pol = _resolve_policy(sales_ex_tax, cost) if sales_ex_tax.is_none() or cost.is_none(): return _none_with_unit(Money, pol) return sales_ex_tax - cost
# ── gross margin (ratio + wrapper) ───────────────────────────────────────────
[docs] @profitability.calc("gross_margin_ratio", depends_on=("gross_profit", "sales")) @skip_if_negative_sales("sales") def gross_margin_ratio( gross_profit: FV[Money], sales: FV[Money], ) -> FV[Ratio]: """Gross margin ratio = gross_profit / sales""" pol = _resolve_policy(gross_profit, sales) if gross_profit.is_none() or sales.is_none(): return _none_with_unit(Ratio, pol) if _is_zero(sales): if pol.arithmetic_strict: raise CalculationError("Gross margin undefined for sales == 0") return _none_with_unit(Ratio, pol) return _ratio_with_policy(gross_profit / sales, pol)
[docs] @profitability.calc("gross_margin_percentage", depends_on=("gross_margin_ratio",)) def gross_margin_percentage(gross_margin_ratio: FV[Ratio]) -> FV[Percent]: """Gross margin as percent (e.g., 0.35 -> '35%').""" pol = _resolve_policy(gross_margin_ratio) if gross_margin_ratio.is_none(): return _none_with_unit(Percent, pol) return gross_margin_ratio.as_percentage()
[docs] @profitability.calc( "gross_margin_ratio_ex_tax", depends_on=("gross_profit_ex_tax", "sales_ex_tax") ) @skip_if_negative_sales("sales_ex_tax") def gross_margin_ratio_ex_tax( gross_profit_ex_tax: FV[Money], sales_ex_tax: FV[Money], ) -> FV[Ratio]: """Gross margin ratio (ex tax) = gross_profit_ex_tax / sales_ex_tax""" pol = _resolve_policy(gross_profit_ex_tax, sales_ex_tax) if gross_profit_ex_tax.is_none() or sales_ex_tax.is_none(): return _none_with_unit(Ratio, pol) if _is_zero(sales_ex_tax): if pol.arithmetic_strict: raise CalculationError( "Gross margin (ex tax) undefined for sales_ex_tax == 0" ) return _none_with_unit(Ratio, pol) return _ratio_with_policy(gross_profit_ex_tax / sales_ex_tax, pol)
[docs] @profitability.calc( "gross_margin_percentage_ex_tax", depends_on=("gross_margin_ratio_ex_tax",) ) def gross_margin_percentage_ex_tax( gross_margin_ratio_ex_tax: FV[Ratio], ) -> FV[Percent]: """Gross margin (ex tax) as percent.""" pol = _resolve_policy(gross_margin_ratio_ex_tax) if gross_margin_ratio_ex_tax.is_none(): return _none_with_unit(Percent, pol) return gross_margin_ratio_ex_tax.as_percentage()
# ── cost ratios (ratio + wrappers) ───────────────────────────────────────────
[docs] @profitability.calc("cost_ratio", depends_on=("cost", "sales")) @skip_if_negative_sales("sales") def cost_ratio(cost: FV[Money], sales: FV[Money]) -> FV[Ratio]: """Cost ratio = cost / sales""" pol = _resolve_policy(cost, sales) if cost.is_none() or sales.is_none(): return _none_with_unit(Ratio, pol) if _is_zero(sales): if pol.arithmetic_strict: raise CalculationError("Cost ratio undefined for sales == 0") return _none_with_unit(Ratio, pol) return _ratio_with_policy(cost / sales, pol)
[docs] @profitability.calc("cost_percent", depends_on=("cost_ratio",)) def cost_percent(cost_ratio: FV[Ratio]) -> FV[Percent]: """Cost as percent.""" pol = _resolve_policy(cost_ratio) if cost_ratio.is_none(): return _none_with_unit(Percent, pol) return cost_ratio.as_percentage()
[docs] @profitability.calc("cost_ratio_ex_tax", depends_on=("cost", "sales_ex_tax")) @skip_if_negative_sales("sales_ex_tax") def cost_ratio_ex_tax( cost: FV[Money], sales_ex_tax: FV[Money], ) -> FV[Ratio]: """Cost ratio (ex tax) = cost / sales_ex_tax""" pol = _resolve_policy(cost, sales_ex_tax) if cost.is_none() or sales_ex_tax.is_none(): return _none_with_unit(Ratio, pol) if _is_zero(sales_ex_tax): if pol.arithmetic_strict: raise CalculationError( "Cost ratio (ex tax) undefined for sales_ex_tax == 0" ) return _none_with_unit(Ratio, pol) return _ratio_with_policy(cost / sales_ex_tax, pol)
[docs] @profitability.calc("cost_percent_ex_tax", depends_on=("cost_ratio_ex_tax",)) def cost_percent_ex_tax(cost_ratio_ex_tax: FV[Ratio]) -> FV[Percent]: """Cost percent (ex tax).""" pol = _resolve_policy(cost_ratio_ex_tax) if cost_ratio_ex_tax.is_none(): return _none_with_unit(Percent, pol) return cost_ratio_ex_tax.as_percentage()
# ── net profit & net margin ──────────────────────────────────────────────────
[docs] @profitability.calc("net_profit", depends_on=("revenue", "total_costs")) def net_profit( revenue: FV[Money], total_costs: FV[Money], ) -> FV[Money]: """Net profit = revenue - total_costs""" pol = _resolve_policy(revenue, total_costs) if revenue.is_none() or total_costs.is_none(): return _none_with_unit(Money, pol) return revenue - total_costs
[docs] @profitability.calc("net_margin_ratio", depends_on=("net_profit", "revenue")) def net_margin_ratio( net_profit: FV[Money], revenue: FV[Money], ) -> FV[Ratio]: """Net margin ratio = net_profit / revenue""" pol = _resolve_policy(net_profit, revenue) if net_profit.is_none() or revenue.is_none(): return _none_with_unit(Ratio, pol) if _is_zero(revenue): if pol.arithmetic_strict: raise CalculationError("Net margin undefined for revenue == 0") return _none_with_unit(Ratio, pol) return _ratio_with_policy(net_profit / revenue, pol)
[docs] @profitability.calc("net_margin_percentage", depends_on=("net_margin_ratio",)) def net_margin_percentage(net_margin_ratio: FV[Ratio]) -> FV[Percent]: """Net margin as percent.""" pol = _resolve_policy(net_margin_ratio) if net_margin_ratio.is_none(): return _none_with_unit(Percent, pol) return net_margin_ratio.as_percentage()
# ── tax-adjusted net margin ──────────────────────────────────────────────────
[docs] @profitability.calc("net_profit_with_tax", depends_on=("sales", "cost", "tax_rate")) @skip_if_negative_sales("sales") def net_profit_with_tax( sales: FV[Money], cost: FV[Money], tax_rate: FV[Percent], ) -> FV[Money]: """ Net profit (tax-adjusted) = (sales / (1 + tax_rate)) - cost """ pol = _resolve_policy(sales, cost, tax_rate) if sales.is_none() or cost.is_none() or tax_rate.is_none(): return _none_with_unit(Money, pol) one = FV(Decimal("1"), policy=pol, unit=Dimensionless) denom = one + tax_rate if _is_zero(denom): if pol.arithmetic_strict: raise CalculationError( "net_profit_with_tax undefined when 1 + tax_rate == 0" ) return _none_with_unit(Money, pol) return (sales / denom) - cost
[docs] @profitability.calc( "net_margin_with_tax_ratio", depends_on=("net_profit_with_tax", "sales_ex_tax") ) @skip_if_negative_sales("sales_ex_tax") def net_margin_with_tax_ratio( net_profit_with_tax: FV[Money], sales_ex_tax: FV[Money], ) -> FV[Ratio]: """Net margin (tax-adjusted) ratio = net_profit_with_tax / sales_ex_tax""" pol = _resolve_policy(net_profit_with_tax, sales_ex_tax) if net_profit_with_tax.is_none() or sales_ex_tax.is_none(): return _none_with_unit(Ratio, pol) if _is_zero(sales_ex_tax): if pol.arithmetic_strict: raise CalculationError( "Net margin (tax-adjusted) undefined for sales_ex_tax == 0" ) return _none_with_unit(Ratio, pol) return _ratio_with_policy(net_profit_with_tax / sales_ex_tax, pol)
[docs] @profitability.calc("net_margin_with_tax", depends_on=("net_margin_with_tax_ratio",)) def net_margin_with_tax(net_margin_with_tax_ratio: FV[Ratio]) -> FV[Percent]: """Net margin with tax as percent.""" pol = _resolve_policy(net_margin_with_tax_ratio) if net_margin_with_tax_ratio.is_none(): return _none_with_unit(Percent, pol) return net_margin_with_tax_ratio.as_percentage()
# ── cost % with tax info provided (ex-tax denominator) ───────────────────────
[docs] @profitability.calc("cost_ratio_with_tax", depends_on=("cost", "sales", "tax_rate")) @skip_if_negative_sales("sales") def cost_ratio_with_tax( cost: FV[Money], sales: FV[Money], tax_rate: FV[Percent], ) -> FV[Ratio]: """ Cost ratio with tax info: denominator is sales ex tax, i.e. cost / (sales / (1 + tax_rate)) """ pol = _resolve_policy(cost, sales, tax_rate) if cost.is_none() or sales.is_none() or tax_rate.is_none(): return _none_with_unit(Ratio, pol) one = FV(Decimal("1"), policy=pol, unit=Dimensionless) denom = one + tax_rate if _is_zero(denom): if pol.arithmetic_strict: raise CalculationError( "cost_ratio_with_tax undefined when 1 + tax_rate == 0" ) return _none_with_unit(Ratio, pol) sales_ex = sales / denom if _is_zero(sales_ex): if pol.arithmetic_strict: raise CalculationError( "cost_ratio_with_tax undefined for sales_ex_tax == 0" ) return _none_with_unit(Ratio, pol) return _ratio_with_policy(cost / sales_ex, pol)
[docs] @profitability.calc("cost_percentage_with_tax", depends_on=("cost_ratio_with_tax",)) def cost_percentage_with_tax(cost_ratio_with_tax: FV[Ratio]) -> FV[Percent]: """Cost percentage with tax.""" pol = _resolve_policy(cost_ratio_with_tax) if cost_ratio_with_tax.is_none(): return _none_with_unit(Percent, pol) return cost_ratio_with_tax.as_percentage()
# ── contribution margin ──────────────────────────────────────────────────────
[docs] @profitability.calc("contribution_margin", depends_on=("revenue", "variable_costs")) def contribution_margin( revenue: FV[Money], variable_costs: FV[Money], ) -> FV[Money]: """Contribution margin = revenue - variable_costs""" pol = _resolve_policy(revenue, variable_costs) if revenue.is_none() or variable_costs.is_none(): return _none_with_unit(Money, pol) return revenue - variable_costs
[docs] @profitability.calc( "contribution_margin_ratio_raw", depends_on=("contribution_margin", "revenue") ) def contribution_margin_ratio_raw( contribution_margin: FV[Money], revenue: FV[Money], ) -> FV[Ratio]: """Contribution margin ratio = contribution_margin / revenue""" pol = _resolve_policy(contribution_margin, revenue) if contribution_margin.is_none() or revenue.is_none(): return _none_with_unit(Ratio, pol) if _is_zero(revenue): if pol.arithmetic_strict: raise CalculationError("Contribution margin undefined for revenue == 0") return _none_with_unit(Ratio, pol) return _ratio_with_policy(contribution_margin / revenue, pol)
# Keep original public name returning percent:
[docs] @profitability.calc( "contribution_margin_ratio", depends_on=("contribution_margin_ratio_raw",) ) def contribution_margin_ratio(contribution_margin_ratio_raw: FV[Ratio]) -> FV[Percent]: """Contribution margin ratio as percent.""" pol = _resolve_policy(contribution_margin_ratio_raw) if contribution_margin_ratio_raw.is_none(): return _none_with_unit(Percent, pol) return contribution_margin_ratio_raw.as_percentage()
# ── operating & EBITDA margins ───────────────────────────────────────────────
[docs] @profitability.calc( "operating_margin_ratio", depends_on=("operating_income", "revenue") ) def operating_margin_ratio( operating_income: FV[Money], revenue: FV[Money], ) -> FV[Ratio]: """Operating margin ratio = operating_income / revenue""" pol = _resolve_policy(operating_income, revenue) if operating_income.is_none() or revenue.is_none(): return _none_with_unit(Ratio, pol) if _is_zero(revenue): if pol.arithmetic_strict: raise CalculationError("Operating margin undefined for revenue == 0") return _none_with_unit(Ratio, pol) return _ratio_with_policy(operating_income / revenue, pol)
[docs] @profitability.calc("operating_margin", depends_on=("operating_margin_ratio",)) def operating_margin(operating_margin_ratio: FV[Ratio]) -> FV[Percent]: """Operating margin as percent.""" pol = _resolve_policy(operating_margin_ratio) if operating_margin_ratio.is_none(): return _none_with_unit(Percent, pol) return operating_margin_ratio.as_percentage()
[docs] @profitability.calc("ebitda_margin_ratio", depends_on=("ebitda", "revenue")) def ebitda_margin_ratio( ebitda: FV[Money], revenue: FV[Money], ) -> FV[Ratio]: """EBITDA margin ratio = ebitda / revenue""" pol = _resolve_policy(ebitda, revenue) if ebitda.is_none() or revenue.is_none(): return _none_with_unit(Ratio, pol) if _is_zero(revenue): if pol.arithmetic_strict: raise CalculationError("EBITDA margin undefined for revenue == 0") return _none_with_unit(Ratio, pol) return _ratio_with_policy(ebitda / revenue, pol)
[docs] @profitability.calc("ebitda_margin", depends_on=("ebitda_margin_ratio",)) def ebitda_margin(ebitda_margin_ratio: FV[Ratio]) -> FV[Percent]: """EBITDA margin as percent.""" pol = _resolve_policy(ebitda_margin_ratio) if ebitda_margin_ratio.is_none(): return _none_with_unit(Percent, pol) return ebitda_margin_ratio.as_percentage()
# ── ROI (ratio + wrapper) ───────────────────────────────────────────────────
[docs] @profitability.calc( "roi_ratio", depends_on=("gain_from_investment", "cost_of_investment") ) def roi_ratio( gain_from_investment: FV[Money], cost_of_investment: FV[Money], ) -> FV[Ratio]: """ROI ratio = gain_from_investment / cost_of_investment""" pol = _resolve_policy(gain_from_investment, cost_of_investment) if gain_from_investment.is_none() or cost_of_investment.is_none(): return _none_with_unit(Ratio, pol) if _is_zero(cost_of_investment): if pol.arithmetic_strict: raise CalculationError("ROI undefined for cost_of_investment == 0") return _none_with_unit(Ratio, pol) return _ratio_with_policy(gain_from_investment / cost_of_investment, pol)
[docs] @profitability.calc("roi", depends_on=("roi_ratio",)) def roi(roi_ratio: FV[Ratio]) -> FV[Percent]: """ROI as percent.""" pol = _resolve_policy(roi_ratio) if roi_ratio.is_none(): return _none_with_unit(Percent, pol) return roi_ratio.as_percentage()