This commit is contained in:
Marc 2014-09-15 12:15:32 +00:00
parent 53b1391b1d
commit a37df75f57
4 changed files with 109 additions and 65 deletions

View file

@ -52,6 +52,7 @@ class BillSelectedOrders(object):
return render(request, self.template, self.context) return render(request, self.template, self.context)
def select_related(self, request): def select_related(self, request):
# TODO use changelist ?
related = self.queryset.get_related().select_related('account__user', 'service') related = self.queryset.get_related().select_related('account__user', 'service')
if not related: if not related:
return self.confirmation(request) return self.confirmation(request)

View file

@ -18,21 +18,29 @@ 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 helpers, settings, pricing from . import helpers, settings, rating
from .handlers import ServiceHandler from .handlers import ServiceHandler
class Plan(models.Model): class Plan(models.Model):
account = models.ForeignKey('accounts.Account', verbose_name=_("account"), name = models.CharField(_("plan"), max_length=128)
related_name='plans') is_default = models.BooleanField(_("is default"), default=False)
name = models.CharField(_("plan"), max_length=128, is_combinable = models.BooleanField(_("is combinable"), default=True)
choices=settings.ORDERS_PLANS, allow_multiple = models.BooleanField(_("allow multipls"), default=False)
default=settings.ORDERS_DEFAULT_PLAN)
def __unicode__(self): def __unicode__(self):
return self.name return self.name
class ContractedPlan(models.Model):
plan = models.ForeignKey(Plan, verbose_name=_("plan"), related_name='contracts')
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='plans')
def __unicode__(self):
return str(self.plan)
class RateQuerySet(models.QuerySet): class RateQuerySet(models.QuerySet):
group_by = queryset.group_by group_by = queryset.group_by
@ -47,8 +55,7 @@ class RateQuerySet(models.QuerySet):
class Rate(models.Model): class Rate(models.Model):
service = models.ForeignKey('orders.Service', verbose_name=_("service"), service = models.ForeignKey('orders.Service', verbose_name=_("service"),
related_name='rates') related_name='rates')
plan = models.CharField(_("plan"), max_length=128, blank=True, plan = models.ForeignKey(Plan, verbose_name=_("plan"), related_name='rates')
choices=(('', _("Default")),) + settings.ORDERS_PLANS)
quantity = models.PositiveIntegerField(_("quantity"), null=True, blank=True) quantity = models.PositiveIntegerField(_("quantity"), null=True, blank=True)
price = models.DecimalField(_("price"), max_digits=12, decimal_places=2) price = models.DecimalField(_("price"), max_digits=12, decimal_places=2)
@ -82,12 +89,11 @@ class Service(models.Model):
COMPENSATE = 'COMPENSATE' COMPENSATE = 'COMPENSATE'
PREPAY = 'PREPAY' PREPAY = 'PREPAY'
POSTPAY = 'POSTPAY' POSTPAY = 'POSTPAY'
BEST_PRICE = 'BEST_PRICE' STEPED_PRICE = 'STEPED_PRICE'
PROGRESSIVE_PRICE = 'PROGRESSIVE_PRICE'
MATCH_PRICE = 'MATCH_PRICE' MATCH_PRICE = 'MATCH_PRICE'
RATE_METHODS = { RATE_METHODS = {
BEST_PRICE: pricing.best_price, STEPED_PRICE: rating.steped_price,
MATCH_PRICE: pricing.match_price, MATCH_PRICE: rating.match_price,
} }
description = models.CharField(_("description"), max_length=256, unique=True) description = models.CharField(_("description"), max_length=256, unique=True)
@ -147,11 +153,10 @@ 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 progressive price")), (STEPED_PRICE, _("Steped price")),
(PROGRESSIVE_PRICE, _("Conservative progressive price")),
(MATCH_PRICE, _("Match price")), (MATCH_PRICE, _("Match price")),
), ),
default=BEST_PRICE) default=STEPED_PRICE)
# orders_effect = models.CharField(_("orders effect"), max_length=16, # orders_effect = models.CharField(_("orders effect"), max_length=16,
# help_text=_("Defines the lookup behaviour when using orders for " # help_text=_("Defines the lookup behaviour when using orders for "
# "the pricing rate computation of this service."), # "the pricing rate computation of this service."),
@ -312,7 +317,7 @@ class OrderQuerySet(models.QuerySet):
for account, services in qs.group_by('account', 'service'): for account, services in qs.group_by('account', 'service'):
bill_lines = [] bill_lines = []
for service, orders in services: for service, orders in services:
lines = service.handler.generate_bill_lines(orders, **options) lines = service.handler.generate_bill_lines(orders, account, **options)
bill_lines.extend(lines) bill_lines.extend(lines)
if commit: if commit:
bills += bill_backend.create_bills(account, bill_lines, **options) bills += bill_backend.create_bills(account, bill_lines, **options)

View file

@ -1,36 +0,0 @@
import sys
def best_price(rates, metric, cache={}):
steps = cache.get('steps')
if not steps:
rates = rates.order_by('quantity').order_by('plan')
ix = 0
steps = []
num = rates.count()
while ix < num:
if ix+1 == num or rates[ix].plan != rates[ix+1].plan:
quantity = sys.maxint
else:
quantity = rates[ix+1].quantity - rates[ix].quantity
steps.append({
'quantity': quantity,
'price': rates[ix].price
})
ix += 1
steps.sort(key=lambda s: s['price'])
cache['steps'] = steps
return steps
def match_price(rates, metric, cache={}):
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 [{
'quantity': sys.maxint,
'price': minimal
}]

View file

@ -33,25 +33,19 @@ class OrderTests(BaseTestCase):
match='not user.is_main and user.has_posix()', match='not user.is_main and user.has_posix()',
billing_period=Service.ANUAL, billing_period=Service.ANUAL,
billing_point=Service.FIXED_DATE, billing_point=Service.FIXED_DATE,
delayed_billing=Service.NEVER, # delayed_billing=Service.NEVER,
is_fee=False, is_fee=False,
metric='', metric='',
pricing_period=Service.BILLING_PERIOD, pricing_period=Service.BILLING_PERIOD,
rate_algorithm=Service.BEST_PRICE, rate_algorithm=Service.STEPED_PRICE,
orders_effect=Service.CONCURRENT, # orders_effect=Service.CONCURRENT,
on_cancel=Service.DISCOUNT, on_cancel=Service.DISCOUNT,
payment_style=Service.PREPAY, payment_style=Service.PREPAY,
trial_period=Service.NEVER, # trial_period=Service.NEVER,
refound_period=Service.NEVER, # refound_period=Service.NEVER,
tax=21, tax=21,
nominal_price=10, nominal_price=10,
) )
service.rates.create(
plan='',
quantity=1,
price=9,
)
self.account = self.create_account()
return service return service
# def test_ftp_account_1_year_fiexed(self): # def test_ftp_account_1_year_fiexed(self):
@ -62,7 +56,8 @@ class OrderTests(BaseTestCase):
def create_ftp(self): def create_ftp(self):
username = '%s_ftp' % random_ascii(10) username = '%s_ftp' % random_ascii(10)
user = User.objects.create_user(username=username, account=self.account) account = self.create_account()
user = User.objects.create_user(username=username, account=account)
POSIX = user._meta.get_field_by_name('posix')[0].model POSIX = user._meta.get_field_by_name('posix')[0].model
POSIX.objects.create(user=user) POSIX.objects.create(user=user)
return user return user
@ -176,7 +171,7 @@ class OrderTests(BaseTestCase):
orders = [order3, order, order1, order2, order4, order5, order6] orders = [order3, order, order1, order2, order4, order5, order6]
self.assertEqual(orders, sorted(orders, cmp=helpers.cmp_billed_until_or_registered_on)) self.assertEqual(orders, sorted(orders, cmp=helpers.cmp_billed_until_or_registered_on))
def test_compensation(self): def atest_compensation(self):
now = timezone.now() now = timezone.now()
order = Order( order = Order(
description='0', description='0',
@ -242,6 +237,85 @@ class OrderTests(BaseTestCase):
self.assertEqual(test_line[1], compensation.end) self.assertEqual(test_line[1], compensation.end)
self.assertEqual(test_line[2], compensation.order) self.assertEqual(test_line[2], compensation.order)
def get_rates(self, account):
rates = self.rates.filter(Q(plan__is_default=True) | Q(plan__contracts__account=account)).order_by('plan', 'quantity').select_related('plan').disctinct()
# match price
candidates = []
selected = False
for rate in rates:
if prev and prev.plan != rate.plan:
if not selected:
candidates.append(prev)
selected = False
if not selected and rate.quantity >= metric:
candidates.append(rate)
selected = True
if not selected:
candidates.append(prev)
candidates.sort(key=lambda r: r.price)
return candidates[0]
# Step price
groups = []
prev = None
for rate in rates:
elif not prev or (not rate.is_combinable and prev.plan != rate.plan):
groups.append([rate])
else:
groups[-1].append(rate)
for group in groups:
for rates in group:
return result
def test_rates(self):
service = self.create_service()
superplan = Plan.objects.create(name='SUPER', allow_multiple=False, is_combinable=False)
service.rates.create(plan=superplan, quantity=1, price=0)
service.rates.create(plan=superplan, quantity=3, price=10)
service.rates.create(plan=superplan, quantity=4, price=9)
service.rates.create(plan=superplan, quantity=10, price=1)
account = self.create_account()
account.plans.create(plan=superplan)
result = service.get_rates(account, 1)
import sys
from decimal import Decimal
rates = [
{'price': Decimal('0.00'), 'quantity': 2},
{'price': Decimal('10.00'), 'quantity': 1},
{'price': Decimal('9.00'), 'quantity': 6},
{'price': Decimal('1.00'), 'quantity': sys.maxint}
]
self.assertEqual(rates, result)
dupeplan = Plan.objects.create(name='DUPE', allow_multiple=True, is_combinable=False)
service.rates.create(plan=dupeplan, quantity=1, price=0)
service.rates.create(plan=dupeplan, quantity=3, price=9)
result = service.get_rates(account, 1)
self.assertEqual(rates, result)
account.plans.create(plan=dupeplan)
rates = [
{'price': Decimal('0.00'), 'quantity': 4},
{'price': Decimal('10.00'), 'quantity': 1},
{'price': Decimal('9.00'), 'quantity': 6},
{'price': Decimal('1.00'), 'quantity': sys.maxint}
]
result = service.get_rates(account, 1)
print 'b', result
self.assertEqual(rates, result)
service.rates.create(plan='HYPER', quantity=1, price=10)
service.rates.create(plan='HYPER', quantity=5, price=0)
service.rates.create(plan='HYPER', quantity=6, price=10)
account.plans.create(name='HYPER')
rates = [
{'price': Decimal('0.00'), 'quantity': 4},
{'price': Decimal('10.00'), 'quantity': 1},
{'price': Decimal('0.00'), 'quantity': 1},
{'price': Decimal('9.00'), 'quantity': 6},
{'price': Decimal('1.00'), 'quantity': sys.maxint}
]
result = service.get_rates(account, 1)
self.assertEqual(rates, result)
# def test_ftp_account_1_year_fiexed(self): # def test_ftp_account_1_year_fiexed(self):
# service = self.create_service() # service = self.create_service()
# now = timezone.now().date()etb # now = timezone.now().date()etb