From 157fd54ce568124e2220d9f56c9c38ee358953ac Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 8 Sep 2014 14:23:06 +0000 Subject: [PATCH] Removed prices app --- TODO.md | 3 - orchestra/admin/menu.py | 5 +- orchestra/apps/orders/admin.py | 16 ++- orchestra/apps/orders/handlers.py | 12 +- orchestra/apps/orders/helpers.py | 47 ++++--- .../apps/orders/migrations/0001_initial.py | 85 +++++++++++++ .../migrations/0002_service_nominal_price.py | 20 +++ .../migrations/0003_auto_20140908_1409.py | 43 +++++++ .../{prices => orders/migrations}/__init__.py | 0 orchestra/apps/orders/models.py | 117 +++++++++++++----- orchestra/apps/orders/pricing.py | 42 +++++++ orchestra/apps/orders/settings.py | 8 ++ orchestra/apps/prices/admin.py | 23 ---- orchestra/apps/prices/models.py | 36 ------ orchestra/apps/prices/settings.py | 10 -- orchestra/bin/orchestra-admin | 2 +- orchestra/conf/base_settings.py | 5 +- 17 files changed, 337 insertions(+), 137 deletions(-) create mode 100644 orchestra/apps/orders/migrations/0001_initial.py create mode 100644 orchestra/apps/orders/migrations/0002_service_nominal_price.py create mode 100644 orchestra/apps/orders/migrations/0003_auto_20140908_1409.py rename orchestra/apps/{prices => orders/migrations}/__init__.py (100%) create mode 100644 orchestra/apps/orders/pricing.py delete mode 100644 orchestra/apps/prices/admin.py delete mode 100644 orchestra/apps/prices/models.py delete mode 100644 orchestra/apps/prices/settings.py diff --git a/TODO.md b/TODO.md index bd8b5f31..2072087f 100644 --- a/TODO.md +++ b/TODO.md @@ -78,9 +78,6 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon * make account_link to autoreplace account on change view. * LAST version of this shit http://wkhtmltopdf.org/downloads.html -* Rename pack to plan ? one can have multiple plans? - -* transaction.process FK? * translations from django.utils import translation diff --git a/orchestra/admin/menu.py b/orchestra/admin/menu.py index 47768d36..e0015028 100644 --- a/orchestra/admin/menu.py +++ b/orchestra/admin/menu.py @@ -50,10 +50,9 @@ def get_account_items(): if isinstalled('orchestra.apps.users'): url = reverse('admin:users_user_changelist') childrens.append(items.MenuItem(_("Users"), url)) - if isinstalled('orchestra.apps.prices'): - url = reverse('admin:prices_pack_changelist') - childrens.append(items.MenuItem(_("Packs"), url)) if isinstalled('orchestra.apps.orders'): + url = reverse('admin:orders_plan_changelist') + childrens.append(items.MenuItem(_("Plans"), url)) url = reverse('admin:orders_order_changelist') childrens.append(items.MenuItem(_("Orders"), url)) if isinstalled('orchestra.apps.bills'): diff --git a/orchestra/apps/orders/admin.py b/orchestra/apps/orders/admin.py index 8288405b..5a330400 100644 --- a/orchestra/apps/orders/admin.py +++ b/orchestra/apps/orders/admin.py @@ -15,7 +15,17 @@ from orchestra.utils.humanize import naturaldate from .actions import BillSelectedOrders from .filters import ActiveOrderListFilter, BilledOrderListFilter -from .models import Service, Order, MetricStorage +from .models import Plan, Rate, Service, Order, MetricStorage + + +class PlanAdmin(AccountAdminMixin, admin.ModelAdmin): + list_display = ('name', 'account_link') + list_filter = ('name',) + + +class RateInline(admin.TabularInline): + model = Rate + ordering = ('plan', 'quantity') class ServiceAdmin(admin.ModelAdmin): @@ -38,9 +48,10 @@ class ServiceAdmin(admin.ModelAdmin): 'classes': ('wide',), 'fields': ('metric', 'pricing_period', 'rate_algorithm', 'orders_effect', 'on_cancel', 'payment_style', - 'trial_period', 'refound_period', 'tax') + 'trial_period', 'refound_period', 'tax', 'nominal_price') }), ) + inlines = [RateInline] def formfield_for_dbfield(self, db_field, **kwargs): """ Improve performance of account field and filter by account """ @@ -117,6 +128,7 @@ class MetricStorageAdmin(admin.ModelAdmin): list_filter = ('order__service',) +admin.site.register(Plan, PlanAdmin) admin.site.register(Service, ServiceAdmin) admin.site.register(Order, OrderAdmin) admin.site.register(MetricStorage, MetricStorageAdmin) diff --git a/orchestra/apps/orders/handlers.py b/orchestra/apps/orders/handlers.py index 2c6fe2c2..9810107b 100644 --- a/orchestra/apps/orders/handlers.py +++ b/orchestra/apps/orders/handlers.py @@ -134,21 +134,21 @@ class ServiceHandler(plugins.Plugin): def get_price_with_orders(self, order, size, ini, end): porders = self.orders.filter(account=order.account).filter( Q(cancelled_on__isnull=True) | Q(cancelled_on__gt=ini) - ).filter(registered_on__lt=end) + ).filter(registered_on__lt=end).order_by('registered_on') price = 0 if self.orders_effect == self.REGISTER_OR_RENEW: - events = get_register_or_renew_events(porders, ini, end) + events = get_register_or_renew_events(porders, order, ini, end) elif self.orders_effect == self.CONCURRENT: - events = get_register_or_cancel_events(porders, ini, end) + events = get_register_or_cancel_events(porders, order, ini, end) else: raise NotImplementedError - for metric, ratio in events: - price += self.get_rate(order, metric) * size * ratio + for metric, position, ratio in events: + price += self.get_price(order, metric, position=position) * size * ratio return price def get_price_with_metric(self, order, size, ini, end): metric = order.get_metric(ini, end) - price = self.get_rate(order, metric) * size + price = self.get_price(order, metric) * size return price def create_line(self, order, price, size, ini, end): diff --git a/orchestra/apps/orders/helpers.py b/orchestra/apps/orders/helpers.py index 91714b47..233f56b9 100644 --- a/orchestra/apps/orders/helpers.py +++ b/orchestra/apps/orders/helpers.py @@ -36,45 +36,54 @@ def get_related_objects(origin, max_depth=2): new_models.append(related) queue.append(new_models) -def get_register_or_cancel_events(porders, ini, end): +def get_register_or_cancel_events(porders, order, ini, end): assert ini <= end, "ini > end" CANCEL = 'cancel' REGISTER = 'register' changes = {} counter = 0 - for order in porders: - if order.cancelled_on: - cancel = order.cancelled_on - if order.billed_until and order.cancelled_on < order.billed_until: - cancel = order.billed_until + for num, porder in enumerate(porders.order_by('registered_on')): + if porder == order: + position = num + if porder.cancelled_on: + cancel = porder.cancelled_on + if porder.billed_until and porder.cancelled_on < porder.billed_until: + cancel = porder.billed_until if cancel > ini and cancel < end: changes.setdefault(cancel, []) - changes[cancel].append(CANCEL) - if order.registered_on <= ini: + changes[cancel].append((CANCEL, num)) + if porder.registered_on <= ini: counter += 1 - elif order.registered_on < end: - changes.setdefault(order.registered_on, []) - changes[order.registered_on].append(REGISTER) + elif porder.registered_on < end: + changes.setdefault(porder.registered_on, []) + changes[porder.registered_on].append((REGISTER, num)) pointer = ini total = float((end-ini).days) for date in sorted(changes.keys()): - yield counter, (date-pointer).days/total - for change in changes[date]: + yield counter, position, (date-pointer).days/total + for change, num in changes[date]: if change is CANCEL: counter -= 1 + if num < position: + position -= 1 else: counter += 1 pointer = date - yield counter, (end-pointer).days/total + yield counter, position, (end-pointer).days/total -def get_register_or_renew_events(handler, porders, ini, end): +def get_register_or_renew_events(handler, porders, order, ini, end): total = float((end-ini).days) for sini, send in handler.get_pricing_slots(ini, end): counter = 0 - for order in porders: - if order.registered_on >= sini and order.registered_on < send: + position = 0 + for porder in porders.order_by('registered_on'): + if porder == order: + position = abs(position) + elif position < 0: + position -= 1 + if porder.registered_on >= sini and porder.registered_on < send: counter += 1 - elif order.billed_until > send or order.cancelled_on > send: + elif porder.billed_until > send or porder.cancelled_on > send: counter += 1 - yield counter, (send-sini)/total + yield counter, position, (send-sini)/total diff --git a/orchestra/apps/orders/migrations/0001_initial.py b/orchestra/apps/orders/migrations/0001_initial.py new file mode 100644 index 00000000..3647692c --- /dev/null +++ b/orchestra/apps/orders/migrations/0001_initial.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '__first__'), + ('contenttypes', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='MetricStorage', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('value', models.BigIntegerField(verbose_name='value')), + ('created_on', models.DateField(auto_now_add=True, verbose_name='created on')), + ('updated_on', models.DateField(auto_now=True, verbose_name='updated on')), + ], + options={ + 'get_latest_by': 'created_on', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('object_id', models.PositiveIntegerField(null=True)), + ('registered_on', models.DateField(auto_now_add=True, verbose_name='registered on')), + ('cancelled_on', models.DateField(null=True, verbose_name='cancelled on', blank=True)), + ('billed_on', models.DateField(null=True, verbose_name='billed on', blank=True)), + ('billed_until', models.DateField(null=True, verbose_name='billed until', blank=True)), + ('ignore', models.BooleanField(default=False, verbose_name='ignore')), + ('description', models.TextField(verbose_name='description', blank=True)), + ('account', models.ForeignKey(related_name=b'orders', verbose_name='account', to='accounts.Account')), + ('content_type', models.ForeignKey(to='contenttypes.ContentType')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Service', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('description', models.CharField(unique=True, max_length=256, verbose_name='description')), + ('match', models.CharField(max_length=256, verbose_name='match', blank=True)), + ('handler_type', models.CharField(blank=True, help_text='Handler used for processing this Service. A handler enables customized behaviour far beyond what options here allow to.', max_length=256, verbose_name='handler', choices=[(b'', 'Default')])), + ('is_active', models.BooleanField(default=True, verbose_name='is active')), + ('billing_period', models.CharField(default=b'ANUAL', choices=[(b'', 'One time service'), (b'MONTHLY', 'Monthly billing'), (b'ANUAL', 'Anual billing')], max_length=16, blank=True, help_text='Renewal period for recurring invoicing', verbose_name='billing period')), + ('billing_point', models.CharField(default=b'ON_FIXED_DATE', help_text='Reference point for calculating the renewal date on recurring invoices', max_length=16, verbose_name='billing point', choices=[(b'ON_REGISTER', 'Registration date'), (b'ON_FIXED_DATE', 'Fixed billing date')])), + ('delayed_billing', models.CharField(default=b'ONE_MONTH', choices=[(b'', 'No delay (inmediate billing)'), (b'TEN_DAYS', 'Ten days'), (b'ONE_MONTH', 'One month')], max_length=16, blank=True, help_text='Period in which this service will be ignored for billing', verbose_name='delayed billing')), + ('is_fee', models.BooleanField(default=False, help_text='Designates whether this service should be billed as membership fee or not', verbose_name='is fee')), + ('metric', models.CharField(help_text='Metric used to compute the pricing rate. Number of orders is used when left blank.', max_length=256, verbose_name='metric', blank=True)), + ('tax', models.PositiveIntegerField(default=0, verbose_name='tax', choices=[(0, 'Duty free'), (7, '7%'), (21, '21%')])), + ('pricing_period', models.CharField(default=b'BILLING_PERIOD', help_text='Period used for calculating the metric used on the pricing rate', max_length=16, verbose_name='pricing period', choices=[(b'BILLING_PERIOD', 'Same as billing period'), (b'MONTHLY', 'Monthly data'), (b'ANUAL', 'Anual data')])), + ('rate_algorithm', models.CharField(default=b'BEST_PRICE', help_text='Algorithm used to interprete the rating table', max_length=16, verbose_name='rate algorithm', choices=[(b'BEST_PRICE', 'Best progressive price'), (b'PROGRESSIVE_PRICE', 'Conservative progressive price'), (b'MATCH_PRICE', 'Match price')])), + ('orders_effect', models.CharField(default=b'CONCURRENT', help_text='Defines the lookup behaviour when using orders for the pricing rate computation of this service.', max_length=16, verbose_name='orders effect', choices=[(b'REGISTER_OR_RENEW', 'Register or renew events'), (b'CONCURRENT', 'Active at every given time')])), + ('on_cancel', models.CharField(default=b'DISCOUNT', help_text='Defines the cancellation behaviour of this service', max_length=16, verbose_name='on cancel', choices=[(b'NOTHING', 'Nothing'), (b'DISCOUNT', 'Discount'), (b'COMPENSATE', 'Discount and compensate'), (b'REFOUND', 'Discount, compensate and refound')])), + ('payment_style', models.CharField(default=b'PREPAY', help_text='Designates whether this service should be paid after consumtion (postpay/on demand) or prepaid', max_length=16, verbose_name='payment style', choices=[(b'PREPAY', 'Prepay'), (b'POSTPAY', 'Postpay (on demand)')])), + ('trial_period', models.CharField(default=b'', choices=[(b'', 'No trial'), (b'TEN_DAYS', 'Ten days'), (b'ONE_MONTH', 'One month')], max_length=16, blank=True, help_text='Period in which no charge will be issued', verbose_name='trial period')), + ('refound_period', models.CharField(default=b'', choices=[(b'', 'Never refound'), (b'TEN_DAYS', 'Ten days'), (b'ONE_MONTH', 'One month'), (b'ALWAYS', 'Always refound')], max_length=16, blank=True, help_text='Period in which automatic refound will be performed on service cancellation', verbose_name='refound period')), + ('content_type', models.ForeignKey(verbose_name='content type', to='contenttypes.ContentType')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.AddField( + model_name='order', + name='service', + field=models.ForeignKey(related_name=b'orders', verbose_name='service', to='orders.Service'), + preserve_default=True, + ), + migrations.AddField( + model_name='metricstorage', + name='order', + field=models.ForeignKey(verbose_name='order', to='orders.Order'), + preserve_default=True, + ), + ] diff --git a/orchestra/apps/orders/migrations/0002_service_nominal_price.py b/orchestra/apps/orders/migrations/0002_service_nominal_price.py new file mode 100644 index 00000000..8a877cc2 --- /dev/null +++ b/orchestra/apps/orders/migrations/0002_service_nominal_price.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='service', + name='nominal_price', + field=models.DecimalField(default=0.0, verbose_name='nominal price', max_digits=12, decimal_places=2), + preserve_default=False, + ), + ] diff --git a/orchestra/apps/orders/migrations/0003_auto_20140908_1409.py b/orchestra/apps/orders/migrations/0003_auto_20140908_1409.py new file mode 100644 index 00000000..e09c3e95 --- /dev/null +++ b/orchestra/apps/orders/migrations/0003_auto_20140908_1409.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '__first__'), + ('orders', '0002_service_nominal_price'), + ] + + operations = [ + migrations.CreateModel( + name='Plan', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(default=b'basic', max_length=128, verbose_name='plan', choices=[(b'basic', 'Basic'), (b'advanced', 'Advanced')])), + ('account', models.ForeignKey(related_name=b'plans', verbose_name='account', to='accounts.Account')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Rate', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('plan', models.CharField(blank=True, max_length=128, verbose_name='plan', choices=[(b'', 'default'), (b'basic', 'Basic'), (b'advanced', 'Advanced')])), + ('quantity', models.PositiveIntegerField(null=True, verbose_name='quantity', blank=True)), + ('value', models.DecimalField(verbose_name='value', max_digits=12, decimal_places=2)), + ('service', models.ForeignKey(related_name=b'rates', verbose_name='service', to='orders.Service')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.AlterUniqueTogether( + name='rate', + unique_together=set([('service', 'plan', 'quantity')]), + ), + ] diff --git a/orchestra/apps/prices/__init__.py b/orchestra/apps/orders/migrations/__init__.py similarity index 100% rename from orchestra/apps/prices/__init__.py rename to orchestra/apps/orders/migrations/__init__.py diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py index 73f61b7b..b35793b3 100644 --- a/orchestra/apps/orders/models.py +++ b/orchestra/apps/orders/models.py @@ -15,10 +15,49 @@ from orchestra.models import queryset from orchestra.utils.apps import autodiscover from orchestra.utils.python import import_class -from . import settings, helpers +from . import helpers, settings, pricing from .handlers import ServiceHandler +class Plan(models.Model): + account = models.ForeignKey('accounts.Account', verbose_name=_("account"), + related_name='plans') + name = models.CharField(_("plan"), max_length=128, + choices=settings.ORDERS_PLANS, + default=settings.ORDERS_DEFAULT_PLAN) + + def __unicode__(self): + return self.name + + +class RateQuerySet(models.QuerySet): + group_by = queryset.group_by + + def by_account(self, account): + # Default allways selected + qset = Q(plan__isnull=True) + for plan in account.plans.all(): + qset |= Q(plan=plan) + return self.filter(qset) + + +class Rate(models.Model): + service = models.ForeignKey('orders.Service', verbose_name=_("service"), + related_name='rates') + plan = models.CharField(_("plan"), max_length=128, blank=True, + choices=(('', _("Default")),) + settings.ORDERS_PLANS) + quantity = models.PositiveIntegerField(_("quantity"), null=True, blank=True) + value = models.DecimalField(_("value"), max_digits=12, decimal_places=2) + + objects = RateQuerySet.as_manager() + + class Meta: + unique_together = ('service', 'plan', 'quantity') + + def __unicode__(self): + return "{}-{}".format(str(self.value), self.quantity) + + autodiscover('handlers') @@ -43,6 +82,10 @@ class Service(models.Model): BEST_PRICE = 'BEST_PRICE' PROGRESSIVE_PRICE = 'PROGRESSIVE_PRICE' MATCH_PRICE = 'MATCH_PRICE' + PRICING_METHODS = { + BEST_PRICE: pricing.best_price, + MATCH_PRICE: pricing.match_price, + } description = models.CharField(_("description"), max_length=256, unique=True) content_type = models.ForeignKey(ContentType, verbose_name=_("content type")) @@ -85,6 +128,8 @@ class Service(models.Model): metric = models.CharField(_("metric"), max_length=256, blank=True, help_text=_("Metric used to compute the pricing rate. " "Number of orders is used when left blank.")) + nominal_price = models.DecimalField(_("nominal price"), max_digits=12, + decimal_places=2) tax = models.PositiveIntegerField(_("tax"), choices=settings.ORDERS_SERVICE_TAXES, default=settings.ORDERS_SERVICE_DEFAUL_TAX) pricing_period = models.CharField(_("pricing period"), max_length=16, @@ -99,8 +144,8 @@ class Service(models.Model): rate_algorithm = models.CharField(_("rate algorithm"), max_length=16, help_text=_("Algorithm used to interprete the rating table"), choices=( - (BEST_PRICE, _("Best price")), - (PROGRESSIVE_PRICE, _("Progressive price")), + (BEST_PRICE, _("Best progressive price")), + (PROGRESSIVE_PRICE, _("Conservative progressive price")), (MATCH_PRICE, _("Match price")), ), default=BEST_PRICE) @@ -121,22 +166,6 @@ class Service(models.Model): (REFOUND, _("Discount, compensate and refound")), ), default=DISCOUNT) - # TODO remove, orders are not disabled (they are cancelled user.is_active) -# on_disable = models.CharField(_("on disable"), max_length=16, -# help_text=_("Defines the behaviour of this service when disabled"), -# choices=( -# (NOTHING, _("Nothing")), -# (DISCOUNT, _("Discount")), -# (REFOUND, _("Refound")), -# ), -# default=DISCOUNT) -# on_register = models.CharField(_("on register"), max_length=16, -# help_text=_("Defines the behaviour of this service on registration"), -# choices=( -# (NOTHING, _("Nothing")), -# (DISCOUNT, _("Discount (fixed BP)")), -# ), -# default=DISCOUNT) payment_style = models.CharField(_("payment style"), max_length=16, help_text=_("Designates whether this service should be paid after " "consumtion (postpay/on demand) or prepaid"), @@ -164,11 +193,6 @@ class Service(models.Model): ), default=NEVER, blank=True) - @property - def nominal_price(self): - # FIXME delete and make it a model field - return 10 - def __unicode__(self): return self.description @@ -226,9 +250,36 @@ class Service(models.Model): return self.billing_period return self.pricing_period - def get_rate(self, order, metric): - # TODO implement - return 12 + def get_price(self, order, metric, position=None): + """ + if position is provided an specific price for that position is returned, + accumulated price is returned otherwise + """ + rates = self.rates.by_account(order.account) + if not rates: + return self.nominal_price + rates = self.rate_method(rates, metric) + counter = 0 + if position is None: + ant_counter = 0 + accumulated = 0 + for rate in self.get_rates(order.account, metric): + counter += rate['number'] + if counter >= metric: + counter = metric + accumulated += (counter - ant_counter) * rate['price'] + return accumulated + ant_counter = counter + accumulated += rate['price'] * rate['number'] + else: + for rate in self.get_rates(order.account, metric): + counter += rate['number'] + if counter >= position: + return rate['price'] + + @property + def rate_method(self, *args, **kwargs): + return self.RATE_METHODS[self.rate_algorithm] class OrderQuerySet(models.QuerySet): @@ -323,8 +374,7 @@ class Order(models.Model): self.save() def get_metric(self, ini, end): - # TODO implement - return 10 + return MetricStorage.get(self, ini, end) class MetricStorage(models.Model): @@ -353,8 +403,11 @@ class MetricStorage(models.Model): @classmethod def get(cls, order, ini, end): - # TODO - pass + try: + return cls.objects.filter(order=order, updated_on__lt=end, + updated_on__gte=ini).latest('updated_on').value + except cls.DoesNotExist: + return 0 @receiver(pre_delete, dispatch_uid="orders.cancel_orders") @@ -379,3 +432,5 @@ def update_orders(sender, **kwargs): accounts.register(Order) +accounts.register(Plan) +services.register(Plan, menu=False) diff --git a/orchestra/apps/orders/pricing.py b/orchestra/apps/orders/pricing.py new file mode 100644 index 00000000..1391deb9 --- /dev/null +++ b/orchestra/apps/orders/pricing.py @@ -0,0 +1,42 @@ +import sys + + +def best_price(rates, metric): + rates = rates.order_by('metric').order_by('plan') + ix = 0 + steps = [] + num = rates.count() + while ix < num: + if ix+1 == num or rates[ix].plan != rates[ix+1].plan: + number = metric + else: + number = rates[ix+1].metric - rates[ix].metric + steps.append({ + 'number': sys.maxint, + 'price': rates[ix].price + }) + ix += 1 + + steps.sort(key=lambda s: s['price']) + acumulated = 0 + for step in steps: + previous = acumulated + acumulated += step['number'] + if acumulated >= metric: + step['number'] = metric - previous + yield step + raise StopIteration + yield step + + +def match_price(rates, metric): + minimal = None + for plan, rates in rates.order_by('-metric').group_by('plan'): + if minimal is None: + minimal = rates[0].price + else: + minimal = min(minimal, rates[0].price) + return [{ + 'number': sys.maxint, + 'price': minimal + }] diff --git a/orchestra/apps/orders/settings.py b/orchestra/apps/orders/settings.py index e6b49d1a..42ea3422 100644 --- a/orchestra/apps/orders/settings.py +++ b/orchestra/apps/orders/settings.py @@ -16,3 +16,11 @@ ORDERS_SERVICE_ANUAL_BILLING_MONTH = getattr(settings, 'ORDERS_SERVICE_ANUAL_BIL ORDERS_BILLING_BACKEND = getattr(settings, 'ORDERS_BILLING_BACKEND', 'orchestra.apps.orders.billing.BillsBackend') + + +ORDERS_PLANS = getattr(settings, 'ORDERS_PLANS', ( + ('basic', _("Basic")), + ('advanced', _("Advanced")), +)) + +ORDERS_DEFAULT_PLAN = getattr(settings, 'ORDERS_DEFAULT_PLAN', 'basic') diff --git a/orchestra/apps/prices/admin.py b/orchestra/apps/prices/admin.py deleted file mode 100644 index 7df5bbbb..00000000 --- a/orchestra/apps/prices/admin.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.contrib import admin - -from orchestra.admin.utils import insertattr -from orchestra.apps.accounts.admin import AccountAdminMixin -from orchestra.apps.orders.models import Service - -from .models import Pack, Rate - - -class PackAdmin(AccountAdminMixin, admin.ModelAdmin): - list_display = ('name', 'account_link') - list_filter = ('name',) - - -admin.site.register(Pack, PackAdmin) - - -class RateInline(admin.TabularInline): - model = Rate - ordering = ('pack', 'quantity') - - -insertattr(Service, 'inlines', RateInline) diff --git a/orchestra/apps/prices/models.py b/orchestra/apps/prices/models.py deleted file mode 100644 index 0c4c297b..00000000 --- a/orchestra/apps/prices/models.py +++ /dev/null @@ -1,36 +0,0 @@ -from django.db import models -from django.contrib.contenttypes.models import ContentType -from django.utils.translation import ugettext_lazy as _ - -from orchestra.core import accounts, services - -from . import settings - - -class Pack(models.Model): - account = models.ForeignKey('accounts.Account', verbose_name=_("account"), - related_name='packs') - name = models.CharField(_("pack"), max_length=128, - choices=settings.PRICES_PACKS, - default=settings.PRICES_DEFAULT_PACK) - - def __unicode__(self): - return self.name - - -class Rate(models.Model): - service = models.ForeignKey('orders.Service', verbose_name=_("service")) - pack = models.CharField(_("pack"), max_length=128, blank=True, - choices=(('', _("default")),) + settings.PRICES_PACKS) - quantity = models.PositiveIntegerField(_("quantity"), null=True, blank=True) - value = models.DecimalField(_("value"), max_digits=12, decimal_places=2) - - class Meta: - unique_together = ('service', 'pack', 'quantity') - - def __unicode__(self): - return "{}-{}".format(str(self.value), self.quantity) - - -accounts.register(Pack) -services.register(Pack, menu=False) diff --git a/orchestra/apps/prices/settings.py b/orchestra/apps/prices/settings.py deleted file mode 100644 index 885657b5..00000000 --- a/orchestra/apps/prices/settings.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.conf import settings -from django.utils.translation import ugettext_lazy as _ - - -PRICES_PACKS = getattr(settings, 'PRICES_PACKS', ( - ('basic', _("Basic")), - ('advanced', _("Advanced")), -)) - -PRICES_DEFAULT_PACK = getattr(settings, 'PRICES_DEFAULT_PACK', 'basic') diff --git a/orchestra/bin/orchestra-admin b/orchestra/bin/orchestra-admin index d39ad91c..41096e69 100755 --- a/orchestra/bin/orchestra-admin +++ b/orchestra/bin/orchestra-admin @@ -139,7 +139,7 @@ function install_requirements () { django-extensions==1.1.1 \ django-transaction-signals==1.0.0 \ django-celery==3.1.10 \ - celery==3.1.7 \ + celery==3.1.13 \ kombu==3.0.8 \ Markdown==2.4 \ django-debug-toolbar==1.2.1 \ diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py index ae2dc62f..85625931 100644 --- a/orchestra/conf/base_settings.py +++ b/orchestra/conf/base_settings.py @@ -79,7 +79,6 @@ INSTALLED_APPS = ( 'orchestra.apps.databases', 'orchestra.apps.vps', 'orchestra.apps.issues', - 'orchestra.apps.prices', 'orchestra.apps.orders', 'orchestra.apps.miscellaneous', 'orchestra.apps.bills', @@ -145,7 +144,7 @@ FLUENT_DASHBOARD_APP_GROUPS = ( 'orchestra.apps.contacts.models.Contact', 'orchestra.apps.users.models.User', 'orchestra.apps.orders.models.Order', - 'orchestra.apps.prices.models.Pack', + 'orchestra.apps.orders.models.Pack', 'orchestra.apps.bills.models.Bill', # 'orchestra.apps.payments.models.PaymentSource', 'orchestra.apps.payments.models.Transaction', @@ -187,7 +186,7 @@ FLUENT_DASHBOARD_APP_ICONS = { 'contacts/contact': 'contact_book.png', 'orders/order': 'basket.png', 'orders/service': 'price.png', - 'prices/pack': 'Pack.png', + 'orders/plan': 'Pack.png', 'bills/bill': 'invoice.png', 'payments/paymentsource': 'card_in_use.png', 'payments/transaction': 'transaction.png',