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.
|
* make account_link to autoreplace account on change view.
|
||||||
|
|
||||||
* LAST version of this shit http://wkhtmltopdf.org/downloads.html
|
* LAST version of this shit http://wkhtmltopdf.org/downloads.html
|
||||||
* Rename pack to plan ? one can have multiple plans?
|
|
||||||
|
|
||||||
* transaction.process FK?
|
|
||||||
|
|
||||||
* translations
|
* translations
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
|
|
|
@ -50,10 +50,9 @@ def get_account_items():
|
||||||
if isinstalled('orchestra.apps.users'):
|
if isinstalled('orchestra.apps.users'):
|
||||||
url = reverse('admin:users_user_changelist')
|
url = reverse('admin:users_user_changelist')
|
||||||
childrens.append(items.MenuItem(_("Users"), url))
|
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'):
|
if isinstalled('orchestra.apps.orders'):
|
||||||
|
url = reverse('admin:orders_plan_changelist')
|
||||||
|
childrens.append(items.MenuItem(_("Plans"), url))
|
||||||
url = reverse('admin:orders_order_changelist')
|
url = reverse('admin:orders_order_changelist')
|
||||||
childrens.append(items.MenuItem(_("Orders"), url))
|
childrens.append(items.MenuItem(_("Orders"), url))
|
||||||
if isinstalled('orchestra.apps.bills'):
|
if isinstalled('orchestra.apps.bills'):
|
||||||
|
|
|
@ -15,7 +15,17 @@ from orchestra.utils.humanize import naturaldate
|
||||||
|
|
||||||
from .actions import BillSelectedOrders
|
from .actions import BillSelectedOrders
|
||||||
from .filters import ActiveOrderListFilter, BilledOrderListFilter
|
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):
|
class ServiceAdmin(admin.ModelAdmin):
|
||||||
|
@ -38,9 +48,10 @@ class ServiceAdmin(admin.ModelAdmin):
|
||||||
'classes': ('wide',),
|
'classes': ('wide',),
|
||||||
'fields': ('metric', 'pricing_period', 'rate_algorithm',
|
'fields': ('metric', 'pricing_period', 'rate_algorithm',
|
||||||
'orders_effect', 'on_cancel', 'payment_style',
|
'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):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Improve performance of account field and filter by account """
|
""" Improve performance of account field and filter by account """
|
||||||
|
@ -117,6 +128,7 @@ class MetricStorageAdmin(admin.ModelAdmin):
|
||||||
list_filter = ('order__service',)
|
list_filter = ('order__service',)
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Plan, PlanAdmin)
|
||||||
admin.site.register(Service, ServiceAdmin)
|
admin.site.register(Service, ServiceAdmin)
|
||||||
admin.site.register(Order, OrderAdmin)
|
admin.site.register(Order, OrderAdmin)
|
||||||
admin.site.register(MetricStorage, MetricStorageAdmin)
|
admin.site.register(MetricStorage, MetricStorageAdmin)
|
||||||
|
|
|
@ -134,21 +134,21 @@ class ServiceHandler(plugins.Plugin):
|
||||||
def get_price_with_orders(self, order, size, ini, end):
|
def get_price_with_orders(self, order, size, ini, end):
|
||||||
porders = self.orders.filter(account=order.account).filter(
|
porders = self.orders.filter(account=order.account).filter(
|
||||||
Q(cancelled_on__isnull=True) | Q(cancelled_on__gt=ini)
|
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
|
price = 0
|
||||||
if self.orders_effect == self.REGISTER_OR_RENEW:
|
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:
|
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:
|
else:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
for metric, ratio in events:
|
for metric, position, ratio in events:
|
||||||
price += self.get_rate(order, metric) * size * ratio
|
price += self.get_price(order, metric, position=position) * size * ratio
|
||||||
return price
|
return price
|
||||||
|
|
||||||
def get_price_with_metric(self, order, size, ini, end):
|
def get_price_with_metric(self, order, size, ini, end):
|
||||||
metric = order.get_metric(ini, end)
|
metric = order.get_metric(ini, end)
|
||||||
price = self.get_rate(order, metric) * size
|
price = self.get_price(order, metric) * size
|
||||||
return price
|
return price
|
||||||
|
|
||||||
def create_line(self, order, price, size, ini, end):
|
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)
|
new_models.append(related)
|
||||||
queue.append(new_models)
|
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"
|
assert ini <= end, "ini > end"
|
||||||
CANCEL = 'cancel'
|
CANCEL = 'cancel'
|
||||||
REGISTER = 'register'
|
REGISTER = 'register'
|
||||||
changes = {}
|
changes = {}
|
||||||
counter = 0
|
counter = 0
|
||||||
for order in porders:
|
for num, porder in enumerate(porders.order_by('registered_on')):
|
||||||
if order.cancelled_on:
|
if porder == order:
|
||||||
cancel = order.cancelled_on
|
position = num
|
||||||
if order.billed_until and order.cancelled_on < order.billed_until:
|
if porder.cancelled_on:
|
||||||
cancel = order.billed_until
|
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:
|
if cancel > ini and cancel < end:
|
||||||
changes.setdefault(cancel, [])
|
changes.setdefault(cancel, [])
|
||||||
changes[cancel].append(CANCEL)
|
changes[cancel].append((CANCEL, num))
|
||||||
if order.registered_on <= ini:
|
if porder.registered_on <= ini:
|
||||||
counter += 1
|
counter += 1
|
||||||
elif order.registered_on < end:
|
elif porder.registered_on < end:
|
||||||
changes.setdefault(order.registered_on, [])
|
changes.setdefault(porder.registered_on, [])
|
||||||
changes[order.registered_on].append(REGISTER)
|
changes[porder.registered_on].append((REGISTER, num))
|
||||||
pointer = ini
|
pointer = ini
|
||||||
total = float((end-ini).days)
|
total = float((end-ini).days)
|
||||||
for date in sorted(changes.keys()):
|
for date in sorted(changes.keys()):
|
||||||
yield counter, (date-pointer).days/total
|
yield counter, position, (date-pointer).days/total
|
||||||
for change in changes[date]:
|
for change, num in changes[date]:
|
||||||
if change is CANCEL:
|
if change is CANCEL:
|
||||||
counter -= 1
|
counter -= 1
|
||||||
|
if num < position:
|
||||||
|
position -= 1
|
||||||
else:
|
else:
|
||||||
counter += 1
|
counter += 1
|
||||||
pointer = date
|
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)
|
total = float((end-ini).days)
|
||||||
for sini, send in handler.get_pricing_slots(ini, end):
|
for sini, send in handler.get_pricing_slots(ini, end):
|
||||||
counter = 0
|
counter = 0
|
||||||
for order in porders:
|
position = 0
|
||||||
if order.registered_on >= sini and order.registered_on < send:
|
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
|
counter += 1
|
||||||
elif order.billed_until > send or order.cancelled_on > send:
|
elif porder.billed_until > send or porder.cancelled_on > send:
|
||||||
counter += 1
|
counter += 1
|
||||||
yield counter, (send-sini)/total
|
yield counter, position, (send-sini)/total
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
|
]
|
|
@ -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.apps import autodiscover
|
||||||
from orchestra.utils.python import import_class
|
from orchestra.utils.python import import_class
|
||||||
|
|
||||||
from . import settings, helpers
|
from . import helpers, settings, pricing
|
||||||
from .handlers import ServiceHandler
|
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')
|
autodiscover('handlers')
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,6 +82,10 @@ class Service(models.Model):
|
||||||
BEST_PRICE = 'BEST_PRICE'
|
BEST_PRICE = 'BEST_PRICE'
|
||||||
PROGRESSIVE_PRICE = 'PROGRESSIVE_PRICE'
|
PROGRESSIVE_PRICE = 'PROGRESSIVE_PRICE'
|
||||||
MATCH_PRICE = 'MATCH_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)
|
description = models.CharField(_("description"), max_length=256, unique=True)
|
||||||
content_type = models.ForeignKey(ContentType, verbose_name=_("content type"))
|
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,
|
metric = models.CharField(_("metric"), max_length=256, blank=True,
|
||||||
help_text=_("Metric used to compute the pricing rate. "
|
help_text=_("Metric used to compute the pricing rate. "
|
||||||
"Number of orders is used when left blank."))
|
"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,
|
tax = models.PositiveIntegerField(_("tax"), choices=settings.ORDERS_SERVICE_TAXES,
|
||||||
default=settings.ORDERS_SERVICE_DEFAUL_TAX)
|
default=settings.ORDERS_SERVICE_DEFAUL_TAX)
|
||||||
pricing_period = models.CharField(_("pricing period"), max_length=16,
|
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,
|
rate_algorithm = models.CharField(_("rate algorithm"), max_length=16,
|
||||||
help_text=_("Algorithm used to interprete the rating table"),
|
help_text=_("Algorithm used to interprete the rating table"),
|
||||||
choices=(
|
choices=(
|
||||||
(BEST_PRICE, _("Best price")),
|
(BEST_PRICE, _("Best progressive price")),
|
||||||
(PROGRESSIVE_PRICE, _("Progressive price")),
|
(PROGRESSIVE_PRICE, _("Conservative progressive price")),
|
||||||
(MATCH_PRICE, _("Match price")),
|
(MATCH_PRICE, _("Match price")),
|
||||||
),
|
),
|
||||||
default=BEST_PRICE)
|
default=BEST_PRICE)
|
||||||
|
@ -121,22 +166,6 @@ class Service(models.Model):
|
||||||
(REFOUND, _("Discount, compensate and refound")),
|
(REFOUND, _("Discount, compensate and refound")),
|
||||||
),
|
),
|
||||||
default=DISCOUNT)
|
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,
|
payment_style = models.CharField(_("payment style"), max_length=16,
|
||||||
help_text=_("Designates whether this service should be paid after "
|
help_text=_("Designates whether this service should be paid after "
|
||||||
"consumtion (postpay/on demand) or prepaid"),
|
"consumtion (postpay/on demand) or prepaid"),
|
||||||
|
@ -164,11 +193,6 @@ class Service(models.Model):
|
||||||
),
|
),
|
||||||
default=NEVER, blank=True)
|
default=NEVER, blank=True)
|
||||||
|
|
||||||
@property
|
|
||||||
def nominal_price(self):
|
|
||||||
# FIXME delete and make it a model field
|
|
||||||
return 10
|
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.description
|
return self.description
|
||||||
|
|
||||||
|
@ -226,9 +250,36 @@ class Service(models.Model):
|
||||||
return self.billing_period
|
return self.billing_period
|
||||||
return self.pricing_period
|
return self.pricing_period
|
||||||
|
|
||||||
def get_rate(self, order, metric):
|
def get_price(self, order, metric, position=None):
|
||||||
# TODO implement
|
"""
|
||||||
return 12
|
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):
|
class OrderQuerySet(models.QuerySet):
|
||||||
|
@ -323,8 +374,7 @@ class Order(models.Model):
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def get_metric(self, ini, end):
|
def get_metric(self, ini, end):
|
||||||
# TODO implement
|
return MetricStorage.get(self, ini, end)
|
||||||
return 10
|
|
||||||
|
|
||||||
|
|
||||||
class MetricStorage(models.Model):
|
class MetricStorage(models.Model):
|
||||||
|
@ -353,8 +403,11 @@ class MetricStorage(models.Model):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, order, ini, end):
|
def get(cls, order, ini, end):
|
||||||
# TODO
|
try:
|
||||||
pass
|
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")
|
@receiver(pre_delete, dispatch_uid="orders.cancel_orders")
|
||||||
|
@ -379,3 +432,5 @@ def update_orders(sender, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
accounts.register(Order)
|
accounts.register(Order)
|
||||||
|
accounts.register(Plan)
|
||||||
|
services.register(Plan, menu=False)
|
||||||
|
|
|
@ -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',
|
ORDERS_BILLING_BACKEND = getattr(settings, 'ORDERS_BILLING_BACKEND',
|
||||||
'orchestra.apps.orders.billing.BillsBackend')
|
'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-extensions==1.1.1 \
|
||||||
django-transaction-signals==1.0.0 \
|
django-transaction-signals==1.0.0 \
|
||||||
django-celery==3.1.10 \
|
django-celery==3.1.10 \
|
||||||
celery==3.1.7 \
|
celery==3.1.13 \
|
||||||
kombu==3.0.8 \
|
kombu==3.0.8 \
|
||||||
Markdown==2.4 \
|
Markdown==2.4 \
|
||||||
django-debug-toolbar==1.2.1 \
|
django-debug-toolbar==1.2.1 \
|
||||||
|
|
|
@ -79,7 +79,6 @@ INSTALLED_APPS = (
|
||||||
'orchestra.apps.databases',
|
'orchestra.apps.databases',
|
||||||
'orchestra.apps.vps',
|
'orchestra.apps.vps',
|
||||||
'orchestra.apps.issues',
|
'orchestra.apps.issues',
|
||||||
'orchestra.apps.prices',
|
|
||||||
'orchestra.apps.orders',
|
'orchestra.apps.orders',
|
||||||
'orchestra.apps.miscellaneous',
|
'orchestra.apps.miscellaneous',
|
||||||
'orchestra.apps.bills',
|
'orchestra.apps.bills',
|
||||||
|
@ -145,7 +144,7 @@ FLUENT_DASHBOARD_APP_GROUPS = (
|
||||||
'orchestra.apps.contacts.models.Contact',
|
'orchestra.apps.contacts.models.Contact',
|
||||||
'orchestra.apps.users.models.User',
|
'orchestra.apps.users.models.User',
|
||||||
'orchestra.apps.orders.models.Order',
|
'orchestra.apps.orders.models.Order',
|
||||||
'orchestra.apps.prices.models.Pack',
|
'orchestra.apps.orders.models.Pack',
|
||||||
'orchestra.apps.bills.models.Bill',
|
'orchestra.apps.bills.models.Bill',
|
||||||
# 'orchestra.apps.payments.models.PaymentSource',
|
# 'orchestra.apps.payments.models.PaymentSource',
|
||||||
'orchestra.apps.payments.models.Transaction',
|
'orchestra.apps.payments.models.Transaction',
|
||||||
|
@ -187,7 +186,7 @@ FLUENT_DASHBOARD_APP_ICONS = {
|
||||||
'contacts/contact': 'contact_book.png',
|
'contacts/contact': 'contact_book.png',
|
||||||
'orders/order': 'basket.png',
|
'orders/order': 'basket.png',
|
||||||
'orders/service': 'price.png',
|
'orders/service': 'price.png',
|
||||||
'prices/pack': 'Pack.png',
|
'orders/plan': 'Pack.png',
|
||||||
'bills/bill': 'invoice.png',
|
'bills/bill': 'invoice.png',
|
||||||
'payments/paymentsource': 'card_in_use.png',
|
'payments/paymentsource': 'card_in_use.png',
|
||||||
'payments/transaction': 'transaction.png',
|
'payments/transaction': 'transaction.png',
|
||||||
|
|
Loading…
Reference in New Issue