Random improvements

This commit is contained in:
Marc 2014-09-10 16:53:09 +00:00
parent 4e3105194c
commit 287f03ce19
41 changed files with 660 additions and 179 deletions

14
TODO.md
View file

@ -87,3 +87,17 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
* help_text on readonly_fields specialy Bill.state. (eg. A bill is in OPEN state when bla bla )
* Create ProForma from orders orders.bill(proforma=True)
* generic confirmation breadcrumbs for single objects
* DirectDebit due date = bill.due_date
* settings.ENABLED_PLUGINS = ('path.module.ClassPlugin',)
* Transaction states: CREATED, PROCESSED, EXECUTED, COMMITED, ABORTED (SECURED, REJECTED?)
* bill.send() -> transacction.EXECUTED when source=None
* transaction.secured() -> bill.paid when bill.total == transaction.value else Error
* bill.paid() -> transacton.SECURED
* bill.bad_debt() -> transaction.ABORTED
* transaction.ABORTED -> bill.bad_debt
- Issue new transaction when current transaction is ABORTED

View file

@ -62,7 +62,7 @@ class ChangeViewActionsMixin(object):
action.url_name)))
return new_urls + urls
def get_change_view_actions(self, obj=None):
def get_change_view_actions(self):
views = []
for action in self.change_view_actions:
if isinstance(action, basestring):
@ -77,11 +77,10 @@ class ChangeViewActionsMixin(object):
return views
def change_view(self, request, object_id, **kwargs):
obj = self.get_object(request, unquote(object_id))
if not 'extra_context' in kwargs:
kwargs['extra_context'] = {}
kwargs['extra_context']['object_tools_items'] = [
action.__dict__ for action in self.get_change_view_actions(obj)
action.__dict__ for action in self.get_change_view_actions()
]
return super(ChangeViewActionsMixin, self).change_view(request, object_id, **kwargs)

View file

@ -90,6 +90,12 @@ def action_to_view(action, modeladmin):
return action_view
def admin_change_url(obj):
opts = obj._meta
view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name)
return reverse(view_name, args=(obj.pk,))
@admin_field
def admin_link(*args, **kwargs):
instance = args[-1]
@ -99,9 +105,7 @@ def admin_link(*args, **kwargs):
obj = get_field_value(instance, kwargs['field'])
if not getattr(obj, 'pk', None):
return '---'
opts = obj._meta
view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name)
url = reverse(view_name, args=(obj.pk,))
url = admin_change_url(obj)
extra = ''
if kwargs['popup']:
extra = 'onclick="return showAddAnotherPopup(this);"'
@ -130,3 +134,12 @@ def admin_date(*args, **kwargs):
return '<span title="{0}">{1}</span>'.format(
escape(str(value)), escape(naturaldate(value)),
)
def get_object_from_url(modeladmin, request):
try:
object_id = int(request.path.split('/')[-3])
except ValueError:
return None
else:
return modeladmin.model.objects.get(pk=object_id)

View file

@ -87,7 +87,6 @@ class AccountAdmin(ExtendedModelAdmin):
def get_queryset(self, request):
""" Select related for performance """
# TODO move invoicecontact to contacts
qs = super(AccountAdmin, self).get_queryset(request)
related = ('user', 'invoicecontact')
return qs.select_related(*related)

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Account',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('type', models.CharField(default=b'INDIVIDUAL', max_length=32, verbose_name='type', choices=[(b'INDIVIDUAL', 'Individual'), (b'ASSOCIATION', 'Association'), (b'CUSTOMER', 'Customer'), (b'COMPANY', 'Company'), (b'PUBLICBODY', 'Public body')])),
('language', models.CharField(default=b'en', max_length=2, verbose_name='language', choices=[(b'en', 'English')])),
('register_date', models.DateTimeField(auto_now_add=True, verbose_name='register date')),
('comments', models.TextField(max_length=256, verbose_name='comments', blank=True)),
('is_active', models.BooleanField(default=True)),
('user', models.OneToOneField(related_name=b'accounts', verbose_name='user', to=settings.AUTH_USER_MODEL)),
],
options={
},
bases=(models.Model,),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='account',
name='user',
field=models.OneToOneField(related_name=b'accounts', null=True, verbose_name='user', to=settings.AUTH_USER_MODEL),
),
]

View file

@ -11,7 +11,7 @@ from . import settings
class Account(models.Model):
user = models.OneToOneField(djsettings.AUTH_USER_MODEL,
verbose_name=_("user"), related_name='accounts')
verbose_name=_("user"), related_name='accounts', null=True)
type = models.CharField(_("type"), choices=settings.ACCOUNTS_TYPES,
max_length=32, default=settings.ACCOUNTS_DEFAULT_TYPE)
language = models.CharField(_("language"), max_length=2,

View file

@ -8,6 +8,7 @@ from django.shortcuts import render
from django.utils.translation import ugettext_lazy as _
from orchestra.admin.forms import adminmodelformset_factory
from orchestra.admin.utils import get_object_from_url
from orchestra.utils.html import html_to_pdf
from .forms import SelectSourceForm
@ -69,6 +70,7 @@ def close_bills(modeladmin, request, queryset):
'app_label': opts.app_label,
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
'formset': formset,
'obj': get_object_from_url(modeladmin, request),
}
return render(request, 'admin/orchestra/generic_confirmation.html', context)
close_bills.verbose_name = _("Close")

View file

@ -19,16 +19,8 @@ from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, Budget,
class BillLineInline(admin.TabularInline):
model = BillLine
fields = ('description', 'rate', 'amount', 'tax', 'total', 'subtotal')
readonly_fields = ('subtotal',)
def subtotal(self, line):
if line.total:
subtotal = 0
for subline in line.sublines.all():
subtotal += subline.total
return line.total - subtotal
return ''
fields = ('description', 'rate', 'amount', 'tax', 'total', 'get_total')
readonly_fields = ('get_total',)
def get_readonly_fields(self, request, obj=None):
if obj and obj.status != Bill.OPEN:
@ -44,9 +36,17 @@ class BillLineInline(admin.TabularInline):
if obj and obj.status != Bill.OPEN:
return False
return super(BillLineInline, self).has_delete_permission(request, obj=obj)
def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """
if db_field.name == 'description':
kwargs['widget'] = forms.TextInput(attrs={'size':'110'})
else:
kwargs['widget'] = forms.TextInput(attrs={'size':'13'})
return super(BillLineInline, self).formfield_for_dbfield(db_field, **kwargs)
class BudgetLineInline(admin.TabularInline):
class BudgetLineInline(BillLineInline):
model = Budget
fields = ('description', 'rate', 'amount', 'tax', 'total')
@ -108,7 +108,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
return fieldsets
def get_change_view_actions(self, obj=None):
actions = super(BillAdmin, self).get_change_view_actions(obj)
actions = super(BillAdmin, self).get_change_view_actions()
discard = []
if obj:
if obj.status != Bill.OPEN:

View file

@ -57,7 +57,7 @@ class Bill(models.Model):
closed_on = models.DateTimeField(_("closed on"), blank=True, null=True)
due_on = models.DateField(_("due on"), null=True, blank=True)
last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True)
total = models.DecimalField(max_digits=12, decimal_places=2)
total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
comments = models.TextField(_("comments"), blank=True)
html = models.TextField(_("HTML"), blank=True)
@ -170,24 +170,18 @@ class Bill(models.Model):
def save(self, *args, **kwargs):
if not self.type:
self.type = self.get_type()
if self.status == self.OPEN:
self.total = self.get_total()
if not self.number or (self.number.startswith('O') and self.status != self.OPEN):
self.set_number()
super(Bill, self).save(*args, **kwargs)
@cached
def get_subtotals(self):
subtotals = {}
for line in self.lines.all():
subtotal, taxes = subtotals.get(line.tax, (0, 0))
subtotal += line.total
for subline in line.sublines.all():
subtotal += subline.total
subtotal += line.get_total()
subtotals[line.tax] = (subtotal, (line.tax/100)*subtotal)
return subtotals
@cached
def get_total(self):
total = 0
for tax, subtotal in self.get_subtotals().iteritems():
@ -246,7 +240,7 @@ class BaseBillLine(models.Model):
def number(self):
lines = type(self).objects.filter(bill=self.bill_id)
return lines.filter(id__lte=self.id).order_by('id').count()
class BudgetLine(BaseBillLine):
pass
@ -259,6 +253,20 @@ class BillLine(BaseBillLine):
auto = models.BooleanField(default=False)
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
related_name='amendment_lines', null=True, blank=True)
def get_total(self):
""" Computes subline discounts """
subtotal = self.total
for subline in self.sublines.all():
subtotal += subline.total
return subtotal
def save(self, *args, **kwargs):
# TODO cost of this shit
super(BillLine, self).save(*args, **kwargs)
if self.bill.status == self.bill.OPEN:
self.bill.total = self.bill.get_total()
self.bill.save()
class BillSubline(models.Model):
@ -268,6 +276,12 @@ class BillSubline(models.Model):
description = models.CharField(_("description"), max_length=256)
total = models.DecimalField(max_digits=12, decimal_places=2)
# TODO type ? Volume and Compensation
def save(self, *args, **kwargs):
# TODO cost of this shit
super(BillSubline, self).save(*args, **kwargs)
if self.line.bill.status == self.line.bill.OPEN:
self.line.bill.total = self.line.bill.get_total()
self.line.bill.save()
accounts.register(Bill)

View file

@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _
CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES',
('SUPPORT', 'ADMIN', 'BILL', 'TECH', 'ADDS', 'EMERGENCY')
('SUPPORT', 'ADMIN', 'BILLING', 'TECH', 'ADDS', 'EMERGENCY')
)

View file

@ -8,8 +8,8 @@ from orchestra.apps.orchestration.models import Server, Route
from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii
from orchestra.utils.system import run
from orchestra.apps.domains import settings, utils, backends
from orchestra.apps.domains.models import Domain, Record
from ... import settings, utils, backends
from ...models import Domain, Record
run = functools.partial(run, display=False)

View file

@ -69,6 +69,10 @@ class BackendOperationInline(admin.TabularInline):
def has_add_permission(self, *args, **kwargs):
return False
def get_queryset(self, request):
queryset = super(BackendOperationInline, self).get_queryset(request)
return queryset.prefetch_related('instance')
def display_mono(field):
@ -106,7 +110,7 @@ class BackendLogAdmin(admin.ModelAdmin):
def get_queryset(self, request):
""" Order by structured name and imporve performance """
qs = super(BackendLogAdmin, self).get_queryset(request)
return qs.select_related('server')
return qs.select_related('server').defer('script', 'stdout')
class ServerAdmin(admin.ModelAdmin):

View file

@ -13,7 +13,7 @@ from . import settings
def BashSSH(backend, log, server, cmds):
from .models import BackendLog
script = '\n\n'.join(['set -e', 'set -o pipefail'] + cmds + ['exit 0'])
script = '\n'.join(['set -e', 'set -o pipefail'] + cmds + ['exit 0'])
script = script.replace('\r', '')
log.script = script
log.save()

View file

@ -1,4 +1,5 @@
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
@ -70,6 +71,9 @@ class BackendLog(models.Model):
@property
def execution_time(self):
return (self.last_update-self.created).total_seconds()
def backend_class(self):
return ServiceBackend.get_backend(self.backend)
class BackendOperation(models.Model):
@ -85,6 +89,7 @@ class BackendOperation(models.Model):
action = models.CharField(_("action"), max_length=64)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
# TODO rename to content_object
instance = generic.GenericForeignKey('content_type', 'object_id')
class Meta:

View file

@ -2,6 +2,7 @@ from django.contrib import admin, messages
from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext
from django.shortcuts import render
from .forms import (BillSelectedOptionsForm, BillSelectConfirmationForm,
@ -41,15 +42,16 @@ class BillSelectedOrders(object):
return self.select_related(request)
self.context.update({
'title': _("Options for billing selected orders, step 1 / 3"),
'step': 'one',
'step': 1,
'form': form,
})
return render(request, self.template, self.context)
def select_related(self, request):
self.options['related_queryset'] = self.queryset.all() #get_related(**options)
related = self.queryset.get_related().select_related('account__user', 'service')
self.options['related_queryset'] = related
form = BillSelectRelatedForm(initial=self.options)
if request.POST.get('step') == 'two':
if int(request.POST.get('step')) >= 2:
form = BillSelectRelatedForm(request.POST, initial=self.options)
if form.is_valid():
select_related = form.cleaned_data['selected_related']
@ -57,14 +59,14 @@ class BillSelectedOrders(object):
return self.confirmation(request)
self.context.update({
'title': _("Select related order for billing, step 2 / 3"),
'step': 'two',
'step': 2,
'form': form,
})
return render(request, self.template, self.context)
def confirmation(self, request):
form = BillSelectConfirmationForm(initial=self.options)
if request.POST:
if int(request.POST.get('step')) >= 3:
bills = self.queryset.bill(commit=True, **self.options)
if not bills:
msg = _("Selected orders do not have pending billing")
@ -72,19 +74,21 @@ class BillSelectedOrders(object):
else:
ids = ','.join([str(bill.id) for bill in bills])
url = reverse('admin:bills_bill_changelist')
context = {
'url': url + '?id=%s' % ids,
'num': len(bills),
'bills': _("bills"),
'msg': _("have been generated"),
}
msg = '<a href="%(url)s">%(num)s %(bills)s</a> %(msg)s' % context
url += '?id__in=%s' % ids
num = len(bills)
msg = ungettext(
'<a href="{url}">One bill</a> has been created.',
'<a href="{url}">{num} bills</a> have been created.',
num).format(url=url, num=num)
msg = mark_safe(msg)
self.modeladmin.message_user(request, msg, messages.INFO)
return
bills = self.queryset.bill(commit=False, **self.options)
self.context.update({
'title': _("Confirmation for billing selected orders"),
'step': 'three',
'step': 3,
'form': form,
'bills': bills,
'selected_related_objects': self.options['selected_related']
})
return render(request, self.template, self.context)

View file

@ -9,38 +9,39 @@ class BillsBackend(object):
def create_bills(self, account, lines):
invoice = None
bills = []
for order, nominal_price, size, ini, end, discounts in lines:
service = order.service
for line in lines:
service = line.order.service
if service.is_fee:
fee, __ = Fee.objects.get_or_create(account=account, status=Fee.OPEN)
line = fee.lines.create(
storedline = fee.lines.create(
rate=service.nominal_price,
amount=size,
total=nominal_price, tax=0,
description=self.format_period(ini, end),
amount=line.size,
total=line.subtotal, tax=0,
description=self.format_period(line.ini, line.end),
)
self.create_sublines(line, discounts)
self.create_sublines(storedline, line.discounts)
bills.append(fee)
else:
if invoice is None:
invoice, __ = Invoice.objects.get_or_create(account=account,
status=Invoice.OPEN)
bills.append(invoice)
description = order.description
description = line.order.description
if service.billing_period != service.NEVER:
description += " %s" % self.format_period(ini, end)
line = invoice.lines.create(
description += " %s" % self.format_period(line.ini, line.end)
storedline = invoice.lines.create(
description=description,
rate=service.nominal_price,
amount=size,
total=nominal_price,
amount=line.size,
# TODO rename line.total > subtotal
total=line.subtotal,
tax=service.tax,
)
self.create_sublines(line, discounts)
self.create_sublines(storedline, line.discounts)
return bills
def format_period(self, ini, end):
ini = ini=ini.strftime("%b, %Y")
ini = ini.strftime("%b, %Y")
end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y")
if ini == end:
return ini
@ -48,8 +49,8 @@ class BillsBackend(object):
def create_sublines(self, line, discounts):
for name, value in discounts:
for discount in discounts:
line.sublines.create(
description=_("Discount per %s") % name,
total=value,
description=_("Discount per %s") % discount.type,
total=discount.total,
)

View file

@ -1,9 +1,11 @@
from django import forms
from django.contrib.admin import widgets
from django.utils import timezone
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.admin.forms import AdminFormMixin
from orchestra.admin.utils import admin_change_url
from .models import Order
@ -21,9 +23,20 @@ class BillSelectedOptionsForm(AdminFormMixin, forms.Form):
help_text=_("Deisgnates whether you want to put this orders on a new "
"open bill, or allow to reuse an existing one."))
def selected_related_choices(queryset):
for order in queryset:
verbose = '<a href="{order_url}">{description}</a> '
verbose += '<a class="account" href="{account_url}">{account}</a>'
verbose = verbose.format(
order_url=admin_change_url(order), description=order.description,
account_url=admin_change_url(order.account), account=str(order.account)
)
yield (order.pk, mark_safe(verbose))
class BillSelectRelatedForm(AdminFormMixin, forms.Form):
selected_related = forms.ModelMultipleChoiceField(queryset=Order.objects.none(),
selected_related = forms.ModelMultipleChoiceField(label=_("Related"),
queryset=Order.objects.none(), widget=forms.CheckboxSelectMultiple,
required=False)
billing_point = forms.DateField(widget=forms.HiddenInput())
fixed_point = forms.BooleanField(widget=forms.HiddenInput(), required=False)
@ -34,11 +47,12 @@ class BillSelectRelatedForm(AdminFormMixin, forms.Form):
queryset = kwargs['initial'].get('related_queryset', None)
if queryset:
self.fields['selected_related'].queryset = queryset
self.fields['selected_related'].choices = selected_related_choices(queryset)
class BillSelectConfirmationForm(forms.Form):
selected_related = forms.ModelMultipleChoiceField(queryset=Order.objects.none(),
widget=forms.HiddenInput(), required=False)
class BillSelectConfirmationForm(AdminFormMixin, forms.Form):
# selected_related = forms.ModelMultipleChoiceField(queryset=Order.objects.none(),
# widget=forms.HiddenInput(), required=False)
billing_point = forms.DateField(widget=forms.HiddenInput())
fixed_point = forms.BooleanField(widget=forms.HiddenInput(), required=False)
create_new_open = forms.BooleanField(widget=forms.HiddenInput(), required=False)

View file

@ -8,6 +8,7 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from orchestra.utils import plugins
from orchestra.utils.python import AttributeDict
from . import settings
from .helpers import get_register_or_cancel_events, get_register_or_renew_events
@ -70,8 +71,8 @@ class ServiceHandler(plugins.Plugin):
day = 1
else:
raise NotImplementedError(msg)
bp = datetime.datetime(year=date.year, month=date.month,
day=day, tzinfo=timezone.get_current_timezone())
bp = datetime.datetime(year=date.year, month=date.month, day=day,
tzinfo=timezone.get_current_timezone())
elif self.billing_period == self.ANUAL:
if self.billing_point == self.ON_REGISTER:
month = order.registered_on.month
@ -151,17 +152,26 @@ class ServiceHandler(plugins.Plugin):
price = self.get_price(order, metric) * size
return price
def create_line(self, order, price, size, ini, end):
nominal_price = self.nominal_price * size
def generate_line(self, order, price, size, ini, end):
subtotal = float(self.nominal_price) * size
discounts = []
if nominal_price > price:
discounts.append(('volume', nominal_price-price))
# TODO Uncomment when prices are done
# elif nominal_price < price:
# raise ValueError("Something is wrong!")
return (order, nominal_price, size, ini, end, discounts)
if subtotal > price:
discounts.append(AttributeDict(**{
'type': 'volume',
'total': price-subtotal
}))
elif subtotal < price:
raise ValueError("Something is wrong!")
return AttributeDict(**{
'order': order,
'subtotal': subtotal,
'size': size,
'ini': ini,
'end': end,
'discounts': discounts,
})
def create_bill_lines(self, orders, **options):
def generate_bill_lines(self, orders, **options):
# For the "boundary conditions" just think that:
# date(2011, 1, 1) is equivalent to datetime(2011, 1, 1, 0, 0, 0)
# In most cases:
@ -175,6 +185,7 @@ class ServiceHandler(plugins.Plugin):
# TODO create discount per compensation
bp = None
lines = []
commit = options.get('commit', True)
for order in orders:
bp = self.get_billing_point(order, bp=bp, **options)
ini = order.billed_until or order.registered_on
@ -184,15 +195,16 @@ class ServiceHandler(plugins.Plugin):
# Number of orders metric; bill line per order
size = self.get_pricing_size(ini, bp)
price = self.get_price_with_orders(order, size, ini, bp)
lines.append(self.create_line(order, price, size, ini, bp))
lines.append(self.generate_line(order, price, size, ini, bp))
else:
# weighted metric; bill line per pricing period
for ini, end in self.get_pricing_slots(ini, bp):
size = self.get_pricing_size(ini, end)
price = self.get_price_with_metric(order, size, ini, end)
lines.append(self.create_line(order, price, size, ini, end))
lines.append(self.generate_line(order, price, size, ini, end))
order.billed_until = bp
order.save() # TODO if commit
if commit:
order.save()
return lines
def compensate(self, orders):

View file

@ -36,13 +36,14 @@ def get_related_objects(origin, max_depth=2):
new_models.append(related)
queue.append(new_models)
def get_register_or_cancel_events(porders, order, ini, end):
assert ini <= end, "ini > end"
CANCEL = 'cancel'
REGISTER = 'register'
changes = {}
counter = 0
for num, porder in enumerate(porders.order_by('registered_on')):
for num, porder in enumerate(porders.order_by('registered_on'), start=1):
if porder == order:
position = num
if porder.cancelled_on:
@ -76,7 +77,7 @@ def get_register_or_renew_events(handler, porders, order, ini, end):
total = float((end-ini).days)
for sini, send in handler.get_pricing_slots(ini, end):
counter = 0
position = 0
position = -1
for porder in porders.order_by('registered_on'):
if porder == order:
position = abs(position)

View file

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('orders', '0003_auto_20140908_1409'),
]
operations = [
migrations.RemoveField(
model_name='rate',
name='value',
),
migrations.AddField(
model_name='rate',
name='price',
field=models.DecimalField(default=1, verbose_name='price', max_digits=12, decimal_places=2),
preserve_default=False,
),
migrations.AlterField(
model_name='rate',
name='plan',
field=models.CharField(blank=True, max_length=128, verbose_name='plan', choices=[(b'', 'Default'), (b'basic', 'Basic'), (b'advanced', 'Advanced')]),
),
]

View file

@ -1,5 +1,8 @@
import sys
from django.db import models
from django.db.models import Q
from django.db.migrations.recorder import MigrationRecorder
from django.db.models import F, Q
from django.db.models.signals import pre_delete, post_delete, post_save
from django.dispatch import receiver
from django.contrib.admin.models import LogEntry
@ -35,7 +38,7 @@ class RateQuerySet(models.QuerySet):
def by_account(self, account):
# Default allways selected
qset = Q(plan__isnull=True)
qset = Q(plan='')
for plan in account.plans.all():
qset |= Q(plan=plan)
return self.filter(qset)
@ -47,7 +50,7 @@ class Rate(models.Model):
plan = models.CharField(_("plan"), max_length=128, blank=True,
choices=(('', _("Default")),) + settings.ORDERS_PLANS)
quantity = models.PositiveIntegerField(_("quantity"), null=True, blank=True)
value = models.DecimalField(_("value"), max_digits=12, decimal_places=2)
price = models.DecimalField(_("price"), max_digits=12, decimal_places=2)
objects = RateQuerySet.as_manager()
@ -55,7 +58,7 @@ class Rate(models.Model):
unique_together = ('service', 'plan', 'quantity')
def __unicode__(self):
return "{}-{}".format(str(self.value), self.quantity)
return "{}-{}".format(str(self.price), self.quantity)
autodiscover('handlers')
@ -82,7 +85,7 @@ class Service(models.Model):
BEST_PRICE = 'BEST_PRICE'
PROGRESSIVE_PRICE = 'PROGRESSIVE_PRICE'
MATCH_PRICE = 'MATCH_PRICE'
PRICING_METHODS = {
RATE_METHODS = {
BEST_PRICE: pricing.best_price,
MATCH_PRICE: pricing.match_price,
}
@ -255,30 +258,47 @@ class Service(models.Model):
if position is provided an specific price for that position is returned,
accumulated price is returned otherwise
"""
rates = self.rates.by_account(order.account)
if not rates:
return self.nominal_price
rates = self.rate_method(rates, metric)
rates = self.get_rates(order.account, metric)
counter = 0
if position is None:
ant_counter = 0
accumulated = 0
for rate in self.get_rates(order.account, metric):
counter += rate['number']
for rate in rates:
counter += rate['quantity']
if counter >= metric:
counter = metric
accumulated += (counter - ant_counter) * rate['price']
return accumulated
return float(accumulated)
ant_counter = counter
accumulated += rate['price'] * rate['number']
accumulated += rate['price'] * rate['quantity']
else:
for rate in self.get_rates(order.account, metric):
counter += rate['number']
for rate in rates:
counter += rate['quantity']
if counter >= position:
return rate['price']
return float(rate['price'])
def get_rates(self, account, metric):
if not hasattr(self, '__cached_rates'):
self.__cached_rates = {}
if account.id in self.__cached_rates:
rates, cache = self.__cached_rates.get(account.id)
else:
rates = self.rates.by_account(account)
cache = {}
if not rates:
rates = [{
'quantity': sys.maxint,
'price': self.nominal_price,
}]
self.__cached_rates[account.id] = (rates, cache)
return rates
self.__cached_rates[account.id] = (rates, cache)
# Caching depends on the specific rating method
return self.rate_method(rates, metric, cache=cache)
@property
def rate_method(self, *args, **kwargs):
def rate_method(self):
return self.RATE_METHODS[self.rate_algorithm]
@ -289,16 +309,26 @@ class OrderQuerySet(models.QuerySet):
bills = []
bill_backend = Order.get_bill_backend()
qs = self.select_related('account', 'service')
commit = options.get('commit', True)
for account, services in qs.group_by('account', 'service'):
bill_lines = []
for service, orders in services:
lines = service.handler.create_bill_lines(orders, **options)
lines = service.handler.generate_bill_lines(orders, **options)
bill_lines.extend(lines)
bills += bill_backend.create_bills(account, bill_lines)
if commit:
bills += bill_backend.create_bills(account, bill_lines)
else:
bills += [(account, bill_lines)]
return bills
def get_related(self):
pass
qs = self.exclude(cancelled_on__isnull=False,
billed_until__gte=F('cancelled_on')).distinct()
original_ids = self.values_list('id', flat=True)
return self.model.objects.exclude(id__in=original_ids).filter(
service__in=qs.values_list('service_id', flat=True),
account__in=qs.values_list('account_id', flat=True)
)
def by_object(self, obj, **kwargs):
ct = ContentType.objects.get_for_model(obj)
@ -421,7 +451,10 @@ def cancel_orders(sender, **kwargs):
@receiver(post_save, dispatch_uid="orders.update_orders")
@receiver(post_delete, dispatch_uid="orders.update_orders_post_delete")
def update_orders(sender, **kwargs):
if sender not in [MetricStorage, LogEntry, Order, Service]:
exclude = (
MetricStorage, LogEntry, Order, Service, ContentType, MigrationRecorder.Migration
)
if sender not in exclude:
instance = kwargs['instance']
if instance.pk:
# post_save

View file

@ -1,35 +1,29 @@
import sys
def best_price(rates, metric):
rates = rates.order_by('metric').order_by('plan')
ix = 0
steps = []
num = rates.count()
while ix < num:
if ix+1 == num or rates[ix].plan != rates[ix+1].plan:
number = metric
else:
number = rates[ix+1].metric - rates[ix].metric
steps.append({
'number': sys.maxint,
'price': rates[ix].price
})
ix += 1
steps.sort(key=lambda s: s['price'])
acumulated = 0
for step in steps:
previous = acumulated
acumulated += step['number']
if acumulated >= metric:
step['number'] = metric - previous
yield step
raise StopIteration
yield step
def best_price(rates, metric, cache={}):
steps = cache.get('steps')
if not steps:
rates = rates.order_by('quantity').order_by('plan')
ix = 0
steps = []
num = rates.count()
while ix < num:
if ix+1 == num or rates[ix].plan != rates[ix+1].plan:
quantity = sys.maxint
else:
quantity = rates[ix+1].quantity - rates[ix].quantity
steps.append({
'quantity': quantity,
'price': rates[ix].price
})
ix += 1
steps.sort(key=lambda s: s['price'])
cache['steps'] = steps
return steps
def match_price(rates, metric):
def match_price(rates, metric, cache={}):
minimal = None
for plan, rates in rates.order_by('-metric').group_by('plan'):
if minimal is None:
@ -37,6 +31,6 @@ def match_price(rates, metric):
else:
minimal = min(minimal, rates[0].price)
return [{
'number': sys.maxint,
'quantity': sys.maxint,
'price': minimal
}]

View file

@ -1,11 +1,18 @@
{% extends "admin/base_site.html" %}
{% load i18n l10n staticfiles admin_urls %}
{% load i18n l10n staticfiles admin_urls utils %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />
<style type="text/css">
.account {
float: right;
margin-right: 400px;
}
</style>
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
@ -19,18 +26,53 @@
{% block content %}
<form action="" method="post">{% csrf_token %}
<div>
<div style="margin:20px;">
{% if bills %}
{% for account, lines in bills %}
<div class="inline-group" id="rates-group">
<div class="tabular inline-related last-related">
<fieldset class="module">
<h2><a href="{% url 'admin:accounts_account_change' account.pk %}">{{ account }}</a></h2>
<table>
<thead>
<tr><th style="width:30%;">Description</th> <th style="width:30%;">Period</th> <th style="width:10%;">Quantity</th> <th style="width:10%;">Price</th></tr>
</thead>
<tbody>
{% for line in lines %}
<tr class="form-row {% if forloop.counter|divisibleby:2 %}row2{% else %}row1{% endif %}">
<td>
<a href="{{ line.order | admin_link }}">{{ line.order.description }}</a>
{% for discount in line.discounts %}
<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Discount per {{ discount.type }}
{% endfor %}
</td>
<td>{{ line.ini | date }} to {{ line.end | date }}</td>
<td>{{ line.size | floatformat:"-2" }}</td>
<td>
&nbsp;{{ line.subtotal | floatformat:"-2" }} &euro;
{% for discount in line.discounts %}<br>{{ discount.total | floatformat:"-2" }} &euro;{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</fieldset>
</div>
</div>
{% endfor %}
{% else %}
{{ form.as_admin }}
{% endif %}
</div>
{% for obj in selected_related_objects %}
<input type="hidden" name="selected_related" value="{{ obj.pk|unlocalize }}" />
{% endfor %}
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
{% endfor %}
<input type="hidden" name="action" value="bill_selected_orders" />
<input type="hidden" name="step" value="{{ step }}" />
<input type="submit" value="{% trans "Yes, create slivers" %}" />
<input type="submit" value="{% if step == 3 %}{% trans "Yes, create bills" %}{% else %}{% trans "Next" %}{% endif %}" />
</div>
</form>
{% endblock %}

View file

View file

@ -0,0 +1,109 @@
import datetime
from dateutil import relativedelta
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from orchestra.apps.accounts.models import Account
from orchestra.apps.users.models import User
from orchestra.utils.tests import BaseTestCase
from ... import settings
from ...models import Service
class OrderTests(BaseTestCase):
DEPENDENCIES = (
'orchestra.apps.orders',
'orchestra.apps.users',
'orchestra.apps.users.roles.posix',
)
def create_account(self):
account = Account.objects.create()
user = User.objects.create_user(username='rata_palida', account=account)
account.user = user
account.save()
return account
def create_service(self):
service = Service.objects.create(
description="FTP Account",
content_type=ContentType.objects.get_for_model(User),
match='not user.is_main and user.has_posix()',
billing_period=Service.ANUAL,
billing_point=Service.FIXED_DATE,
delayed_billing=Service.NEVER,
is_fee=False,
metric='',
pricing_period=Service.BILLING_PERIOD,
rate_algorithm=Service.BEST_PRICE,
orders_effect=Service.CONCURRENT,
on_cancel=Service.DISCOUNT,
payment_style=Service.PREPAY,
trial_period=Service.NEVER,
refound_period=Service.NEVER,
tax=21,
nominal_price=10,
)
service.rates.create(
plan='',
quantity=1,
price=9,
)
account = self.create_account()
user = User.objects.create_user(username='rata_palida_ftp', account=account)
POSIX = user._meta.get_field_by_name('posix')[0].model
POSIX.objects.create(user=user)
return service
# def test_ftp_account_1_year_fiexed(self):
# service = self.create_service()
# bp = timezone.now().date() + relativedelta.relativedelta(years=1)
# bills = service.orders.bill(billing_point=bp, fixed_point=True)
# self.assertEqual(20, bills[0].get_total())
def test_ftp_account_1_year_fiexed(self):
service = self.create_service()
now = timezone.now().date()
month = settings.ORDERS_SERVICE_ANUAL_BILLING_MONTH
ini = datetime.datetime(year=now.year, month=month,
day=1, tzinfo=timezone.get_current_timezone())
order = service.orders.all()[0]
order.registered_on = ini
order.save()
bp = ini
bills = service.orders.bill(billing_point=bp, fixed_point=False, commit=False)
print bills[0][1][0].subtotal
print bills
bp = ini + relativedelta.relativedelta(months=12)
bills = service.orders.bill(billing_point=bp, fixed_point=False, commit=False)
print bills[0][1][0].subtotal
print bills
# def test_ftp_account_2_year_fiexed(self):
# service = self.create_service()
# bp = timezone.now().date() + relativedelta.relativedelta(years=2)
# bills = service.orders.bill(billing_point=bp, fixed_point=True)
# self.assertEqual(40, bills[0].get_total())
#
# def test_ftp_account_6_month_fixed(self):
# service = self.create_service()
# bp = timezone.now().date() + relativedelta.relativedelta(months=6)
# bills = service.orders.bill(billing_point=bp, fixed_point=True)
# self.assertEqual(6, bills[0].get_total())
#
# def test_ftp_account_next_billing_point(self):
# service = self.create_service()
# now = timezone.now().date()
# bp_month = settings.ORDERS_SERVICE_ANUAL_BILLING_MONTH
# if date.month > bp_month:
# bp = datetime.datetime(year=now.year+1, month=bp_month,
# day=1, tzinfo=timezone.get_current_timezone())
# else:
# bp = datetime.datetime(year=now.year, month=bp_month,
# day=1, tzinfo=timezone.get_current_timezone())
#
# days = (bp - now).days
# bills = service.orders.bill(billing_point=bp, fixed_point=False)
# self.assertEqual(40, bills[0].get_total())

View file

@ -1,5 +1,29 @@
from django.contrib import messages
from django.shortcuts import render
from django.utils.translation import ugettext_lazy as _
from .methods import PaymentMethod
from .models import Transaction
def process_transactions(modeladmin, request, queryset):
processes = []
if queryset.exclude(state=Transaction.WAITTING_PROCESSING).exists():
msg = _("Selected transactions must be on '{state}' state")
messages.error(request, msg.format(state=Transaction.WAITTING_PROCESSING))
return
for method, transactions in queryset.group_by('source__method'):
if method is not None:
PaymentMethod.get_plugin(method)().process(transactions)
method = PaymentMethod.get_plugin(method)
procs = method.process(transactions)
processes += procs
if not processes:
return
opts = modeladmin.model._meta
context = {
'title': _("Huston, be advised"),
'action_name': _("Process"),
'processes': processes,
'opts': opts,
'app_label': opts.app_label,
}
return render(request, 'admin/payments/transaction/get_processes.html', context)

View file

@ -29,7 +29,7 @@ class PaymentMethod(plugins.Plugin):
return data[self.number_field]
def get_bill_message(self, source):
raise NotImplementedError
return ''
class PaymentSourceDataForm(forms.ModelForm):

View file

@ -42,7 +42,8 @@ class SEPADirectDebit(PaymentMethod):
return _("This bill will been automatically charged to your bank account "
" with IBAN number<br><strong>%s</strong>.") % source.number
def process(self, transactions):
@classmethod
def process(cls, transactions):
debts = []
credits = []
for transaction in transactions:
@ -50,15 +51,20 @@ class SEPADirectDebit(PaymentMethod):
credits.append(transaction)
else:
debts.append(transaction)
processes = []
if debts:
self._process_debts(debts)
proc = cls.process_debts(debts)
processes.append(proc)
if credits:
self._process_credits(credits)
proc = cls.process_credits(credits)
processes.append(proc)
return processes
def _process_credits(self, transactions):
@classmethod
def process_credits(cls, transactions):
from ..models import TransactionProcess
self.process = TransactionProcess.objects.create()
context = self.get_context(transactions)
process = TransactionProcess.objects.create()
context = cls.get_context(transactions)
sepa = lxml.builder.ElementMaker(
nsmap = {
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
@ -67,9 +73,9 @@ class SEPADirectDebit(PaymentMethod):
)
sepa = sepa.Document(
E.CstmrCdtTrfInitn(
self._get_header(context),
cls.get_header(context),
E.PmtInf( # Payment Info
E.PmtInfId(str(self.process.id)), # Payment Id
E.PmtInfId(str(process.id)), # Payment Id
E.PmtMtd("TRF"), # Payment Method
E.NbOfTxs(context['num_transactions']), # Number of Transactions
E.CtrlSum(context['total']), # Control Sum
@ -89,17 +95,19 @@ class SEPADirectDebit(PaymentMethod):
E.BIC(context['bic'])
)
),
*list(self._get_credit_transactions(transactions)) # Transactions
*list(cls.get_credit_transactions(transactions, process)) # Transactions
)
)
)
file_name = 'credit-transfer-%i.xml' % self.process.id
self._process_xml(sepa, 'pain.001.001.03.xsd', file_name)
file_name = 'credit-transfer-%i.xml' % process.id
cls.process_xml(sepa, 'pain.001.001.03.xsd', file_name, process)
return process
def _process_debts(self, transactions):
@classmethod
def process_debts(cls, transactions):
from ..models import TransactionProcess
self.process = TransactionProcess.objects.create()
context = self.get_context(transactions)
process = TransactionProcess.objects.create()
context = cls.get_context(transactions)
sepa = lxml.builder.ElementMaker(
nsmap = {
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
@ -108,9 +116,9 @@ class SEPADirectDebit(PaymentMethod):
)
sepa = sepa.Document(
E.CstmrDrctDbtInitn(
self._get_header(context),
cls.get_header(context, process),
E.PmtInf( # Payment Info
E.PmtInfId(str(self.process.id)), # Payment Id
E.PmtInfId(str(process.id)), # Payment Id
E.PmtMtd("DD"), # Payment Method
E.NbOfTxs(context['num_transactions']), # Number of Transactions
E.CtrlSum(context['total']), # Control Sum
@ -139,14 +147,16 @@ class SEPADirectDebit(PaymentMethod):
E.BIC(context['bic'])
)
),
*list(self._get_debt_transactions(transactions)) # Transactions
*list(cls.get_debt_transactions(transactions, process)) # Transactions
)
)
)
file_name = 'direct-debit-%i.xml' % self.process.id
self._process_xml(sepa, 'pain.008.001.02.xsd', file_name)
file_name = 'direct-debit-%i.xml' % process.id
cls.process_xml(sepa, 'pain.008.001.02.xsd', file_name, process)
return process
def get_context(self, transactions):
@classmethod
def get_context(cls, transactions):
return {
'name': settings.PAYMENTS_DD_CREDITOR_NAME,
'iban': settings.PAYMENTS_DD_CREDITOR_IBAN,
@ -157,9 +167,10 @@ class SEPADirectDebit(PaymentMethod):
'num_transactions': str(len(transactions)),
}
def _get_debt_transactions(self, transactions):
@classmethod
def get_debt_transactions(cls, transactions, process):
for transaction in transactions:
transaction.process = self.process
transaction.process = process
account = transaction.account
data = transaction.source.data
transaction.state = transaction.WAITTING_CONFIRMATION
@ -197,9 +208,10 @@ class SEPADirectDebit(PaymentMethod):
),
)
def _get_credit_transactions(self, transactions):
@classmethod
def get_credit_transactions(transactions, process):
for transaction in transactions:
transaction.process = self.process
transaction.process = process
account = transaction.account
data = transaction.source.data
transaction.state = transaction.WAITTING_CONFIRMATION
@ -231,9 +243,10 @@ class SEPADirectDebit(PaymentMethod):
),
)
def _get_header(self, context):
@classmethod
def get_header(cls, context, process):
return E.GrpHdr( # Group Header
E.MsgId(str(self.process.id)), # Message Id
E.MsgId(str(process.id)), # Message Id
E.CreDtTm( # Creation Date Time
context['now'].strftime("%Y-%m-%dT%H:%M:%S")
),
@ -251,7 +264,8 @@ class SEPADirectDebit(PaymentMethod):
)
)
def _process_xml(self, sepa, xsd, file_name):
@classmethod
def process_xml(cls, sepa, xsd, file_name, process):
# http://www.iso20022.org/documents/messages/1_0_version/pain/schemas/pain.008.001.02.zip
path = os.path.dirname(os.path.realpath(__file__))
xsd_path = os.path.join(path, xsd)
@ -259,9 +273,9 @@ class SEPADirectDebit(PaymentMethod):
schema = etree.XMLSchema(schema_doc)
sepa = etree.parse(StringIO(etree.tostring(sepa)))
schema.assertValid(sepa)
self.process.file = file_name
self.process.save()
sepa.write(self.process.file.path,
process.file = file_name
process.save()
sepa.write(process.file.path,
pretty_print=True,
xml_declaration=True,
encoding='UTF-8')

View file

@ -0,0 +1,18 @@
{% extends "admin/orchestra/generic_confirmation.html" %}
{% load i18n admin_urls utils %}
{% block content %}
<p>The following transaction processes have been generated, you may want to save them on your computer now.</p>
<ul>
{% for proc in processes %}
<li> <a href="{{ proc.id }}">Process #{{ proc.id }}</a>
{% if proc.file %}
<ul><li>File: <a href="{{ proc.file.url }}">{{ proc.file }}</a></li></ul>
{% endif %}
{% if proc.data %}
<ul><li>Data: {{ proc.data }}</li></ul>
{% endif %}
</li>
{% endfor %}
</ul>
{% endblock %}

View file

@ -70,6 +70,10 @@ class ResourceDataAdmin(admin.ModelAdmin):
readonly_fields = ('content_object_link',)
content_object_link = admin_link('content_object')
def get_queryset(self, request):
queryset = super(ResourceDataAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_object')
class MonitorDataAdmin(admin.ModelAdmin):
@ -78,6 +82,10 @@ class MonitorDataAdmin(admin.ModelAdmin):
readonly_fields = ('content_object_link',)
content_object_link = admin_link('content_object')
def get_queryset(self, request):
queryset = super(MonitorDataAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_object')
admin.site.register(Resource, ResourceAdmin)

View file

@ -47,4 +47,3 @@ class UserChangeForm(forms.ModelForm):
# This is done here, rather than on the field, because the
# field does not have access to the initial value
return self.initial["password"]

View file

@ -14,7 +14,7 @@ class User(auth.AbstractBaseUser):
validators=[validators.RegexValidator(r'^[\w.-]+$',
_("Enter a valid username."), 'invalid')])
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='users', null=True)
related_name='users')
first_name = models.CharField(_("first name"), max_length=30, blank=True)
last_name = models.CharField(_("last name"), max_length=30, blank=True)
email = models.EmailField(_('email address'), blank=True)

View file

@ -13,10 +13,10 @@ class Register(object):
def register(self, name, model):
if name in self._registry:
raise KeyError("%s already registered" % name)
def has_role(user):
def has_role(user, model=model):
try:
getattr(user, name)
except models.DoesNotExist:
except model.DoesNotExist:
return False
return True
setattr(User, 'has_%s' % name, has_role)

View file

@ -144,7 +144,7 @@ FLUENT_DASHBOARD_APP_GROUPS = (
'orchestra.apps.contacts.models.Contact',
'orchestra.apps.users.models.User',
'orchestra.apps.orders.models.Order',
'orchestra.apps.orders.models.Pack',
'orchestra.apps.orders.models.Plan',
'orchestra.apps.bills.models.Bill',
# 'orchestra.apps.payments.models.PaymentSource',
'orchestra.apps.payments.models.Transaction',

View file

@ -0,0 +1,67 @@
{% extends "admin/base_site.html" %}
{% load i18n l10n %}
{% load url from future %}
{% load admin_urls static utils %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />
<link rel="stylesheet" type="text/css" href="{% static "orchestra/css/hide-inline-id.css" %}" />
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=app_label %}">{{ app_label|capfirst|escape }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
{% if obj %}
&rsaquo; <a href="{% url opts|admin_urlname:'change' obj.pk %}">{{ obj }}</a>
&rsaquo; {{ action_name }}
{% else %}
&rsaquo; {{ action_name }} multiple objects
{% endif %}
</div>
{% endblock %}
{% block content %}
<div>
<div style="margin:20px;">
<p>{{ content_message | safe }}</p>
<ul>
{% for display_object in display_objects %}
<li> <a href="{% url 'admin:nodes_node_change' deletable_object.id %}">{{ deletable_object }} </a></li>
{% endfor %}
</ul>
<form action="" method="post">{% csrf_token %}
{% if form %}
<fieldset class="module aligned">
{% for field in form %}
<div class="form-row ">
<div >
{{ field.errors }}
{% if field|is_checkbox %}
{{ field }} <label for="{{ field.id_for_label }}" class="vCheckboxLabel">{{ field.label }}</label>
{% else %}
{{ field.label_tag }} {{ field }}
{% endif %}
<p class="help">{{ field.help_text|safe }}</p>
</div>
</div>
{% endfor %}
</fieldset>
{% endif %}
{% if formset %}
{{ formset.as_admin }}
{% endif %}
<div>
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
{% endfor %}
<input type="hidden" name="action" value="{{ action_value }}" />
<input type="hidden" name="post" value="generic_confirmation" />
<input type="submit" value="{% trans "Yes, I'm sure" %}" />
</div>
</form>
{% endblock %}

View file

@ -3,6 +3,7 @@ from django.core.urlresolvers import reverse, NoReverseMatch
from django.forms import CheckboxInput
from orchestra import get_version
from orchestra.admin.utils import admin_change_url
register = template.Library()
@ -41,6 +42,11 @@ def size(value, length):
return str(value) + (' '*num_spaces)
@register.filter(name='is_checkbox')
@register.filter
def is_checkbox(field):
return isinstance(field.field.widget, CheckboxInput)
@register.filter
def admin_link(obj):
return admin_change_url(obj)

View file

@ -65,3 +65,8 @@ class OrderedSet(collections.MutableSet):
return len(self) == len(other) and list(self) == list(other)
return set(self) == set(other)
class AttributeDict(dict):
def __init__(self, *args, **kwargs):
super(AttributeDict, self).__init__(*args, **kwargs)
self.__dict__ = self

View file

@ -76,8 +76,9 @@ class BaseLiveServerTestCase(AppDependencyMixin, LiveServerTestCase):
self.account = Account.objects.create(name='orchestra')
self.username = 'orchestra'
self.password = 'orchestra'
self.user = User.objects.create_superuser(username='orchestra', password='orchestra',
email='orchestra@orchestra.org', account=self.account)
self.user = User.objects.create_superuser(username='orchestra',
password='orchestra', email='orchestra@orchestra.org',
account=self.account)
def admin_login(self):
session = SessionStore()