Cosmetics

This commit is contained in:
Marc 2014-09-30 14:46:29 +00:00
parent f83571afc9
commit 23d62c2d77
19 changed files with 117 additions and 90 deletions

13
TODO.md
View File

@ -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 * AccountAdminMixin auto adds 'account__name' on searchfields and handle account_link on fieldsets
* account defiition: * Separate panel from server passwords? Store passwords on panel?
* 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?
* What fields we really need on contacts? name email phone and what more? * What fields we really need on contacts? name email phone and what more?
* Redirect junk emails and delete every 30 days?

View File

@ -30,10 +30,7 @@ class AccountAdmin(auth.UserAdmin, ExtendedModelAdmin):
'fields': ('first_name', 'last_name', 'email', ('type', 'language'), 'comments'), 'fields': ('first_name', 'last_name', 'email', ('type', 'language'), 'comments'),
}), }),
(_("Permissions"), { (_("Permissions"), {
'fields': ('is_superuser', 'is_active') 'fields': ('is_superuser',)
}),
(_("Important dates"), {
'fields': ('last_login', 'date_joined')
}), }),
) )
fieldsets = ( fieldsets = (
@ -47,6 +44,7 @@ class AccountAdmin(auth.UserAdmin, ExtendedModelAdmin):
'fields': ('is_superuser', 'is_active') 'fields': ('is_superuser', 'is_active')
}), }),
(_("Important dates"), { (_("Important dates"), {
'classes': ('collapse',),
'fields': ('last_login', 'date_joined') 'fields': ('last_login', 'date_joined')
}), }),
) )

View File

@ -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()

View File

@ -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()

View File

@ -3,8 +3,11 @@ import zipfile
from django.contrib import messages from django.contrib import messages
from django.contrib.admin import helpers 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.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 django.utils.translation import ugettext_lazy as _
from orchestra.admin.forms import adminmodelformset_factory from orchestra.admin.forms import adminmodelformset_factory
@ -13,6 +16,26 @@ from orchestra.utils.html import html_to_pdf
from .forms import SelectSourceForm 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 <a href="%s">provide</a> 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 <a href="%s">provide</a> one') % link
if msg:
# TODO custom template
return HttpResponseServerError(mark_safe(msg))
def download_bills(modeladmin, request, queryset): def download_bills(modeladmin, request, queryset):
if queryset.count() > 1: if queryset.count() > 1:
@ -35,6 +58,9 @@ download_bills.url_name = 'download'
def view_bill(modeladmin, request, queryset): def view_bill(modeladmin, request, queryset):
bill = queryset.get() bill = queryset.get()
error = validate_contact(bill)
if error:
return error
html = bill.html or bill.render() html = bill.html or bill.render()
return HttpResponse(html) return HttpResponse(html)
view_bill.verbose_name = _("View") view_bill.verbose_name = _("View")
@ -46,6 +72,10 @@ def close_bills(modeladmin, request, queryset):
if not queryset: if not queryset:
messages.warning(request, _("Selected bills should be in open state")) messages.warning(request, _("Selected bills should be in open state"))
return return
for bill in queryset:
error = validate_contact(bill)
if error:
return error
SelectSourceFormSet = adminmodelformset_factory(modeladmin, SelectSourceForm, extra=0) SelectSourceFormSet = adminmodelformset_factory(modeladmin, SelectSourceForm, extra=0)
formset = SelectSourceFormSet(queryset=queryset) formset = SelectSourceFormSet(queryset=queryset)
if request.POST.get('post') == 'generic_confirmation': if request.POST.get('post') == 'generic_confirmation':
@ -79,6 +109,10 @@ close_bills.url_name = 'close'
def send_bills(modeladmin, request, queryset): def send_bills(modeladmin, request, queryset):
for bill in queryset:
error = validate_contact(bill)
if error:
return error
for bill in queryset: for bill in queryset:
bill.send() bill.send()
modeladmin.log_change(request, bill, 'Sent') modeladmin.log_change(request, bill, 'Sent')

View File

@ -1,7 +1,9 @@
from django import forms 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.core.urlresolvers import reverse
from django.db import models from django.db import models
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
@ -11,8 +13,7 @@ from orchestra.apps.accounts.admin import AccountAdminMixin
from . import settings from . import settings
from .actions import download_bills, view_bill, close_bills, send_bills from .actions import download_bills, view_bill, close_bills, send_bills
from .filters import BillTypeListFilter from .filters import BillTypeListFilter
from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, from .models import Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine
BillLine)
PAYMENT_STATE_COLORS = { PAYMENT_STATE_COLORS = {
@ -144,14 +145,18 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
qs = qs.annotate(models.Count('lines')) qs = qs.annotate(models.Count('lines'))
qs = qs.prefetch_related('lines', 'lines__sublines') qs = qs.prefetch_related('lines', 'lines__sublines')
return qs return qs
# def change_view(self, request, object_id, **kwargs): def change_view(self, request, object_id, **kwargs):
# opts = self.model._meta bill = self.get_object(request, unquote(object_id))
# if opts.module_name == 'bill': # TODO raise404, here and everywhere
# obj = self.get_object(request, unquote(object_id)) if not hasattr(bill.account, 'invoicecontact'):
# return redirect( create_link = reverse('admin:accounts_account_change', args=(bill.account_id,))
# reverse('admin:bills_%s_change' % obj.type.lower(), args=[obj.pk])) create_link += '#invoicecontact-group'
# return super(BillAdmin, self).change_view(request, object_id, **kwargs) messages.warning(request, mark_safe(_(
'Be aware, related contact doesn\'t have a billing contact defined, '
'bill can not be generated until one is <a href="%s">provided</a>' % create_link
)))
return super(BillAdmin, self).change_view(request, object_id, **kwargs)
admin.site.register(Bill, BillAdmin) admin.site.register(Bill, BillAdmin)

View File

@ -144,6 +144,7 @@ class Bill(models.Model):
self.save() self.save()
def send(self): def send(self):
html = self.html or self.render()
self.account.send_email( self.account.send_email(
template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE, template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE,
context={ context={
@ -151,7 +152,7 @@ class Bill(models.Model):
}, },
contacts=(Contact.BILLING,), contacts=(Contact.BILLING,),
attachments=[ 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 self.is_sent = True

View File

@ -96,11 +96,7 @@ class ContactInline(InvoiceContactInline):
def has_invoice(account): def has_invoice(account):
try: return hasattr(account, 'invoicecontact')
account.invoicecontact
except InvoiceContact.DoesNotExist:
return False
return True
has_invoice.boolean = True has_invoice.boolean = True
has_invoice.admin_order_field = 'invoicecontact' has_invoice.admin_order_field = 'invoicecontact'

View File

@ -61,6 +61,9 @@ class InvoiceContact(models.Model):
country = models.CharField(_("country"), max_length=20, country = models.CharField(_("country"), max_length=20,
default=settings.CONTACTS_DEFAULT_COUNTRY) default=settings.CONTACTS_DEFAULT_COUNTRY)
vat = models.CharField(_("VAT number"), max_length=64) vat = models.CharField(_("VAT number"), max_length=64)
def __unicode__(self):
return self.name
accounts.register(Contact) accounts.register(Contact)

View File

@ -13,7 +13,7 @@ from .models import Address
class MailSystemUserBackend(ServiceController): class MailSystemUserBackend(ServiceController):
verbose_name = _("Mail system user") verbose_name = _("Mail system user")
model = 'mails.Mailbox' 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' DEFAULT_GROUP = 'postfix'
@ -35,16 +35,33 @@ class MailSystemUserBackend(ServiceController):
"# Sieve Filter\n" "# Sieve Filter\n"
"# Generated by Orchestra %s\n\n" % now "# Generated by Orchestra %s\n\n" % now
) )
if mailbox.use_custom_filtering: if mailbox.custom_filtering:
context['filtering'] += mailbox.custom_filtering context['filtering'] += mailbox.custom_filtering
else: else:
context['filtering'] += settings.EMAILS_DEFAUL_FILTERING context['filtering'] += settings.EMAILS_DEFAUL_FILTERING
context['filter_path'] = os.path.join(context['home'], '.orchestra.sieve') context['filter_path'] = os.path.join(context['home'], '.orchestra.sieve')
self.append("echo '%(filtering)s' > %(filter_path)s" % context) 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): def save(self, mailbox):
context = self.get_context(mailbox) context = self.get_context(mailbox)
self.create_user(context) self.create_user(context)
self.set_quota(mailbox, context)
self.generate_filter(mailbox, context) self.generate_filter(mailbox, context)
def delete(self, mailbox): def delete(self, mailbox):
@ -56,7 +73,7 @@ class MailSystemUserBackend(ServiceController):
def get_context(self, mailbox): def get_context(self, mailbox):
context = { context = {
'name': mailbox.nam, 'name': mailbox.name,
'username': mailbox.name, 'username': mailbox.name,
'password': mailbox.password if mailbox.is_active else '*%s' % mailbox.password, 'password': mailbox.password if mailbox.is_active else '*%s' % mailbox.password,
'group': self.DEFAULT_GROUP 'group': self.DEFAULT_GROUP
@ -155,6 +172,6 @@ class MaildirDisk(ServiceMonitor):
def get_context(self, mailbox): def get_context(self, mailbox):
context = MailSystemUserBackend().get_context(mailbox) 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['rr_path'] = os.path.join(context['home'], 'Maildir/maildirsize')
context['object_id'] = mailbox.pk context['object_id'] = mailbox.pk
return context return context

View File

@ -1,5 +1,6 @@
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.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
@ -30,6 +31,10 @@ class Mailbox(models.Model):
def __unicode__(self): def __unicode__(self):
return self.name return self.name
@cached_property
def active(self):
return self.is_active and self.account.is_active
class Address(models.Model): class Address(models.Model):

View File

@ -68,7 +68,7 @@ class BackendLog(models.Model):
@property @property
def execution_time(self): 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): def backend_class(self):
return ServiceBackend.get_backend(self.backend) return ServiceBackend.get_backend(self.backend)

View File

@ -29,7 +29,7 @@ class OrderAdmin(AccountAdminMixin, admin.ModelAdmin):
def display_billed_until(self, order): def display_billed_until(self, order):
value = order.billed_until value = order.billed_until
color = '' color = ''
if value and value < timezone.now(): if value and value < timezone.now().date():
color = 'style="color:red;"' color = 'style="color:red;"'
return '<span title="{raw}" {color}>{human}</span>'.format( return '<span title="{raw}" {color}>{human}</span>'.format(
raw=escape(str(value)), color=color, human=escape(naturaldate(value)), raw=escape(str(value)), color=color, human=escape(naturaldate(value)),

View File

@ -15,12 +15,12 @@ class BillSelectedOptionsForm(AdminFormMixin, forms.Form):
label=_("Billing point"), widget=widgets.AdminDateWidget, label=_("Billing point"), widget=widgets.AdminDateWidget,
help_text=_("Date you want to bill selected orders")) help_text=_("Date you want to bill selected orders"))
fixed_point = forms.BooleanField(initial=False, required=False, 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 " help_text=_("Deisgnates whether you want the billing point to be an "
"exact date, or adapt it to the billing period.")) "exact date, or adapt it to the billing period."))
is_proforma = forms.BooleanField(initial=False, required=False, is_proforma = forms.BooleanField(initial=False, required=False,
label=_("Pro-forma, billing simulation"), label=_("Pro-forma (billing simulation)"),
help_text=_("O.")) help_text=_("Creates a Pro Forma instead of billing the orders."))
new_open = forms.BooleanField(initial=False, required=False, new_open = forms.BooleanField(initial=False, required=False,
label=_("Create a new open bill"), label=_("Create a new open bill"),
help_text=_("Deisgnates whether you want to put this orders on a new " help_text=_("Deisgnates whether you want to put this orders on a new "

View File

@ -58,6 +58,26 @@ class OrderQuerySet(models.QuerySet):
return self.exclude(**qs) return self.exclude(**qs)
return self.filter(**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): def pricing_orders(self, ini, end):
return self.filter(billed_until__isnull=False, billed_until__gt=ini, return self.filter(billed_until__isnull=False, billed_until__gt=ini,
registered_on__lt=end) registered_on__lt=end)
@ -103,7 +123,7 @@ class Order(models.Model):
@classmethod @classmethod
def update_orders(cls, instance, service=None): def update_orders(cls, instance, service=None):
if service is 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) services = Service.get_services(instance)
else: else:
services = [service] services = [service]

View File

@ -14,7 +14,7 @@ def monitor(resource_id):
# Execute monitors # Execute monitors
for monitor_name in resource.monitors: for monitor_name in resource.monitors:
backend = ServiceMonitor.get_backend(monitor_name) backend = ServiceMonitor.get_backend(monitor_name)
model = get_model(*backend.model.split('.')) model = get_model(backend.model)
operations = [] operations = []
# Execute monitor # Execute monitor
for obj in model.objects.all(): for obj in model.objects.all():

View File

@ -14,7 +14,7 @@ from .models import SystemUser
class SystemUserAdmin(AccountAdminMixin, ExtendedModelAdmin): class SystemUserAdmin(AccountAdminMixin, ExtendedModelAdmin):
list_display = ('username', 'account_link', 'shell', 'home', 'is_active',) list_display = ('username', 'account_link', 'shell', 'home', 'is_active',)
list_filter = ('is_active',) list_filter = ('is_active', 'shell')
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('username', 'password', 'account_link', 'is_active') 'fields': ('username', 'password', 'account_link', 'is_active')