Cosmetics
This commit is contained in:
parent
f83571afc9
commit
23d62c2d77
13
TODO.md
13
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?
|
||||
|
|
|
@ -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')
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
|
@ -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 <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):
|
||||
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')
|
||||
|
|
|
@ -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 <a href="%s">provided</a>' % create_link
|
||||
)))
|
||||
return super(BillAdmin, self).change_view(request, object_id, **kwargs)
|
||||
|
||||
|
||||
admin.site.register(Bill, BillAdmin)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 '<span title="{raw}" {color}>{human}</span>'.format(
|
||||
raw=escape(str(value)), color=color, human=escape(naturaldate(value)),
|
||||
|
|
|
@ -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 "
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue