Removed prices app
This commit is contained in:
parent
fc44c8bfc0
commit
157fd54ce5
3
TODO.md
3
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
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
85
orchestra/apps/orders/migrations/0001_initial.py
Normal file
85
orchestra/apps/orders/migrations/0001_initial.py
Normal file
|
@ -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,
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
]
|
43
orchestra/apps/orders/migrations/0003_auto_20140908_1409.py
Normal file
43
orchestra/apps/orders/migrations/0003_auto_20140908_1409.py
Normal file
|
@ -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')]),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
42
orchestra/apps/orders/pricing.py
Normal file
42
orchestra/apps/orders/pricing.py
Normal file
|
@ -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
|
||||
}]
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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')
|
|
@ -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 \
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue