From a37df75f573c93b0b0e0c04e6a28caabe3462b18 Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 15 Sep 2014 12:15:32 +0000 Subject: [PATCH] Billing --- orchestra/apps/orders/actions.py | 1 + orchestra/apps/orders/models.py | 37 ++++--- orchestra/apps/orders/pricing.py | 36 ------- .../orders/tests/functional_tests/tests.py | 100 +++++++++++++++--- 4 files changed, 109 insertions(+), 65 deletions(-) delete mode 100644 orchestra/apps/orders/pricing.py diff --git a/orchestra/apps/orders/actions.py b/orchestra/apps/orders/actions.py index 964a9e47..645a3c24 100644 --- a/orchestra/apps/orders/actions.py +++ b/orchestra/apps/orders/actions.py @@ -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) diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py index 8041fbaf..a4ca2a0d 100644 --- a/orchestra/apps/orders/models.py +++ b/orchestra/apps/orders/models.py @@ -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) diff --git a/orchestra/apps/orders/pricing.py b/orchestra/apps/orders/pricing.py deleted file mode 100644 index 86ecd195..00000000 --- a/orchestra/apps/orders/pricing.py +++ /dev/null @@ -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 - }] diff --git a/orchestra/apps/orders/tests/functional_tests/tests.py b/orchestra/apps/orders/tests/functional_tests/tests.py index f37db9fe..0274d2de 100644 --- a/orchestra/apps/orders/tests/functional_tests/tests.py +++ b/orchestra/apps/orders/tests/functional_tests/tests.py @@ -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