2014-09-15 15:36:24 +00:00
|
|
|
import sys
|
|
|
|
|
2015-04-04 18:10:39 +00:00
|
|
|
from django.utils.translation import ugettext_lazy as _
|
2015-03-31 12:39:08 +00:00
|
|
|
|
2014-09-26 15:05:20 +00:00
|
|
|
from orchestra.utils.python import AttrDict
|
2014-09-15 15:36:24 +00:00
|
|
|
|
|
|
|
|
2015-05-12 14:04:20 +00:00
|
|
|
def _compute_steps(rates, metric):
|
2014-09-15 15:36:24 +00:00
|
|
|
value = 0
|
|
|
|
num = len(rates)
|
|
|
|
accumulated = 0
|
2014-09-16 14:35:00 +00:00
|
|
|
barrier = 1
|
|
|
|
next_barrier = None
|
2014-09-15 15:36:24 +00:00
|
|
|
end = False
|
|
|
|
ix = 0
|
|
|
|
steps = []
|
|
|
|
while ix < num and not end:
|
2014-09-16 14:35:00 +00:00
|
|
|
fold = 1
|
|
|
|
# Multiple contractions
|
|
|
|
while ix < num-1 and rates[ix] == rates[ix+1]:
|
|
|
|
ix += 1
|
|
|
|
fold += 1
|
2014-09-15 15:36:24 +00:00
|
|
|
if ix+1 == num:
|
|
|
|
quantity = metric - accumulated
|
2014-09-16 14:35:00 +00:00
|
|
|
next_barrier = quantity
|
2014-09-15 15:36:24 +00:00
|
|
|
else:
|
|
|
|
quantity = rates[ix+1].quantity - rates[ix].quantity
|
2014-09-16 14:35:00 +00:00
|
|
|
next_barrier = quantity
|
|
|
|
if rates[ix+1].price > rates[ix].price:
|
|
|
|
quantity *= fold
|
2014-09-15 15:36:24 +00:00
|
|
|
if accumulated+quantity > metric:
|
|
|
|
quantity = metric - accumulated
|
|
|
|
end = True
|
|
|
|
price = rates[ix].price
|
2014-09-26 15:05:20 +00:00
|
|
|
steps.append(AttrDict(**{
|
2014-09-15 15:36:24 +00:00
|
|
|
'quantity': quantity,
|
|
|
|
'price': price,
|
2014-09-16 14:35:00 +00:00
|
|
|
'barrier': barrier,
|
2014-09-15 15:36:24 +00:00
|
|
|
}))
|
|
|
|
accumulated += quantity
|
2014-09-16 14:35:00 +00:00
|
|
|
barrier += next_barrier
|
2014-09-15 15:36:24 +00:00
|
|
|
value += quantity*price
|
|
|
|
ix += 1
|
|
|
|
return value, steps
|
|
|
|
|
|
|
|
|
2015-05-14 13:28:54 +00:00
|
|
|
def _standardize(rates):
|
2014-09-26 10:38:50 +00:00
|
|
|
"""
|
|
|
|
Support for incomplete rates
|
|
|
|
When first rate (quantity=5, price=10) defaults to nominal_price
|
|
|
|
"""
|
2015-05-14 13:28:54 +00:00
|
|
|
std_rates = []
|
|
|
|
minimal = rates[0].quantity
|
|
|
|
for rate in rates:
|
|
|
|
if rate.quantity == 0:
|
|
|
|
rate.quantity = 1
|
|
|
|
elif rate.quantity == minimal and rate.quantity > 1:
|
|
|
|
service = rate.service
|
|
|
|
rate_class = type(rate)
|
|
|
|
std_rates.append(
|
|
|
|
rate_class(service=service, plan=rate.plan, quantity=1, price=service.nominal_price)
|
2014-09-26 10:38:50 +00:00
|
|
|
)
|
2015-05-14 13:28:54 +00:00
|
|
|
std_rates.append(rate)
|
|
|
|
return std_rates
|
2014-09-26 10:38:50 +00:00
|
|
|
|
|
|
|
|
2014-09-15 15:36:24 +00:00
|
|
|
def step_price(rates, metric):
|
2015-05-12 14:04:20 +00:00
|
|
|
if rates.query.order_by != ['plan', 'quantity']:
|
|
|
|
raise ValueError("rates queryset should be ordered by 'plan' and 'quantity'")
|
2014-09-15 15:36:24 +00:00
|
|
|
# Step price
|
|
|
|
group = []
|
2015-04-02 16:14:55 +00:00
|
|
|
minimal = (sys.maxsize, [])
|
|
|
|
for plan, rates in rates.group_by('plan').items():
|
2015-05-14 13:28:54 +00:00
|
|
|
rates = _standardize(rates)
|
2015-05-12 14:04:20 +00:00
|
|
|
value, steps = _compute_steps(rates, metric)
|
2014-09-15 15:36:24 +00:00
|
|
|
if plan.is_combinable:
|
|
|
|
group.append(steps)
|
|
|
|
else:
|
|
|
|
minimal = min(minimal, (value, steps), key=lambda v: v[0])
|
|
|
|
if len(group) == 1:
|
2015-05-12 14:04:20 +00:00
|
|
|
value, steps = _compute_steps(rates, metric)
|
2014-09-15 15:36:24 +00:00
|
|
|
minimal = min(minimal, (value, steps), key=lambda v: v[0])
|
|
|
|
elif len(group) > 1:
|
|
|
|
# Merge
|
|
|
|
steps = []
|
|
|
|
for rates in group:
|
|
|
|
steps += rates
|
|
|
|
steps.sort(key=lambda s: s.price)
|
|
|
|
result = []
|
|
|
|
counter = 0
|
|
|
|
value = 0
|
|
|
|
ix = 0
|
|
|
|
targets = []
|
|
|
|
while counter < metric:
|
|
|
|
barrier = steps[ix].barrier
|
|
|
|
if barrier <= counter+1:
|
|
|
|
price = steps[ix].price
|
|
|
|
quantity = steps[ix].quantity
|
|
|
|
if quantity + counter > metric:
|
|
|
|
quantity = metric - counter
|
|
|
|
else:
|
|
|
|
for target in targets:
|
|
|
|
if counter + quantity >= target:
|
|
|
|
quantity = (counter+quantity+1) - target
|
|
|
|
steps[ix].quantity -= quantity
|
|
|
|
if not steps[ix].quantity:
|
|
|
|
steps.pop(ix)
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
steps.pop(ix)
|
|
|
|
counter += quantity
|
|
|
|
value += quantity*price
|
|
|
|
if result and result[-1].price == price:
|
|
|
|
result[-1].quantity += quantity
|
|
|
|
else:
|
2014-09-26 15:05:20 +00:00
|
|
|
result.append(AttrDict(quantity=quantity, price=price))
|
2014-09-15 15:36:24 +00:00
|
|
|
ix = 0
|
|
|
|
targets = []
|
|
|
|
else:
|
|
|
|
targets.append(barrier)
|
|
|
|
ix += 1
|
|
|
|
minimal = min(minimal, (value, result), key=lambda v: v[0])
|
|
|
|
return minimal[1]
|
2015-03-31 12:39:08 +00:00
|
|
|
step_price.verbose_name = _("Step price")
|
2015-05-14 13:28:54 +00:00
|
|
|
step_price.help_text = _("All rates with a quantity lower or equal than the metric are applied. "
|
2015-04-24 14:03:42 +00:00
|
|
|
"Nominal price will be used when initial block is missing.")
|
2014-09-15 15:36:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
def match_price(rates, metric):
|
2015-05-12 14:04:20 +00:00
|
|
|
if rates.query.order_by != ['plan', 'quantity']:
|
|
|
|
raise ValueError("rates queryset should be ordered by 'plan' and 'quantity'")
|
2014-09-15 15:36:24 +00:00
|
|
|
candidates = []
|
|
|
|
selected = False
|
|
|
|
prev = None
|
2015-05-14 13:28:54 +00:00
|
|
|
rates = _standardize(rates.distinct())
|
2014-09-26 10:38:50 +00:00
|
|
|
for rate in rates:
|
2014-09-23 16:23:36 +00:00
|
|
|
if prev:
|
|
|
|
if prev.plan != rate.plan:
|
|
|
|
if not selected and prev.quantity <= metric:
|
|
|
|
candidates.append(prev)
|
|
|
|
selected = False
|
|
|
|
if not selected and rate.quantity > metric:
|
|
|
|
if prev.quantity <= metric:
|
|
|
|
candidates.append(prev)
|
|
|
|
selected = True
|
2014-09-15 15:36:24 +00:00
|
|
|
prev = rate
|
|
|
|
if not selected and prev.quantity <= metric:
|
|
|
|
candidates.append(prev)
|
|
|
|
candidates.sort(key=lambda r: r.price)
|
2014-09-23 16:23:36 +00:00
|
|
|
if candidates:
|
2014-09-26 15:05:20 +00:00
|
|
|
return [AttrDict(**{
|
2014-09-23 16:23:36 +00:00
|
|
|
'quantity': metric,
|
|
|
|
'price': candidates[0].price,
|
|
|
|
})]
|
|
|
|
return None
|
2015-03-31 12:39:08 +00:00
|
|
|
match_price.verbose_name = _("Match price")
|
2015-04-24 14:03:42 +00:00
|
|
|
match_price.help_text = _("Only <b>the rate</b> with a) inmediate inferior metric and b) lower price is applied. "
|
|
|
|
"Nominal price will be used when initial block is missing.")
|
2015-05-08 14:05:57 +00:00
|
|
|
|
|
|
|
|
|
|
|
def best_price(rates, metric):
|
2015-05-12 14:04:20 +00:00
|
|
|
if rates.query.order_by != ['plan', 'quantity']:
|
|
|
|
raise ValueError("rates queryset should be ordered by 'plan' and 'quantity'")
|
2015-05-11 14:05:39 +00:00
|
|
|
candidates = []
|
2015-05-12 14:04:20 +00:00
|
|
|
for plan, rates in rates.group_by('plan').items():
|
2015-05-14 13:28:54 +00:00
|
|
|
rates = _standardize(rates)
|
2015-05-12 14:04:20 +00:00
|
|
|
plan_candidates = []
|
|
|
|
for rate in rates:
|
|
|
|
if rate.quantity > metric:
|
|
|
|
break
|
|
|
|
if plan_candidates:
|
2015-05-14 13:28:54 +00:00
|
|
|
ant = plan_candidates[-1]
|
|
|
|
if ant.price == rate.price:
|
|
|
|
# Multiple plans support
|
|
|
|
ant.fold += 1
|
|
|
|
else:
|
|
|
|
ant.quantity = rate.quantity-1
|
|
|
|
plan_candidates.append(AttrDict(
|
|
|
|
price=rate.price,
|
|
|
|
quantity=metric,
|
|
|
|
fold=1,
|
|
|
|
))
|
|
|
|
else:
|
|
|
|
plan_candidates.append(AttrDict(
|
|
|
|
price=rate.price,
|
|
|
|
quantity=metric,
|
|
|
|
fold=1,
|
|
|
|
))
|
2015-05-12 14:04:20 +00:00
|
|
|
candidates.extend(plan_candidates)
|
|
|
|
results = []
|
|
|
|
accumulated = 0
|
|
|
|
for candidate in sorted(candidates, key=lambda c: c.price):
|
2015-05-14 13:28:54 +00:00
|
|
|
if candidate.quantity < accumulated:
|
|
|
|
# Out of barrier
|
|
|
|
continue
|
|
|
|
candidate.quantity *= candidate.fold
|
|
|
|
if accumulated+candidate.quantity > metric:
|
2015-05-12 14:04:20 +00:00
|
|
|
quantity = metric - accumulated
|
|
|
|
else:
|
2015-05-14 13:28:54 +00:00
|
|
|
quantity = candidate.quantity
|
|
|
|
accumulated += quantity
|
2015-05-12 14:04:20 +00:00
|
|
|
if quantity:
|
|
|
|
if results and results[-1].price == candidate.price:
|
|
|
|
results[-1].quantity += quantity
|
|
|
|
else:
|
|
|
|
results.append(AttrDict(**{
|
|
|
|
'quantity': quantity,
|
|
|
|
'price': candidate.price
|
|
|
|
}))
|
|
|
|
return results
|
2015-05-08 14:05:57 +00:00
|
|
|
best_price.verbose_name = _("Best price")
|
2015-05-14 13:28:54 +00:00
|
|
|
best_price.help_text = _("Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).")
|