Fixes on billing order with metric
This commit is contained in:
parent
c992d5004c
commit
8f1d05873c
9
TODO.md
9
TODO.md
|
@ -14,7 +14,6 @@ TODO
|
|||
|
||||
* add `BackendLog` retry action
|
||||
* move invoice contact to invoices app?
|
||||
* wrapper around reverse('admin:....') `link()` and `link_factory()`
|
||||
* PHPbBckendMiixin with get_php_ini
|
||||
* Apache: `IncludeOptional /etc/apache2/extra-vhos[t]/account-site-custom.con[f]`
|
||||
* rename account.user to primary_user
|
||||
|
@ -40,11 +39,8 @@ TODO
|
|||
Remember that, as always with QuerySets, any subsequent chained methods which imply a different database query will ignore previously cached results, and retrieve data using a fresh database query.
|
||||
* profile select_related vs prefetch_related
|
||||
|
||||
* use HTTP OPTIONS instead of configuration endpoint, or rename to settings?
|
||||
|
||||
* Log changes from rest api (serialized objects)
|
||||
|
||||
|
||||
* passlib; nano /usr/local/lib/python2.7/dist-packages/passlib/ext/django/utils.py SortedDict -> collections.OrderedDict
|
||||
* pip install pyinotify
|
||||
|
||||
|
@ -105,3 +101,8 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
|
|||
|
||||
|
||||
* create log file at /var/log/orchestra.log and rotate
|
||||
|
||||
* order.register_at
|
||||
@property
|
||||
def register_on(self):
|
||||
return order.register_at.date()
|
||||
|
|
|
@ -62,6 +62,9 @@ class Bill(models.Model):
|
|||
|
||||
objects = BillManager()
|
||||
|
||||
class Meta:
|
||||
get_latest_by = 'created_on'
|
||||
|
||||
def __unicode__(self):
|
||||
return self.number
|
||||
|
||||
|
|
|
@ -10,23 +10,27 @@ class BillsBackend(object):
|
|||
bill = None
|
||||
bills = []
|
||||
create_new = options.get('new_open', False)
|
||||
is_proforma = options.get('is_proforma', False)
|
||||
proforma = options.get('proforma', False)
|
||||
for line in lines:
|
||||
service = line.order.service
|
||||
# Create bill if needed
|
||||
if bill is None or service.is_fee:
|
||||
if is_proforma:
|
||||
if proforma:
|
||||
if create_new:
|
||||
bill = ProForma.objects.create(account=account)
|
||||
else:
|
||||
bill, __ = ProForma.objects.get_or_create(account=account, is_open=True)
|
||||
bill = ProForma.objects.filter(account=account, is_open=True).last()
|
||||
if not bill:
|
||||
bill = ProForma.objects.create(account=account, is_open=True)
|
||||
elif service.is_fee:
|
||||
bill = Fee.objects.create(account=account)
|
||||
else:
|
||||
if create_new:
|
||||
bill = Invoice.objects.create(account=account)
|
||||
else:
|
||||
bill, __ = Invoice.objects.get_or_create(account=account, is_open=True)
|
||||
bill = Invoice.objects.filter(account=account, is_open=True).last()
|
||||
if not bill:
|
||||
bill = Invoice.objects.create(account=account, is_open=True)
|
||||
bills.append(bill)
|
||||
# Create bill line
|
||||
billine = bill.lines.create(
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import decimal
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
@ -122,13 +123,15 @@ class Order(models.Model):
|
|||
def update(self):
|
||||
instance = self.content_object
|
||||
handler = self.service.handler
|
||||
metric = ''
|
||||
if handler.metric:
|
||||
metric = handler.get_metric(instance)
|
||||
if metric is not None:
|
||||
MetricStorage.store(self, metric)
|
||||
metric = ', metric:{}'.format(metric)
|
||||
description = "{}: {}".format(handler.description, str(instance))
|
||||
logger.info("UPDATED order id: {id} description:{description}".format(
|
||||
id=self.id, description=description))
|
||||
logger.info("UPDATED order id:{id}, description:{description}{metric}".format(
|
||||
id=self.id, description=description, metric=metric))
|
||||
if self.description != description:
|
||||
self.description = description
|
||||
self.save()
|
||||
|
@ -143,10 +146,10 @@ class Order(models.Model):
|
|||
|
||||
|
||||
class MetricStorage(models.Model):
|
||||
order = models.ForeignKey(Order, verbose_name=_("order"))
|
||||
value = models.BigIntegerField(_("value"))
|
||||
created_on = models.DateField(_("created on"), auto_now_add=True)
|
||||
updated_on = models.DateField(_("updated on"), auto_now=True)
|
||||
order = models.ForeignKey(Order, verbose_name=_("order"), related_name='metrics')
|
||||
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
|
||||
created_on = models.DateField(_("created"), auto_now_add=True)
|
||||
updated_on = models.DateTimeField(_("updated"))
|
||||
|
||||
class Meta:
|
||||
get_latest_by = 'created_on'
|
||||
|
@ -156,23 +159,24 @@ class MetricStorage(models.Model):
|
|||
|
||||
@classmethod
|
||||
def store(cls, order, value):
|
||||
now = timezone.now()
|
||||
try:
|
||||
metric = cls.objects.filter(order=order).latest()
|
||||
except cls.DoesNotExist:
|
||||
cls.objects.create(order=order, value=value)
|
||||
cls.objects.create(order=order, value=value, updated_on=now)
|
||||
else:
|
||||
if metric.value != value:
|
||||
cls.objects.create(order=order, value=value)
|
||||
cls.objects.create(order=order, value=value, updated_on=now)
|
||||
else:
|
||||
metric.updated_on = now
|
||||
metric.save()
|
||||
|
||||
@classmethod
|
||||
def get(cls, order, ini, end):
|
||||
try:
|
||||
return cls.objects.filter(order=order, updated_on__lt=end,
|
||||
updated_on__gte=ini).latest('updated_on').value
|
||||
return order.metrics.filter(updated_on__lt=end, updated_on__gte=ini).latest('updated_on').value
|
||||
except cls.DoesNotExist:
|
||||
return 0
|
||||
return decimal.Decimal(0)
|
||||
|
||||
|
||||
_excluded_models = (MetricStorage, LogEntry, Order, ContentType, MigrationRecorder.Migration)
|
||||
|
|
|
@ -4,31 +4,28 @@ import sys
|
|||
|
||||
from dateutil import relativedelta
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import F
|
||||
from django.utils import timezone
|
||||
|
||||
from orchestra.apps.accounts.models import Account
|
||||
from orchestra.apps.services.models import Service
|
||||
from orchestra.apps.services.models import Service, Plan
|
||||
from orchestra.apps.services import settings as services_settings
|
||||
from orchestra.apps.users.models import User
|
||||
from orchestra.utils.tests import BaseTestCase, random_ascii
|
||||
|
||||
|
||||
class BillingTests(BaseTestCase):
|
||||
DEPENDENCIES = (
|
||||
'orchestra.apps.services',
|
||||
'orchestra.apps.users',
|
||||
'orchestra.apps.users.roles.posix',
|
||||
)
|
||||
|
||||
class BaseBillingTest(BaseTestCase):
|
||||
def create_account(self):
|
||||
account = Account.objects.create()
|
||||
user = User.objects.create_user(username='rata_palida', account=account)
|
||||
account.user = user
|
||||
account.save()
|
||||
return account
|
||||
|
||||
|
||||
|
||||
class FTPBillingTest(BaseBillingTest):
|
||||
def create_ftp_service(self):
|
||||
service = Service.objects.create(
|
||||
return Service.objects.create(
|
||||
description="FTP Account",
|
||||
content_type=ContentType.objects.get_for_model(User),
|
||||
match='not user.is_main and user.has_posix()',
|
||||
|
@ -36,19 +33,18 @@ class BillingTests(BaseTestCase):
|
|||
billing_point=Service.FIXED_DATE,
|
||||
is_fee=False,
|
||||
metric='',
|
||||
pricing_period=Service.BILLING_PERIOD,
|
||||
pricing_period=Service.NEVER,
|
||||
rate_algorithm=Service.STEP_PRICE,
|
||||
on_cancel=Service.DISCOUNT,
|
||||
payment_style=Service.PREPAY,
|
||||
tax=0,
|
||||
nominal_price=10,
|
||||
)
|
||||
return service
|
||||
|
||||
def create_ftp(self, account=None):
|
||||
username = '%s_ftp' % random_ascii(10)
|
||||
if not account:
|
||||
account = self.create_account()
|
||||
username = '%s_ftp' % random_ascii(10)
|
||||
user = User.objects.create_user(username=username, account=account)
|
||||
POSIX = user._meta.get_field_by_name('posix')[0].model
|
||||
POSIX.objects.create(user=user)
|
||||
|
@ -98,8 +94,12 @@ class BillingTests(BaseTestCase):
|
|||
user = self.create_ftp(account=account)
|
||||
first_bp = timezone.now().date() + relativedelta.relativedelta(years=2)
|
||||
bills = service.orders.bill(billing_point=first_bp, fixed_point=True)
|
||||
self.assertEqual(1, service.orders.active().count())
|
||||
user.delete()
|
||||
self.assertEqual(0, service.orders.active().count())
|
||||
user = self.create_ftp(account=account)
|
||||
self.assertEqual(1, service.orders.active().count())
|
||||
self.assertEqual(2, service.orders.count())
|
||||
bp = timezone.now().date() + relativedelta.relativedelta(years=1)
|
||||
bills = service.orders.bill(billing_point=bp, fixed_point=True, new_open=True)
|
||||
discount = bills[0].lines.order_by('id')[0].sublines.get()
|
||||
|
@ -109,3 +109,204 @@ class BillingTests(BaseTestCase):
|
|||
order = service.orders.order_by('-id').first()
|
||||
self.assertEqual(first_bp, order.billed_until)
|
||||
self.assertEqual(decimal.Decimal(0), bills[0].get_total())
|
||||
|
||||
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),
|
||||
match="miscellaneous.is_active and miscellaneous.service.name.lower() == 'domain .es'",
|
||||
billing_period=Service.ANUAL,
|
||||
billing_point=Service.ON_REGISTER,
|
||||
is_fee=False,
|
||||
metric='',
|
||||
pricing_period=Service.BILLING_PERIOD,
|
||||
rate_algorithm=Service.STEP_PRICE,
|
||||
on_cancel=Service.NOTHING,
|
||||
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)
|
||||
service.rates.create(plan=plan, quantity=4, price=9)
|
||||
service.rates.create(plan=plan, quantity=6, price=6)
|
||||
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)
|
||||
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_domain(self):
|
||||
service = self.create_domain_service()
|
||||
account = self.create_account()
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill()
|
||||
self.assertEqual(0, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill()
|
||||
self.assertEqual(10, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill()
|
||||
self.assertEqual(20, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill()
|
||||
self.assertEqual(29, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill()
|
||||
self.assertEqual(38, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill()
|
||||
self.assertEqual(44, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill()
|
||||
self.assertEqual(50, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill()
|
||||
self.assertEqual(56, bills[0].get_total())
|
||||
|
||||
def test_domain_proforma(self):
|
||||
service = self.create_domain_service()
|
||||
account = self.create_account()
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(proforma=True, new_open=True)
|
||||
self.assertEqual(0, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(proforma=True, new_open=True)
|
||||
self.assertEqual(10, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(proforma=True, new_open=True)
|
||||
self.assertEqual(20, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(proforma=True, new_open=True)
|
||||
self.assertEqual(29, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(proforma=True, new_open=True)
|
||||
self.assertEqual(38, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(proforma=True, new_open=True)
|
||||
self.assertEqual(44, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(proforma=True, new_open=True)
|
||||
self.assertEqual(50, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(proforma=True, new_open=True)
|
||||
self.assertEqual(56, bills[0].get_total())
|
||||
|
||||
def test_domain_cumulative(self):
|
||||
service = self.create_domain_service()
|
||||
account = self.create_account()
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(proforma=True)
|
||||
self.assertEqual(0, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(proforma=True)
|
||||
self.assertEqual(10, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(proforma=True)
|
||||
self.assertEqual(30, bills[0].get_total())
|
||||
|
||||
def test_domain_new_open(self):
|
||||
service = self.create_domain_service()
|
||||
account = self.create_account()
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(new_open=True)
|
||||
self.assertEqual(0, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(new_open=True)
|
||||
self.assertEqual(10, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(new_open=True)
|
||||
self.assertEqual(10, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(new_open=True)
|
||||
self.assertEqual(9, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(new_open=True)
|
||||
self.assertEqual(9, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(new_open=True)
|
||||
self.assertEqual(6, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(new_open=True)
|
||||
self.assertEqual(6, bills[0].get_total())
|
||||
self.create_domain(account=account)
|
||||
bills = service.orders.bill(new_open=True)
|
||||
self.assertEqual(6, bills[0].get_total())
|
||||
|
||||
|
||||
class TrafficBillingTest(BaseBillingTest):
|
||||
def create_traffic_service(self):
|
||||
service = Service.objects.create(
|
||||
description="Traffic",
|
||||
content_type=ContentType.objects.get_for_model(Account),
|
||||
match="account.is_active",
|
||||
billing_period=Service.MONTHLY,
|
||||
billing_point=Service.FIXED_DATE,
|
||||
is_fee=False,
|
||||
metric='account.resources.traffic.used',
|
||||
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_traffic_resource(self):
|
||||
from orchestra.apps.resources.models import Resource
|
||||
self.resource = Resource.objects.create(
|
||||
name='traffic',
|
||||
content_type=ContentType.objects.get_for_model(Account),
|
||||
period=Resource.MONTHLY_SUM,
|
||||
verbose_name='Account Traffic',
|
||||
unit='GB',
|
||||
scale=10**9,
|
||||
ondemand=True,
|
||||
monitors='FTPTraffic',
|
||||
)
|
||||
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)
|
||||
data = ResourceData.get_or_create(account, self.resource)
|
||||
data.update()
|
||||
|
||||
def test_traffic(self):
|
||||
service = self.create_traffic_service()
|
||||
resource = self.create_traffic_resource()
|
||||
account = self.create_account()
|
||||
|
||||
self.report_traffic(account, timezone.now(), 10**9)
|
||||
bills = service.orders.bill(commit=False)
|
||||
self.assertEqual([(account, [])], bills)
|
||||
|
||||
# Prepay
|
||||
delta = datetime.timedelta(days=60)
|
||||
date = (timezone.now()-delta).date()
|
||||
order = service.orders.get()
|
||||
order.registered_on = date
|
||||
order.save()
|
||||
|
||||
self.report_traffic(account, date, 10**9*9)
|
||||
order.metrics.update(updated_on=F('updated_on')-delta)
|
||||
bills = service.orders.bill(proforma=True)
|
||||
self.assertEqual(0, bills[0].get_total())
|
||||
|
||||
self.report_traffic(account, date, 10**10*9)
|
||||
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())
|
||||
|
|
|
@ -172,6 +172,9 @@ class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
|
|||
display_transactions.short_description = _("Transactions")
|
||||
display_transactions.allow_tags = True
|
||||
|
||||
def has_add_permission(self, *args, **kwargs):
|
||||
return False
|
||||
|
||||
def get_change_view_actions(self, obj=None):
|
||||
actions = super(TransactionProcessAdmin, self).get_change_view_actions()
|
||||
exclude = []
|
||||
|
|
|
@ -52,7 +52,7 @@ class ServiceMonitor(ServiceBackend):
|
|||
return line.split()
|
||||
|
||||
def store(self, log):
|
||||
""" stores montirod values from stdout """
|
||||
""" stores monitored values from stdout """
|
||||
from .models import MonitorData
|
||||
name = self.get_name()
|
||||
app_label, model_name = self.model.split('.')
|
||||
|
|
|
@ -11,7 +11,7 @@ from .backends import ServiceMonitor
|
|||
|
||||
def compute_resource_usage(data):
|
||||
""" Computes MonitorData.used based on related monitors """
|
||||
MonitorData = type(data)
|
||||
from .models import MonitorData
|
||||
resource = data.resource
|
||||
today = timezone.now()
|
||||
result = 0
|
||||
|
@ -29,9 +29,7 @@ def compute_resource_usage(data):
|
|||
objects = monitor_model.objects.filter(**{fields: data.object_id})
|
||||
pks = objects.values_list('id', flat=True)
|
||||
ct = ContentType.objects.get_for_model(monitor_model)
|
||||
dataset = MonitorData.objects.filter(monitor=monitor,
|
||||
content_type=ct, object_id__in=pks)
|
||||
|
||||
dataset = MonitorData.objects.filter(monitor=monitor, content_type=ct, object_id__in=pks)
|
||||
# Process dataset according to resource.period
|
||||
if resource.period == resource.MONTHLY_AVG:
|
||||
try:
|
||||
|
@ -39,11 +37,9 @@ def compute_resource_usage(data):
|
|||
except MonitorData.DoesNotExist:
|
||||
continue
|
||||
has_result = True
|
||||
epoch = datetime(year=today.year, month=today.month, day=1,
|
||||
tzinfo=timezone.utc)
|
||||
epoch = datetime(year=today.year, month=today.month, day=1, tzinfo=timezone.utc)
|
||||
total = (epoch-last.date).total_seconds()
|
||||
dataset = dataset.filter(date__year=today.year,
|
||||
date__month=today.month)
|
||||
dataset = dataset.filter(date__year=today.year, date__month=today.month)
|
||||
for data in dataset:
|
||||
slot = (previous-data.date).total_seconds()
|
||||
result += data.value * slot/total
|
||||
|
@ -62,7 +58,5 @@ def compute_resource_usage(data):
|
|||
continue
|
||||
has_result = True
|
||||
else:
|
||||
msg = "%s support not implemented" % data.period
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
raise NotImplementedError("%s support not implemented" % data.period)
|
||||
return result/resource.scale if has_result else None
|
||||
|
|
|
@ -2,11 +2,11 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core import validators
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from djcelery.models import PeriodicTask, CrontabSchedule
|
||||
|
||||
from orchestra.models import queryset, fields
|
||||
from orchestra.utils.functional import cached
|
||||
|
||||
from . import helpers
|
||||
from .backends import ServiceMonitor
|
||||
|
@ -43,6 +43,7 @@ class Resource(models.Model):
|
|||
default=LAST,
|
||||
help_text=_("Operation used for aggregating this resource monitored"
|
||||
"data."))
|
||||
# TODO rename to on_deman
|
||||
ondemand = models.BooleanField(_("on demand"), default=False,
|
||||
help_text=_("If enabled the resource will not be pre-allocated, "
|
||||
"but allocated under the application demand"))
|
||||
|
@ -79,6 +80,7 @@ class Resource(models.Model):
|
|||
return "{}-{}".format(str(self.content_type), self.name)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# created = not self.pk
|
||||
super(Resource, self).save(*args, **kwargs)
|
||||
# Create Celery periodic task
|
||||
name = 'monitor.%s' % str(self)
|
||||
|
@ -98,6 +100,8 @@ class Resource(models.Model):
|
|||
elif task.crontab != self.crontab:
|
||||
task.crontab = self.crontab
|
||||
task.save()
|
||||
# if created:
|
||||
# create_resource_relation()
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super(Resource, self).delete(*args, **kwargs)
|
||||
|
@ -136,6 +140,13 @@ class ResourceData(models.Model):
|
|||
|
||||
def get_used(self):
|
||||
return helpers.compute_resource_usage(self)
|
||||
|
||||
def update(self, current=None):
|
||||
if current is None:
|
||||
current = self.get_used()
|
||||
self.used = current or 0
|
||||
self.last_update = timezone.now()
|
||||
self.save()
|
||||
|
||||
|
||||
class MonitorData(models.Model):
|
||||
|
@ -169,7 +180,6 @@ def create_resource_relation():
|
|||
resource = Resource.objects.get(content_type__model=model,
|
||||
name=attr, is_active=True)
|
||||
data = ResourceData(content_object=self.obj, resource=resource)
|
||||
setattr(self, attr, data)
|
||||
return data
|
||||
|
||||
def __get__(self, obj, cls):
|
||||
|
|
|
@ -27,15 +27,22 @@ def monitor(resource_id):
|
|||
model = resource.content_type.model_class()
|
||||
for obj in model.objects.all():
|
||||
data = ResourceData.get_or_create(obj, resource)
|
||||
current = data.get_used()
|
||||
data.update()
|
||||
if not resource.disable_trigger:
|
||||
if data.used < data.allocated and current > data.allocated:
|
||||
if data.used < data.allocated:
|
||||
op = Operation.create(backend, obj, Operation.EXCEED)
|
||||
operations.append(op)
|
||||
elif data.used > data.allocated and current < data.allocated:
|
||||
elif data.used < data.allocated:
|
||||
op = Operation.create(backend, obj, Operation.RECOVERY)
|
||||
operation.append(op)
|
||||
data.used = current or 0
|
||||
data.last_update = timezone.now()
|
||||
data.save()
|
||||
# data = ResourceData.get_or_create(obj, resource)
|
||||
# current = data.get_used()
|
||||
# if not resource.disable_trigger:
|
||||
# if data.used < data.allocated and current > data.allocated:
|
||||
# op = Operation.create(backend, obj, Operation.EXCEED)
|
||||
# operations.append(op)
|
||||
# elif data.used > data.allocated and current < data.allocated:
|
||||
# op = Operation.create(backend, obj, Operation.RECOVERY)
|
||||
# operation.append(op)
|
||||
# data.update(current=current)
|
||||
Operation.execute(operations)
|
||||
|
|
|
@ -65,6 +65,8 @@ class ServiceHandler(plugins.Plugin):
|
|||
date = bp
|
||||
if self.payment_style == self.PREPAY:
|
||||
date += relativedelta.relativedelta(months=1)
|
||||
else:
|
||||
date = timezone.now().date()
|
||||
if self.billing_point == self.ON_REGISTER:
|
||||
day = order.registered_on.day
|
||||
elif self.billing_point == self.FIXED_DATE:
|
||||
|
@ -84,7 +86,7 @@ class ServiceHandler(plugins.Plugin):
|
|||
raise NotImplementedError(msg)
|
||||
year = bp.year
|
||||
if self.payment_style == self.POSTPAY:
|
||||
year = bo.year - relativedelta.relativedelta(years=1)
|
||||
year = bp.year - relativedelta.relativedelta(years=1)
|
||||
if bp.month >= month:
|
||||
year = bp.year + 1
|
||||
bp = datetime.datetime(year=year, month=month, day=day,
|
||||
|
@ -116,10 +118,19 @@ class ServiceHandler(plugins.Plugin):
|
|||
return decimal.Decimal(size)
|
||||
|
||||
def get_pricing_slots(self, ini, end):
|
||||
day = 1
|
||||
month = settings.SERVICES_SERVICE_ANUAL_BILLING_MONTH
|
||||
if self.billing_point == self.ON_REGISTER:
|
||||
day = ini.day
|
||||
month = ini.month
|
||||
period = self.get_pricing_period()
|
||||
if period == self.MONTHLY:
|
||||
ini = datetime.datetime(year=ini.year, month=ini.month, day=day,
|
||||
tzinfo=timezone.get_current_timezone()).date()
|
||||
rdelta = relativedelta.relativedelta(months=1)
|
||||
elif period == self.ANUAL:
|
||||
ini = datetime.datetime(year=ini.year, month=month, day=day,
|
||||
tzinfo=timezone.get_current_timezone()).date()
|
||||
rdelta = relativedelta.relativedelta(years=1)
|
||||
elif period == self.NEVER:
|
||||
yield ini, end
|
||||
|
@ -128,10 +139,9 @@ class ServiceHandler(plugins.Plugin):
|
|||
raise NotImplementedError
|
||||
while True:
|
||||
next = ini + rdelta
|
||||
if next >= end:
|
||||
yield ini, end
|
||||
break
|
||||
yield ini, next
|
||||
if next >= end:
|
||||
break
|
||||
ini = next
|
||||
|
||||
def generate_discount(self, line, dtype, price):
|
||||
|
@ -213,12 +223,12 @@ class ServiceHandler(plugins.Plugin):
|
|||
for order in porders:
|
||||
bu = getattr(order, 'new_billed_until', order.billed_until)
|
||||
if bu:
|
||||
if order.register >= ini and order.register < end:
|
||||
if order.registered_on > ini and order.registered_on <= end:
|
||||
counter += 1
|
||||
if order.register != bu and bu >= ini and bu < end:
|
||||
if order.registered_on != bu and bu > ini and bu <= end:
|
||||
counter += 1
|
||||
if order.billed_until and order.billed_until != bu:
|
||||
if order.register != order.billed_until and order.billed_until >= ini and order.billed_until < end:
|
||||
if order.registered_on != order.billed_until and order.billed_until > ini and order.billed_until <= end:
|
||||
counter += 1
|
||||
return counter
|
||||
|
||||
|
@ -230,7 +240,7 @@ class ServiceHandler(plugins.Plugin):
|
|||
size = self.get_price_size(ini, end)
|
||||
metric = len(orders)
|
||||
interval = helpers.Interval(ini=ini, end=end)
|
||||
for position, order in enumerate(orders):
|
||||
for position, order in enumerate(orders, start=1):
|
||||
csize = 0
|
||||
compensations = getattr(order, '_compensations', [])
|
||||
# Compensations < new_billed_until
|
||||
|
@ -269,14 +279,14 @@ class ServiceHandler(plugins.Plugin):
|
|||
def bill_registered_or_renew_events(self, account, porders, rates, commit=True):
|
||||
# Before registration
|
||||
lines = []
|
||||
perido = self.get_pricing_period()
|
||||
period = self.get_pricing_period()
|
||||
if period == self.MONTHLY:
|
||||
rdelta = relativedelta.relativedelta(months=1)
|
||||
elif period == self.ANUAL:
|
||||
rdelta = relativedelta.relativedelta(years=1)
|
||||
elif period == self.NEVER:
|
||||
raise NotImplementedError("Rates with no pricing period?")
|
||||
for position, order in enumerate(porders):
|
||||
for position, order in enumerate(porders, start=1):
|
||||
if hasattr(order, 'new_billed_until'):
|
||||
pend = order.billed_until or order.registered_on
|
||||
pini = pend - rdelta
|
||||
|
@ -298,6 +308,7 @@ class ServiceHandler(plugins.Plugin):
|
|||
if commit:
|
||||
order.billed_until = order.new_billed_until
|
||||
order.save()
|
||||
return lines
|
||||
|
||||
def bill_with_orders(self, orders, account, **options):
|
||||
# For the "boundary conditions" just think that:
|
||||
|
@ -340,7 +351,7 @@ 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:
|
||||
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)
|
||||
else:
|
||||
# TODO compensation in this case?
|
||||
|
@ -371,6 +382,7 @@ class ServiceHandler(plugins.Plugin):
|
|||
# TODO filter out orders with cancelled_on < billed_until ?
|
||||
lines = []
|
||||
commit = options.get('commit', True)
|
||||
bp = None
|
||||
for order in orders:
|
||||
bp = self.get_billing_point(order, bp=bp, **options)
|
||||
ini = order.billed_until or order.registered_on
|
||||
|
@ -381,25 +393,17 @@ class ServiceHandler(plugins.Plugin):
|
|||
prev = None
|
||||
lines_info = []
|
||||
for ini, end in self.get_pricing_slots(ini, bp):
|
||||
size = self.get_price_size(ini, end)
|
||||
metric = order.get_metric(ini, end)
|
||||
price = self.get_price(order, metric)
|
||||
current = AttributeDict(price=price, size=size, ini=ini, end=end)
|
||||
if prev and prev.metric == current.metric and prev.end == current.end:
|
||||
prev.end = current.end
|
||||
prev.size += current.size
|
||||
prev.price += current.price
|
||||
else:
|
||||
lines_info.append(current)
|
||||
prev = current
|
||||
for line in lines_info:
|
||||
lines.append(self.generate_line(order, price, size, ini, end))
|
||||
lines.append(self.generate_line(order, price, metric, ini, end))
|
||||
if commit:
|
||||
order.billed_until = order.new_billed_until
|
||||
order.save()
|
||||
return lines
|
||||
|
||||
def generate_bill_lines(self, orders, account, **options):
|
||||
if options.get('proforma', False):
|
||||
options['commit'] = False
|
||||
if not self.metric:
|
||||
lines = self.bill_with_orders(orders, account, **options)
|
||||
else:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import decimal
|
||||
import sys
|
||||
|
||||
from django.db import models
|
||||
|
@ -281,14 +282,14 @@ class Service(models.Model):
|
|||
if counter >= metric:
|
||||
counter = metric
|
||||
accumulated += (counter - ant_counter) * rate['price']
|
||||
return float(accumulated)
|
||||
return decimal.Decimal(accumulated)
|
||||
ant_counter = counter
|
||||
accumulated += rate['price'] * rate['quantity']
|
||||
else:
|
||||
for rate in rates:
|
||||
counter += rate['quantity']
|
||||
if counter >= position:
|
||||
return float(rate['price'])
|
||||
return decimal.Decimal(rate['price'])
|
||||
|
||||
def get_rates(self, account, cache=True):
|
||||
# rates are cached per account
|
||||
|
|
|
@ -50,7 +50,7 @@ class HandlerTests(BaseTestCase):
|
|||
billing_point=Service.FIXED_DATE,
|
||||
is_fee=False,
|
||||
metric='',
|
||||
pricing_period=Service.BILLING_PERIOD,
|
||||
pricing_period=Service.NEVER,
|
||||
rate_algorithm=Service.STEP_PRICE,
|
||||
on_cancel=Service.DISCOUNT,
|
||||
payment_style=Service.PREPAY,
|
||||
|
|
|
@ -38,8 +38,7 @@ class User(auth.AbstractBaseUser):
|
|||
|
||||
@property
|
||||
def is_main(self):
|
||||
# TODO chicken and egg
|
||||
return not self.account.user_id or self.account.user == self
|
||||
return self.account.user == self
|
||||
|
||||
def get_full_name(self):
|
||||
full_name = '%s %s' % (self.first_name, self.last_name)
|
||||
|
|
Loading…
Reference in a new issue