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')