Billing
This commit is contained in:
parent
53b1391b1d
commit
a37df75f57
|
@ -52,6 +52,7 @@ class BillSelectedOrders(object):
|
|||
return render(request, self.template, self.context)
|
||||
|
||||
def select_related(self, request):
|
||||
# TODO use changelist ?
|
||||
related = self.queryset.get_related().select_related('account__user', 'service')
|
||||
if not related:
|
||||
return self.confirmation(request)
|
||||
|
|
|
@ -18,21 +18,29 @@ from orchestra.models import queryset
|
|||
from orchestra.utils.apps import autodiscover
|
||||
from orchestra.utils.python import import_class
|
||||
|
||||
from . import helpers, settings, pricing
|
||||
from . import helpers, settings, rating
|
||||
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)
|
||||
name = models.CharField(_("plan"), max_length=128)
|
||||
is_default = models.BooleanField(_("is default"), default=False)
|
||||
is_combinable = models.BooleanField(_("is combinable"), default=True)
|
||||
allow_multiple = models.BooleanField(_("allow multipls"), default=False)
|
||||
|
||||
def __unicode__(self):
|
||||
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):
|
||||
group_by = queryset.group_by
|
||||
|
||||
|
@ -47,8 +55,7 @@ class RateQuerySet(models.QuerySet):
|
|||
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)
|
||||
plan = models.ForeignKey(Plan, verbose_name=_("plan"), related_name='rates')
|
||||
quantity = models.PositiveIntegerField(_("quantity"), null=True, blank=True)
|
||||
price = models.DecimalField(_("price"), max_digits=12, decimal_places=2)
|
||||
|
||||
|
@ -82,12 +89,11 @@ class Service(models.Model):
|
|||
COMPENSATE = 'COMPENSATE'
|
||||
PREPAY = 'PREPAY'
|
||||
POSTPAY = 'POSTPAY'
|
||||
BEST_PRICE = 'BEST_PRICE'
|
||||
PROGRESSIVE_PRICE = 'PROGRESSIVE_PRICE'
|
||||
STEPED_PRICE = 'STEPED_PRICE'
|
||||
MATCH_PRICE = 'MATCH_PRICE'
|
||||
RATE_METHODS = {
|
||||
BEST_PRICE: pricing.best_price,
|
||||
MATCH_PRICE: pricing.match_price,
|
||||
STEPED_PRICE: rating.steped_price,
|
||||
MATCH_PRICE: rating.match_price,
|
||||
}
|
||||
|
||||
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,
|
||||
help_text=_("Algorithm used to interprete the rating table"),
|
||||
choices=(
|
||||
(BEST_PRICE, _("Best progressive price")),
|
||||
(PROGRESSIVE_PRICE, _("Conservative progressive price")),
|
||||
(STEPED_PRICE, _("Steped price")),
|
||||
(MATCH_PRICE, _("Match price")),
|
||||
),
|
||||
default=BEST_PRICE)
|
||||
default=STEPED_PRICE)
|
||||
# orders_effect = models.CharField(_("orders effect"), max_length=16,
|
||||
# help_text=_("Defines the lookup behaviour when using orders for "
|
||||
# "the pricing rate computation of this service."),
|
||||
|
@ -312,7 +317,7 @@ class OrderQuerySet(models.QuerySet):
|
|||
for account, services in qs.group_by('account', 'service'):
|
||||
bill_lines = []
|
||||
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)
|
||||
if commit:
|
||||
bills += bill_backend.create_bills(account, bill_lines, **options)
|
||||
|
|
|
@ -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
|
||||
}]
|
|
@ -33,25 +33,19 @@ class OrderTests(BaseTestCase):
|
|||
match='not user.is_main and user.has_posix()',
|
||||
billing_period=Service.ANUAL,
|
||||
billing_point=Service.FIXED_DATE,
|
||||
delayed_billing=Service.NEVER,
|
||||
# delayed_billing=Service.NEVER,
|
||||
is_fee=False,
|
||||
metric='',
|
||||
pricing_period=Service.BILLING_PERIOD,
|
||||
rate_algorithm=Service.BEST_PRICE,
|
||||
orders_effect=Service.CONCURRENT,
|
||||
rate_algorithm=Service.STEPED_PRICE,
|
||||
# orders_effect=Service.CONCURRENT,
|
||||
on_cancel=Service.DISCOUNT,
|
||||
payment_style=Service.PREPAY,
|
||||
trial_period=Service.NEVER,
|
||||
refound_period=Service.NEVER,
|
||||
# trial_period=Service.NEVER,
|
||||
# refound_period=Service.NEVER,
|
||||
tax=21,
|
||||
nominal_price=10,
|
||||
)
|
||||
service.rates.create(
|
||||
plan='',
|
||||
quantity=1,
|
||||
price=9,
|
||||
)
|
||||
self.account = self.create_account()
|
||||
return service
|
||||
|
||||
# def test_ftp_account_1_year_fiexed(self):
|
||||
|
@ -62,7 +56,8 @@ class OrderTests(BaseTestCase):
|
|||
|
||||
def create_ftp(self):
|
||||
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.objects.create(user=user)
|
||||
return user
|
||||
|
@ -176,7 +171,7 @@ class OrderTests(BaseTestCase):
|
|||
orders = [order3, order, order1, order2, order4, order5, order6]
|
||||
self.assertEqual(orders, sorted(orders, cmp=helpers.cmp_billed_until_or_registered_on))
|
||||
|
||||
def test_compensation(self):
|
||||
def atest_compensation(self):
|
||||
now = timezone.now()
|
||||
order = Order(
|
||||
description='0',
|
||||
|
@ -242,6 +237,85 @@ class OrderTests(BaseTestCase):
|
|||
self.assertEqual(test_line[1], compensation.end)
|
||||
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):
|
||||
# service = self.create_service()
|
||||
# now = timezone.now().date()etb
|
||||
|
|
Loading…
Reference in a new issue