88 lines
2.7 KiB
Python
88 lines
2.7 KiB
Python
"""Pricing engine.
|
|
|
|
Applies active `price_rules` (cost -> markup bands) and computes net profit:
|
|
|
|
retail = cost * (1 + markup_pct/100) (floored at cost + shipping + fees)
|
|
net profit = retail - cost - shipping - fees
|
|
margin_pct = profit / retail * 100
|
|
|
|
`min_price` (product floor) is respected — retail is never below it.
|
|
"""
|
|
from decimal import ROUND_HALF_UP, Decimal
|
|
|
|
from app.models import PriceRule
|
|
|
|
CENT = Decimal("0.01")
|
|
|
|
|
|
def _q(value) -> Decimal:
|
|
try:
|
|
return Decimal(str(value or 0))
|
|
except Exception:
|
|
return Decimal("0")
|
|
|
|
|
|
def get_active_rules(db):
|
|
return (
|
|
db.query(PriceRule)
|
|
.filter(PriceRule.active.is_(True))
|
|
.order_by(PriceRule.min_cost)
|
|
.all()
|
|
)
|
|
|
|
|
|
def find_rule(db, cost):
|
|
cost = _q(cost)
|
|
for rule in get_active_rules(db):
|
|
if cost >= _q(rule.min_cost) and (rule.max_cost is None or cost < _q(rule.max_cost)):
|
|
return rule
|
|
return None
|
|
|
|
|
|
def calculate_price(db, cost, shipping=0, fees=0, min_price=None):
|
|
"""Return dict(retail, profit, margin, margin_pct, markup_pct, rule_id, ...) or None.
|
|
|
|
None means no active rule applies — callers must NOT publish such products.
|
|
"""
|
|
cost = _q(cost)
|
|
shipping = _q(shipping)
|
|
fees = _q(fees)
|
|
rule = find_rule(db, cost)
|
|
if rule is None:
|
|
return None
|
|
|
|
markup_pct = _q(rule.markup_pct)
|
|
retail = (cost * (1 + markup_pct / 100)).quantize(CENT, rounding=ROUND_HALF_UP)
|
|
|
|
# floor: cost + shipping + fees, raised by min_margin_pct if configured
|
|
floor = (cost + shipping + fees).quantize(CENT, rounding=ROUND_HALF_UP)
|
|
min_margin_pct = _q(rule.min_margin_pct)
|
|
if min_margin_pct > 0:
|
|
floor = max(floor, (floor + retail * min_margin_pct / 100).quantize(CENT, rounding=ROUND_HALF_UP))
|
|
if retail < floor:
|
|
retail = floor
|
|
|
|
# never below the product's minimum price
|
|
if min_price is not None and retail < _q(min_price):
|
|
retail = _q(min_price).quantize(CENT, rounding=ROUND_HALF_UP)
|
|
|
|
profit = (retail - cost - shipping - fees).quantize(CENT, rounding=ROUND_HALF_UP)
|
|
margin_pct = (profit / retail * 100).quantize(Decimal("0.01")) if retail > 0 else Decimal("0")
|
|
|
|
return {
|
|
"cost": cost,
|
|
"shipping": shipping,
|
|
"fees": fees,
|
|
"retail": retail,
|
|
"profit": profit,
|
|
"margin": profit,
|
|
"margin_pct": float(margin_pct),
|
|
"markup_pct": float(markup_pct),
|
|
"rule_id": rule.id,
|
|
}
|
|
|
|
|
|
def estimate_net_profit(retail, cost, shipping=0, fees=0):
|
|
"""Net profit = retail - (cost + shipping + fees)."""
|
|
return (_q(retail) - _q(cost) - _q(shipping) - _q(fees)).quantize(CENT, rounding=ROUND_HALF_UP)
|