From 8fdec05908496107a4d637b41769257e07a045a3 Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Tue, 12 May 2015 14:04:20 +0000 Subject: [PATCH] added best_price rating method --- TODO.md | 7 +- orchestra/contrib/bills/models.py | 2 +- orchestra/contrib/orchestration/manager.py | 4 +- orchestra/contrib/plans/ratings.py | 64 +++++++++++-------- orchestra/contrib/services/handlers.py | 4 +- orchestra/contrib/services/models.py | 10 ++- .../contrib/services/tests/test_handler.py | 2 +- orchestra/core/caches.py | 2 +- 8 files changed, 58 insertions(+), 37 deletions(-) diff --git a/TODO.md b/TODO.md index d8dfef36..12195713 100644 --- a/TODO.md +++ b/TODO.md @@ -355,4 +355,9 @@ resorce monitoring more efficient, less mem an better queries for calc current d # best_price rating method -# select contact with one result: redirect +# bill this https://orchestra.pangea.org/admin/orders/order/8236/ should be already billed, <= vs < +# Convert rating method from function to PluginClass +# Tests can not run because django.db.utils.ProgrammingError: relation "accounts_account" does not exist + + + diff --git a/orchestra/contrib/bills/models.py b/orchestra/contrib/bills/models.py index 6e924e8d..f83a4db2 100644 --- a/orchestra/contrib/bills/models.py +++ b/orchestra/contrib/bills/models.py @@ -233,7 +233,7 @@ class Bill(models.Model): subtotals = {} lines = self.lines.annotate(totals=(F('subtotal') + Coalesce(F('sublines__total'), 0))) for tax, total in lines.values_list('tax', 'totals'): - subtotal, taxes = subtotals.get(tax, (0, 0)) + subtotal, taxes = subtotals.get(tax) or (0, 0) subtotal += total subtotals[tax] = (subtotal, round(tax/100*subtotal, 2)) return subtotals diff --git a/orchestra/contrib/orchestration/manager.py b/orchestra/contrib/orchestration/manager.py index 8e3390c2..b2506b5b 100644 --- a/orchestra/contrib/orchestration/manager.py +++ b/orchestra/contrib/orchestration/manager.py @@ -146,8 +146,8 @@ def execute(scripts, serialize=False, async=None): def collect(instance, action, **kwargs): """ collect operations """ - operations = kwargs.get('operations', OrderedSet()) - route_cache = kwargs.get('route_cache', {}) + operations = kwargs.get('operations') or OrderedSet() + route_cache = kwargs.get('route_cache') or {} for backend_cls in ServiceBackend.get_backends(): # Check if there exists a related instance to be executed for this backend and action instances = [] diff --git a/orchestra/contrib/plans/ratings.py b/orchestra/contrib/plans/ratings.py index 58bb8d4d..18606f7d 100644 --- a/orchestra/contrib/plans/ratings.py +++ b/orchestra/contrib/plans/ratings.py @@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.utils.python import AttrDict -def _compute(rates, metric): +def _compute_steps(rates, metric): value = 0 num = len(rates) accumulated = 0 @@ -65,18 +65,20 @@ def _prepend_missing(rates): def step_price(rates, metric): + if rates.query.order_by != ['plan', 'quantity']: + raise ValueError("rates queryset should be ordered by 'plan' and 'quantity'") # Step price group = [] minimal = (sys.maxsize, []) for plan, rates in rates.group_by('plan').items(): rates = _prepend_missing(rates) - value, steps = _compute(rates, metric) + value, steps = _compute_steps(rates, metric) if plan.is_combinable: group.append(steps) else: minimal = min(minimal, (value, steps), key=lambda v: v[0]) if len(group) == 1: - value, steps = _compute(rates, metric) + value, steps = _compute_steps(rates, metric) minimal = min(minimal, (value, steps), key=lambda v: v[0]) elif len(group) > 1: # Merge @@ -125,6 +127,8 @@ step_price.help_text = _("All rates with a quantity lower than the metric are ap def match_price(rates, metric): + if rates.query.order_by != ['plan', 'quantity']: + raise ValueError("rates queryset should be ordered by 'plan' and 'quantity'") candidates = [] selected = False prev = None @@ -155,29 +159,37 @@ match_price.help_text = _("Only the rate with a) inmediate inferior metri def best_price(rates, metric): + if rates.query.order_by != ['plan', 'quantity']: + raise ValueError("rates queryset should be ordered by 'plan' and 'quantity'") candidates = [] - selected = False - prev = None - rates = _prepend_missing(rates.distinct()) - for rate in rates: - 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 - prev = rate - if not selected and prev.quantity <= metric: - candidates.append(prev) - candidates.sort(key=lambda r: r.price) - if candidates: - return [AttrDict(**{ - 'quantity': metric, - 'price': candidates[0].price, - })] - return None + for plan, rates in rates.group_by('plan').items(): + rates = _prepend_missing(rates) + plan_candidates = [] + for rate in rates: + if rate.quantity > metric: + break + if plan_candidates: + plan_candidates[-1].barrier = rate.quantity + plan_candidates.append(AttrDict( + price=rate.price, + barrier=metric, + )) + candidates.extend(plan_candidates) + results = [] + accumulated = 0 + for candidate in sorted(candidates, key=lambda c: c.price): + if accumulated+candidate.barrier > metric: + quantity = metric - accumulated + else: + quantity = candidate.barrier + 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 best_price.verbose_name = _("Best price") best_price.help_text = _("Produces the best possible price given all active rating lines.") diff --git a/orchestra/contrib/services/handlers.py b/orchestra/contrib/services/handlers.py index c8a2e67b..902305fc 100644 --- a/orchestra/contrib/services/handlers.py +++ b/orchestra/contrib/services/handlers.py @@ -138,7 +138,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): def get_billing_point(self, order, bp=None, **options): not_cachable = self.billing_point == self.FIXED_DATE and options.get('fixed_point') if not_cachable or bp is None: - bp = options.get('billing_point', timezone.now().date()) + bp = options.get('billing_point') or timezone.now().date() if not options.get('fixed_point'): msg = ("Support for '%s' period and '%s' point is not implemented" % (self.get_billing_period_display(), self.get_billing_point_display())) @@ -501,7 +501,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): if charged is None: charged = metric size = self.get_price_size(cini, cend) - new_price += self.get_price(order, metric) * size + new_price += self.get_price(account, metric) * size new_metric += metric size = self.get_price_size(rini, bp) old_price = self.get_price(account, charged) * size diff --git a/orchestra/contrib/services/models.py b/orchestra/contrib/services/models.py index fdca42ef..2a6f2574 100644 --- a/orchestra/contrib/services/models.py +++ b/orchestra/contrib/services/models.py @@ -221,15 +221,19 @@ class Service(models.Model): counter += rate['quantity'] if counter >= position: return decimal.Decimal(str(rate['price'])) - + def get_rates(self, account, cache=True): # rates are cached per account if not cache: return self.rates.by_account(account) if not hasattr(self, '__cached_rates'): self.__cached_rates = {} - rates = self.__cached_rates.get(account.id, self.rates.by_account(account)) - return rates + try: + return self.__cached_rates[account.id] + except KeyError: + rates = self.rates.by_account(account) + self.__cached_rates[account.id] = rates + return rates @property def rate_method(self): diff --git a/orchestra/contrib/services/tests/test_handler.py b/orchestra/contrib/services/tests/test_handler.py index 01e42d95..ddf1e9cc 100644 --- a/orchestra/contrib/services/tests/test_handler.py +++ b/orchestra/contrib/services/tests/test_handler.py @@ -16,7 +16,7 @@ class Order(object): last_id = 0 def __init__(self, **kwargs): - self.registered_on = kwargs.get('registered_on', timezone.now().date()) + self.registered_on = kwargs.get('registered_on') or timezone.now().date() self.billed_until = kwargs.get('billed_until', None) self.cancelled_on = kwargs.get('cancelled_on', None) type(self).last_id += 1 diff --git a/orchestra/core/caches.py b/orchestra/core/caches.py index 4550d827..15e8d826 100644 --- a/orchestra/core/caches.py +++ b/orchestra/core/caches.py @@ -27,7 +27,7 @@ def get_request_cache(): class RequestCacheMiddleware(object): def process_request(self, request): - cache = _request_cache.get(currentThread(), RequestCache()) + cache = _request_cache.get(currentThread()) or RequestCache() _request_cache[currentThread()] = cache cache.clear()