diff --git a/TODO.md b/TODO.md
index 78a04b9f..1aad2219 100644
--- a/TODO.md
+++ b/TODO.md
@@ -131,15 +131,10 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* AccountAdminMixin auto adds 'account__name' on searchfields and handle account_link on fieldsets
-* account defiition:
- * identify a customer or a person
- * has one main system user for running website
- * pangea staff are different accounts
- * An account identify a person
- * Maybe merge users into accounts? again. Account contains main_users, users contains FTP shit
- * Separate panel from server passwords?
- * Store passwords on panel?
-
+* Separate panel from server passwords? Store passwords on panel?
* What fields we really need on contacts? name email phone and what more?
+
+
+* Redirect junk emails and delete every 30 days?
diff --git a/orchestra/apps/accounts/admin.py b/orchestra/apps/accounts/admin.py
index ea779c04..2960fd33 100644
--- a/orchestra/apps/accounts/admin.py
+++ b/orchestra/apps/accounts/admin.py
@@ -30,10 +30,7 @@ class AccountAdmin(auth.UserAdmin, ExtendedModelAdmin):
'fields': ('first_name', 'last_name', 'email', ('type', 'language'), 'comments'),
}),
(_("Permissions"), {
- 'fields': ('is_superuser', 'is_active')
- }),
- (_("Important dates"), {
- 'fields': ('last_login', 'date_joined')
+ 'fields': ('is_superuser',)
}),
)
fieldsets = (
@@ -47,6 +44,7 @@ class AccountAdmin(auth.UserAdmin, ExtendedModelAdmin):
'fields': ('is_superuser', 'is_active')
}),
(_("Important dates"), {
+ 'classes': ('collapse',),
'fields': ('last_login', 'date_joined')
}),
)
diff --git a/orchestra/apps/accounts/management/__init__.py b/orchestra/apps/accounts/management/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/orchestra/apps/accounts/management/commands/__init__.py b/orchestra/apps/accounts/management/commands/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/orchestra/apps/accounts/management/commands/createinitialaccount.py b/orchestra/apps/accounts/management/commands/createinitialaccount.py
deleted file mode 100644
index 63eb9dfa..00000000
--- a/orchestra/apps/accounts/management/commands/createinitialaccount.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from optparse import make_option
-
-from django.core.management.base import BaseCommand
-from django.db import transaction
-
-from orchestra.apps.accounts.models import Account
-
-
-class Command(BaseCommand):
- def __init__(self, *args, **kwargs):
- super(Command, self).__init__(*args, **kwargs)
- self.option_list = BaseCommand.option_list + (
- make_option('--noinput', action='store_false', dest='interactive',
- default=True),
- make_option('--username', action='store', dest='username'),
- make_option('--password', action='store', dest='password'),
- make_option('--email', action='store', dest='email'),
- )
-
- option_list = BaseCommand.option_list
- help = 'Used to create an initial account.'
-
- @transaction.atomic
- def handle(self, *args, **options):
- interactive = options.get('interactive')
- if not interactive:
- email = options.get('email')
- username = options.get('username')
- password = options.get('password')
- account = Account.objects.create(name=username)
- account.main_user = account.users.create_superuser(username, email, password, account=account, is_main=True)
- account.save()
diff --git a/orchestra/apps/accounts/management/commands/createsuperuser.py b/orchestra/apps/accounts/management/commands/createsuperuser.py
deleted file mode 100644
index abd4d26e..00000000
--- a/orchestra/apps/accounts/management/commands/createsuperuser.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from django.contrib.auth import get_user_model
-from django.contrib.auth.management.commands import createsuperuser
-
-from orchestra.apps.accounts.models import Account
-
-
-class Command(createsuperuser.Command):
- def handle(self, *args, **options):
- super(Command, self).handle(*args, **options)
- raise NotImplementedError
- users = get_user_model().objects.filter()
- if len(users) == 1 and not Account.objects.all().exists():
- user = users[0]
- user.account = Account.objects.create(user=user)
- user.save()
diff --git a/orchestra/apps/bills/actions.py b/orchestra/apps/bills/actions.py
index 1f157831..0bdbc8ed 100644
--- a/orchestra/apps/bills/actions.py
+++ b/orchestra/apps/bills/actions.py
@@ -3,8 +3,11 @@ import zipfile
from django.contrib import messages
from django.contrib.admin import helpers
-from django.http import HttpResponse
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse, HttpResponseServerError
from django.shortcuts import render
+from django.utils.encoding import force_text
+from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.admin.forms import adminmodelformset_factory
@@ -13,6 +16,26 @@ from orchestra.utils.html import html_to_pdf
from .forms import SelectSourceForm
+def validate_contact(bill):
+ """ checks if all the preconditions for bill generation are met """
+ msg = ''
+ if not hasattr(bill.account, 'invoicecontact'):
+ account = force_text(bill.account)
+ link = reverse('admin:accounts_account_change', args=(bill.account_id,))
+ link += '#invoicecontact-group'
+ msg += _('Related account "%s" doesn\'t have a declared invoice contact\n') % account
+ msg += _('You should provide one') % link
+ main = type(bill).account.field.rel.to.get_main()
+ if not hasattr(main, 'invoicecontact'):
+ account = force_text(main)
+ link = reverse('admin:accounts_account_change', args=(main.id,))
+ link += '#invoicecontact-group'
+ msg += _('Main account "%s" doesn\'t have a declared invoice contact\n') % account
+ msg += _('You should provide one') % link
+ if msg:
+ # TODO custom template
+ return HttpResponseServerError(mark_safe(msg))
+
def download_bills(modeladmin, request, queryset):
if queryset.count() > 1:
@@ -35,6 +58,9 @@ download_bills.url_name = 'download'
def view_bill(modeladmin, request, queryset):
bill = queryset.get()
+ error = validate_contact(bill)
+ if error:
+ return error
html = bill.html or bill.render()
return HttpResponse(html)
view_bill.verbose_name = _("View")
@@ -46,6 +72,10 @@ def close_bills(modeladmin, request, queryset):
if not queryset:
messages.warning(request, _("Selected bills should be in open state"))
return
+ for bill in queryset:
+ error = validate_contact(bill)
+ if error:
+ return error
SelectSourceFormSet = adminmodelformset_factory(modeladmin, SelectSourceForm, extra=0)
formset = SelectSourceFormSet(queryset=queryset)
if request.POST.get('post') == 'generic_confirmation':
@@ -79,6 +109,10 @@ close_bills.url_name = 'close'
def send_bills(modeladmin, request, queryset):
+ for bill in queryset:
+ error = validate_contact(bill)
+ if error:
+ return error
for bill in queryset:
bill.send()
modeladmin.log_change(request, bill, 'Sent')
diff --git a/orchestra/apps/bills/admin.py b/orchestra/apps/bills/admin.py
index b0942c83..5c59bf80 100644
--- a/orchestra/apps/bills/admin.py
+++ b/orchestra/apps/bills/admin.py
@@ -1,7 +1,9 @@
from django import forms
-from django.contrib import admin
+from django.contrib import admin, messages
+from django.contrib.admin.utils import unquote
from django.core.urlresolvers import reverse
from django.db import models
+from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
@@ -11,8 +13,7 @@ from orchestra.apps.accounts.admin import AccountAdminMixin
from . import settings
from .actions import download_bills, view_bill, close_bills, send_bills
from .filters import BillTypeListFilter
-from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma,
- BillLine)
+from .models import Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine
PAYMENT_STATE_COLORS = {
@@ -144,14 +145,18 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
qs = qs.annotate(models.Count('lines'))
qs = qs.prefetch_related('lines', 'lines__sublines')
return qs
-
-# def change_view(self, request, object_id, **kwargs):
-# opts = self.model._meta
-# if opts.module_name == 'bill':
-# obj = self.get_object(request, unquote(object_id))
-# return redirect(
-# reverse('admin:bills_%s_change' % obj.type.lower(), args=[obj.pk]))
-# return super(BillAdmin, self).change_view(request, object_id, **kwargs)
+
+ def change_view(self, request, object_id, **kwargs):
+ bill = self.get_object(request, unquote(object_id))
+ # TODO raise404, here and everywhere
+ if not hasattr(bill.account, 'invoicecontact'):
+ create_link = reverse('admin:accounts_account_change', args=(bill.account_id,))
+ create_link += '#invoicecontact-group'
+ messages.warning(request, mark_safe(_(
+ 'Be aware, related contact doesn\'t have a billing contact defined, '
+ 'bill can not be generated until one is provided' % create_link
+ )))
+ return super(BillAdmin, self).change_view(request, object_id, **kwargs)
admin.site.register(Bill, BillAdmin)
diff --git a/orchestra/apps/bills/models.py b/orchestra/apps/bills/models.py
index 86ecf6d1..3e861f36 100644
--- a/orchestra/apps/bills/models.py
+++ b/orchestra/apps/bills/models.py
@@ -144,6 +144,7 @@ class Bill(models.Model):
self.save()
def send(self):
+ html = self.html or self.render()
self.account.send_email(
template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE,
context={
@@ -151,7 +152,7 @@ class Bill(models.Model):
},
contacts=(Contact.BILLING,),
attachments=[
- ('%s.pdf' % self.number, html_to_pdf(self.html), 'application/pdf')
+ ('%s.pdf' % self.number, html_to_pdf(html), 'application/pdf')
]
)
self.is_sent = True
diff --git a/orchestra/apps/contacts/admin.py b/orchestra/apps/contacts/admin.py
index ead27d3e..d076feab 100644
--- a/orchestra/apps/contacts/admin.py
+++ b/orchestra/apps/contacts/admin.py
@@ -96,11 +96,7 @@ class ContactInline(InvoiceContactInline):
def has_invoice(account):
- try:
- account.invoicecontact
- except InvoiceContact.DoesNotExist:
- return False
- return True
+ return hasattr(account, 'invoicecontact')
has_invoice.boolean = True
has_invoice.admin_order_field = 'invoicecontact'
diff --git a/orchestra/apps/contacts/models.py b/orchestra/apps/contacts/models.py
index c7fd4c93..b9adbcc3 100644
--- a/orchestra/apps/contacts/models.py
+++ b/orchestra/apps/contacts/models.py
@@ -61,6 +61,9 @@ class InvoiceContact(models.Model):
country = models.CharField(_("country"), max_length=20,
default=settings.CONTACTS_DEFAULT_COUNTRY)
vat = models.CharField(_("VAT number"), max_length=64)
+
+ def __unicode__(self):
+ return self.name
accounts.register(Contact)
diff --git a/orchestra/apps/mails/backends.py b/orchestra/apps/mails/backends.py
index c4306d4c..e68992e2 100644
--- a/orchestra/apps/mails/backends.py
+++ b/orchestra/apps/mails/backends.py
@@ -13,7 +13,7 @@ from .models import Address
class MailSystemUserBackend(ServiceController):
verbose_name = _("Mail system user")
model = 'mails.Mailbox'
- # TODO related_models = ('resources__content_type') ??
+ # TODO related_models = ('resources__content_type') ?? needed for updating disk usage from resource.data
DEFAULT_GROUP = 'postfix'
@@ -35,16 +35,33 @@ class MailSystemUserBackend(ServiceController):
"# Sieve Filter\n"
"# Generated by Orchestra %s\n\n" % now
)
- if mailbox.use_custom_filtering:
+ if mailbox.custom_filtering:
context['filtering'] += mailbox.custom_filtering
else:
context['filtering'] += settings.EMAILS_DEFAUL_FILTERING
context['filter_path'] = os.path.join(context['home'], '.orchestra.sieve')
self.append("echo '%(filtering)s' > %(filter_path)s" % context)
+ def set_quota(self, mailbox, context):
+ if not hasattr(mailbox, 'resources'):
+ return
+ context.update({
+ 'maildir_path': '~%(username)s/Maildir' % context,
+ 'maildirsize_path': '~%(username)s/Maildir/maildirsize' % context,
+ 'quota': mailbox.resources.disk.allocated*1000*1000,
+ })
+ self.append("mkdir -p %(maildir_path)s" % context)
+ self.append(
+ "sed -i '1s/.*/%(quota)s,S/' %(maildirsize_path)s || {"
+ " echo '%(quota)s,S' > %(maildirsize_path)s && "
+ " chown %(username)s %(maildirsize_path)s;"
+ "}" % context
+ )
+
def save(self, mailbox):
context = self.get_context(mailbox)
self.create_user(context)
+ self.set_quota(mailbox, context)
self.generate_filter(mailbox, context)
def delete(self, mailbox):
@@ -56,7 +73,7 @@ class MailSystemUserBackend(ServiceController):
def get_context(self, mailbox):
context = {
- 'name': mailbox.nam,
+ 'name': mailbox.name,
'username': mailbox.name,
'password': mailbox.password if mailbox.is_active else '*%s' % mailbox.password,
'group': self.DEFAULT_GROUP
@@ -155,6 +172,6 @@ class MaildirDisk(ServiceMonitor):
def get_context(self, mailbox):
context = MailSystemUserBackend().get_context(mailbox)
context['home'] = settings.EMAILS_HOME % context
- context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize')
+ context['rr_path'] = os.path.join(context['home'], 'Maildir/maildirsize')
context['object_id'] = mailbox.pk
return context
diff --git a/orchestra/apps/mails/models.py b/orchestra/apps/mails/models.py
index c761e6b0..b9ecf593 100644
--- a/orchestra/apps/mails/models.py
+++ b/orchestra/apps/mails/models.py
@@ -1,5 +1,6 @@
from django.core.validators import RegexValidator
from django.db import models
+from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from orchestra.core import services
@@ -30,6 +31,10 @@ class Mailbox(models.Model):
def __unicode__(self):
return self.name
+
+ @cached_property
+ def active(self):
+ return self.is_active and self.account.is_active
class Address(models.Model):
diff --git a/orchestra/apps/orchestration/models.py b/orchestra/apps/orchestration/models.py
index 3bd6aeb4..bfff6ce4 100644
--- a/orchestra/apps/orchestration/models.py
+++ b/orchestra/apps/orchestration/models.py
@@ -68,7 +68,7 @@ class BackendLog(models.Model):
@property
def execution_time(self):
- return (self.last_update-self.created).total_seconds()
+ return (self.updated_at-self.created_at).total_seconds()
def backend_class(self):
return ServiceBackend.get_backend(self.backend)
diff --git a/orchestra/apps/orders/admin.py b/orchestra/apps/orders/admin.py
index 6cf277cb..368d7f46 100644
--- a/orchestra/apps/orders/admin.py
+++ b/orchestra/apps/orders/admin.py
@@ -29,7 +29,7 @@ class OrderAdmin(AccountAdminMixin, admin.ModelAdmin):
def display_billed_until(self, order):
value = order.billed_until
color = ''
- if value and value < timezone.now():
+ if value and value < timezone.now().date():
color = 'style="color:red;"'
return '{human}'.format(
raw=escape(str(value)), color=color, human=escape(naturaldate(value)),
diff --git a/orchestra/apps/orders/forms.py b/orchestra/apps/orders/forms.py
index 8e98fb5b..31cce901 100644
--- a/orchestra/apps/orders/forms.py
+++ b/orchestra/apps/orders/forms.py
@@ -15,12 +15,12 @@ class BillSelectedOptionsForm(AdminFormMixin, forms.Form):
label=_("Billing point"), widget=widgets.AdminDateWidget,
help_text=_("Date you want to bill selected orders"))
fixed_point = forms.BooleanField(initial=False, required=False,
- label=_("fixed point"),
+ label=_("Fixed point"),
help_text=_("Deisgnates whether you want the billing point to be an "
"exact date, or adapt it to the billing period."))
is_proforma = forms.BooleanField(initial=False, required=False,
- label=_("Pro-forma, billing simulation"),
- help_text=_("O."))
+ label=_("Pro-forma (billing simulation)"),
+ help_text=_("Creates a Pro Forma instead of billing the orders."))
new_open = forms.BooleanField(initial=False, required=False,
label=_("Create a new open bill"),
help_text=_("Deisgnates whether you want to put this orders on a new "
diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py
index dbbaa764..7e4a1323 100644
--- a/orchestra/apps/orders/models.py
+++ b/orchestra/apps/orders/models.py
@@ -58,6 +58,26 @@ class OrderQuerySet(models.QuerySet):
return self.exclude(**qs)
return self.filter(**qs)
+ def get_related(self, **options):
+ Service = get_model(settings.ORDERS_SERVICE_MODEL)
+ conflictive = self.filter(service__metric='')
+ conflictive = conflictive.exclude(service__billing_period=Service.NEVER)
+ conflictive = conflictive.select_related('service').group_by('account_id', 'service')
+ qs = Q()
+ for account_id, services in conflictive.iteritems():
+ for service, orders in services.iteritems():
+ end = datetime.date.min
+ bp = None
+ for order in orders:
+ bp = service.handler.get_billing_point(order, **options)
+ end = max(end, bp)
+ qs = qs | Q(
+ Q(service=service, account=account_id, registered_on__lt=end) &
+ Q(Q(billed_until__isnull=True) | Q(billed_until__lt=end))
+ )
+ ids = self.values_list('id', flat=True)
+ return self.model.objects.filter(qs).exclude(id__in=ids)
+
def pricing_orders(self, ini, end):
return self.filter(billed_until__isnull=False, billed_until__gt=ini,
registered_on__lt=end)
@@ -103,7 +123,7 @@ class Order(models.Model):
@classmethod
def update_orders(cls, instance, service=None):
if service is None:
- Service = get_model(*settings.ORDERS_SERVICE_MODEL.split('.'))
+ Service = get_model(settings.ORDERS_SERVICE_MODEL)
services = Service.get_services(instance)
else:
services = [service]
diff --git a/orchestra/apps/resources/tasks.py b/orchestra/apps/resources/tasks.py
index 5719f8bd..1e65d0e4 100644
--- a/orchestra/apps/resources/tasks.py
+++ b/orchestra/apps/resources/tasks.py
@@ -14,7 +14,7 @@ def monitor(resource_id):
# Execute monitors
for monitor_name in resource.monitors:
backend = ServiceMonitor.get_backend(monitor_name)
- model = get_model(*backend.model.split('.'))
+ model = get_model(backend.model)
operations = []
# Execute monitor
for obj in model.objects.all():
diff --git a/orchestra/apps/systemusers/admin.py b/orchestra/apps/systemusers/admin.py
index 93b3d54b..0abe1cd3 100644
--- a/orchestra/apps/systemusers/admin.py
+++ b/orchestra/apps/systemusers/admin.py
@@ -14,7 +14,7 @@ from .models import SystemUser
class SystemUserAdmin(AccountAdminMixin, ExtendedModelAdmin):
list_display = ('username', 'account_link', 'shell', 'home', 'is_active',)
- list_filter = ('is_active',)
+ list_filter = ('is_active', 'shell')
fieldsets = (
(None, {
'fields': ('username', 'password', 'account_link', 'is_active')