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