Major cleanup
This commit is contained in:
parent
af323ebe25
commit
99b14f9c9f
19
TODO.md
19
TODO.md
|
@ -59,12 +59,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
|
||||||
dependency collector with max_recursion that matches the number of dots on service.match and service.metric
|
dependency collector with max_recursion that matches the number of dots on service.match and service.metric
|
||||||
|
|
||||||
|
|
||||||
* Be consistent with dates:
|
|
||||||
* created_on date
|
|
||||||
* created_at datetime
|
|
||||||
|
|
||||||
at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
|
|
||||||
|
|
||||||
|
|
||||||
* backend logs with hal logo
|
* backend logs with hal logo
|
||||||
* Use logs for storing monitored values
|
* Use logs for storing monitored values
|
||||||
|
@ -82,13 +76,6 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
|
||||||
|
|
||||||
* help_text on readonly_fields specialy Bill.state. (eg. A bill is in OPEN state when bla bla )
|
* help_text on readonly_fields specialy Bill.state. (eg. A bill is in OPEN state when bla bla )
|
||||||
|
|
||||||
* Create ProForma from orders orders.bill(proforma=True)
|
|
||||||
|
|
||||||
* generic confirmation breadcrumbs for single objects
|
|
||||||
|
|
||||||
* DirectDebit due date = bill.due_date
|
|
||||||
|
|
||||||
* settings.ENABLED_PLUGINS = ('path.module.ClassPlugin',)
|
|
||||||
|
|
||||||
* Transaction states: CREATED, PROCESSED, EXECUTED, COMMITED, ABORTED (SECURED, REJECTED?)
|
* Transaction states: CREATED, PROCESSED, EXECUTED, COMMITED, ABORTED (SECURED, REJECTED?)
|
||||||
* bill.send() -> transacction.EXECUTED when source=None
|
* bill.send() -> transacction.EXECUTED when source=None
|
||||||
|
@ -108,5 +95,7 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
|
||||||
return order.register_at.date()
|
return order.register_at.date()
|
||||||
|
|
||||||
|
|
||||||
* latest by 'id' *always*
|
* mail backend related_models = ('resources__content_type') ??
|
||||||
* replace add_now by default=lambda: timezone.now()
|
* ignore orders
|
||||||
|
|
||||||
|
* Redmine, BSCW and other applications management
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from functools import wraps, partial
|
from functools import wraps, partial
|
||||||
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import available_attrs
|
from django.utils.decorators import available_attrs
|
||||||
|
@ -61,8 +60,10 @@ def action_with_confirmation(action_name=None, extra_context={},
|
||||||
|
|
||||||
if len(queryset) == 1:
|
if len(queryset) == 1:
|
||||||
objects_name = force_text(opts.verbose_name)
|
objects_name = force_text(opts.verbose_name)
|
||||||
|
obj = queryset.get()
|
||||||
else:
|
else:
|
||||||
objects_name = force_text(opts.verbose_name_plural)
|
objects_name = force_text(opts.verbose_name_plural)
|
||||||
|
obj = None
|
||||||
if not action_name:
|
if not action_name:
|
||||||
action_name = func.__name__
|
action_name = func.__name__
|
||||||
context = {
|
context = {
|
||||||
|
@ -73,6 +74,7 @@ def action_with_confirmation(action_name=None, extra_context={},
|
||||||
'action_value': action_value,
|
'action_value': action_value,
|
||||||
'queryset': queryset,
|
'queryset': queryset,
|
||||||
'opts': opts,
|
'opts': opts,
|
||||||
|
'obj': obj,
|
||||||
'app_label': app_label,
|
'app_label': app_label,
|
||||||
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ from django.shortcuts import redirect
|
||||||
from django.utils import importlib
|
from django.utils import importlib
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from orchestra.models.utils import get_field_value
|
from orchestra.models.utils import get_field_value
|
||||||
from orchestra.utils import humanize
|
from orchestra.utils import humanize
|
||||||
|
|
|
@ -6,7 +6,6 @@ from orchestra.utils.apps import autodiscover as module_autodiscover
|
||||||
from orchestra.utils.python import import_class
|
from orchestra.utils.python import import_class
|
||||||
|
|
||||||
from .helpers import insert_links, replace_collectionmethodname
|
from .helpers import insert_links, replace_collectionmethodname
|
||||||
from .root import APIRoot
|
|
||||||
|
|
||||||
|
|
||||||
def collectionlink(**kwargs):
|
def collectionlink(**kwargs):
|
||||||
|
|
|
@ -2,7 +2,6 @@ from django import forms
|
||||||
from django.conf.urls import patterns, url
|
from django.conf.urls import patterns, url
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.contrib.admin.util import unquote
|
from django.contrib.admin.util import unquote
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.six.moves.urllib.parse import parse_qsl
|
from django.utils.six.moves.urllib.parse import parse_qsl
|
||||||
|
@ -115,7 +114,6 @@ class AccountListAdmin(AccountAdmin):
|
||||||
select_account.order_admin_field = 'user__username'
|
select_account.order_admin_field = 'user__username'
|
||||||
|
|
||||||
def changelist_view(self, request, extra_context=None):
|
def changelist_view(self, request, extra_context=None):
|
||||||
opts = self.model._meta
|
|
||||||
original_app_label = request.META['PATH_INFO'].split('/')[-5]
|
original_app_label = request.META['PATH_INFO'].split('/')[-5]
|
||||||
original_model = request.META['PATH_INFO'].split('/')[-4]
|
original_model = request.META['PATH_INFO'].split('/')[-4]
|
||||||
context = {
|
context = {
|
||||||
|
@ -182,7 +180,6 @@ class AccountAdminMixin(object):
|
||||||
|
|
||||||
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
|
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
|
||||||
account_id = self.get_account_from_preserve_filters(request)
|
account_id = self.get_account_from_preserve_filters(request)
|
||||||
verb = 'change' if object_id else 'add'
|
|
||||||
if not object_id:
|
if not object_id:
|
||||||
if account_id:
|
if account_id:
|
||||||
# Preselect account
|
# Preselect account
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from django.contrib.admin import SimpleListFilter
|
from django.contrib.admin import SimpleListFilter
|
||||||
from django.utils.encoding import force_text
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
from optparse import make_option
|
from optparse import make_option
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
from orchestra.apps.accounts.models import Account
|
from orchestra.apps.accounts.models import Account
|
||||||
from orchestra.apps.users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
@ -29,5 +28,4 @@ class Command(BaseCommand):
|
||||||
username = options.get('username')
|
username = options.get('username')
|
||||||
password = options.get('password')
|
password = options.get('password')
|
||||||
account = Account.objects.create()
|
account = Account.objects.create()
|
||||||
user = User.objects.create_superuser(username, email, password,
|
account.users.create_superuser(username, email, password, is_main=True)
|
||||||
account=account, is_main=True)
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0002_auto_20140909_1850'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='account',
|
||||||
|
name='register_date',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='account',
|
||||||
|
name='registered_on',
|
||||||
|
field=models.DateField(default=datetime.datetime(2014, 9, 26, 13, 25, 49, 42008), verbose_name='registered', auto_now_add=True),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,6 +1,5 @@
|
||||||
from django.conf import settings as djsettings
|
from django.conf import settings as djsettings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.core import services
|
from orchestra.core import services
|
||||||
|
@ -18,7 +17,7 @@ class Account(models.Model):
|
||||||
language = models.CharField(_("language"), max_length=2,
|
language = models.CharField(_("language"), max_length=2,
|
||||||
choices=settings.ACCOUNTS_LANGUAGES,
|
choices=settings.ACCOUNTS_LANGUAGES,
|
||||||
default=settings.ACCOUNTS_DEFAULT_LANGUAGE)
|
default=settings.ACCOUNTS_DEFAULT_LANGUAGE)
|
||||||
register_date = models.DateTimeField(_("register date"), auto_now_add=True)
|
registered_on = models.DateField(_("registered"), auto_now_add=True)
|
||||||
comments = models.TextField(_("comments"), max_length=256, blank=True)
|
comments = models.TextField(_("comments"), max_length=256, blank=True)
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
from orchestra.admin.utils import admin_link, admin_date
|
from orchestra.admin.utils import admin_date
|
||||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
|
@ -53,8 +53,8 @@ class BillLineInline(admin.TabularInline):
|
||||||
|
|
||||||
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
'number', 'is_open', 'type_link', 'account_link', 'created_on_display',
|
'number', 'type_link', 'account_link', 'created_on_display',
|
||||||
'num_lines', 'display_total', 'display_payment_state'
|
'num_lines', 'display_total', 'display_payment_state', 'is_open'
|
||||||
)
|
)
|
||||||
list_filter = (BillTypeListFilter, 'is_open',)
|
list_filter = (BillTypeListFilter, 'is_open',)
|
||||||
add_fields = ('account', 'type', 'is_open', 'due_on', 'comments')
|
add_fields = ('account', 'type', 'is_open', 'due_on', 'comments')
|
||||||
|
|
18
orchestra/apps/bills/migrations/0008_auto_20140926_1218.py
Normal file
18
orchestra/apps/bills/migrations/0008_auto_20140926_1218.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bills', '0007_auto_20140918_1454'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='bill',
|
||||||
|
options={'get_latest_by': 'id'},
|
||||||
|
),
|
||||||
|
]
|
46
orchestra/apps/bills/migrations/0009_auto_20140926_1220.py
Normal file
46
orchestra/apps/bills/migrations/0009_auto_20140926_1220.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('orders', '__first__'),
|
||||||
|
('bills', '0008_auto_20140926_1218'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='billline',
|
||||||
|
name='created_on',
|
||||||
|
field=models.DateField(default=datetime.datetime(2014, 9, 26, 12, 20, 24, 908200), verbose_name='created', auto_now_add=True),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='billline',
|
||||||
|
name='order_billed_on',
|
||||||
|
field=models.DateField(null=True, verbose_name='order billed', blank=True),
|
||||||
|
preserve_default=True,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='billline',
|
||||||
|
name='order_billed_until',
|
||||||
|
field=models.DateField(null=True, verbose_name='order billed until', blank=True),
|
||||||
|
preserve_default=True,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='billline',
|
||||||
|
name='order_id',
|
||||||
|
field=models.ForeignKey(blank=True, to='orders.Order', help_text='Informative link back to the order', null=True),
|
||||||
|
preserve_default=True,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='billsubline',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(default=b'OTHER', max_length=16, verbose_name='type', choices=[(b'VOLUME', 'Volume'), (b'COMPENSATION', 'Compensation'), (b'OTHER', 'Other')]),
|
||||||
|
preserve_default=True,
|
||||||
|
),
|
||||||
|
]
|
29
orchestra/apps/bills/migrations/0010_auto_20140926_1326.py
Normal file
29
orchestra/apps/bills/migrations/0010_auto_20140926_1326.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bills', '0009_auto_20140926_1220'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bill',
|
||||||
|
name='closed_on',
|
||||||
|
field=models.DateField(null=True, verbose_name='closed on', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bill',
|
||||||
|
name='created_on',
|
||||||
|
field=models.DateField(auto_now_add=True, verbose_name='created on'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bill',
|
||||||
|
name='last_modified_on',
|
||||||
|
field=models.DateField(auto_now=True, verbose_name='last modified on'),
|
||||||
|
),
|
||||||
|
]
|
25
orchestra/apps/bills/migrations/0011_auto_20140926_1334.py
Normal file
25
orchestra/apps/bills/migrations/0011_auto_20140926_1334.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bills', '0010_auto_20140926_1326'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='bill',
|
||||||
|
name='last_modified_on',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='bill',
|
||||||
|
name='updated_on',
|
||||||
|
field=models.DateField(default=datetime.date(2014, 9, 26), verbose_name='updated on', auto_now=True),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
19
orchestra/apps/bills/migrations/0012_auto_20140926_1458.py
Normal file
19
orchestra/apps/bills/migrations/0012_auto_20140926_1458.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bills', '0011_auto_20140926_1334'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='billline',
|
||||||
|
old_name='order_id',
|
||||||
|
new_name='order',
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,6 +1,6 @@
|
||||||
import inspect
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from django.core.validators import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.template import loader, Context
|
from django.template import loader, Context
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -9,8 +9,8 @@ from django.utils.functional import cached_property
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.apps.accounts.models import Account
|
from orchestra.apps.accounts.models import Account
|
||||||
|
from orchestra.apps.contacts.models import Contact
|
||||||
from orchestra.core import accounts
|
from orchestra.core import accounts
|
||||||
from orchestra.utils.functional import cached
|
|
||||||
from orchestra.utils.html import html_to_pdf
|
from orchestra.utils.html import html_to_pdf
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
|
@ -53,13 +53,12 @@ class Bill(models.Model):
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
||||||
related_name='%(class)s')
|
related_name='%(class)s')
|
||||||
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
||||||
created_on = models.DateTimeField(_("created on"), auto_now_add=True)
|
created_on = models.DateField(_("created on"), auto_now_add=True)
|
||||||
closed_on = models.DateTimeField(_("closed on"), blank=True, null=True)
|
closed_on = models.DateField(_("closed on"), blank=True, null=True)
|
||||||
# TODO rename to is_closed
|
|
||||||
is_open = models.BooleanField(_("is open"), default=True)
|
is_open = models.BooleanField(_("is open"), default=True)
|
||||||
is_sent = models.BooleanField(_("is sent"), default=False)
|
is_sent = models.BooleanField(_("is sent"), default=False)
|
||||||
due_on = models.DateField(_("due on"), null=True, blank=True)
|
due_on = models.DateField(_("due on"), null=True, blank=True)
|
||||||
last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True)
|
updated_on = models.DateField(_("updated on"), auto_now=True)
|
||||||
total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
||||||
comments = models.TextField(_("comments"), blank=True)
|
comments = models.TextField(_("comments"), blank=True)
|
||||||
html = models.TextField(_("HTML"), blank=True)
|
html = models.TextField(_("HTML"), blank=True)
|
||||||
|
@ -145,7 +144,6 @@ class Bill(models.Model):
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def send(self):
|
def send(self):
|
||||||
from orchestra.apps.contacts.models import Contact
|
|
||||||
self.account.send_email(
|
self.account.send_email(
|
||||||
template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE,
|
template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE,
|
||||||
context={
|
context={
|
||||||
|
@ -241,9 +239,13 @@ class BillLine(models.Model):
|
||||||
quantity = models.DecimalField(_("quantity"), max_digits=12, decimal_places=2)
|
quantity = models.DecimalField(_("quantity"), max_digits=12, decimal_places=2)
|
||||||
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
|
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
|
||||||
tax = models.PositiveIntegerField(_("tax"))
|
tax = models.PositiveIntegerField(_("tax"))
|
||||||
# TODO
|
# Undo
|
||||||
# order_id = models.ForeignKey('orders.Order', null=True, blank=True,
|
order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=True,
|
||||||
# help_text=_("Informative link back to the order"))
|
help_text=_("Informative link back to the order"))
|
||||||
|
order_billed_on = models.DateField(_("order billed"), null=True, blank=True)
|
||||||
|
order_billed_until = models.DateField(_("order billed until"), null=True, blank=True)
|
||||||
|
created_on = models.DateField(_("created"), auto_now_add=True)
|
||||||
|
# Amendment
|
||||||
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
|
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
|
||||||
related_name='amendment_lines', null=True, blank=True)
|
related_name='amendment_lines', null=True, blank=True)
|
||||||
|
|
||||||
|
@ -262,6 +264,17 @@ class BillLine(models.Model):
|
||||||
total += subline.total
|
total += subline.total
|
||||||
return total
|
return total
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
# TODO warn user that undoing bills with compensations lead to compensation lost
|
||||||
|
for attr in ['order_id', 'order_billed_on', 'order_billed_until']:
|
||||||
|
if not getattr(self, attr):
|
||||||
|
raise ValidationError(_("Not enough information stored for undoing"))
|
||||||
|
if self.created_on != self.order.billed_on:
|
||||||
|
raise ValidationError(_("Dates don't match"))
|
||||||
|
self.order.billed_until = self.order_billed_until
|
||||||
|
self.order.billed_on = self.order_billed_on
|
||||||
|
self.delete()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# TODO cost and consistency of this shit
|
# TODO cost and consistency of this shit
|
||||||
super(BillLine, self).save(*args, **kwargs)
|
super(BillLine, self).save(*args, **kwargs)
|
||||||
|
@ -272,10 +285,20 @@ class BillLine(models.Model):
|
||||||
|
|
||||||
class BillSubline(models.Model):
|
class BillSubline(models.Model):
|
||||||
""" Subline used for describing an item discount """
|
""" Subline used for describing an item discount """
|
||||||
|
VOLUME = 'VOLUME'
|
||||||
|
COMPENSATION = 'COMPENSATION'
|
||||||
|
OTHER = 'OTHER'
|
||||||
|
TYPES = (
|
||||||
|
(VOLUME, _("Volume")),
|
||||||
|
(COMPENSATION, _("Compensation")),
|
||||||
|
(OTHER, _("Other")),
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: order info for undoing
|
||||||
line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines')
|
line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines')
|
||||||
description = models.CharField(_("description"), max_length=256)
|
description = models.CharField(_("description"), max_length=256)
|
||||||
total = models.DecimalField(max_digits=12, decimal_places=2)
|
total = models.DecimalField(max_digits=12, decimal_places=2)
|
||||||
# TODO type ? Volume and Compensation
|
type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# TODO cost of this shit
|
# TODO cost of this shit
|
||||||
|
|
|
@ -36,3 +36,6 @@ BILLS_SELLER_BANK_ACCOUNT = getattr(settings, 'BILLS_SELLER_BANK_ACCOUNT', '0000
|
||||||
|
|
||||||
BILLS_EMAIL_NOTIFICATION_TEMPLATE = getattr(settings, 'BILLS_EMAIL_NOTIFICATION_TEMPLATE',
|
BILLS_EMAIL_NOTIFICATION_TEMPLATE = getattr(settings, 'BILLS_EMAIL_NOTIFICATION_TEMPLATE',
|
||||||
'bills/bill-notification.email')
|
'bills/bill-notification.email')
|
||||||
|
|
||||||
|
|
||||||
|
BILLS_ORDER_MODEL = getattr(settings, 'BILLS_ORDER_MODEL', 'orders.Order')
|
||||||
|
|
|
@ -78,9 +78,9 @@
|
||||||
{% with sublines=line.sublines.all %}
|
{% with sublines=line.sublines.all %}
|
||||||
<span class="{% if not sublines %}last {% endif %}column-id">{{ line.id }}</span>
|
<span class="{% if not sublines %}last {% endif %}column-id">{{ line.id }}</span>
|
||||||
<span class="{% if not sublines %}last {% endif %}column-description">{{ line.description }}</span>
|
<span class="{% if not sublines %}last {% endif %}column-description">{{ line.description }}</span>
|
||||||
<span class="{% if not sublines %}last {% endif %}column-quantity">{{ line.amount|default:" " }}</span>
|
<span class="{% if not sublines %}last {% endif %}column-quantity">{{ line.quantity|default:" " }}</span>
|
||||||
<span class="{% if not sublines %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% endif %}</span>
|
<span class="{% if not sublines %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% endif %}</span>
|
||||||
<span class="{% if not sublines %}last {% endif %}column-subtotal">{{ line.total }} &{{ currency.lower }};</span>
|
<span class="{% if not sublines %}last {% endif %}column-subtotal">{{ line.subtotal }} &{{ currency.lower }};</span>
|
||||||
<br>
|
<br>
|
||||||
{% for subline in sublines %}
|
{% for subline in sublines %}
|
||||||
<span class="{% if forloop.last %}last {% endif %}subline column-id"> </span>
|
<span class="{% if forloop.last %}last {% endif %}subline column-id"> </span>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES',
|
CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES',
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
from django.db import models
|
|
||||||
from django.conf.urls import patterns
|
from django.conf.urls import patterns
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from orchestra.api import router, SetPasswordApiMixin
|
from orchestra.api import router, SetPasswordApiMixin
|
||||||
from orchestra.apps.accounts.api import AccountApiMixin
|
from orchestra.apps.accounts.api import AccountApiMixin
|
||||||
|
|
|
@ -77,21 +77,21 @@ class MysqlDisk(ServiceMonitor):
|
||||||
verbose_name = _("MySQL disk")
|
verbose_name = _("MySQL disk")
|
||||||
|
|
||||||
def exceeded(self, db):
|
def exceeded(self, db):
|
||||||
context = self.get_context(obj)
|
context = self.get_context(db)
|
||||||
self.append("mysql -e '"
|
self.append("mysql -e '"
|
||||||
"UPDATE db SET Insert_priv=\"N\", Create_priv=\"N\""
|
"UPDATE db SET Insert_priv=\"N\", Create_priv=\"N\""
|
||||||
" WHERE Db=\"%(db_name)s\";'" % context
|
" WHERE Db=\"%(db_name)s\";'" % context
|
||||||
)
|
)
|
||||||
|
|
||||||
def recovery(self, db):
|
def recovery(self, db):
|
||||||
context = self.get_context(obj)
|
context = self.get_context(db)
|
||||||
self.append("mysql -e '"
|
self.append("mysql -e '"
|
||||||
"UPDATE db SET Insert_priv=\"Y\", Create_priv=\"Y\""
|
"UPDATE db SET Insert_priv=\"Y\", Create_priv=\"Y\""
|
||||||
" WHERE Db=\"%(db_name)s\";'" % context
|
" WHERE Db=\"%(db_name)s\";'" % context
|
||||||
)
|
)
|
||||||
|
|
||||||
def monitor(self, db):
|
def monitor(self, db):
|
||||||
context = self.get_context(obj)
|
context = self.get_context(db)
|
||||||
self.append(
|
self.append(
|
||||||
"echo %(db_id)s $(mysql -B -e '"
|
"echo %(db_id)s $(mysql -B -e '"
|
||||||
" SELECT sum( data_length + index_length ) \"Size\"\n"
|
" SELECT sum( data_length + index_length ) \"Size\"\n"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.forms import UserCreationForm, ReadOnlyPasswordHashField
|
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
@ -109,7 +109,6 @@ class ReadOnlySQLPasswordHashField(ReadOnlyPasswordHashField):
|
||||||
if 'Invalid' not in original:
|
if 'Invalid' not in original:
|
||||||
return original
|
return original
|
||||||
encoded = value
|
encoded = value
|
||||||
final_attrs = self.build_attrs(attrs)
|
|
||||||
if not encoded:
|
if not encoded:
|
||||||
summary = mark_safe("<strong>%s</strong>" % _("No password set."))
|
summary = mark_safe("<strong>%s</strong>" % _("No password set."))
|
||||||
else:
|
else:
|
||||||
|
|
14
orchestra/apps/domains/actions.py
Normal file
14
orchestra/apps/domains/actions.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
def view_zone(modeladmin, request, queryset):
|
||||||
|
zone = queryset.get()
|
||||||
|
context = {
|
||||||
|
'opts': modeladmin.model._meta,
|
||||||
|
'object': zone,
|
||||||
|
'title': _("%s zone content") % zone.origin.name
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'admin/domains/domain/view_zone.html', context)
|
||||||
|
view_zone.url_name = 'view-zone'
|
||||||
|
view_zone.verbose_name = _("View zone")
|
|
@ -1,17 +1,13 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf.urls import patterns, url
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin.util import unquote
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.db.models import F
|
|
||||||
from django.template.response import TemplateResponse
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin
|
from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin
|
||||||
from orchestra.admin.utils import wrap_admin_view, admin_link, change_url
|
from orchestra.admin.utils import admin_link, change_url
|
||||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||||
from orchestra.utils import apps
|
from orchestra.utils import apps
|
||||||
|
|
||||||
|
from .actions import view_zone
|
||||||
from .forms import RecordInlineFormSet, DomainAdminForm
|
from .forms import RecordInlineFormSet, DomainAdminForm
|
||||||
from .filters import TopDomainListFilter
|
from .filters import TopDomainListFilter
|
||||||
from .models import Domain, Record
|
from .models import Domain, Record
|
||||||
|
@ -61,6 +57,7 @@ class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin
|
||||||
('top_domain', 'True'),
|
('top_domain', 'True'),
|
||||||
)
|
)
|
||||||
form = DomainAdminForm
|
form = DomainAdminForm
|
||||||
|
change_view_actions = [view_zone]
|
||||||
|
|
||||||
def structured_name(self, domain):
|
def structured_name(self, domain):
|
||||||
if not domain.is_top:
|
if not domain.is_top:
|
||||||
|
@ -89,35 +86,12 @@ class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin
|
||||||
websites.short_description = _("Websites")
|
websites.short_description = _("Websites")
|
||||||
websites.allow_tags = True
|
websites.allow_tags = True
|
||||||
|
|
||||||
def get_urls(self):
|
|
||||||
""" Returns the additional urls for the change view links """
|
|
||||||
urls = super(DomainAdmin, self).get_urls()
|
|
||||||
admin_site = self.admin_site
|
|
||||||
opts = self.model._meta
|
|
||||||
urls = patterns("",
|
|
||||||
url('^(\d+)/view-zone/$',
|
|
||||||
wrap_admin_view(self, self.view_zone_view),
|
|
||||||
name='domains_domain_view_zone')
|
|
||||||
) + urls
|
|
||||||
return urls
|
|
||||||
|
|
||||||
def view_zone_view(self, request, object_id):
|
|
||||||
zone = self.get_object(request, unquote(object_id))
|
|
||||||
context = {
|
|
||||||
'opts': self.model._meta,
|
|
||||||
'object': zone,
|
|
||||||
'title': _("%s zone content") % zone.origin.name
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'admin/domains/domain/view_zone.html',
|
|
||||||
context)
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
""" Order by structured name and imporve performance """
|
""" Order by structured name and imporve performance """
|
||||||
qs = super(DomainAdmin, self).get_queryset(request)
|
qs = super(DomainAdmin, self).get_queryset(request)
|
||||||
qs = qs.select_related('top')
|
qs = qs.select_related('top')
|
||||||
# qs = qs.select_related('top')
|
|
||||||
# For some reason if we do this we know for sure that join table will be called T4
|
# For some reason if we do this we know for sure that join table will be called T4
|
||||||
__ = str(qs.query)
|
str(qs.query)
|
||||||
qs = qs.extra(
|
qs = qs.extra(
|
||||||
select={'structured_name': 'CONCAT(T4.name, domains_domain.name)'},
|
select={'structured_name': 'CONCAT(T4.name, domains_domain.name)'},
|
||||||
).order_by('structured_name')
|
).order_by('structured_name')
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import os
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
|
|
42
orchestra/apps/domains/migrations/0001_initial.py
Normal file
42
orchestra/apps/domains/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
import orchestra.core.validators
|
||||||
|
import orchestra.apps.domains.validators
|
||||||
|
import orchestra.apps.domains.utils
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0002_auto_20140909_1850'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Domain',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('name', models.CharField(unique=True, max_length=256, verbose_name='name', validators=[orchestra.core.validators.validate_hostname, orchestra.apps.domains.validators.validate_allowed_domain])),
|
||||||
|
('serial', models.IntegerField(default=orchestra.apps.domains.utils.generate_zone_serial, help_text='Serial number', verbose_name='serial')),
|
||||||
|
('account', models.ForeignKey(related_name=b'domains', verbose_name='Account', blank=True, to='accounts.Account')),
|
||||||
|
('top', models.ForeignKey(related_name=b'subdomains', to='domains.Domain', null=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
},
|
||||||
|
bases=(models.Model,),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Record',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('type', models.CharField(max_length=32, verbose_name='type', choices=[(b'MX', b'MX'), (b'NS', b'NS'), (b'CNAME', b'CNAME'), (b'A', 'A (IPv4 address)'), (b'AAAA', 'AAAA (IPv6 address)'), (b'SRV', b'SRV'), (b'TXT', b'TXT'), (b'SOA', b'SOA')])),
|
||||||
|
('value', models.CharField(max_length=256, verbose_name='value')),
|
||||||
|
('domain', models.ForeignKey(related_name=b'records', verbose_name='domain', to='domains.Domain')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
},
|
||||||
|
bases=(models.Model,),
|
||||||
|
),
|
||||||
|
]
|
21
orchestra/apps/domains/migrations/0002_record_ttl.py
Normal file
21
orchestra/apps/domains/migrations/0002_record_ttl.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
import orchestra.apps.domains.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('domains', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='record',
|
||||||
|
name='ttl',
|
||||||
|
field=models.CharField(default='', validators=[orchestra.apps.domains.validators.validate_zone_interval], max_length=8, blank=True, help_text='Record TTL, defaults to 1h', verbose_name='TTL'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
0
orchestra/apps/domains/migrations/__init__.py
Normal file
0
orchestra/apps/domains/migrations/__init__.py
Normal file
|
@ -1,11 +1,11 @@
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.core import services
|
from orchestra.core import services
|
||||||
from orchestra.core.validators import (validate_ipv4_address, validate_ipv6_address,
|
from orchestra.core.validators import (validate_ipv4_address, validate_ipv6_address,
|
||||||
validate_hostname, validate_ascii)
|
validate_hostname, validate_ascii)
|
||||||
|
from orchestra.utils.python import AttrDict
|
||||||
|
|
||||||
from . import settings, validators, utils
|
from . import settings, validators, utils
|
||||||
|
|
||||||
|
@ -69,13 +69,17 @@ class Domain(models.Model):
|
||||||
# Update serial and insert at 0
|
# Update serial and insert at 0
|
||||||
value = record.value.split()
|
value = record.value.split()
|
||||||
value[2] = str(self.serial)
|
value[2] = str(self.serial)
|
||||||
records.insert(0, (record.SOA, ' '.join(value)))
|
records.insert(0,
|
||||||
|
AttrDict(type=record.SOA, ttl=record.get_ttl(), value=' '.join(value))
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
records.append((record.type, record.value))
|
records.append(
|
||||||
|
AttrDict(type=record.type, ttl=record.get_ttl(), value=record.value)
|
||||||
|
)
|
||||||
if not self.top:
|
if not self.top:
|
||||||
if Record.NS not in types:
|
if Record.NS not in types:
|
||||||
for ns in settings.DOMAINS_DEFAULT_NS:
|
for ns in settings.DOMAINS_DEFAULT_NS:
|
||||||
records.append((Record.NS, ns))
|
records.append(AttrDict(type=Record.NS, value=ns))
|
||||||
if Record.SOA not in types:
|
if Record.SOA not in types:
|
||||||
soa = [
|
soa = [
|
||||||
"%s." % settings.DOMAINS_DEFAULT_NAME_SERVER,
|
"%s." % settings.DOMAINS_DEFAULT_NAME_SERVER,
|
||||||
|
@ -86,18 +90,28 @@ class Domain(models.Model):
|
||||||
settings.DOMAINS_DEFAULT_EXPIRATION,
|
settings.DOMAINS_DEFAULT_EXPIRATION,
|
||||||
settings.DOMAINS_DEFAULT_MIN_CACHING_TIME
|
settings.DOMAINS_DEFAULT_MIN_CACHING_TIME
|
||||||
]
|
]
|
||||||
records.insert(0, (Record.SOA, ' '.join(soa)))
|
records.insert(0, AttrDict(type=Record.SOA, value=' '.join(soa)))
|
||||||
no_cname = Record.CNAME not in types
|
no_cname = Record.CNAME not in types
|
||||||
if Record.MX not in types and no_cname:
|
if Record.MX not in types and no_cname:
|
||||||
for mx in settings.DOMAINS_DEFAULT_MX:
|
for mx in settings.DOMAINS_DEFAULT_MX:
|
||||||
records.append((Record.MX, mx))
|
records.append(AttrDict(type=Record.MX, value=mx))
|
||||||
if (Record.A not in types and Record.AAAA not in types) and no_cname:
|
if (Record.A not in types and Record.AAAA not in types) and no_cname:
|
||||||
records.append((Record.A, settings.DOMAINS_DEFAULT_A))
|
records.append(AttrDict(type=Record.A, value=settings.DOMAINS_DEFAULT_A))
|
||||||
result = ''
|
result = ''
|
||||||
for type, value in records:
|
for record in records:
|
||||||
name = '%s.%s' % (self.name, ' '*(37-len(self.name)))
|
name = '{name}.{spaces}'.format(
|
||||||
type = '%s %s' % (type, ' '*(7-len(type)))
|
name=self.name, spaces=' ' * (37-len(self.name))
|
||||||
result += '%s IN %s %s\n' % (name, type, value)
|
)
|
||||||
|
ttl = record.get('ttl', settings.DOMAINS_DEFAULT_TTL)
|
||||||
|
ttl = '{spaces}{ttl}'.format(
|
||||||
|
spaces=' ' * (7-len(ttl)), ttl=ttl
|
||||||
|
)
|
||||||
|
type = '{type} {spaces}'.format(
|
||||||
|
type=record.type, spaces=' ' * (7-len(record.type))
|
||||||
|
)
|
||||||
|
result += '{name} {ttl} IN {type} {value}\n'.format(
|
||||||
|
name=name, ttl=ttl, type=type, value=record.value
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
@ -150,13 +164,15 @@ class Record(models.Model):
|
||||||
(SOA, "SOA"),
|
(SOA, "SOA"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO TTL
|
|
||||||
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records')
|
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records')
|
||||||
type = models.CharField(max_length=32, choices=TYPE_CHOICES)
|
ttl = models.CharField(_("TTL"), max_length=8, blank=True,
|
||||||
value = models.CharField(max_length=256)
|
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
|
||||||
|
validators=[validators.validate_zone_interval])
|
||||||
|
type = models.CharField(_("type"), max_length=32, choices=TYPE_CHOICES)
|
||||||
|
value = models.CharField(_("value"), max_length=256)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return "%s IN %s %s" % (self.domain, self.type, self.value)
|
return "%s %s IN %s %s" % (self.domain, self.get_ttl(), self.type, self.value)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
""" validates record value based on its type """
|
""" validates record value based on its type """
|
||||||
|
@ -172,6 +188,8 @@ class Record(models.Model):
|
||||||
self.SOA: validators.validate_soa_record,
|
self.SOA: validators.validate_soa_record,
|
||||||
}
|
}
|
||||||
mapp[self.type](self.value)
|
mapp[self.type](self.value)
|
||||||
|
|
||||||
|
def get_ttl(self):
|
||||||
|
return self.ttl or settings.DOMAINS_DEFAULT_TTL
|
||||||
|
|
||||||
services.register(Domain)
|
services.register(Domain)
|
||||||
|
|
|
@ -124,7 +124,7 @@ class DomainTestMixin(object):
|
||||||
self.assertNotEqual(hostmaster, soa[5])
|
self.assertNotEqual(hostmaster, soa[5])
|
||||||
|
|
||||||
def validate_update(self, server_addr, domain_name):
|
def validate_update(self, server_addr, domain_name):
|
||||||
domain = Domain.objects.get(name=domain_name)
|
Domain.objects.get(name=domain_name)
|
||||||
context = {
|
context = {
|
||||||
'domain_name': domain_name,
|
'domain_name': domain_name,
|
||||||
'server_addr': server_addr
|
'server_addr': server_addr
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from django.db import IntegrityError, transaction
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from ..models import Domain
|
from ..models import Domain
|
||||||
|
|
|
@ -56,7 +56,7 @@ class MessageReadOnlyInline(admin.TabularInline):
|
||||||
def content_html(self, msg):
|
def content_html(self, msg):
|
||||||
context = {
|
context = {
|
||||||
'number': msg.number,
|
'number': msg.number,
|
||||||
'time': admin_date('created_on')(msg),
|
'time': admin_date('created_at')(msg),
|
||||||
'author': admin_link('author')(msg) if msg.author else msg.author_name,
|
'author': admin_link('author')(msg) if msg.author else msg.author_name,
|
||||||
}
|
}
|
||||||
summary = _("#%(number)i Updated by %(author)s about %(time)s") % context
|
summary = _("#%(number)i Updated by %(author)s about %(time)s") % context
|
||||||
|
@ -98,11 +98,11 @@ class MessageInline(admin.TabularInline):
|
||||||
class TicketInline(admin.TabularInline):
|
class TicketInline(admin.TabularInline):
|
||||||
fields = [
|
fields = [
|
||||||
'ticket_id', 'subject', 'creator_link', 'owner_link', 'colored_state',
|
'ticket_id', 'subject', 'creator_link', 'owner_link', 'colored_state',
|
||||||
'colored_priority', 'created', 'last_modified'
|
'colored_priority', 'created', 'updated'
|
||||||
]
|
]
|
||||||
readonly_fields = [
|
readonly_fields = [
|
||||||
'ticket_id', 'subject', 'creator_link', 'owner_link', 'colored_state',
|
'ticket_id', 'subject', 'creator_link', 'owner_link', 'colored_state',
|
||||||
'colored_priority', 'created', 'last_modified'
|
'colored_priority', 'created', 'updated'
|
||||||
]
|
]
|
||||||
model = Ticket
|
model = Ticket
|
||||||
extra = 0
|
extra = 0
|
||||||
|
@ -110,8 +110,8 @@ class TicketInline(admin.TabularInline):
|
||||||
|
|
||||||
creator_link = admin_link('creator')
|
creator_link = admin_link('creator')
|
||||||
owner_link = admin_link('owner')
|
owner_link = admin_link('owner')
|
||||||
created = admin_link('created_on')
|
created = admin_link('created_at')
|
||||||
last_modified = admin_link('last_modified_on')
|
updated = admin_link('updated_at')
|
||||||
colored_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
colored_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
||||||
colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
||||||
|
|
||||||
|
@ -121,10 +121,10 @@ class TicketInline(admin.TabularInline):
|
||||||
ticket_id.allow_tags = True
|
ticket_id.allow_tags = True
|
||||||
|
|
||||||
|
|
||||||
class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeViewActions,
|
class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin):
|
||||||
list_display = [
|
list_display = [
|
||||||
'unbold_id', 'bold_subject', 'display_creator', 'display_owner',
|
'unbold_id', 'bold_subject', 'display_creator', 'display_owner',
|
||||||
'display_queue', 'display_priority', 'display_state', 'last_modified'
|
'display_queue', 'display_priority', 'display_state', 'updated'
|
||||||
]
|
]
|
||||||
list_display_links = ('unbold_id', 'bold_subject')
|
list_display_links = ('unbold_id', 'bold_subject')
|
||||||
list_filter = [
|
list_filter = [
|
||||||
|
@ -134,7 +134,7 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView
|
||||||
('my_tickets', lambda r: 'True' if not r.user.is_superuser else 'False'),
|
('my_tickets', lambda r: 'True' if not r.user.is_superuser else 'False'),
|
||||||
('state', 'OPEN')
|
('state', 'OPEN')
|
||||||
)
|
)
|
||||||
date_hierarchy = 'created_on'
|
date_hierarchy = 'created_at'
|
||||||
search_fields = [
|
search_fields = [
|
||||||
'id', 'subject', 'creator__username', 'creator__email', 'queue__name',
|
'id', 'subject', 'creator__username', 'creator__email', 'queue__name',
|
||||||
'owner__username'
|
'owner__username'
|
||||||
|
@ -192,20 +192,20 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView
|
||||||
display_creator = admin_link('creator')
|
display_creator = admin_link('creator')
|
||||||
display_queue = admin_link('queue')
|
display_queue = admin_link('queue')
|
||||||
display_owner = admin_link('owner')
|
display_owner = admin_link('owner')
|
||||||
last_modified = admin_date('last_modified_on')
|
updated = admin_date('updated')
|
||||||
display_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
display_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
||||||
display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
||||||
|
|
||||||
def display_summary(self, ticket):
|
def display_summary(self, ticket):
|
||||||
context = {
|
context = {
|
||||||
'creator': admin_link('creator')(self, ticket) if ticket.creator else ticket.creator_name,
|
'creator': admin_link('creator')(self, ticket) if ticket.creator else ticket.creator_name,
|
||||||
'created': admin_date('created_on')(ticket),
|
'created': admin_date('created_at')(ticket),
|
||||||
'updated': '',
|
'updated': '',
|
||||||
}
|
}
|
||||||
msg = ticket.messages.last()
|
msg = ticket.messages.last()
|
||||||
if msg:
|
if msg:
|
||||||
context.update({
|
context.update({
|
||||||
'updated': admin_date('created_on')(msg),
|
'updated': admin_date('created_at')(msg),
|
||||||
'updater': admin_link('author')(self, msg) if msg.author else msg.author_name,
|
'updater': admin_link('author')(self, msg) if msg.author else msg.author_name,
|
||||||
})
|
})
|
||||||
context['updated'] = '. Updated by %(updater)s about %(updated)s' % context
|
context['updated'] = '. Updated by %(updater)s about %(updated)s' % context
|
||||||
|
@ -283,7 +283,7 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView
|
||||||
def message_preview_view(self, request):
|
def message_preview_view(self, request):
|
||||||
""" markdown preview render via ajax """
|
""" markdown preview render via ajax """
|
||||||
data = request.POST.get("data")
|
data = request.POST.get("data")
|
||||||
data_formated = markdowt_tn(strip_tags(data))
|
data_formated = markdown(strip_tags(data))
|
||||||
return HttpResponse(data_formated)
|
return HttpResponse(data_formated)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
|
|
||||||
from orchestra.admin.utils import change_url
|
|
||||||
from orchestra.apps.users.models import User
|
from orchestra.apps.users.models import User
|
||||||
from orchestra.forms.widgets import ReadOnlyWidget
|
from orchestra.forms.widgets import ReadOnlyWidget
|
||||||
|
|
||||||
|
@ -42,7 +40,6 @@ class MessageInlineForm(forms.ModelForm):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(MessageInlineForm, self).__init__(*args, **kwargs)
|
super(MessageInlineForm, self).__init__(*args, **kwargs)
|
||||||
admin_link = change_url(self.user)
|
|
||||||
self.fields['created_on'].widget = ReadOnlyWidget('')
|
self.fields['created_on'].widget = ReadOnlyWidget('')
|
||||||
|
|
||||||
def clean_content(self):
|
def clean_content(self):
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from django.conf import settings as djsettings
|
from django.conf import settings as djsettings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.apps.contacts import settings as contacts_settings
|
from orchestra.apps.contacts import settings as contacts_settings
|
||||||
|
@ -68,13 +67,13 @@ class Ticket(models.Model):
|
||||||
priority = models.CharField(_("priority"), max_length=32, choices=PRIORITIES,
|
priority = models.CharField(_("priority"), max_length=32, choices=PRIORITIES,
|
||||||
default=MEDIUM)
|
default=MEDIUM)
|
||||||
state = models.CharField(_("state"), max_length=32, choices=STATES, default=NEW)
|
state = models.CharField(_("state"), max_length=32, choices=STATES, default=NEW)
|
||||||
created_on = models.DateTimeField(_("created on"), auto_now_add=True)
|
created_at = models.DateTimeField(_("created"), auto_now_add=True)
|
||||||
last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True)
|
updated_at = models.DateTimeField(_("modified"), auto_now=True)
|
||||||
cc = models.TextField("CC", help_text=_("emails to send a carbon copy to"),
|
cc = models.TextField("CC", help_text=_("emails to send a carbon copy to"),
|
||||||
blank=True)
|
blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-last_modified_on"]
|
ordering = ['-updated_at']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return unicode(self.pk)
|
return unicode(self.pk)
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from django.template import Template, Context
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from orchestra.apps.orchestration import ServiceController
|
from orchestra.apps.orchestration import ServiceController
|
||||||
from orchestra.apps.resources import ServiceMonitor
|
from orchestra.apps.resources import ServiceMonitor
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.auth.admin import UserAdmin
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
from orchestra.admin.utils import insertattr, admin_link, change_url
|
from orchestra.admin.utils import admin_link, change_url
|
||||||
from orchestra.apps.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin
|
from orchestra.apps.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin
|
||||||
from orchestra.apps.domains.forms import DomainIterator
|
|
||||||
|
|
||||||
from .filters import HasMailboxListFilter, HasForwardListFilter, HasAddressListFilter
|
from .filters import HasMailboxListFilter, HasForwardListFilter, HasAddressListFilter
|
||||||
from .models import Mailbox, Address, Autoresponse
|
from .models import Mailbox, Address, Autoresponse
|
||||||
|
|
|
@ -7,6 +7,7 @@ from orchestra.apps.orchestration import ServiceController
|
||||||
from orchestra.apps.resources import ServiceMonitor
|
from orchestra.apps.resources import ServiceMonitor
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
|
from .models import Address
|
||||||
|
|
||||||
|
|
||||||
class MailSystemUserBackend(ServiceController):
|
class MailSystemUserBackend(ServiceController):
|
||||||
|
@ -152,7 +153,7 @@ class MaildirDisk(ServiceMonitor):
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_context(self, mailbox):
|
def get_context(self, mailbox):
|
||||||
context = MailSystemUserBackend().get_context(site)
|
context = MailSystemUserBackend().get_context(mailbox)
|
||||||
context['home'] = settings.EMAILS_HOME % context
|
context['home'] = settings.EMAILS_HOME % context
|
||||||
context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize')
|
context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize')
|
||||||
context['object_id'] = mailbox.pk
|
context['object_id'] = mailbox.pk
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
import re
|
|
||||||
|
|
||||||
from django.contrib.auth.hashers import check_password, make_password
|
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
|
@ -44,7 +44,6 @@ def validate_forward(value):
|
||||||
|
|
||||||
|
|
||||||
def validate_sieve(value):
|
def validate_sieve(value):
|
||||||
from .models import Mailbox
|
|
||||||
sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest()
|
sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest()
|
||||||
path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name)
|
path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name)
|
||||||
with open(path, 'wb') as f:
|
with open(path, 'wb') as f:
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
@ -89,18 +88,18 @@ class BackendLogAdmin(admin.ModelAdmin):
|
||||||
)
|
)
|
||||||
list_display_links = ('id', 'backend')
|
list_display_links = ('id', 'backend')
|
||||||
list_filter = ('state', 'backend')
|
list_filter = ('state', 'backend')
|
||||||
date_hierarchy = 'last_update'
|
date_hierarchy = 'updated_at'
|
||||||
inlines = [BackendOperationInline]
|
inlines = [BackendOperationInline]
|
||||||
fields = [
|
fields = [
|
||||||
'backend', 'server_link', 'state', 'mono_script', 'mono_stdout',
|
'backend', 'server_link', 'state', 'mono_script', 'mono_stdout',
|
||||||
'mono_stderr', 'mono_traceback', 'exit_code', 'task_id', 'display_created',
|
'mono_stderr', 'mono_traceback', 'exit_code', 'task_id', 'display_created',
|
||||||
'display_last_update', 'execution_time'
|
'display_updated', 'execution_time'
|
||||||
]
|
]
|
||||||
readonly_fields = fields
|
readonly_fields = fields
|
||||||
|
|
||||||
server_link = admin_link('server')
|
server_link = admin_link('server')
|
||||||
display_last_update = admin_date('last_update')
|
display_updated = admin_date('updated_at')
|
||||||
display_created = admin_date('created')
|
display_created = admin_date('created_at')
|
||||||
display_state = admin_colored('state', colors=STATE_COLORS)
|
display_state = admin_colored('state', colors=STATE_COLORS)
|
||||||
mono_script = display_mono('script')
|
mono_script = display_mono('script')
|
||||||
mono_stdout = display_mono('stdout')
|
mono_stdout = display_mono('stdout')
|
||||||
|
@ -111,6 +110,9 @@ class BackendLogAdmin(admin.ModelAdmin):
|
||||||
""" Order by structured name and imporve performance """
|
""" Order by structured name and imporve performance """
|
||||||
qs = super(BackendLogAdmin, self).get_queryset(request)
|
qs = super(BackendLogAdmin, self).get_queryset(request)
|
||||||
return qs.select_related('server').defer('script', 'stdout')
|
return qs.select_related('server').defer('script', 'stdout')
|
||||||
|
|
||||||
|
def has_add_permission(self, *args, **kwargs):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class ServerAdmin(admin.ModelAdmin):
|
class ServerAdmin(admin.ModelAdmin):
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from orchestra.utils import plugins
|
from orchestra.utils import plugins
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
from django.contrib.contenttypes import generic
|
from django.contrib.contenttypes import generic
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
@ -62,8 +60,8 @@ class BackendLog(models.Model):
|
||||||
exit_code = models.IntegerField(_("exit code"), null=True)
|
exit_code = models.IntegerField(_("exit code"), null=True)
|
||||||
task_id = models.CharField(_("task ID"), max_length=36, unique=True, null=True,
|
task_id = models.CharField(_("task ID"), max_length=36, unique=True, null=True,
|
||||||
help_text="Celery task ID when used as execution backend")
|
help_text="Celery task ID when used as execution backend")
|
||||||
created = models.DateTimeField(_("created"), auto_now_add=True)
|
created_at = models.DateTimeField(_("created"), auto_now_add=True)
|
||||||
last_update = models.DateTimeField(_("last update"), auto_now=True)
|
updated_at = models.DateTimeField(_("updated"), auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
get_latest_by = 'id'
|
get_latest_by = 'id'
|
||||||
|
@ -89,7 +87,7 @@ class BackendOperation(models.Model):
|
||||||
action = models.CharField(_("action"), max_length=64)
|
action = models.CharField(_("action"), max_length=64)
|
||||||
content_type = models.ForeignKey(ContentType)
|
content_type = models.ForeignKey(ContentType)
|
||||||
object_id = models.PositiveIntegerField()
|
object_id = models.PositiveIntegerField()
|
||||||
# TODO rename to content_object
|
|
||||||
instance = generic.GenericForeignKey('content_type', 'object_id')
|
instance = generic.GenericForeignKey('content_type', 'object_id')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
from django.db import IntegrityError, transaction
|
|
||||||
|
|
||||||
from orchestra.utils.tests import BaseTestCase
|
from orchestra.utils.tests import BaseTestCase
|
||||||
|
|
||||||
from .. import operations, backends
|
from .. import operations, backends
|
||||||
|
|
|
@ -78,7 +78,7 @@ class BillSelectedOrders(object):
|
||||||
if int(request.POST.get('step')) >= 3:
|
if int(request.POST.get('step')) >= 3:
|
||||||
bills = self.queryset.bill(commit=True, **self.options)
|
bills = self.queryset.bill(commit=True, **self.options)
|
||||||
for order in self.queryset:
|
for order in self.queryset:
|
||||||
modeladmin.log_change(request, order, 'Billed')
|
self.modeladmin.log_change(request, order, 'Billed')
|
||||||
if not bills:
|
if not bills:
|
||||||
msg = _("Selected orders do not have pending billing")
|
msg = _("Selected orders do not have pending billing")
|
||||||
self.modeladmin.message_user(request, msg, messages.WARNING)
|
self.modeladmin.message_user(request, msg, messages.WARNING)
|
||||||
|
|
|
@ -3,7 +3,6 @@ from django.utils import timezone
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ChangeListDefaultFilter
|
|
||||||
from orchestra.admin.utils import admin_link, admin_date
|
from orchestra.admin.utils import admin_link, admin_date
|
||||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||||
from orchestra.utils.humanize import naturaldate
|
from orchestra.utils.humanize import naturaldate
|
||||||
|
@ -13,7 +12,7 @@ from .filters import ActiveOrderListFilter, BilledOrderListFilter
|
||||||
from .models import Order, MetricStorage
|
from .models import Order, MetricStorage
|
||||||
|
|
||||||
|
|
||||||
class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin):
|
class OrderAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
'id', 'service', 'account_link', 'content_object_link',
|
'id', 'service', 'account_link', 'content_object_link',
|
||||||
'display_registered_on', 'display_billed_until', 'display_cancelled_on'
|
'display_registered_on', 'display_billed_until', 'display_cancelled_on'
|
||||||
|
@ -22,9 +21,6 @@ class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin):
|
||||||
list_filter = (ActiveOrderListFilter, BilledOrderListFilter, 'service',)
|
list_filter = (ActiveOrderListFilter, BilledOrderListFilter, 'service',)
|
||||||
actions = (BillSelectedOrders(),)
|
actions = (BillSelectedOrders(),)
|
||||||
date_hierarchy = 'registered_on'
|
date_hierarchy = 'registered_on'
|
||||||
default_changelist_filters = (
|
|
||||||
('is_active', 'True'),
|
|
||||||
)
|
|
||||||
|
|
||||||
content_object_link = admin_link('content_object', order=False)
|
content_object_link = admin_link('content_object', order=False)
|
||||||
display_registered_on = admin_date('registered_on')
|
display_registered_on = admin_date('registered_on')
|
||||||
|
|
|
@ -2,7 +2,7 @@ import datetime
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.apps.bills.models import Invoice, Fee, ProForma, BillLine, BillSubline
|
from orchestra.apps.bills.models import Invoice, Fee, ProForma
|
||||||
|
|
||||||
|
|
||||||
class BillsBackend(object):
|
class BillsBackend(object):
|
||||||
|
@ -39,6 +39,10 @@ class BillsBackend(object):
|
||||||
subtotal=line.subtotal,
|
subtotal=line.subtotal,
|
||||||
tax=service.tax,
|
tax=service.tax,
|
||||||
description=self.get_line_description(line),
|
description=self.get_line_description(line),
|
||||||
|
|
||||||
|
order=line.order,
|
||||||
|
order_billed_on=line.order.old_billed_on,
|
||||||
|
order_billed_until=line.order.old_billed_until
|
||||||
)
|
)
|
||||||
self.create_sublines(billine, line.discounts)
|
self.create_sublines(billine, line.discounts)
|
||||||
return bills
|
return bills
|
||||||
|
@ -46,7 +50,6 @@ class BillsBackend(object):
|
||||||
def format_period(self, ini, end):
|
def format_period(self, ini, end):
|
||||||
ini = ini.strftime("%b, %Y")
|
ini = ini.strftime("%b, %Y")
|
||||||
end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y")
|
end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y")
|
||||||
# TODO if diff is less than a month: write the month only
|
|
||||||
if ini == end:
|
if ini == end:
|
||||||
return ini
|
return ini
|
||||||
return _("{ini} to {end}").format(ini=ini, end=end)
|
return _("{ini} to {end}").format(ini=ini, end=end)
|
||||||
|
@ -67,6 +70,7 @@ class BillsBackend(object):
|
||||||
def create_sublines(self, line, discounts):
|
def create_sublines(self, line, discounts):
|
||||||
for discount in discounts:
|
for discount in discounts:
|
||||||
line.sublines.create(
|
line.sublines.create(
|
||||||
description=_("Discount per %s") % discount.type,
|
description=_("Discount per %s") % discount.type.lower(),
|
||||||
total=discount.total,
|
total=discount.total,
|
||||||
|
type=discount.type,
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,7 +13,6 @@ class ActiveOrderListFilter(SimpleListFilter):
|
||||||
return (
|
return (
|
||||||
('True', _("Active")),
|
('True', _("Active")),
|
||||||
('False', _("Inactive")),
|
('False', _("Inactive")),
|
||||||
('None', _("All")),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
|
@ -23,12 +22,6 @@ class ActiveOrderListFilter(SimpleListFilter):
|
||||||
return queryset.inactive()
|
return queryset.inactive()
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def choices(self, cl):
|
|
||||||
""" Remove default All """
|
|
||||||
choices = iter(super(ActiveOrderListFilter, self).choices(cl))
|
|
||||||
choices.next()
|
|
||||||
return choices
|
|
||||||
|
|
||||||
|
|
||||||
class BilledOrderListFilter(SimpleListFilter):
|
class BilledOrderListFilter(SimpleListFilter):
|
||||||
""" Filter tickets by created_by according to request.user """
|
""" Filter tickets by created_by according to request.user """
|
||||||
|
|
|
@ -1,28 +1,24 @@
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import logging
|
import logging
|
||||||
import sys
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.migrations.recorder import MigrationRecorder
|
from django.db.migrations.recorder import MigrationRecorder
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.db.models.loading import get_model
|
from django.db.models.loading import get_model
|
||||||
from django.db.models.signals import pre_delete, post_delete, post_save
|
from django.db.models.signals import post_delete, post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.contrib.admin.models import LogEntry
|
from django.contrib.admin.models import LogEntry
|
||||||
from django.contrib.contenttypes import generic
|
from django.contrib.contenttypes import generic
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.core import caches, services, accounts
|
from orchestra.core import accounts
|
||||||
from orchestra.models import queryset
|
from orchestra.models import queryset
|
||||||
from orchestra.utils.apps import autodiscover
|
|
||||||
from orchestra.utils.python import import_class
|
from orchestra.utils.python import import_class
|
||||||
|
|
||||||
from . import helpers, settings
|
from . import helpers, settings
|
||||||
from .handlers import ServiceHandler
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -39,8 +35,13 @@ class OrderQuerySet(models.QuerySet):
|
||||||
for account, services in qs.group_by('account', 'service').iteritems():
|
for account, services in qs.group_by('account', 'service').iteritems():
|
||||||
bill_lines = []
|
bill_lines = []
|
||||||
for service, orders in services.iteritems():
|
for service, orders in services.iteritems():
|
||||||
|
for order in orders:
|
||||||
|
# Saved for undoing support
|
||||||
|
order.old_billed_on = order.billed_on
|
||||||
|
order.old_billed_until = order.billed_until
|
||||||
lines = service.handler.generate_bill_lines(orders, account, **options)
|
lines = service.handler.generate_bill_lines(orders, account, **options)
|
||||||
bill_lines.extend(lines)
|
bill_lines.extend(lines)
|
||||||
|
# TODO make this consistent always returning the same fucking objects
|
||||||
if commit:
|
if commit:
|
||||||
bills += bill_backend.create_bills(account, bill_lines, **options)
|
bills += bill_backend.create_bills(account, bill_lines, **options)
|
||||||
else:
|
else:
|
||||||
|
@ -73,7 +74,7 @@ class OrderQuerySet(models.QuerySet):
|
||||||
|
|
||||||
def inactive(self, **kwargs):
|
def inactive(self, **kwargs):
|
||||||
""" return inactive orders """
|
""" return inactive orders """
|
||||||
return self.filter(cancelled_on__lt=timezone.now(), **kwargs)
|
return self.filter(cancelled_on__lte=timezone.now(), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Order(models.Model):
|
class Order(models.Model):
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
ORDERS_BILLING_BACKEND = getattr(settings, 'ORDERS_BILLING_BACKEND',
|
ORDERS_BILLING_BACKEND = getattr(settings, 'ORDERS_BILLING_BACKEND',
|
||||||
|
|
|
@ -26,8 +26,8 @@ def process_transactions(modeladmin, request, queryset):
|
||||||
method = PaymentMethod.get_plugin(method)
|
method = PaymentMethod.get_plugin(method)
|
||||||
procs = method.process(transactions)
|
procs = method.process(transactions)
|
||||||
processes += procs
|
processes += procs
|
||||||
for transaction in transactions:
|
for trans in transactions:
|
||||||
modeladmin.log_change(request, transaction, 'Processed')
|
modeladmin.log_change(request, trans, 'Processed')
|
||||||
if not processes:
|
if not processes:
|
||||||
return
|
return
|
||||||
opts = modeladmin.model._meta
|
opts = modeladmin.model._meta
|
||||||
|
@ -44,9 +44,9 @@ def process_transactions(modeladmin, request, queryset):
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@action_with_confirmation()
|
@action_with_confirmation()
|
||||||
def mark_as_executed(modeladmin, request, queryset, extra_context={}):
|
def mark_as_executed(modeladmin, request, queryset, extra_context={}):
|
||||||
for transaction in queryset:
|
for trans in queryset:
|
||||||
transaction.mark_as_executed()
|
trans.mark_as_executed()
|
||||||
modeladmin.log_change(request, transaction, 'Executed')
|
modeladmin.log_change(request, trans, 'Executed')
|
||||||
msg = _("%s selected transactions have been marked as executed.") % queryset.count()
|
msg = _("%s selected transactions have been marked as executed.") % queryset.count()
|
||||||
modeladmin.message_user(request, msg)
|
modeladmin.message_user(request, msg)
|
||||||
mark_as_executed.url_name = 'execute'
|
mark_as_executed.url_name = 'execute'
|
||||||
|
@ -56,9 +56,9 @@ mark_as_executed.verbose_name = _("Mark as executed")
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@action_with_confirmation()
|
@action_with_confirmation()
|
||||||
def mark_as_secured(modeladmin, request, queryset):
|
def mark_as_secured(modeladmin, request, queryset):
|
||||||
for transaction in queryset:
|
for trans in queryset:
|
||||||
transaction.mark_as_secured()
|
trans.mark_as_secured()
|
||||||
modeladmin.log_change(request, transaction, 'Secured')
|
modeladmin.log_change(request, trans, 'Secured')
|
||||||
msg = _("%s selected transactions have been marked as secured.") % queryset.count()
|
msg = _("%s selected transactions have been marked as secured.") % queryset.count()
|
||||||
modeladmin.message_user(request, msg)
|
modeladmin.message_user(request, msg)
|
||||||
mark_as_secured.url_name = 'secure'
|
mark_as_secured.url_name = 'secure'
|
||||||
|
@ -68,9 +68,9 @@ mark_as_secured.verbose_name = _("Mark as secured")
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@action_with_confirmation()
|
@action_with_confirmation()
|
||||||
def mark_as_rejected(modeladmin, request, queryset):
|
def mark_as_rejected(modeladmin, request, queryset):
|
||||||
for transaction in queryset:
|
for trans in queryset:
|
||||||
transaction.mark_as_rejected()
|
trans.mark_as_rejected()
|
||||||
modeladmin.log_change(request, transaction, 'Rejected')
|
modeladmin.log_change(request, trans, 'Rejected')
|
||||||
msg = _("%s selected transactions have been marked as rejected.") % queryset.count()
|
msg = _("%s selected transactions have been marked as rejected.") % queryset.count()
|
||||||
modeladmin.message_user(request, msg)
|
modeladmin.message_user(request, msg)
|
||||||
mark_as_rejected.url_name = 'reject'
|
mark_as_rejected.url_name = 'reject'
|
||||||
|
@ -90,7 +90,7 @@ def _format_display_objects(modeladmin, request, queryset, related):
|
||||||
for related in getattr(obj.transactions, attr)():
|
for related in getattr(obj.transactions, attr)():
|
||||||
subobjects.append(
|
subobjects.append(
|
||||||
mark_safe('{0}: <a href="{1}">{2}</a> will be marked as {3}'.format(
|
mark_safe('{0}: <a href="{1}">{2}</a> will be marked as {3}'.format(
|
||||||
capfirst(subobj.get_type().lower()), change_url(subobj), subobj, verb))
|
capfirst(related.get_type().lower()), change_url(related), related, verb))
|
||||||
)
|
)
|
||||||
objects.append(subobjects)
|
objects.append(subobjects)
|
||||||
return {'display_objects': objects}
|
return {'display_objects': objects}
|
||||||
|
@ -127,9 +127,9 @@ abort.verbose_name = _("Abort")
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@action_with_confirmation(extra_context=_format_commit)
|
@action_with_confirmation(extra_context=_format_commit)
|
||||||
def commit(modeladmin, request, queryset):
|
def commit(modeladmin, request, queryset):
|
||||||
for transaction in queryset:
|
for trans in queryset:
|
||||||
transaction.mark_as_rejected()
|
trans.mark_as_rejected()
|
||||||
modeladmin.log_change(request, transaction, 'Rejected')
|
modeladmin.log_change(request, trans, 'Rejected')
|
||||||
msg = _("%s selected transactions have been marked as rejected.") % queryset.count()
|
msg = _("%s selected transactions have been marked as rejected.") % queryset.count()
|
||||||
modeladmin.message_user(request, msg)
|
modeladmin.message_user(request, msg)
|
||||||
commit.url_name = 'commit'
|
commit.url_name = 'commit'
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from django import forms
|
|
||||||
from django.conf.urls import patterns, url
|
from django.conf.urls import patterns, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
@ -37,7 +36,6 @@ class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
""" Hooks select account url """
|
""" Hooks select account url """
|
||||||
urls = super(PaymentSourceAdmin, self).get_urls()
|
urls = super(PaymentSourceAdmin, self).get_urls()
|
||||||
admin_site = self.admin_site
|
|
||||||
opts = self.model._meta
|
opts = self.model._meta
|
||||||
info = opts.app_label, opts.model_name
|
info = opts.app_label, opts.model_name
|
||||||
select_urls = patterns("",
|
select_urls = patterns("",
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from jsonfield import JSONField
|
from jsonfield import JSONField
|
||||||
|
@ -99,8 +98,8 @@ class Transaction(models.Model):
|
||||||
default=WAITTING_PROCESSING)
|
default=WAITTING_PROCESSING)
|
||||||
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
|
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
|
||||||
currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY)
|
currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY)
|
||||||
created_on = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(_("created"), auto_now_add=True)
|
||||||
modified_on = models.DateTimeField(auto_now=True)
|
modified_at = models.DateTimeField(_("modified"), auto_now=True)
|
||||||
|
|
||||||
objects = TransactionQuerySet.as_manager()
|
objects = TransactionQuerySet.as_manager()
|
||||||
|
|
||||||
|
|
|
@ -15,16 +15,16 @@ from .models import Resource, ResourceData, MonitorData
|
||||||
|
|
||||||
class ResourceAdmin(ExtendedModelAdmin):
|
class ResourceAdmin(ExtendedModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
'id', 'verbose_name', 'content_type', 'period', 'ondemand',
|
'id', 'verbose_name', 'content_type', 'period', 'on_demand',
|
||||||
'default_allocation', 'unit', 'disable_trigger', 'crontab',
|
'default_allocation', 'unit', 'disable_trigger', 'crontab',
|
||||||
)
|
)
|
||||||
list_filter = (UsedContentTypeFilter, 'period', 'ondemand', 'disable_trigger')
|
list_filter = (UsedContentTypeFilter, 'period', 'on_demand', 'disable_trigger')
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
'fields': ('name', 'content_type', 'period'),
|
'fields': ('name', 'content_type', 'period'),
|
||||||
}),
|
}),
|
||||||
(_("Configuration"), {
|
(_("Configuration"), {
|
||||||
'fields': ('verbose_name', 'unit', 'scale', 'ondemand',
|
'fields': ('verbose_name', 'unit', 'scale', 'on_demand',
|
||||||
'default_allocation', 'disable_trigger', 'is_active'),
|
'default_allocation', 'disable_trigger', 'is_active'),
|
||||||
}),
|
}),
|
||||||
(_("Monitoring"), {
|
(_("Monitoring"), {
|
||||||
|
@ -64,7 +64,7 @@ class ResourceAdmin(ExtendedModelAdmin):
|
||||||
|
|
||||||
class ResourceDataAdmin(admin.ModelAdmin):
|
class ResourceDataAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
'id', 'resource', 'used', 'allocated', 'last_update', 'content_object_link'
|
'id', 'resource', 'used', 'allocated', 'updated_at', 'content_object_link'
|
||||||
)
|
)
|
||||||
list_filter = ('resource',)
|
list_filter = ('resource',)
|
||||||
readonly_fields = ('content_object_link',)
|
readonly_fields = ('content_object_link',)
|
||||||
|
@ -77,7 +77,7 @@ class ResourceDataAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
|
||||||
class MonitorDataAdmin(admin.ModelAdmin):
|
class MonitorDataAdmin(admin.ModelAdmin):
|
||||||
list_display = ('id', 'monitor', 'date', 'value', 'content_object_link')
|
list_display = ('id', 'monitor', 'created_at', 'value', 'content_object_link')
|
||||||
list_filter = ('monitor',)
|
list_filter = ('monitor',)
|
||||||
readonly_fields = ('content_object_link',)
|
readonly_fields = ('content_object_link',)
|
||||||
|
|
||||||
|
@ -118,16 +118,16 @@ def resource_inline_factory(resources):
|
||||||
formset = ResourceInlineFormSet
|
formset = ResourceInlineFormSet
|
||||||
can_delete = False
|
can_delete = False
|
||||||
fields = (
|
fields = (
|
||||||
'verbose_name', 'used', 'display_last_update', 'allocated', 'unit'
|
'verbose_name', 'used', 'display_updated', 'allocated', 'unit'
|
||||||
)
|
)
|
||||||
readonly_fields = ('used', 'display_last_update')
|
readonly_fields = ('used', 'display_updated')
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
'all': ('orchestra/css/hide-inline-id.css',)
|
'all': ('orchestra/css/hide-inline-id.css',)
|
||||||
}
|
}
|
||||||
|
|
||||||
display_last_update = admin_date('last_update', default=_("Never"))
|
display_updated = admin_date('updated_at', default=_("Never"))
|
||||||
|
|
||||||
def has_add_permission(self, *args, **kwargs):
|
def has_add_permission(self, *args, **kwargs):
|
||||||
""" Hidde add another """
|
""" Hidde add another """
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.contrib.contenttypes import generic
|
|
||||||
|
|
||||||
from orchestra.utils import running_syncdb
|
from orchestra.utils import running_syncdb
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.html import escape
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from djcelery.humanize import naturaldate
|
|
||||||
|
|
||||||
from orchestra.forms.widgets import ShowTextWidget, ReadOnlyWidget
|
from orchestra.forms.widgets import ShowTextWidget, ReadOnlyWidget
|
||||||
|
|
||||||
|
@ -21,7 +19,7 @@ class ResourceForm(forms.ModelForm):
|
||||||
if self.resource:
|
if self.resource:
|
||||||
self.fields['verbose_name'].initial = self.resource.verbose_name
|
self.fields['verbose_name'].initial = self.resource.verbose_name
|
||||||
self.fields['unit'].initial = self.resource.unit
|
self.fields['unit'].initial = self.resource.unit
|
||||||
if self.resource.ondemand:
|
if self.resource.on_demand:
|
||||||
self.fields['allocated'].required = False
|
self.fields['allocated'].required = False
|
||||||
self.fields['allocated'].widget = ReadOnlyWidget(None, '')
|
self.fields['allocated'].widget = ReadOnlyWidget(None, '')
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -38,13 +38,15 @@ def compute_resource_usage(data):
|
||||||
continue
|
continue
|
||||||
has_result = True
|
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()
|
total = (last.created_at-epoch).total_seconds()
|
||||||
dataset = dataset.filter(date__year=today.year, date__month=today.month)
|
dataset = dataset.filter(created_at__year=today.year, created_at__month=today.month)
|
||||||
|
ini = epoch
|
||||||
for data in dataset:
|
for data in dataset:
|
||||||
slot = (previous-data.date).total_seconds()
|
slot = (data.created_at-ini).total_seconds()
|
||||||
result += data.value * slot/total
|
result += data.value * slot/total
|
||||||
|
ini = data.created_at
|
||||||
elif resource.period == resource.MONTHLY_SUM:
|
elif resource.period == resource.MONTHLY_SUM:
|
||||||
dataset = dataset.filter(date__year=today.year, date__month=today.month)
|
dataset = dataset.filter(created_at__year=today.year, created_at__month=today.month)
|
||||||
# FIXME Aggregation of 0s returns None! django bug?
|
# FIXME Aggregation of 0s returns None! django bug?
|
||||||
# value = dataset.aggregate(models.Sum('value'))['value__sum']
|
# value = dataset.aggregate(models.Sum('value'))['value__sum']
|
||||||
values = dataset.values_list('value', flat=True)
|
values = dataset.values_list('value', flat=True)
|
||||||
|
|
78
orchestra/apps/resources/migrations/0001_initial.py
Normal file
78
orchestra/apps/resources/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
import orchestra.models.fields
|
||||||
|
import django.core.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('djcelery', '__first__'),
|
||||||
|
('contenttypes', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MonitorData',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('monitor', models.CharField(max_length=256, verbose_name='monitor', choices=[(b'Apache2Backend', 'Apache 2'), (b'Apache2Traffic', 'Apache 2 Traffic'), (b'AutoresponseBackend', 'Mail autoresponse'), (b'AwstatsBackend', 'Awstats'), (b'Bind9MasterDomainBackend', 'Bind9 master domain'), (b'Bind9SlaveDomainBackend', 'Bind9 slave domain'), (b'DokuWikiMuBackend', 'DokuWiki multisite'), (b'DrupalMuBackend', 'Drupal multisite'), (b'FTPTraffic', 'FTP traffic'), (b'MailSystemUserBackend', 'Mail system user'), (b'MaildirDisk', 'Maildir disk usage'), (b'MailmanBackend', b'Mailman'), (b'MailmanTraffic', b'MailmanTraffic'), (b'MySQLDBBackend', b'MySQL database'), (b'MySQLPermissionBackend', b'MySQL permission'), (b'MySQLUserBackend', b'MySQL user'), (b'MysqlDisk', 'MySQL disk'), (b'OpenVZTraffic', b'OpenVZTraffic'), (b'PHPFPMBackend', 'PHP-FPM'), (b'PHPFcgidBackend', 'PHP-Fcgid'), (b'PostfixAddressBackend', 'Postfix address'), (b'ServiceController', b'ServiceController'), (b'ServiceMonitor', b'ServiceMonitor'), (b'StaticBackend', 'Static'), (b'SystemUserBackend', 'System User'), (b'SystemUserDisk', 'System user disk'), (b'WebalizerBackend', 'Webalizer'), (b'WordpressMuBackend', 'Wordpress multisite')])),
|
||||||
|
('object_id', models.PositiveIntegerField()),
|
||||||
|
('date', models.DateTimeField(auto_now_add=True, verbose_name='date')),
|
||||||
|
('value', models.DecimalField(verbose_name='value', max_digits=16, decimal_places=2)),
|
||||||
|
('content_type', models.ForeignKey(to='contenttypes.ContentType')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'get_latest_by': 'id',
|
||||||
|
'verbose_name_plural': 'monitor data',
|
||||||
|
},
|
||||||
|
bases=(models.Model,),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Resource',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('name', models.CharField(help_text='Required. 32 characters or fewer. Lowercase letters, digits and hyphen only.', max_length=32, verbose_name='name', validators=[django.core.validators.RegexValidator(b'^[a-z0-9_\\-]+$', 'Enter a valid name.', b'invalid')])),
|
||||||
|
('verbose_name', models.CharField(max_length=256, verbose_name='verbose name')),
|
||||||
|
('period', models.CharField(default=b'LAST', help_text='Operation used for aggregating this resource monitoreddata.', max_length=16, verbose_name='period', choices=[(b'LAST', 'Last'), (b'MONTHLY_SUM', 'Monthly Sum'), (b'MONTHLY_AVG', 'Monthly Average')])),
|
||||||
|
('ondemand', models.BooleanField(default=False, help_text='If enabled the resource will not be pre-allocated, but allocated under the application demand', verbose_name='on demand')),
|
||||||
|
('default_allocation', models.PositiveIntegerField(help_text='Default allocation value used when this is not an on demand resource', null=True, verbose_name='default allocation', blank=True)),
|
||||||
|
('unit', models.CharField(help_text='The unit in which this resource is measured. For example GB, KB or subscribers', max_length=16, verbose_name='unit')),
|
||||||
|
('scale', models.PositiveIntegerField(help_text='Scale in which this resource monitoring resoults should be prorcessed to match with unit.', verbose_name='scale')),
|
||||||
|
('disable_trigger', models.BooleanField(default=False, help_text='Disables monitors exeeded and recovery triggers', verbose_name='disable trigger')),
|
||||||
|
('monitors', orchestra.models.fields.MultiSelectField(blank=True, help_text='Monitor backends used for monitoring this resource.', max_length=256, verbose_name='monitors', choices=[(b'Apache2Backend', 'Apache 2'), (b'Apache2Traffic', 'Apache 2 Traffic'), (b'AutoresponseBackend', 'Mail autoresponse'), (b'AwstatsBackend', 'Awstats'), (b'Bind9MasterDomainBackend', 'Bind9 master domain'), (b'Bind9SlaveDomainBackend', 'Bind9 slave domain'), (b'DokuWikiMuBackend', 'DokuWiki multisite'), (b'DrupalMuBackend', 'Drupal multisite'), (b'FTPTraffic', 'FTP traffic'), (b'MailSystemUserBackend', 'Mail system user'), (b'MaildirDisk', 'Maildir disk usage'), (b'MailmanBackend', b'Mailman'), (b'MailmanTraffic', b'MailmanTraffic'), (b'MySQLDBBackend', b'MySQL database'), (b'MySQLPermissionBackend', b'MySQL permission'), (b'MySQLUserBackend', b'MySQL user'), (b'MysqlDisk', 'MySQL disk'), (b'OpenVZTraffic', b'OpenVZTraffic'), (b'PHPFPMBackend', 'PHP-FPM'), (b'PHPFcgidBackend', 'PHP-Fcgid'), (b'PostfixAddressBackend', 'Postfix address'), (b'ServiceController', b'ServiceController'), (b'ServiceMonitor', b'ServiceMonitor'), (b'StaticBackend', 'Static'), (b'SystemUserBackend', 'System User'), (b'SystemUserDisk', 'System user disk'), (b'WebalizerBackend', 'Webalizer'), (b'WordpressMuBackend', 'Wordpress multisite')])),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='is active')),
|
||||||
|
('content_type', models.ForeignKey(help_text='Model where this resource will be hooked.', to='contenttypes.ContentType')),
|
||||||
|
('crontab', models.ForeignKey(blank=True, to='djcelery.CrontabSchedule', help_text='Crontab for periodic execution. Leave it empty to disable periodic monitoring', null=True, verbose_name='crontab')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
},
|
||||||
|
bases=(models.Model,),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ResourceData',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('object_id', models.PositiveIntegerField()),
|
||||||
|
('used', models.PositiveIntegerField(null=True)),
|
||||||
|
('last_update', models.DateTimeField(null=True)),
|
||||||
|
('allocated', models.PositiveIntegerField(null=True, blank=True)),
|
||||||
|
('content_type', models.ForeignKey(to='contenttypes.ContentType')),
|
||||||
|
('resource', models.ForeignKey(related_name=b'dataset', to='resources.Resource')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'resource data',
|
||||||
|
},
|
||||||
|
bases=(models.Model,),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='resourcedata',
|
||||||
|
unique_together=set([('resource', 'content_type', 'object_id')]),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='resource',
|
||||||
|
unique_together=set([('name', 'content_type'), ('verbose_name', 'content_type')]),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('resources', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='resource',
|
||||||
|
old_name='ondemand',
|
||||||
|
new_name='on_demand',
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,70 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('resources', '0002_auto_20140926_1143'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='monitordata',
|
||||||
|
name='date',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='resourcedata',
|
||||||
|
name='last_update',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='monitordata',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(default=datetime.datetime(2014, 9, 26, 13, 25, 33, 290000), verbose_name='created', auto_now_add=True),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='resourcedata',
|
||||||
|
name='updated_at',
|
||||||
|
field=models.DateTimeField(null=True, verbose_name='updated'),
|
||||||
|
preserve_default=True,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='monitordata',
|
||||||
|
name='content_type',
|
||||||
|
field=models.ForeignKey(verbose_name='content type', to='contenttypes.ContentType'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='monitordata',
|
||||||
|
name='object_id',
|
||||||
|
field=models.PositiveIntegerField(verbose_name='object id'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='resourcedata',
|
||||||
|
name='allocated',
|
||||||
|
field=models.PositiveIntegerField(null=True, verbose_name='allocated', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='resourcedata',
|
||||||
|
name='content_type',
|
||||||
|
field=models.ForeignKey(verbose_name='content type', to='contenttypes.ContentType'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='resourcedata',
|
||||||
|
name='object_id',
|
||||||
|
field=models.PositiveIntegerField(verbose_name='object id'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='resourcedata',
|
||||||
|
name='resource',
|
||||||
|
field=models.ForeignKey(related_name=b'dataset', verbose_name='resource', to='resources.Resource'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='resourcedata',
|
||||||
|
name='used',
|
||||||
|
field=models.PositiveIntegerField(null=True, verbose_name='used'),
|
||||||
|
),
|
||||||
|
]
|
0
orchestra/apps/resources/migrations/__init__.py
Normal file
0
orchestra/apps/resources/migrations/__init__.py
Normal file
|
@ -43,8 +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
|
on_demand = 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"))
|
||||||
default_allocation = models.PositiveIntegerField(_("default allocation"),
|
default_allocation = models.PositiveIntegerField(_("default allocation"),
|
||||||
|
@ -116,12 +115,12 @@ class Resource(models.Model):
|
||||||
|
|
||||||
class ResourceData(models.Model):
|
class ResourceData(models.Model):
|
||||||
""" Stores computed resource usage and allocation """
|
""" Stores computed resource usage and allocation """
|
||||||
resource = models.ForeignKey(Resource, related_name='dataset')
|
resource = models.ForeignKey(Resource, related_name='dataset', verbose_name=_("resource"))
|
||||||
content_type = models.ForeignKey(ContentType)
|
content_type = models.ForeignKey(ContentType, verbose_name=_("content type"))
|
||||||
object_id = models.PositiveIntegerField()
|
object_id = models.PositiveIntegerField(_("object id"))
|
||||||
used = models.PositiveIntegerField(null=True)
|
used = models.PositiveIntegerField(_("used"), null=True)
|
||||||
last_update = models.DateTimeField(null=True)
|
updated_at = models.DateTimeField(_("updated"), null=True)
|
||||||
allocated = models.PositiveIntegerField(null=True, blank=True)
|
allocated = models.PositiveIntegerField(_("allocated"), null=True, blank=True)
|
||||||
|
|
||||||
content_object = GenericForeignKey()
|
content_object = GenericForeignKey()
|
||||||
|
|
||||||
|
@ -146,7 +145,7 @@ class ResourceData(models.Model):
|
||||||
if current is None:
|
if current is None:
|
||||||
current = self.get_used()
|
current = self.get_used()
|
||||||
self.used = current or 0
|
self.used = current or 0
|
||||||
self.last_update = timezone.now()
|
self.updated_at = timezone.now()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
|
@ -154,9 +153,9 @@ class MonitorData(models.Model):
|
||||||
""" Stores monitored data """
|
""" Stores monitored data """
|
||||||
monitor = models.CharField(_("monitor"), max_length=256,
|
monitor = models.CharField(_("monitor"), max_length=256,
|
||||||
choices=ServiceMonitor.get_plugin_choices())
|
choices=ServiceMonitor.get_plugin_choices())
|
||||||
content_type = models.ForeignKey(ContentType)
|
content_type = models.ForeignKey(ContentType, verbose_name=_("content type"))
|
||||||
object_id = models.PositiveIntegerField()
|
object_id = models.PositiveIntegerField(_("object id"))
|
||||||
date = models.DateTimeField(_("date"), auto_now_add=True)
|
created_at = models.DateTimeField(_("created"), auto_now_add=True)
|
||||||
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
|
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
|
||||||
|
|
||||||
content_object = GenericForeignKey()
|
content_object = GenericForeignKey()
|
||||||
|
|
|
@ -44,12 +44,12 @@ if not running_syncdb():
|
||||||
msg = "Unknown or duplicated resource '%s'." % resource
|
msg = "Unknown or duplicated resource '%s'." % resource
|
||||||
raise serializers.ValidationError(msg)
|
raise serializers.ValidationError(msg)
|
||||||
resources.remove(resource)
|
resources.remove(resource)
|
||||||
if not resource.ondemand and not data.allocated:
|
if not resource.on_demand and not data.allocated:
|
||||||
data.allocated = resource.default_allocation
|
data.allocated = resource.default_allocation
|
||||||
result.append(data)
|
result.append(data)
|
||||||
for resource in resources:
|
for resource in resources:
|
||||||
data = ResourceData(resource=resource)
|
data = ResourceData(resource=resource)
|
||||||
if not resource.ondemand:
|
if not resource.on_demand:
|
||||||
data.allocated = resource.default_allocation
|
data.allocated = resource.default_allocation
|
||||||
result.append(data)
|
result.append(data)
|
||||||
attrs[source] = result
|
attrs[source] = result
|
||||||
|
@ -64,7 +64,7 @@ if not running_syncdb():
|
||||||
ret['available_resources'] = [
|
ret['available_resources'] = [
|
||||||
{
|
{
|
||||||
'name': resource.name,
|
'name': resource.name,
|
||||||
'ondemand': resource.ondemand,
|
'on_demand': resource.on_demand,
|
||||||
'default_allocation': resource.default_allocation
|
'default_allocation': resource.default_allocation
|
||||||
} for resource in resources
|
} for resource in resources
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from django.db.models.loading import get_model
|
from django.db.models.loading import get_model
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from orchestra.apps.orchestration.models import BackendOperation as Operation
|
from orchestra.apps.orchestration.models import BackendOperation as Operation
|
||||||
|
|
||||||
|
@ -34,7 +33,7 @@ def monitor(resource_id):
|
||||||
operations.append(op)
|
operations.append(op)
|
||||||
elif data.used < data.allocated:
|
elif data.used < data.allocated:
|
||||||
op = Operation.create(backend, obj, Operation.RECOVERY)
|
op = Operation.create(backend, obj, Operation.RECOVERY)
|
||||||
operation.append(op)
|
operations.append(op)
|
||||||
# data = ResourceData.get_or_create(obj, resource)
|
# data = ResourceData.get_or_create(obj, resource)
|
||||||
# current = data.get_used()
|
# current = data.get_used()
|
||||||
# if not resource.disable_trigger:
|
# if not resource.disable_trigger:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.contrib import messages
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,8 +7,23 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
def update_orders(modeladmin, request, queryset):
|
def update_orders(modeladmin, request, queryset):
|
||||||
for service in queryset:
|
for service in queryset:
|
||||||
service.update_orders()
|
service.update_orders()
|
||||||
modeladmin.log_change(request, transaction, 'Update orders')
|
modeladmin.log_change(request, service, 'Update orders')
|
||||||
msg = _("Orders for %s selected services have been updated.") % queryset.count()
|
msg = _("Orders for %s selected services have been updated.") % queryset.count()
|
||||||
modeladmin.message_user(request, msg)
|
modeladmin.message_user(request, msg)
|
||||||
update_orders.url_name = 'update-orders'
|
update_orders.url_name = 'update-orders'
|
||||||
update_orders.verbose_name = _("Update orders")
|
update_orders.verbose_name = _("Update orders")
|
||||||
|
|
||||||
|
|
||||||
|
def view_help(modeladmin, request, queryset):
|
||||||
|
opts = modeladmin.model._meta
|
||||||
|
context = {
|
||||||
|
'title': _("Need some help?"),
|
||||||
|
'opts': opts,
|
||||||
|
'queryset': queryset,
|
||||||
|
'obj': queryset.get(),
|
||||||
|
'action_name': _("help"),
|
||||||
|
'app_label': opts.app_label,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'admin/services/service/help.html', context)
|
||||||
|
view_help.url_name = 'help'
|
||||||
|
view_help.verbose_name = _("Help")
|
||||||
|
|
|
@ -9,7 +9,7 @@ from orchestra.admin.filters import UsedContentTypeFilter
|
||||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||||
from orchestra.core import services
|
from orchestra.core import services
|
||||||
|
|
||||||
from .actions import update_orders
|
from .actions import update_orders, view_help
|
||||||
from .models import Plan, ContractedPlan, Rate, Service
|
from .models import Plan, ContractedPlan, Rate, Service
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
|
||||||
)
|
)
|
||||||
inlines = [RateInline]
|
inlines = [RateInline]
|
||||||
actions = [update_orders]
|
actions = [update_orders]
|
||||||
change_view_actions = actions
|
change_view_actions = actions + [view_help]
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Improve performance of account field and filter by account """
|
""" Improve performance of account field and filter by account """
|
||||||
|
|
|
@ -4,12 +4,11 @@ import decimal
|
||||||
|
|
||||||
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 Q
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.utils import plugins
|
from orchestra.utils import plugins
|
||||||
from orchestra.utils.python import AttributeDict
|
from orchestra.utils.python import AttrDict
|
||||||
|
|
||||||
from . import settings, helpers
|
from . import settings, helpers
|
||||||
|
|
||||||
|
@ -21,6 +20,8 @@ class ServiceHandler(plugins.Plugin):
|
||||||
|
|
||||||
Relax and enjoy the journey.
|
Relax and enjoy the journey.
|
||||||
"""
|
"""
|
||||||
|
_VOLUME = 'VOLUME'
|
||||||
|
_COMPENSATION = 'COMPENSATION'
|
||||||
|
|
||||||
model = None
|
model = None
|
||||||
|
|
||||||
|
@ -160,7 +161,7 @@ class ServiceHandler(plugins.Plugin):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def generate_discount(self, line, dtype, price):
|
def generate_discount(self, line, dtype, price):
|
||||||
line.discounts.append(AttributeDict(**{
|
line.discounts.append(AttrDict(**{
|
||||||
'type': dtype,
|
'type': dtype,
|
||||||
'total': price,
|
'total': price,
|
||||||
}))
|
}))
|
||||||
|
@ -182,7 +183,7 @@ class ServiceHandler(plugins.Plugin):
|
||||||
if not computed:
|
if not computed:
|
||||||
price = price * size
|
price = price * size
|
||||||
subtotal = self.nominal_price * size * metric
|
subtotal = self.nominal_price * size * metric
|
||||||
line = AttributeDict(**{
|
line = AttrDict(**{
|
||||||
'order': order,
|
'order': order,
|
||||||
'subtotal': subtotal,
|
'subtotal': subtotal,
|
||||||
'ini': ini,
|
'ini': ini,
|
||||||
|
@ -197,7 +198,7 @@ class ServiceHandler(plugins.Plugin):
|
||||||
discounted += dprice
|
discounted += dprice
|
||||||
subtotal += discounted
|
subtotal += discounted
|
||||||
if subtotal > price:
|
if subtotal > price:
|
||||||
self.generate_discount(line, 'volume', price-subtotal)
|
self.generate_discount(line, self._VOLUME, price-subtotal)
|
||||||
return line
|
return line
|
||||||
|
|
||||||
def assign_compensations(self, givers, receivers, **options):
|
def assign_compensations(self, givers, receivers, **options):
|
||||||
|
@ -225,7 +226,6 @@ class ServiceHandler(plugins.Plugin):
|
||||||
|
|
||||||
def apply_compensations(self, order, only_beyond=False):
|
def apply_compensations(self, order, only_beyond=False):
|
||||||
dsize = 0
|
dsize = 0
|
||||||
discounts = ()
|
|
||||||
ini = order.billed_until or order.registered_on
|
ini = order.billed_until or order.registered_on
|
||||||
end = order.new_billed_until
|
end = order.new_billed_until
|
||||||
beyond = end
|
beyond = end
|
||||||
|
@ -296,7 +296,7 @@ class ServiceHandler(plugins.Plugin):
|
||||||
cprice += dsize*price
|
cprice += dsize*price
|
||||||
if cprice:
|
if cprice:
|
||||||
discounts = (
|
discounts = (
|
||||||
('compensation', -cprice),
|
(self._COMPENSATION, -cprice),
|
||||||
)
|
)
|
||||||
if new_end:
|
if new_end:
|
||||||
size = self.get_price_size(order.new_billed_until, new_end)
|
size = self.get_price_size(order.new_billed_until, new_end)
|
||||||
|
@ -323,11 +323,12 @@ class ServiceHandler(plugins.Plugin):
|
||||||
discounts = ()
|
discounts = ()
|
||||||
dsize, new_end = self.apply_compensations(order)
|
dsize, new_end = self.apply_compensations(order)
|
||||||
if dsize:
|
if dsize:
|
||||||
discounts=(('compensation', -dsize*price),)
|
discounts=(
|
||||||
|
(self._COMPENSATION, -dsize*price),
|
||||||
|
)
|
||||||
if new_end:
|
if new_end:
|
||||||
order.new_billed_until = new_end
|
order.new_billed_until = new_end
|
||||||
end = new_end
|
end = new_end
|
||||||
size = self.get_price_size(ini, end)
|
|
||||||
line = self.generate_line(order, price, ini, end, discounts=discounts)
|
line = self.generate_line(order, price, ini, end, discounts=discounts)
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
return lines
|
return lines
|
||||||
|
@ -395,7 +396,7 @@ class ServiceHandler(plugins.Plugin):
|
||||||
dsize, new_end = self.apply_compensations(order)
|
dsize, new_end = self.apply_compensations(order)
|
||||||
if dsize:
|
if dsize:
|
||||||
discounts=(
|
discounts=(
|
||||||
('compensation', -dsize*price),
|
(self._COMPENSATION, -dsize*price),
|
||||||
)
|
)
|
||||||
if new_end:
|
if new_end:
|
||||||
order.new_billed_until = new_end
|
order.new_billed_until = new_end
|
||||||
|
|
|
@ -1,24 +1,18 @@
|
||||||
import decimal
|
import decimal
|
||||||
import sys
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Q
|
from django.db.models import Q
|
||||||
from django.db.models.loading import get_model
|
from django.db.models.loading import get_model
|
||||||
from django.db.models.signals import pre_delete, post_delete, post_save
|
|
||||||
from django.dispatch import receiver
|
|
||||||
from django.contrib.contenttypes import generic
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.validators import ValidationError
|
from django.core.validators import ValidationError
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.core import caches, services, accounts
|
from orchestra.core import caches, services, accounts
|
||||||
from orchestra.models import queryset
|
from orchestra.models import queryset
|
||||||
from orchestra.utils.apps import autodiscover
|
from orchestra.utils.apps import autodiscover
|
||||||
from orchestra.utils.python import import_class
|
|
||||||
|
|
||||||
from . import helpers, settings, rating
|
from . import settings, rating
|
||||||
from .handlers import ServiceHandler
|
from .handlers import ServiceHandler
|
||||||
|
|
||||||
|
|
||||||
|
@ -329,7 +323,7 @@ class Service(models.Model):
|
||||||
def update_orders(self):
|
def update_orders(self):
|
||||||
order_model = get_model(settings.SERVICES_ORDER_MODEL)
|
order_model = get_model(settings.SERVICES_ORDER_MODEL)
|
||||||
related_model = self.content_type.model_class()
|
related_model = self.content_type.model_class()
|
||||||
for instance in related_model.objects.all():
|
for instance in related_model.objects.all().select_related('account__user'):
|
||||||
order_model.update_orders(instance, service=self)
|
order_model.update_orders(instance, service=self)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from orchestra.utils.python import AttributeDict
|
from orchestra.utils.python import AttrDict
|
||||||
|
|
||||||
|
|
||||||
def _compute(rates, metric):
|
def _compute(rates, metric):
|
||||||
|
@ -30,7 +30,7 @@ def _compute(rates, metric):
|
||||||
quantity = metric - accumulated
|
quantity = metric - accumulated
|
||||||
end = True
|
end = True
|
||||||
price = rates[ix].price
|
price = rates[ix].price
|
||||||
steps.append(AttributeDict(**{
|
steps.append(AttrDict(**{
|
||||||
'quantity': quantity,
|
'quantity': quantity,
|
||||||
'price': price,
|
'price': price,
|
||||||
'barrier': barrier,
|
'barrier': barrier,
|
||||||
|
@ -109,7 +109,7 @@ def step_price(rates, metric):
|
||||||
if result and result[-1].price == price:
|
if result and result[-1].price == price:
|
||||||
result[-1].quantity += quantity
|
result[-1].quantity += quantity
|
||||||
else:
|
else:
|
||||||
result.append(AttributeDict(quantity=quantity, price=price))
|
result.append(AttrDict(quantity=quantity, price=price))
|
||||||
ix = 0
|
ix = 0
|
||||||
targets = []
|
targets = []
|
||||||
else:
|
else:
|
||||||
|
@ -139,7 +139,7 @@ def match_price(rates, metric):
|
||||||
candidates.append(prev)
|
candidates.append(prev)
|
||||||
candidates.sort(key=lambda r: r.price)
|
candidates.sort(key=lambda r: r.price)
|
||||||
if candidates:
|
if candidates:
|
||||||
return [AttributeDict(**{
|
return [AttrDict(**{
|
||||||
'quantity': metric,
|
'quantity': metric,
|
||||||
'price': candidates[0].price,
|
'price': candidates[0].price,
|
||||||
})]
|
})]
|
||||||
|
|
BIN
orchestra/apps/services/static/services/img/services.png
Normal file
BIN
orchestra/apps/services/static/services/img/services.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 91 KiB |
485
orchestra/apps/services/static/services/img/services.svg
Normal file
485
orchestra/apps/services/static/services/img/services.svg
Normal file
|
@ -0,0 +1,485 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="1052.3622"
|
||||||
|
height="744.09448"
|
||||||
|
id="svg2"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="0.48.3.1 r9886"
|
||||||
|
sodipodi:docname="services.svg"
|
||||||
|
inkscape:export-filename="/home/glic3rinu/orchestra/django-orchestra/orchestra/apps/services/static/services/img/services.png"
|
||||||
|
inkscape:export-xdpi="90"
|
||||||
|
inkscape:export-ydpi="90">
|
||||||
|
<defs
|
||||||
|
id="defs4" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="0.70710678"
|
||||||
|
inkscape:cx="559.86324"
|
||||||
|
inkscape:cy="278.12745"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:guide-bbox="true"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1024"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="27"
|
||||||
|
inkscape:window-maximized="1" />
|
||||||
|
<metadata
|
||||||
|
id="metadata7">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-308.2677)">
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:FreeMono Bold"
|
||||||
|
x="132.85733"
|
||||||
|
y="526.45612"
|
||||||
|
id="text2985"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan2987"
|
||||||
|
x="132.85733"
|
||||||
|
y="526.45612">Orders</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||||
|
x="133.94112"
|
||||||
|
y="853.0473"
|
||||||
|
id="text2989"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan2991"
|
||||||
|
x="133.94112"
|
||||||
|
y="853.0473"
|
||||||
|
style="font-size:22px;text-align:center;line-height:94.99999880999999391%;text-anchor:middle">Metric</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||||
|
x="294.6925"
|
||||||
|
y="431.67795"
|
||||||
|
id="text2993"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan2995"
|
||||||
|
x="294.6925"
|
||||||
|
y="431.67795">Periodic</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="294.6925"
|
||||||
|
y="453.67804"
|
||||||
|
id="tspan2997">billing</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||||
|
x="294.77344"
|
||||||
|
y="597.10419"
|
||||||
|
id="text2999"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3001"
|
||||||
|
x="294.77344"
|
||||||
|
y="597.10419">One-time</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="294.77344"
|
||||||
|
y="619.10431"
|
||||||
|
id="tspan3003">service</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||||
|
x="488.67383"
|
||||||
|
y="472.50183"
|
||||||
|
id="text3005"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3007"
|
||||||
|
x="488.67383"
|
||||||
|
y="472.50183">Pricing</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="488.67383"
|
||||||
|
y="494.50192"
|
||||||
|
id="tspan3009">period</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||||
|
x="488.91711"
|
||||||
|
y="390.854"
|
||||||
|
id="text3011"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3013"
|
||||||
|
x="488.91711"
|
||||||
|
y="390.854">No pricing</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="488.91711"
|
||||||
|
y="412.8541"
|
||||||
|
id="tspan3015">period</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||||
|
x="294.6925"
|
||||||
|
y="758.26892"
|
||||||
|
id="text2993-8"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan2995-2"
|
||||||
|
x="294.6925"
|
||||||
|
y="758.26892">Periodic</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="294.6925"
|
||||||
|
y="780.26904"
|
||||||
|
id="tspan2997-4">billing</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||||
|
x="294.77344"
|
||||||
|
y="923.69543"
|
||||||
|
id="text2999-4"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3001-2"
|
||||||
|
x="294.77344"
|
||||||
|
y="923.69543">One-time</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="294.77344"
|
||||||
|
y="945.69556"
|
||||||
|
id="tspan3003-9">service</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||||
|
x="488.67383"
|
||||||
|
y="554.14972"
|
||||||
|
id="text3005-7"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3007-9"
|
||||||
|
x="488.67383"
|
||||||
|
y="554.14972">Pricing</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="488.67383"
|
||||||
|
y="576.14984"
|
||||||
|
id="tspan3009-3">period</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||||
|
x="488.91711"
|
||||||
|
y="635.79749"
|
||||||
|
id="text3011-6"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3013-2"
|
||||||
|
x="488.91711"
|
||||||
|
y="635.79749">No pricing</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="488.91711"
|
||||||
|
y="657.79761"
|
||||||
|
id="tspan3015-0">period</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||||
|
x="488.67383"
|
||||||
|
y="799.09296"
|
||||||
|
id="text3005-6"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3007-8"
|
||||||
|
x="488.67383"
|
||||||
|
y="799.09296">Pricing</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="488.67383"
|
||||||
|
y="821.09308"
|
||||||
|
id="tspan3009-2">period</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||||
|
x="488.91711"
|
||||||
|
y="717.44519"
|
||||||
|
id="text3011-1"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3013-6"
|
||||||
|
x="488.91711"
|
||||||
|
y="717.44519">No pricing</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="488.91711"
|
||||||
|
y="739.44531"
|
||||||
|
id="tspan3015-3">period</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||||
|
x="488.67383"
|
||||||
|
y="962.38898"
|
||||||
|
id="text3005-1"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3007-3"
|
||||||
|
x="488.67383"
|
||||||
|
y="962.38898">Pricing</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="488.67383"
|
||||||
|
y="984.3891"
|
||||||
|
id="tspan3009-7">period</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||||
|
x="488.91711"
|
||||||
|
y="880.74097"
|
||||||
|
id="text3011-64"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3013-1"
|
||||||
|
x="488.91711"
|
||||||
|
y="880.74097">No pricing</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="488.91711"
|
||||||
|
y="902.74109"
|
||||||
|
id="tspan3015-08">period</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||||
|
x="583.38361"
|
||||||
|
y="379.86563"
|
||||||
|
id="text3898"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3900"
|
||||||
|
x="583.38361"
|
||||||
|
y="379.86563"
|
||||||
|
style="font-weight:bold;font-size:22px">Mail accounts</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="583.38361"
|
||||||
|
y="401.86572"
|
||||||
|
id="tspan3912">Concurrent (changes)</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="583.38361"
|
||||||
|
y="423.86581"
|
||||||
|
id="tspan3902">Compensate on <tspan
|
||||||
|
style="font-weight:bold;line-height:94.99999880999999391%;-inkscape-font-specification:FreeMono Bold;font-size:22px"
|
||||||
|
id="tspan3904">prepay</tspan></tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||||
|
x="586.6123"
|
||||||
|
y="461.51324"
|
||||||
|
id="text3906"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3908"
|
||||||
|
x="586.6123"
|
||||||
|
y="461.51324"
|
||||||
|
style="font-weight:bold;font-size:22px">Domains</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="586.6123"
|
||||||
|
y="483.51334"
|
||||||
|
id="tspan3914">Register or renew events</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="586.6123"
|
||||||
|
y="505.51343"
|
||||||
|
id="tspan3910">Compensate on <tspan
|
||||||
|
style="font-weight:bold;line-height:94.99999880999999391%;font-size:22px"
|
||||||
|
id="tspan3993">prepay</tspan></tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||||
|
x="590.04663"
|
||||||
|
y="554.09149"
|
||||||
|
id="text3916"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3918"
|
||||||
|
x="590.04663"
|
||||||
|
y="554.09149"
|
||||||
|
style="font-weight:bold;font-size:22px">Plans</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="590.04663"
|
||||||
|
y="576.09161"
|
||||||
|
id="tspan3920">Always one order</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||||
|
x="590.58252"
|
||||||
|
y="635.97125"
|
||||||
|
id="text3922"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="590.58252"
|
||||||
|
y="635.97125"
|
||||||
|
id="tspan3926"
|
||||||
|
style="font-weight:bold;font-size:22px">CMS installation</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="590.58252"
|
||||||
|
y="657.97137"
|
||||||
|
id="tspan3930">Register or renew events</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="590.58252"
|
||||||
|
y="679.97144"
|
||||||
|
id="tspan3932" /></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||||
|
x="591.32349"
|
||||||
|
y="777.26685"
|
||||||
|
id="text3934"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3936"
|
||||||
|
x="591.32349"
|
||||||
|
y="777.26685"
|
||||||
|
style="font-weight:bold;font-size:22px">Traffic consumption</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="591.32349"
|
||||||
|
y="799.26697"
|
||||||
|
id="tspan3938">Metric period lookup</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="591.32349"
|
||||||
|
y="821.26703"
|
||||||
|
id="tspan3940">Prepay and != billing_period</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="591.32349"
|
||||||
|
y="843.26715"
|
||||||
|
id="tspan3995"
|
||||||
|
style="font-style:italic;-inkscape-font-specification:FreeMono Italic;font-size:22px"> NotImplemented</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||||
|
x="591.32349"
|
||||||
|
y="717.61884"
|
||||||
|
id="text3942"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3944"
|
||||||
|
x="591.32349"
|
||||||
|
y="717.61884"
|
||||||
|
style="font-weight:bold;font-size:22px">Mailbox size</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="591.32349"
|
||||||
|
y="739.61896"
|
||||||
|
id="tspan3946">Concurrent (changes)</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||||
|
x="590.1192"
|
||||||
|
y="882.65179"
|
||||||
|
id="text3942-8"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3944-2"
|
||||||
|
x="590.1192"
|
||||||
|
y="882.65179"
|
||||||
|
style="font-weight:bold;font-size:22px">Jobs</tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="590.1192"
|
||||||
|
y="904.65192"
|
||||||
|
id="tspan3946-6">Last known metric</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||||
|
x="591.06866"
|
||||||
|
y="973.33081"
|
||||||
|
id="text3972"
|
||||||
|
sodipodi:linespacing="94.999999%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3974"
|
||||||
|
x="591.06866"
|
||||||
|
y="973.33081"
|
||||||
|
style="font-style:italic;-inkscape-font-specification:FreeMono Italic;font-size:22px">NotImplement</tspan></text>
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 228.73934,436.50836 -23.61913,0 0,164.75267 23.53877,0"
|
||||||
|
id="path4013"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cccc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 205.55487,521.14184 -23.98356,0"
|
||||||
|
id="path4019"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 228.73934,764.42561 -23.61913,0 0,164.7526 23.53877,0"
|
||||||
|
id="path4013-2"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cccc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 205.55487,849.05914 -23.98356,0"
|
||||||
|
id="path4019-1"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 407.81779,398.68928 -23.61908,0 0,80.25672 23.5387,0"
|
||||||
|
id="path4013-6"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cccc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 384.6333,438.12728 -23.98362,0"
|
||||||
|
id="path4019-18"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 407.81779,561.72165 -23.61908,0 0,80.25664 23.5387,0"
|
||||||
|
id="path4013-6-63"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cccc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 384.6333,601.15965 -23.98362,0"
|
||||||
|
id="path4019-18-4"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 407.81779,726.60658 -23.61908,0 0,80.25672 23.5387,0"
|
||||||
|
id="path4013-6-8"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cccc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 384.6333,766.04458 -23.98362,0"
|
||||||
|
id="path4019-18-3"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 407.81779,889.63886 -23.61908,0 0,80.25667 23.5387,0"
|
||||||
|
id="path4013-6-0"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cccc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 384.6333,929.07689 -23.98362,0"
|
||||||
|
id="path4019-18-8"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 22 KiB |
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends "admin/orchestra/generic_confirmation.html" %}
|
||||||
|
{% load i18n l10n %}
|
||||||
|
{% load url from future %}
|
||||||
|
{% load admin_urls static utils %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div>
|
||||||
|
<div style="margin:20px;">
|
||||||
|
Enjoy my friend.
|
||||||
|
<img src="{% static "services/img/services.png" %}"</img>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -41,7 +41,7 @@ class DomainBillingTest(BaseBillingTest):
|
||||||
return account.miscellaneous.create(service=domain_service, description=domain_name)
|
return account.miscellaneous.create(service=domain_service, description=domain_name)
|
||||||
|
|
||||||
def test_domain(self):
|
def test_domain(self):
|
||||||
service = self.create_domain_service()
|
self.create_domain_service()
|
||||||
account = self.create_account()
|
account = self.create_account()
|
||||||
self.create_domain(account=account)
|
self.create_domain(account=account)
|
||||||
bills = account.orders.bill()
|
bills = account.orders.bill()
|
||||||
|
@ -69,7 +69,7 @@ class DomainBillingTest(BaseBillingTest):
|
||||||
self.assertEqual(56, bills[0].get_total())
|
self.assertEqual(56, bills[0].get_total())
|
||||||
|
|
||||||
def test_domain_proforma(self):
|
def test_domain_proforma(self):
|
||||||
service = self.create_domain_service()
|
self.create_domain_service()
|
||||||
account = self.create_account()
|
account = self.create_account()
|
||||||
self.create_domain(account=account)
|
self.create_domain(account=account)
|
||||||
bills = account.orders.bill(proforma=True, new_open=True)
|
bills = account.orders.bill(proforma=True, new_open=True)
|
||||||
|
@ -97,7 +97,7 @@ class DomainBillingTest(BaseBillingTest):
|
||||||
self.assertEqual(56, bills[0].get_total())
|
self.assertEqual(56, bills[0].get_total())
|
||||||
|
|
||||||
def test_domain_cumulative(self):
|
def test_domain_cumulative(self):
|
||||||
service = self.create_domain_service()
|
self.create_domain_service()
|
||||||
account = self.create_account()
|
account = self.create_account()
|
||||||
self.create_domain(account=account)
|
self.create_domain(account=account)
|
||||||
bills = account.orders.bill(proforma=True)
|
bills = account.orders.bill(proforma=True)
|
||||||
|
@ -110,7 +110,7 @@ class DomainBillingTest(BaseBillingTest):
|
||||||
self.assertEqual(30, bills[0].get_total())
|
self.assertEqual(30, bills[0].get_total())
|
||||||
|
|
||||||
def test_domain_new_open(self):
|
def test_domain_new_open(self):
|
||||||
service = self.create_domain_service()
|
self.create_domain_service()
|
||||||
account = self.create_account()
|
account = self.create_account()
|
||||||
self.create_domain(account=account)
|
self.create_domain(account=account)
|
||||||
bills = account.orders.bill(new_open=True)
|
bills = account.orders.bill(new_open=True)
|
||||||
|
|
|
@ -43,14 +43,14 @@ class FTPBillingTest(BaseBillingTest):
|
||||||
|
|
||||||
def test_ftp_account_1_year_fiexed(self):
|
def test_ftp_account_1_year_fiexed(self):
|
||||||
service = self.create_ftp_service()
|
service = self.create_ftp_service()
|
||||||
user = self.create_ftp()
|
self.create_ftp()
|
||||||
bp = timezone.now().date() + relativedelta(years=1)
|
bp = timezone.now().date() + relativedelta(years=1)
|
||||||
bills = service.orders.bill(billing_point=bp, fixed_point=True)
|
bills = service.orders.bill(billing_point=bp, fixed_point=True)
|
||||||
self.assertEqual(10, bills[0].get_total())
|
self.assertEqual(10, bills[0].get_total())
|
||||||
|
|
||||||
def test_ftp_account_2_year_fiexed(self):
|
def test_ftp_account_2_year_fiexed(self):
|
||||||
service = self.create_ftp_service()
|
service = self.create_ftp_service()
|
||||||
user = self.create_ftp()
|
self.create_ftp()
|
||||||
bp = timezone.now().date() + relativedelta(years=2)
|
bp = timezone.now().date() + relativedelta(years=2)
|
||||||
bills = service.orders.bill(billing_point=bp, fixed_point=True)
|
bills = service.orders.bill(billing_point=bp, fixed_point=True)
|
||||||
self.assertEqual(20, bills[0].get_total())
|
self.assertEqual(20, bills[0].get_total())
|
||||||
|
@ -79,7 +79,7 @@ class FTPBillingTest(BaseBillingTest):
|
||||||
|
|
||||||
def test_ftp_account_with_compensation(self):
|
def test_ftp_account_with_compensation(self):
|
||||||
account = self.create_account()
|
account = self.create_account()
|
||||||
service = self.create_ftp_service()
|
self.create_ftp_service()
|
||||||
user = self.create_ftp(account=account)
|
user = self.create_ftp(account=account)
|
||||||
first_bp = timezone.now().date() + relativedelta(years=2)
|
first_bp = timezone.now().date() + relativedelta(years=2)
|
||||||
bills = account.orders.bill(billing_point=first_bp, fixed_point=True)
|
bills = account.orders.bill(billing_point=first_bp, fixed_point=True)
|
||||||
|
|
|
@ -39,13 +39,13 @@ class JobBillingTest(BaseBillingTest):
|
||||||
return account.miscellaneous.create(service=service, description=description, amount=amount)
|
return account.miscellaneous.create(service=service, description=description, amount=amount)
|
||||||
|
|
||||||
def test_job(self):
|
def test_job(self):
|
||||||
service = self.create_job_service()
|
self.create_job_service()
|
||||||
account = self.create_account()
|
account = self.create_account()
|
||||||
|
|
||||||
job = self.create_job(5, account=account)
|
self.create_job(5, account=account)
|
||||||
bill = account.orders.bill()[0]
|
bill = account.orders.bill()[0]
|
||||||
self.assertEqual(5*20, bill.get_total())
|
self.assertEqual(5*20, bill.get_total())
|
||||||
|
|
||||||
job = self.create_job(100, account=account)
|
self.create_job(100, account=account)
|
||||||
bill = account.orders.bill(new_open=True)[0]
|
bill = account.orders.bill(new_open=True)[0]
|
||||||
self.assertEqual(100*15, bill.get_total())
|
self.assertEqual(100*15, bill.get_total())
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.utils import timezone
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from orchestra.apps.mails.models import Mailbox
|
from orchestra.apps.mails.models import Mailbox
|
||||||
from orchestra.apps.resources.models import Resource, ResourceData, MonitorData
|
from orchestra.apps.resources.models import Resource, ResourceData
|
||||||
from orchestra.utils.tests import random_ascii
|
from orchestra.utils.tests import random_ascii
|
||||||
|
|
||||||
from ...models import Service, Plan
|
from ...models import Service, Plan
|
||||||
|
@ -62,7 +62,7 @@ class MailboxBillingTest(BaseBillingTest):
|
||||||
verbose_name='Mailbox disk',
|
verbose_name='Mailbox disk',
|
||||||
unit='GB',
|
unit='GB',
|
||||||
scale=10**9,
|
scale=10**9,
|
||||||
ondemand=False,
|
on_demand=False,
|
||||||
monitors='MaildirDisk',
|
monitors='MaildirDisk',
|
||||||
)
|
)
|
||||||
return self.resource
|
return self.resource
|
||||||
|
|
|
@ -42,7 +42,7 @@ class PlanBillingTest(BaseBillingTest):
|
||||||
|
|
||||||
def test_plan(self):
|
def test_plan(self):
|
||||||
account = self.create_account()
|
account = self.create_account()
|
||||||
service = self.create_plan_service()
|
self.create_plan_service()
|
||||||
self.create_plan(account=account)
|
self.create_plan(account=account)
|
||||||
bill = account.orders.bill().pop()
|
bill = account.orders.bill().pop()
|
||||||
self.assertEqual(bill.FEE, bill.type)
|
self.assertEqual(bill.FEE, bill.type)
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import datetime
|
|
||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -46,26 +44,23 @@ class BaseTrafficBillingTest(BaseBillingTest):
|
||||||
verbose_name='Account Traffic',
|
verbose_name='Account Traffic',
|
||||||
unit='GB',
|
unit='GB',
|
||||||
scale=10**9,
|
scale=10**9,
|
||||||
ondemand=True,
|
on_demand=True,
|
||||||
monitors='FTPTraffic',
|
monitors='FTPTraffic',
|
||||||
)
|
)
|
||||||
return self.resource
|
return self.resource
|
||||||
|
|
||||||
def report_traffic(self, account, value):
|
def report_traffic(self, account, value):
|
||||||
ct = ContentType.objects.get_for_model(Account)
|
MonitorData.objects.create(monitor='FTPTraffic', content_object=account.user, value=value)
|
||||||
object_id = account.pk
|
|
||||||
MonitorData.objects.create(monitor='FTPTraffic', content_object=account.user,
|
|
||||||
value=value, date=timezone.now())
|
|
||||||
data = ResourceData.get_or_create(account, self.resource)
|
data = ResourceData.get_or_create(account, self.resource)
|
||||||
data.update()
|
data.update()
|
||||||
|
|
||||||
|
|
||||||
class TrafficBillingTest(BaseTrafficBillingTest):
|
class TrafficBillingTest(BaseTrafficBillingTest):
|
||||||
def test_traffic(self):
|
def test_traffic(self):
|
||||||
service = self.create_traffic_service()
|
self.create_traffic_service()
|
||||||
resource = self.create_traffic_resource()
|
self.create_traffic_resource()
|
||||||
account = self.create_account()
|
account = self.create_account()
|
||||||
now = timezone.now().date()
|
now = timezone.now()
|
||||||
|
|
||||||
self.report_traffic(account, 10**9)
|
self.report_traffic(account, 10**9)
|
||||||
bill = account.orders.bill(commit=False)[0]
|
bill = account.orders.bill(commit=False)[0]
|
||||||
|
@ -82,8 +77,8 @@ class TrafficBillingTest(BaseTrafficBillingTest):
|
||||||
self.assertEqual((90-10)*10, bill.get_total())
|
self.assertEqual((90-10)*10, bill.get_total())
|
||||||
|
|
||||||
def test_multiple_traffics(self):
|
def test_multiple_traffics(self):
|
||||||
service = self.create_traffic_service()
|
self.create_traffic_service()
|
||||||
resource = self.create_traffic_resource()
|
self.create_traffic_resource()
|
||||||
account1 = self.create_account()
|
account1 = self.create_account()
|
||||||
account2 = self.create_account()
|
account2 = self.create_account()
|
||||||
self.report_traffic(account1, 10**10)
|
self.report_traffic(account1, 10**10)
|
||||||
|
@ -129,13 +124,13 @@ class TrafficPrepayBillingTest(BaseTrafficBillingTest):
|
||||||
return account.miscellaneous.create(service=service, description=name, amount=amount)
|
return account.miscellaneous.create(service=service, description=name, amount=amount)
|
||||||
|
|
||||||
def test_traffic_prepay(self):
|
def test_traffic_prepay(self):
|
||||||
service = self.create_traffic_service()
|
self.create_traffic_service()
|
||||||
prepay_service = self.create_prepay_service()
|
self.create_prepay_service()
|
||||||
account = self.create_account()
|
account = self.create_account()
|
||||||
self.create_traffic_resource()
|
self.create_traffic_resource()
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
|
||||||
prepay = self.create_prepay(10, account=account)
|
self.create_prepay(10, account=account)
|
||||||
bill = account.orders.bill(proforma=True)[0]
|
bill = account.orders.bill(proforma=True)[0]
|
||||||
self.assertEqual(10*50, bill.get_total())
|
self.assertEqual(10*50, bill.get_total())
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import sys
|
|
||||||
|
|
||||||
from dateutil import relativedelta
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
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.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
|
||||||
|
|
||||||
from .. import settings, helpers
|
from .. import helpers
|
||||||
from ..models import Service, Plan, Rate
|
from ..models import Service, Plan
|
||||||
|
|
||||||
|
|
||||||
class Order(object):
|
class Order(object):
|
||||||
|
|
|
@ -55,7 +55,6 @@ class UserAdmin(AccountAdminMixin, auth.UserAdmin, ExtendedModelAdmin):
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
""" Returns the additional urls for the change view links """
|
""" Returns the additional urls for the change view links """
|
||||||
urls = super(UserAdmin, self).get_urls()
|
urls = super(UserAdmin, self).get_urls()
|
||||||
admin_site = self.admin_site
|
|
||||||
opts = self.model._meta
|
opts = self.model._meta
|
||||||
new_urls = patterns("")
|
new_urls = patterns("")
|
||||||
for role in self.roles:
|
for role in self.roles:
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from orchestra.api import router, SetPasswordApiMixin
|
from orchestra.api import router, SetPasswordApiMixin
|
||||||
from orchestra.apps.accounts.api import AccountApiMixin
|
from orchestra.apps.accounts.api import AccountApiMixin
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from django.contrib.auth import models as auth
|
from django.contrib.auth import models as auth
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
|
from django.core.mail import send_mail
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
from ..models import User
|
from ..models import User
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.admin.util import unquote, get_deleted_objects
|
from django.contrib.admin.util import unquote, get_deleted_objects
|
||||||
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
|
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.db import router
|
from django.db import router
|
||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import Http404, HttpResponseRedirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
from django.contrib import admin
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from orchestra.admin.utils import insertattr
|
from orchestra.admin.utils import insertattr
|
||||||
from orchestra.apps.users.roles.admin import RoleAdmin
|
from orchestra.apps.users.roles.admin import RoleAdmin
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.admin import UserAdmin
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
from orchestra.admin.utils import insertattr, admin_link
|
from orchestra.admin.utils import insertattr, admin_link
|
||||||
from orchestra.apps.accounts.admin import SelectAccountAdminMixin
|
from orchestra.apps.accounts.admin import SelectAccountAdminMixin
|
||||||
from orchestra.apps.domains.forms import DomainIterator
|
|
||||||
from orchestra.apps.users.roles.admin import RoleAdmin
|
from orchestra.apps.users.roles.admin import RoleAdmin
|
||||||
|
|
||||||
from .forms import MailRoleAdminForm
|
from .forms import MailRoleAdminForm
|
||||||
|
|
|
@ -7,6 +7,7 @@ from orchestra.apps.orchestration import ServiceController
|
||||||
from orchestra.apps.resources import ServiceMonitor
|
from orchestra.apps.resources import ServiceMonitor
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
|
from .models import Address
|
||||||
|
|
||||||
|
|
||||||
class MailSystemUserBackend(ServiceController):
|
class MailSystemUserBackend(ServiceController):
|
||||||
|
@ -152,7 +153,7 @@ class MaildirDisk(ServiceMonitor):
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_context(self, mailbox):
|
def get_context(self, mailbox):
|
||||||
context = MailSystemUserBackend().get_context(site)
|
context = MailSystemUserBackend().get_context(mailbox)
|
||||||
context['home'] = settings.EMAILS_HOME % context
|
context['home'] = settings.EMAILS_HOME % context
|
||||||
context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize')
|
context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize')
|
||||||
context['object_id'] = mailbox.pk
|
context['object_id'] = mailbox.pk
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
import re
|
|
||||||
|
|
||||||
from django.contrib.auth.hashers import check_password, make_password
|
|
||||||
from django.core.validators import RegexValidator
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,6 @@ def validate_forward(value):
|
||||||
|
|
||||||
|
|
||||||
def validate_sieve(value):
|
def validate_sieve(value):
|
||||||
from .models import Mailbox
|
|
||||||
sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest()
|
sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest()
|
||||||
path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name)
|
path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name)
|
||||||
with open(path, 'wb') as f:
|
with open(path, 'wb') as f:
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
from django.contrib import admin
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from orchestra.admin.utils import insertattr
|
from orchestra.admin.utils import insertattr
|
||||||
from orchestra.apps.users.roles.admin import RoleAdmin
|
from orchestra.apps.users.roles.admin import RoleAdmin
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from django.conf.urls import patterns
|
from django.conf.urls import patterns
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from orchestra.apps.resources import ServiceMonitor
|
from orchestra.apps.resources import ServiceMonitor
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.forms import UserCreationForm, ReadOnlyPasswordHashField
|
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from orchestra.api import router
|
from orchestra.api import router
|
||||||
from orchestra.apps.accounts.api import AccountApiMixin
|
from orchestra.apps.accounts.api import AccountApiMixin
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import os
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.apps.orchestration import ServiceController
|
from orchestra.apps.orchestration import ServiceController
|
||||||
|
@ -21,7 +23,7 @@ class DokuWikiMuBackend(WebAppServiceMixin, ServiceController):
|
||||||
self.append("rm -fr %(app_path)s" % context)
|
self.append("rm -fr %(app_path)s" % context)
|
||||||
|
|
||||||
def get_context(self, webapp):
|
def get_context(self, webapp):
|
||||||
context = super(DokuwikiMuBackend, self).get_context(webapp)
|
context = super(DokuWikiMuBackend, self).get_context(webapp)
|
||||||
context.update({
|
context.update({
|
||||||
'template': settings.WEBAPPS_DOKUWIKIMU_TEMPLATE_PATH,
|
'template': settings.WEBAPPS_DOKUWIKIMU_TEMPLATE_PATH,
|
||||||
'app_path': os.path.join(settings.WEBAPPS_DOKUWIKIMU_FARM_PATH, webapp.name)
|
'app_path': os.path.join(settings.WEBAPPS_DOKUWIKIMU_FARM_PATH, webapp.name)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
@ -55,6 +54,7 @@ class WordpressMuBackend(WebAppServiceMixin, ServiceController):
|
||||||
'blog[email]': email,
|
'blog[email]': email,
|
||||||
'_wpnonce_add-blog': wpnonce,
|
'_wpnonce_add-blog': wpnonce,
|
||||||
}
|
}
|
||||||
|
# TODO validate response
|
||||||
response = session.post(url, data=data)
|
response = session.post(url, data=data)
|
||||||
|
|
||||||
def delete_blog(self, webapp, server):
|
def delete_blog(self, webapp, server):
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
from orchestra.admin.utils import admin_link, change_url
|
from orchestra.admin.utils import admin_link, change_url
|
||||||
from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin
|
from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin
|
||||||
from orchestra.apps.accounts.widgets import account_related_field_widget_factory
|
|
||||||
|
|
||||||
from .models import Content, Website, WebsiteOption
|
from .models import Content, Website, WebsiteOption
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from orchestra.api import router
|
from orchestra.api import router
|
||||||
from orchestra.apps.accounts.api import AccountApiMixin
|
from orchestra.apps.accounts.api import AccountApiMixin
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import textwrap
|
import textwrap
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from django.template import Template, Context
|
from django.template import Template, Context
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -123,6 +124,7 @@ class Apache2Backend(ServiceController):
|
||||||
def get_protections(self, site):
|
def get_protections(self, site):
|
||||||
protections = ""
|
protections = ""
|
||||||
__, regex = settings.WEBSITES_OPTIONS['directory_protection']
|
__, regex = settings.WEBSITES_OPTIONS['directory_protection']
|
||||||
|
context = self.get_context(site)
|
||||||
for protection in site.options.filter(name='directory_protection'):
|
for protection in site.options.filter(name='directory_protection'):
|
||||||
path, name, passwd = re.match(regex, protection.value).groups()
|
path, name, passwd = re.match(regex, protection.value).groups()
|
||||||
path = os.path.join(context['root'], path)
|
path = os.path.join(context['root'], path)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue