Fixes on billing with metric
This commit is contained in:
parent
8f1d05873c
commit
5a031b81cb
|
@ -11,7 +11,7 @@ from . import settings
|
||||||
|
|
||||||
class MailSystemUserBackend(ServiceController):
|
class MailSystemUserBackend(ServiceController):
|
||||||
verbose_name = _("Mail system user")
|
verbose_name = _("Mail system user")
|
||||||
model = 'mail.Mailbox'
|
model = 'mails.Mailbox'
|
||||||
# TODO related_models = ('resources__content_type') ??
|
# TODO related_models = ('resources__content_type') ??
|
||||||
|
|
||||||
DEFAULT_GROUP = 'postfix'
|
DEFAULT_GROUP = 'postfix'
|
||||||
|
@ -66,7 +66,7 @@ class MailSystemUserBackend(ServiceController):
|
||||||
|
|
||||||
class PostfixAddressBackend(ServiceController):
|
class PostfixAddressBackend(ServiceController):
|
||||||
verbose_name = _("Postfix address")
|
verbose_name = _("Postfix address")
|
||||||
model = 'mail.Address'
|
model = 'mails.Address'
|
||||||
|
|
||||||
def include_virtdomain(self, context):
|
def include_virtdomain(self, context):
|
||||||
self.append(
|
self.append(
|
||||||
|
@ -140,7 +140,7 @@ class AutoresponseBackend(ServiceController):
|
||||||
|
|
||||||
|
|
||||||
class MaildirDisk(ServiceMonitor):
|
class MaildirDisk(ServiceMonitor):
|
||||||
model = 'email.Mailbox'
|
model = 'mails.Mailbox'
|
||||||
resource = ServiceMonitor.DISK
|
resource = ServiceMonitor.DISK
|
||||||
verbose_name = _("Maildir disk usage")
|
verbose_name = _("Maildir disk usage")
|
||||||
|
|
||||||
|
|
|
@ -185,10 +185,10 @@ _excluded_models = (MetricStorage, LogEntry, Order, ContentType, MigrationRecord
|
||||||
def cancel_orders(sender, **kwargs):
|
def cancel_orders(sender, **kwargs):
|
||||||
if sender not in _excluded_models:
|
if sender not in _excluded_models:
|
||||||
instance = kwargs['instance']
|
instance = kwargs['instance']
|
||||||
if hasattr(instance, 'account'):
|
if sender in services:
|
||||||
for order in Order.objects.by_object(instance).active():
|
for order in Order.objects.by_object(instance).active():
|
||||||
order.cancel()
|
order.cancel()
|
||||||
else:
|
elif not hasattr(instance, 'account'):
|
||||||
related = helpers.get_related_objects(instance)
|
related = helpers.get_related_objects(instance)
|
||||||
if related and related != instance:
|
if related and related != instance:
|
||||||
Order.update_orders(related)
|
Order.update_orders(related)
|
||||||
|
@ -198,9 +198,9 @@ def cancel_orders(sender, **kwargs):
|
||||||
def update_orders(sender, **kwargs):
|
def update_orders(sender, **kwargs):
|
||||||
if sender not in _excluded_models:
|
if sender not in _excluded_models:
|
||||||
instance = kwargs['instance']
|
instance = kwargs['instance']
|
||||||
if hasattr(instance, 'account'):
|
if sender in services:
|
||||||
Order.update_orders(instance)
|
Order.update_orders(instance)
|
||||||
else:
|
elif not hasattr(instance, 'account'):
|
||||||
related = helpers.get_related_objects(instance)
|
related = helpers.get_related_objects(instance)
|
||||||
if related and related != instance:
|
if related and related != instance:
|
||||||
Order.update_orders(related)
|
Order.update_orders(related)
|
||||||
|
|
|
@ -8,6 +8,9 @@ from django.db.models import F
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from orchestra.apps.accounts.models import Account
|
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.models import Service, Plan
|
||||||
from orchestra.apps.services import settings as services_settings
|
from orchestra.apps.services import settings as services_settings
|
||||||
from orchestra.apps.users.models import User
|
from orchestra.apps.users.models import User
|
||||||
|
@ -17,7 +20,7 @@ from orchestra.utils.tests import BaseTestCase, random_ascii
|
||||||
class BaseBillingTest(BaseTestCase):
|
class BaseBillingTest(BaseTestCase):
|
||||||
def create_account(self):
|
def create_account(self):
|
||||||
account = Account.objects.create()
|
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.user = user
|
||||||
account.save()
|
account.save()
|
||||||
return account
|
return account
|
||||||
|
@ -109,10 +112,13 @@ class FTPBillingTest(BaseBillingTest):
|
||||||
order = service.orders.order_by('-id').first()
|
order = service.orders.order_by('-id').first()
|
||||||
self.assertEqual(first_bp, order.billed_until)
|
self.assertEqual(first_bp, order.billed_until)
|
||||||
self.assertEqual(decimal.Decimal(0), bills[0].get_total())
|
self.assertEqual(decimal.Decimal(0), bills[0].get_total())
|
||||||
|
|
||||||
|
def test_ftp_account_with_rates(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DomainBillingTest(BaseBillingTest):
|
class DomainBillingTest(BaseBillingTest):
|
||||||
def create_domain_service(self):
|
def create_domain_service(self):
|
||||||
from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous
|
|
||||||
service = Service.objects.create(
|
service = Service.objects.create(
|
||||||
description="Domain .ES",
|
description="Domain .ES",
|
||||||
content_type=ContentType.objects.get_for_model(Miscellaneous),
|
content_type=ContentType.objects.get_for_model(Miscellaneous),
|
||||||
|
@ -136,7 +142,6 @@ class DomainBillingTest(BaseBillingTest):
|
||||||
return service
|
return service
|
||||||
|
|
||||||
def create_domain(self, account=None):
|
def create_domain(self, account=None):
|
||||||
from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous
|
|
||||||
if not account:
|
if not account:
|
||||||
account = self.create_account()
|
account = self.create_account()
|
||||||
domain_name = '%s.es' % random_ascii(10)
|
domain_name = '%s.es' % random_ascii(10)
|
||||||
|
@ -278,7 +283,6 @@ class TrafficBillingTest(BaseBillingTest):
|
||||||
return self.resource
|
return self.resource
|
||||||
|
|
||||||
def report_traffic(self, account, date, value):
|
def report_traffic(self, account, date, value):
|
||||||
from orchestra.apps.resources.models import ResourceData, MonitorData
|
|
||||||
ct = ContentType.objects.get_for_model(Account)
|
ct = ContentType.objects.get_for_model(Account)
|
||||||
object_id = account.pk
|
object_id = account.pk
|
||||||
MonitorData.objects.create(monitor='FTPTraffic', content_object=account.user, value=value, date=date)
|
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)
|
order.metrics.filter(id=3).update(updated_on=F('updated_on')-delta)
|
||||||
bills = service.orders.bill(proforma=True)
|
bills = service.orders.bill(proforma=True)
|
||||||
self.assertEqual(900, bills[0].get_total())
|
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
|
||||||
|
|
|
@ -80,7 +80,7 @@ class Resource(models.Model):
|
||||||
return "{}-{}".format(str(self.content_type), self.name)
|
return "{}-{}".format(str(self.content_type), self.name)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# created = not self.pk
|
created = not self.pk
|
||||||
super(Resource, self).save(*args, **kwargs)
|
super(Resource, self).save(*args, **kwargs)
|
||||||
# Create Celery periodic task
|
# Create Celery periodic task
|
||||||
name = 'monitor.%s' % str(self)
|
name = 'monitor.%s' % str(self)
|
||||||
|
@ -100,8 +100,8 @@ class Resource(models.Model):
|
||||||
elif task.crontab != self.crontab:
|
elif task.crontab != self.crontab:
|
||||||
task.crontab = self.crontab
|
task.crontab = self.crontab
|
||||||
task.save()
|
task.save()
|
||||||
# if created:
|
if created:
|
||||||
# create_resource_relation()
|
create_resource_relation()
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
super(Resource, self).delete(*args, **kwargs)
|
super(Resource, self).delete(*args, **kwargs)
|
||||||
|
|
|
@ -218,7 +218,6 @@ class ServiceHandler(plugins.Plugin):
|
||||||
return dsize, cend
|
return dsize, cend
|
||||||
|
|
||||||
def get_register_or_renew_events(self, porders, ini, end):
|
def get_register_or_renew_events(self, porders, ini, end):
|
||||||
# TODO count intermediat billing points too
|
|
||||||
counter = 0
|
counter = 0
|
||||||
for order in porders:
|
for order in porders:
|
||||||
bu = getattr(order, 'new_billed_until', order.billed_until)
|
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 = self.get_price(account, metric, position=position, rates=rates)
|
||||||
price = price * size
|
price = price * size
|
||||||
cprice = price * (size-csize)
|
cprice = price * (size-csize)
|
||||||
if order in prices:
|
if order in priced:
|
||||||
priced[order][0] += price
|
priced[order][0] += price
|
||||||
priced[order][1] += cprice
|
priced[order][1] += cprice
|
||||||
else:
|
else:
|
||||||
priced[order] = (price, cprice)
|
priced[order] = (price, cprice)
|
||||||
lines = []
|
lines = []
|
||||||
for order, prices in priced.iteritems():
|
for order, prices in priced.iteritems():
|
||||||
|
discounts = ()
|
||||||
# Generate lines and discounts from order.nominal_price
|
# Generate lines and discounts from order.nominal_price
|
||||||
price, cprice = prices
|
price, cprice = prices
|
||||||
# Compensations > new_billed_until
|
# Compensations > new_billed_until
|
||||||
|
@ -351,8 +351,8 @@ class ServiceHandler(plugins.Plugin):
|
||||||
porders = related_orders.pricing_orders(ini, end)
|
porders = related_orders.pricing_orders(ini, end)
|
||||||
porders = list(set(orders).union(set(porders)))
|
porders = list(set(orders).union(set(porders)))
|
||||||
porders.sort(cmp=helpers.cmp_billed_until_or_registered_on)
|
porders.sort(cmp=helpers.cmp_billed_until_or_registered_on)
|
||||||
if self.billing_period != self.NEVER and self.get_pricing_period == self.NEVER:
|
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)
|
lines = self.bill_concurrent_orders(account, porders, rates, ini, end, commit=commit)
|
||||||
else:
|
else:
|
||||||
# TODO compensation in this case?
|
# TODO compensation in this case?
|
||||||
lines = self.bill_registered_or_renew_events(account, porders, rates, commit=commit)
|
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
|
# weighted metric; bill line per pricing period
|
||||||
prev = None
|
prev = None
|
||||||
lines_info = []
|
lines_info = []
|
||||||
for ini, end in self.get_pricing_slots(ini, bp):
|
if self.billing_period != self.NEVER:
|
||||||
metric = order.get_metric(ini, end)
|
if self.get_pricing_period() == self.NEVER:
|
||||||
price = self.get_price(order, metric)
|
# Changes
|
||||||
lines.append(self.generate_line(order, price, metric, ini, end))
|
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:
|
if commit:
|
||||||
order.billed_until = order.new_billed_until
|
order.billed_until = order.new_billed_until
|
||||||
order.save()
|
order.save()
|
||||||
|
|
Loading…
Reference in a new issue