Removed prices app

This commit is contained in:
Marc 2014-09-08 14:23:06 +00:00
parent fc44c8bfc0
commit 157fd54ce5
17 changed files with 337 additions and 137 deletions

View File

@ -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

View File

@ -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'):

View File

@ -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)

View File

@ -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):

View File

@ -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

View 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,
),
]

View File

@ -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,
),
]

View 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')]),
),
]

View File

@ -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)

View 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
}]

View File

@ -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')

View File

@ -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)

View File

@ -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)

View File

@ -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')

View File

@ -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 \

View File

@ -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',