From 5a031b81cb91d42db5f0fe3885735de6d35875ec Mon Sep 17 00:00:00 2001 From: Marc Date: Tue, 23 Sep 2014 11:13:50 +0000 Subject: [PATCH] Fixes on billing with metric --- orchestra/apps/mails/backends.py | 6 +- orchestra/apps/orders/models.py | 8 +- .../orders/tests/functional_tests/tests.py | 156 +++++++++++++++++- orchestra/apps/resources/models.py | 6 +- orchestra/apps/services/handlers.py | 35 +++- 5 files changed, 189 insertions(+), 22 deletions(-) diff --git a/orchestra/apps/mails/backends.py b/orchestra/apps/mails/backends.py index c1283d22..24a10276 100644 --- a/orchestra/apps/mails/backends.py +++ b/orchestra/apps/mails/backends.py @@ -11,7 +11,7 @@ from . import settings class MailSystemUserBackend(ServiceController): verbose_name = _("Mail system user") - model = 'mail.Mailbox' + model = 'mails.Mailbox' # TODO related_models = ('resources__content_type') ?? DEFAULT_GROUP = 'postfix' @@ -66,7 +66,7 @@ class MailSystemUserBackend(ServiceController): class PostfixAddressBackend(ServiceController): verbose_name = _("Postfix address") - model = 'mail.Address' + model = 'mails.Address' def include_virtdomain(self, context): self.append( @@ -140,7 +140,7 @@ class AutoresponseBackend(ServiceController): class MaildirDisk(ServiceMonitor): - model = 'email.Mailbox' + model = 'mails.Mailbox' resource = ServiceMonitor.DISK verbose_name = _("Maildir disk usage") diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py index 69263dde..15f49b81 100644 --- a/orchestra/apps/orders/models.py +++ b/orchestra/apps/orders/models.py @@ -185,10 +185,10 @@ _excluded_models = (MetricStorage, LogEntry, Order, ContentType, MigrationRecord def cancel_orders(sender, **kwargs): if sender not in _excluded_models: instance = kwargs['instance'] - if hasattr(instance, 'account'): + if sender in services: for order in Order.objects.by_object(instance).active(): order.cancel() - else: + elif not hasattr(instance, 'account'): related = helpers.get_related_objects(instance) if related and related != instance: Order.update_orders(related) @@ -198,9 +198,9 @@ def cancel_orders(sender, **kwargs): def update_orders(sender, **kwargs): if sender not in _excluded_models: instance = kwargs['instance'] - if hasattr(instance, 'account'): + if sender in services: Order.update_orders(instance) - else: + elif not hasattr(instance, 'account'): related = helpers.get_related_objects(instance) if related and related != instance: Order.update_orders(related) diff --git a/orchestra/apps/orders/tests/functional_tests/tests.py b/orchestra/apps/orders/tests/functional_tests/tests.py index 7e1180e3..fc6bf5bf 100644 --- a/orchestra/apps/orders/tests/functional_tests/tests.py +++ b/orchestra/apps/orders/tests/functional_tests/tests.py @@ -8,6 +8,9 @@ from django.db.models import F from django.utils import timezone from orchestra.apps.accounts.models import Account +from orchestra.apps.mails.models import Mailbox +from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous +from orchestra.apps.resources.models import Resource, ResourceData, MonitorData from orchestra.apps.services.models import Service, Plan from orchestra.apps.services import settings as services_settings from orchestra.apps.users.models import User @@ -17,7 +20,7 @@ from orchestra.utils.tests import BaseTestCase, random_ascii class BaseBillingTest(BaseTestCase): def create_account(self): account = Account.objects.create() - user = User.objects.create_user(username='rata_palida', account=account) + user = User.objects.create_user(username='account_%s' % random_ascii(5), account=account) account.user = user account.save() return account @@ -109,10 +112,13 @@ class FTPBillingTest(BaseBillingTest): order = service.orders.order_by('-id').first() self.assertEqual(first_bp, order.billed_until) self.assertEqual(decimal.Decimal(0), bills[0].get_total()) + + def test_ftp_account_with_rates(self): + pass + class DomainBillingTest(BaseBillingTest): def create_domain_service(self): - from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous service = Service.objects.create( description="Domain .ES", content_type=ContentType.objects.get_for_model(Miscellaneous), @@ -136,7 +142,6 @@ class DomainBillingTest(BaseBillingTest): return service def create_domain(self, account=None): - from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous if not account: account = self.create_account() domain_name = '%s.es' % random_ascii(10) @@ -278,7 +283,6 @@ class TrafficBillingTest(BaseBillingTest): return self.resource def report_traffic(self, account, date, value): - from orchestra.apps.resources.models import ResourceData, MonitorData ct = ContentType.objects.get_for_model(Account) object_id = account.pk MonitorData.objects.create(monitor='FTPTraffic', content_object=account.user, value=value, date=date) @@ -310,3 +314,147 @@ class TrafficBillingTest(BaseBillingTest): order.metrics.filter(id=3).update(updated_on=F('updated_on')-delta) bills = service.orders.bill(proforma=True) self.assertEqual(900, bills[0].get_total()) + + def test_multiple_traffics(self): + service = self.create_traffic_service() + resource = self.create_traffic_resource() + account1 = self.create_account() + account2 = self.create_account() + + +class MailboxBillingTest(BaseBillingTest): + def create_mailbox_service(self): + service = Service.objects.create( + description="Mailbox", + content_type=ContentType.objects.get_for_model(Mailbox), + match="True", + billing_period=Service.ANUAL, + billing_point=Service.FIXED_DATE, + is_fee=False, + metric='', + pricing_period=Service.NEVER, + rate_algorithm=Service.STEP_PRICE, + on_cancel=Service.DISCOUNT, + payment_style=Service.PREPAY, + tax=0, + nominal_price=10 + ) + plan = Plan.objects.create(is_default=True, name='Default') + service.rates.create(plan=plan, quantity=1, price=0) + service.rates.create(plan=plan, quantity=5, price=10) + return service + + def create_mailbox_disk_service(self): + service = Service.objects.create( + description="Mailbox disk", + content_type=ContentType.objects.get_for_model(Mailbox), + match="True", + billing_period=Service.ANUAL, + billing_point=Service.FIXED_DATE, + is_fee=False, + metric='max((mailbox.resources.disk.allocated or 0) -1, 0)', + pricing_period=Service.NEVER, + rate_algorithm=Service.STEP_PRICE, + on_cancel=Service.DISCOUNT, + payment_style=Service.PREPAY, + tax=0, + nominal_price=10 + ) + plan = Plan.objects.create(is_default=True, name='Default') + service.rates.create(plan=plan, quantity=1, price=0) + service.rates.create(plan=plan, quantity=2, price=10) + return service + + def create_disk_resource(self): + self.resource = Resource.objects.create( + name='disk', + content_type=ContentType.objects.get_for_model(Mailbox), + period=Resource.LAST, + verbose_name='Mailbox disk', + unit='GB', + scale=10**9, + ondemand=False, + monitors='MaildirDisk', + ) + return self.resource + + def allocate_disk(self, mailbox, value): + data = ResourceData.get_or_create(mailbox, self.resource) + data.allocated = value + data.save() + + def create_mailbox(self, account=None): + if not account: + account = self.create_account() + mailbox_name = '%s@orchestra.lan' % random_ascii(10) + return Mailbox.objects.create(name=mailbox_name, account=account) + + def test_mailbox_size(self): + service = self.create_mailbox_service() + disk_service = self.create_mailbox_disk_service() + self.create_disk_resource() + account = self.create_account() + mailbox = self.create_mailbox(account=account) + self.allocate_disk(mailbox, 10) + bill = service.orders.bill()[0] + self.assertEqual(0, bill.get_total()) + bill = disk_service.orders.bill()[0] + for line in bill.lines.all(): + for discount in line.sublines.all(): + print discount.__dict__ + self.assertEqual(80, bill.get_total()) + mailbox = self.create_mailbox(account=account) + mailbox = self.create_mailbox(account=account) + mailbox = self.create_mailbox(account=account) + mailbox = self.create_mailbox(account=account) + mailbox = self.create_mailbox(account=account) + bill = service.orders.bill()[0] + print disk_service.orders.bill()[0].get_total() + + +class JobBillingTest(BaseBillingTest): + def create_job_service(self): + service = Service.objects.create( + description="Random job", + content_type=ContentType.objects.get_for_model(Miscellaneous), + match="miscellaneous.is_active and miscellaneous.service.name.lower() == 'job'", + billing_period=Service.MONTHLY, + billing_point=Service.FIXED_DATE, + is_fee=False, + metric='mailbox.resources.disk.allocated', + pricing_period=Service.BILLING_PERIOD, + rate_algorithm=Service.STEP_PRICE, + on_cancel=Service.NOTHING, + payment_style=Service.POSTPAY, + tax=0, + nominal_price=10 + ) + plan = Plan.objects.create(is_default=True, name='Default') + service.rates.create(plan=plan, quantity=1, price=0) + service.rates.create(plan=plan, quantity=11, price=10) + return service + + def create_job(self, account=None): + if not account: + account = self.create_account() + job_name = '%s.es' % random_ascii(10) + job_service, __ = MiscService.objects.get_or_create(name='job', description='Random job') + return Miscellaneous.objects.create(service=job_service, description=job_name, account=account) + + def test_job(self): + pass + + +class PlanBillingTest(BaseBillingTest): + def create_plan_service(self): + pass + + def create_plan(self): + if not account: + account = self.create_account() + domain_name = '%s.es' % random_ascii(10) + domain_service, __ = MiscService.objects.get_or_create(name='domain .es', description='Domain .ES') + return Miscellaneous.objects.create(service=domain_service, description=domain_name, account=account) + + def test_plan(self): + pass diff --git a/orchestra/apps/resources/models.py b/orchestra/apps/resources/models.py index ec4d0c91..c8a4fb2b 100644 --- a/orchestra/apps/resources/models.py +++ b/orchestra/apps/resources/models.py @@ -80,7 +80,7 @@ class Resource(models.Model): return "{}-{}".format(str(self.content_type), self.name) def save(self, *args, **kwargs): -# created = not self.pk + created = not self.pk super(Resource, self).save(*args, **kwargs) # Create Celery periodic task name = 'monitor.%s' % str(self) @@ -100,8 +100,8 @@ class Resource(models.Model): elif task.crontab != self.crontab: task.crontab = self.crontab task.save() -# if created: -# create_resource_relation() + if created: + create_resource_relation() def delete(self, *args, **kwargs): super(Resource, self).delete(*args, **kwargs) diff --git a/orchestra/apps/services/handlers.py b/orchestra/apps/services/handlers.py index 3346ea51..47a1a3d0 100644 --- a/orchestra/apps/services/handlers.py +++ b/orchestra/apps/services/handlers.py @@ -218,7 +218,6 @@ class ServiceHandler(plugins.Plugin): return dsize, cend def get_register_or_renew_events(self, porders, ini, end): - # TODO count intermediat billing points too counter = 0 for order in porders: bu = getattr(order, 'new_billed_until', order.billed_until) @@ -251,13 +250,14 @@ class ServiceHandler(plugins.Plugin): price = self.get_price(account, metric, position=position, rates=rates) price = price * size cprice = price * (size-csize) - if order in prices: + if order in priced: priced[order][0] += price priced[order][1] += cprice else: priced[order] = (price, cprice) lines = [] for order, prices in priced.iteritems(): + discounts = () # Generate lines and discounts from order.nominal_price price, cprice = prices # Compensations > new_billed_until @@ -351,8 +351,8 @@ class ServiceHandler(plugins.Plugin): porders = related_orders.pricing_orders(ini, end) porders = list(set(orders).union(set(porders))) porders.sort(cmp=helpers.cmp_billed_until_or_registered_on) - if self.billing_period != self.NEVER and self.get_pricing_period == self.NEVER: - liens = self.bill_concurrent_orders(account, porders, rates, ini, end, commit=commit) + if self.billing_period != self.NEVER and self.get_pricing_period() == self.NEVER: + lines = self.bill_concurrent_orders(account, porders, rates, ini, end, commit=commit) else: # TODO compensation in this case? lines = self.bill_registered_or_renew_events(account, porders, rates, commit=commit) @@ -392,10 +392,29 @@ class ServiceHandler(plugins.Plugin): # weighted metric; bill line per pricing period prev = None lines_info = [] - for ini, end in self.get_pricing_slots(ini, bp): - metric = order.get_metric(ini, end) - price = self.get_price(order, metric) - lines.append(self.generate_line(order, price, metric, ini, end)) + if self.billing_period != self.NEVER: + if self.get_pricing_period() == self.NEVER: + # Changes + for ini, end, metric in order.get_metric(ini, end, changes=True) + size = self.get_price_size(ini, end) + price = self.get_price(order, metric) + price = price * size + # TODO metric and size in invoice (period and quantity) + lines.append(self.generate_line(order, price, metric, ini, end)) + else: + # pricing_slots + for ini, end in self.get_pricing_slots(ini, bp): + metric = order.get_metric(ini, end) + price = self.get_price(order, metric) + lines.append(self.generate_line(order, price, metric, ini, end)) + else: + if self.get_pricing_period() == self.NEVER: + # get metric + metric = order.get_metric(ini, end) + price = self.get_price(order, metric) + lines.append(self.generate_line(order, price, metric, ini, end)) + else: + raise NotImplementedError if commit: order.billed_until = order.new_billed_until order.save()