diff --git a/orchestra/apps/mailboxes/admin.py b/orchestra/apps/mailboxes/admin.py
index 7e4e663e..209110b6 100644
--- a/orchestra/apps/mailboxes/admin.py
+++ b/orchestra/apps/mailboxes/admin.py
@@ -1,4 +1,5 @@
import copy
+from urlparse import parse_qs
from django import forms
from django.contrib import admin
@@ -25,19 +26,22 @@ class AutoresponseInline(admin.StackedInline):
return super(AutoresponseInline, self).formfield_for_dbfield(db_field, **kwargs)
-class MailboxAdmin(ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdmin):
+class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin):
list_display = (
'name', 'account_link', 'filtering', 'display_addresses'
)
list_filter = (HasAddressListFilter, 'filtering')
add_fieldsets = (
(None, {
- 'fields': ('account', 'name', 'password1', 'password2', 'filtering'),
+ 'fields': ('account_link', 'name', 'password1', 'password2', 'filtering'),
}),
(_("Custom filtering"), {
'classes': ('collapse',),
'fields': ('custom_filtering',),
}),
+ (_("Addresses"), {
+ 'fields': ('addresses',)
+ }),
)
fieldsets = (
(None, {
@@ -48,10 +52,10 @@ class MailboxAdmin(ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdm
'fields': ('custom_filtering',),
}),
(_("Addresses"), {
- 'fields': ('addresses_field',)
+ 'fields': ('addresses',)
}),
)
- readonly_fields = ('account_link', 'display_addresses', 'addresses_field')
+ readonly_fields = ('account_link', 'display_addresses')
change_readonly_fields = ('name',)
add_form = MailboxCreationForm
form = MailboxChangeForm
@@ -73,24 +77,15 @@ class MailboxAdmin(ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdm
fieldsets[1][1]['classes'] = fieldsets[0][1]['fields'] + ('open',)
return fieldsets
- def addresses_field(self, mailbox):
- """ Address form field with "Add address" button """
- account = mailbox.account
- add_url = reverse('admin:mailboxes_address_add')
- add_url += '?account=%d&mailboxes=%s' % (account.pk, mailbox.pk)
- img = ''
- onclick = 'onclick="return showAddAnotherPopup(this);"'
- add_link = '{img} Add address'.format(
- add_url=add_url, onclick=onclick, img=img)
- value = '%s
' % add_link
- for pk, name, domain in mailbox.addresses.values_list('pk', 'name', 'domain__name'):
- url = reverse('admin:mailboxes_address_change', args=(pk,))
- name = '%s@%s' % (name, domain)
- value += '
%s' % (url, name)
- value = '' % value
- return mark_safe('%s
' % value)
- addresses_field.short_description = _("Addresses")
- addresses_field.allow_tags = True
+ def get_form(self, *args, **kwargs):
+ form = super(MailboxAdmin, self).get_form(*args, **kwargs)
+ form.modeladmin = self
+ return form
+
+ def save_model(self, request, obj, form, change):
+ """ save hacky mailbox.addresses """
+ super(MailboxAdmin, self).save_model(request, obj, form, change)
+ obj.addresses = form.cleaned_data['addresses']
class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
@@ -138,6 +133,15 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
""" Select related for performance """
qs = super(AddressAdmin, self).get_queryset(request)
return qs.select_related('domain')
+
+ def get_fields(self, request, obj=None):
+ """ Remove mailboxes field when creating address from a popup i.e. from mailbox add form """
+ fields = super(AddressAdmin, self).get_fields(request, obj=obj)
+ if '_to_field' in parse_qs(request.META['QUERY_STRING']):
+ # Add address popup
+ fields = list(fields)
+ fields.remove('mailboxes')
+ return fields
admin.site.register(Mailbox, MailboxAdmin)
diff --git a/orchestra/apps/mailboxes/forms.py b/orchestra/apps/mailboxes/forms.py
index f05bb145..7d9c376e 100644
--- a/orchestra/apps/mailboxes/forms.py
+++ b/orchestra/apps/mailboxes/forms.py
@@ -1,10 +1,41 @@
from django import forms
+from django.contrib.admin import widgets
+from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.forms import UserCreationForm, UserChangeForm
+from orchestra.utils.python import AttrDict
+
+from .models import Address, Mailbox
-class CleanCustomFilteringMixin(object):
+class MailboxForm(forms.ModelForm):
+ """ hacky form for adding reverse M2M form field for Mailbox.addresses """
+ addresses = forms.ModelMultipleChoiceField(queryset=Address.objects, required=False,
+ widget=widgets.FilteredSelectMultiple(verbose_name=_('Pizzas'), is_stacked=False))
+
+ def __init__(self, *args, **kwargs):
+ super(MailboxForm, self).__init__(*args, **kwargs)
+ field = AttrDict(**{
+ 'to': Address,
+ 'get_related_field': lambda: AttrDict(name='id'),
+ })
+ widget = self.fields['addresses'].widget
+ self.fields['addresses'].widget = widgets.RelatedFieldWidgetWrapper(widget, field,
+ self.modeladmin.admin_site, can_add_related=True)
+ old_render = self.fields['addresses'].widget.render
+ def render(*args, **kwargs):
+ output = old_render(*args, **kwargs)
+ args = 'account=%i' % self.modeladmin.account.pk
+ output = output.replace('/add/?', '/add/?%s&' % args)
+ return mark_safe(output)
+ self.fields['addresses'].widget.render = render
+ queryset = self.fields['addresses'].queryset
+ self.fields['addresses'].queryset = queryset.filter(account=self.modeladmin.account.pk)
+
+ if self.instance and self.instance.pk:
+ self.fields['addresses'].initial = self.instance.addresses.all()
+
def clean_custom_filtering(self):
filtering = self.cleaned_data['filtering']
custom_filtering = self.cleaned_data['custom_filtering']
@@ -13,11 +44,12 @@ class CleanCustomFilteringMixin(object):
return custom_filtering
-class MailboxChangeForm(CleanCustomFilteringMixin, UserChangeForm):
+
+class MailboxChangeForm(UserChangeForm, MailboxForm):
pass
-class MailboxCreationForm(CleanCustomFilteringMixin, UserCreationForm):
+class MailboxCreationForm(UserCreationForm, MailboxForm):
def clean_name(self):
# Since model.clean() will check this, this is redundant,
# but it sets a nicer error message than the ORM and avoids conflicts with contrib.auth
@@ -26,11 +58,12 @@ class MailboxCreationForm(CleanCustomFilteringMixin, UserCreationForm):
self._meta.model._default_manager.get(name=name)
except self._meta.model.DoesNotExist:
return name
- raise forms.ValidationError(self.error_messages['duplicate_name'])
+ raise forms.ValidationError(self.error_messages['duplicate_username'])
class AddressForm(forms.ModelForm):
def clean(self):
cleaned_data = super(AddressForm, self).clean()
- if not cleaned_data['mailboxes'] and not cleaned_data['forward']:
+ if not cleaned_data.get('mailboxes', True) and not cleaned_data['forward']:
raise forms.ValidationError(_("Mailboxes or forward address should be provided"))
+
diff --git a/orchestra/apps/mailboxes/models.py b/orchestra/apps/mailboxes/models.py
index d183cd9e..05bcc208 100644
--- a/orchestra/apps/mailboxes/models.py
+++ b/orchestra/apps/mailboxes/models.py
@@ -29,9 +29,6 @@ class Mailbox(models.Model):
help_text=_("Arbitrary email filtering in sieve language. "
"This overrides any automatic junk email filtering"))
is_active = models.BooleanField(_("active"), default=True)
-# addresses = models.ManyToManyField('mailboxes.Address',
-# verbose_name=_("addresses"),
-# related_name='mailboxes', blank=True)
class Meta:
verbose_name_plural = _("mailboxes")
diff --git a/orchestra/apps/orders/admin.py b/orchestra/apps/orders/admin.py
index ace28745..bc77ec53 100644
--- a/orchestra/apps/orders/admin.py
+++ b/orchestra/apps/orders/admin.py
@@ -15,10 +15,9 @@ from .models import Order, MetricStorage
class OrderAdmin(ChangeListDefaultFilter, AccountAdminMixin, admin.ModelAdmin):
list_display = (
- 'id', 'service', 'account_link', 'content_object_link',
+ 'id', 'service_link', 'account_link', 'content_object_link',
'display_registered_on', 'display_billed_until', 'display_cancelled_on'
)
- list_display_links = ('id', 'service')
list_filter = (ActiveOrderListFilter, BilledOrderListFilter, IgnoreOrderListFilter, 'service',)
default_changelist_filters = (
('ignore', '0'),
@@ -26,6 +25,7 @@ class OrderAdmin(ChangeListDefaultFilter, AccountAdminMixin, admin.ModelAdmin):
actions = (BillSelectedOrders(), mark_as_ignored, mark_as_not_ignored)
date_hierarchy = 'registered_on'
+ service_link = admin_link('service')
content_object_link = admin_link('content_object', order=False)
display_registered_on = admin_date('registered_on')
display_cancelled_on = admin_date('cancelled_on')
diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py
index 88821a98..38994cf7 100644
--- a/orchestra/apps/orders/models.py
+++ b/orchestra/apps/orders/models.py
@@ -69,18 +69,24 @@ class OrderQuerySet(models.QuerySet):
for service, orders in services.iteritems():
if not service.rates.exists():
continue
+ ini = datetime.date.max
end = datetime.date.min
bp = None
for order in orders:
bp = service.handler.get_billing_point(order, **options)
end = max(end, bp)
- # FIXME exclude cancelled except cancelled and billed > ini
- qs = qs | Q(
- Q(service=service, account=account_id, registered_on__lt=end) &
- Q(Q(billed_until__isnull=True) | Q(billed_until__lt=end))
+ ini = min(ini, order.billed_until or order.registered_on)
+ qs |= Q(
+ Q(service=service, account=account_id, registered_on__lt=end) & Q(
+ Q(billed_until__isnull=True) | Q(billed_until__lt=end)
+ ) & Q(
+ Q(cancelled_on__isnull=True) | Q(cancelled_on__gt=ini)
+ )
)
+ if not qs:
+ return self.model.objects.none()
ids = self.values_list('id', flat=True)
- return self.model.objects.filter(qs).exclude(id__in=ids, ignore=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,
diff --git a/orchestra/apps/payments/actions.py b/orchestra/apps/payments/actions.py
index b29b191c..9d702895 100644
--- a/orchestra/apps/payments/actions.py
+++ b/orchestra/apps/payments/actions.py
@@ -5,7 +5,7 @@ from django.db import transaction
from django.shortcuts import render
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ungettext, ugettext_lazy as _
from orchestra.admin.decorators import action_with_confirmation
from orchestra.admin.utils import change_url
@@ -47,7 +47,11 @@ def mark_as_executed(modeladmin, request, queryset, extra_context={}):
for trans in queryset:
trans.mark_as_executed()
modeladmin.log_change(request, trans, _("Executed"))
- msg = _("%s selected transactions have been marked as executed.") % queryset.count()
+ num = len(queryset)
+ msg = ungettext(
+ _("One selected transaction has been marked as executed."),
+ _("%s selected transactions have been marked as executed.") % num,
+ num)
modeladmin.message_user(request, msg)
mark_as_executed.url_name = 'execute'
mark_as_executed.verbose_name = _("Mark as executed")
@@ -59,7 +63,11 @@ def mark_as_secured(modeladmin, request, queryset):
for trans in queryset:
trans.mark_as_secured()
modeladmin.log_change(request, trans, _("Secured"))
- msg = _("%s selected transactions have been marked as secured.") % queryset.count()
+ num = len(queryset)
+ msg = ungettext(
+ _("One selected transaction has been marked as secured."),
+ _("%s selected transactions have been marked as secured.") % num,
+ num)
modeladmin.message_user(request, msg)
mark_as_secured.url_name = 'secure'
mark_as_secured.verbose_name = _("Mark as secured")
@@ -71,7 +79,11 @@ def mark_as_rejected(modeladmin, request, queryset):
for trans in queryset:
trans.mark_as_rejected()
modeladmin.log_change(request, trans, _("Rejected"))
- msg = _("%s selected transactions have been marked as rejected.") % queryset.count()
+ num = len(queryset)
+ msg = ungettext(
+ _("One selected transaction has been marked as rejected."),
+ _("%s selected transactions have been marked as rejected.") % num,
+ num)
modeladmin.message_user(request, msg)
mark_as_rejected.url_name = 'reject'
mark_as_rejected.verbose_name = _("Mark as rejected")
@@ -89,8 +101,8 @@ def _format_display_objects(modeladmin, request, queryset, related):
attr, verb = related
for related in getattr(obj.transactions, attr)():
subobjects.append(
- mark_safe('{0}: {2} will be marked as {3}'.format(
- capfirst(related.get_type().lower()), change_url(related), related, verb))
+ mark_safe('Transaction: {} will be marked as {}'.format(
+ change_url(related), related, verb))
)
objects.append(subobjects)
return {'display_objects': objects}
@@ -106,7 +118,11 @@ def mark_process_as_executed(modeladmin, request, queryset):
for process in queryset:
process.mark_as_executed()
modeladmin.log_change(request, process, _("Executed"))
- msg = _("%s selected processes have been marked as executed.") % queryset.count()
+ num = len(queryset)
+ msg = ungettext(
+ _("One selected process has been marked as executed."),
+ _("%s selected processes have been marked as executed.") % num,
+ num)
modeladmin.message_user(request, msg)
mark_process_as_executed.url_name = 'executed'
mark_process_as_executed.verbose_name = _("Mark as executed")
@@ -118,7 +134,11 @@ def abort(modeladmin, request, queryset):
for process in queryset:
process.abort()
modeladmin.log_change(request, process, _("Aborted"))
- msg = _("%s selected processes have been aborted.") % queryset.count()
+ num = len(queryset)
+ msg = ungettext(
+ _("One selected process has been aborted."),
+ _("%s selected processes have been aborted.") % num,
+ num)
modeladmin.message_user(request, msg)
abort.url_name = 'abort'
abort.verbose_name = _("Abort")
@@ -130,7 +150,11 @@ def commit(modeladmin, request, queryset):
for trans in queryset:
trans.mark_as_rejected()
modeladmin.log_change(request, trans, _("Rejected"))
- msg = _("%s selected transactions have been marked as rejected.") % queryset.count()
+ num = len(queryset)
+ msg = ungettext(
+ _("One selected transaction has been marked as rejected."),
+ _("%s selected transactions have been marked as rejected.") % num,
+ num)
modeladmin.message_user(request, msg)
commit.url_name = 'commit'
commit.verbose_name = _("Commit")
diff --git a/orchestra/apps/payments/admin.py b/orchestra/apps/payments/admin.py
index 843f248f..f5eb98ef 100644
--- a/orchestra/apps/payments/admin.py
+++ b/orchestra/apps/payments/admin.py
@@ -2,9 +2,9 @@ from django.contrib import admin
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
-from orchestra.admin import ChangeViewActionsMixin, SelectPluginAdminMixin
+from orchestra.admin import ChangeViewActionsMixin, SelectPluginAdminMixin, ExtendedModelAdmin
from orchestra.admin.utils import admin_colored, admin_link
-from orchestra.apps.accounts.admin import AccountAdminMixin
+from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin
from . import actions
from .methods import PaymentMethod
@@ -51,19 +51,47 @@ class TransactionInline(admin.TabularInline):
return False
-class TransactionAdmin(ChangeViewActionsMixin, AccountAdminMixin, admin.ModelAdmin):
+class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
list_display = (
'id', 'bill_link', 'account_link', 'source_link', 'display_state',
'amount', 'process_link'
)
list_filter = ('source__method', 'state')
+ fieldsets = (
+ (None, {
+ 'classes': ('wide',),
+ 'fields': (
+ 'account_link',
+ 'bill_link',
+ 'source_link',
+ 'display_state',
+ 'amount',
+ 'currency',
+ 'process_link'
+ )
+ }),
+ )
+ add_fieldsets = (
+ (None, {
+ 'classes': ('wide',),
+ 'fields': (
+ 'bill',
+ 'source',
+ 'display_state',
+ 'amount',
+ 'currency',
+ 'process'
+ )
+ }),
+ )
actions = (
actions.process_transactions, actions.mark_as_executed,
actions.mark_as_secured, actions.mark_as_rejected
)
change_view_actions = actions
- filter_by_account_fields = ['source']
- readonly_fields = ('bill_link', 'display_state', 'process_link', 'account_link')
+ filter_by_account_fields = ('bill', 'source')
+ change_readonly_fields = ('amount', 'currency')
+ readonly_fields = ('bill_link', 'display_state', 'process_link', 'account_link', 'source_link')
bill_link = admin_link('bill')
source_link = admin_link('source')
@@ -93,7 +121,7 @@ class TransactionAdmin(ChangeViewActionsMixin, AccountAdminMixin, admin.ModelAdm
class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
list_display = ('id', 'file_url', 'display_transactions', 'created_at')
fields = ('data', 'file_url', 'created_at')
- readonly_fields = ('file_url', 'display_transactions', 'created_at')
+ readonly_fields = ('data', 'file_url', 'display_transactions', 'created_at')
inlines = [TransactionInline]
actions = (actions.mark_process_as_executed, actions.abort, actions.commit)
change_view_actions = actions
diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py
index 3f11d9f7..256ef131 100644
--- a/orchestra/apps/services/models.py
+++ b/orchestra/apps/services/models.py
@@ -41,7 +41,7 @@ class ContractedPlan(models.Model):
return str(self.plan)
def clean(self):
- if not self.pk and not self.plan.allow_multipls:
+ if not self.pk and not self.plan.allow_multiples:
if ContractedPlan.objects.filter(plan=self.plan, account=self.account).exists():
raise ValidationError("A contracted plan for this account already exists")