Refactoring payment process
This commit is contained in:
parent
721bcd9002
commit
1456c457fc
|
@ -2,9 +2,12 @@ from functools import wraps, partial
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import available_attrs
|
from django.utils.decorators import available_attrs
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.text import capfirst
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
def admin_field(method):
|
def admin_field(method):
|
||||||
|
@ -24,6 +27,17 @@ def admin_field(method):
|
||||||
return admin_field_wrapper
|
return admin_field_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def format_display_objects(modeladmin, request, queryset):
|
||||||
|
from .utils import change_url
|
||||||
|
opts = modeladmin.model._meta
|
||||||
|
objects = []
|
||||||
|
for obj in queryset:
|
||||||
|
objects.append(format_html('{0}: <a href="{1}">{2}</a>',
|
||||||
|
capfirst(opts.verbose_name), change_url(obj), obj)
|
||||||
|
)
|
||||||
|
return objects
|
||||||
|
|
||||||
|
|
||||||
def action_with_confirmation(action_name=None, extra_context={},
|
def action_with_confirmation(action_name=None, extra_context={},
|
||||||
template='admin/orchestra/generic_confirmation.html'):
|
template='admin/orchestra/generic_confirmation.html'):
|
||||||
"""
|
"""
|
||||||
|
@ -31,11 +45,12 @@ def action_with_confirmation(action_name=None, extra_context={},
|
||||||
If custom template is provided the form must contain:
|
If custom template is provided the form must contain:
|
||||||
<input type="hidden" name="post" value="generic_confirmation" />
|
<input type="hidden" name="post" value="generic_confirmation" />
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(func, extra_context=extra_context, template=template, action_name=action_name):
|
def decorator(func, extra_context=extra_context, template=template, action_name=action_name):
|
||||||
@wraps(func, assigned=available_attrs(func))
|
@wraps(func, assigned=available_attrs(func))
|
||||||
def inner(modeladmin, request, queryset, action_name=action_name):
|
def inner(modeladmin, request, queryset, action_name=action_name, extra_context=extra_context):
|
||||||
# The user has already confirmed the action.
|
# The user has already confirmed the action.
|
||||||
if request.POST.get('post') == "generic_confirmation":
|
if request.POST.get('post') == 'generic_confirmation':
|
||||||
stay = func(modeladmin, request, queryset)
|
stay = func(modeladmin, request, queryset)
|
||||||
if not stay:
|
if not stay:
|
||||||
return
|
return
|
||||||
|
@ -51,19 +66,23 @@ def action_with_confirmation(action_name=None, extra_context={},
|
||||||
if not action_name:
|
if not action_name:
|
||||||
action_name = func.__name__
|
action_name = func.__name__
|
||||||
context = {
|
context = {
|
||||||
"title": "Are you sure?",
|
'title': _("Are you sure?"),
|
||||||
"content_message": "Are you sure you want to %s the selected %s?" %
|
'content_message': _("Are you sure you want to {action} the selected {item}?").format(
|
||||||
(action_name, objects_name),
|
action=action_name, item=objects_name),
|
||||||
"action_name": action_name.capitalize(),
|
'action_name': action_name.capitalize(),
|
||||||
"action_value": action_value,
|
'action_value': action_value,
|
||||||
"display_objects": queryset,
|
|
||||||
'queryset': queryset,
|
'queryset': queryset,
|
||||||
"opts": opts,
|
'opts': opts,
|
||||||
"app_label": app_label,
|
'app_label': app_label,
|
||||||
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if callable(extra_context):
|
||||||
|
extra_context = extra_context(modeladmin, request, queryset)
|
||||||
context.update(extra_context)
|
context.update(extra_context)
|
||||||
|
if 'display_objects' not in context:
|
||||||
|
# Compute it only when necessary
|
||||||
|
context['display_objects'] = format_display_objects(modeladmin, request, queryset)
|
||||||
|
|
||||||
# Display the confirmation page
|
# Display the confirmation page
|
||||||
return TemplateResponse(request, template,
|
return TemplateResponse(request, template,
|
||||||
|
|
|
@ -91,7 +91,7 @@ def action_to_view(action, modeladmin):
|
||||||
return action_view
|
return action_view
|
||||||
|
|
||||||
|
|
||||||
def admin_change_url(obj):
|
def change_url(obj):
|
||||||
opts = obj._meta
|
opts = obj._meta
|
||||||
view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name)
|
view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name)
|
||||||
return reverse(view_name, args=(obj.pk,))
|
return reverse(view_name, args=(obj.pk,))
|
||||||
|
@ -106,7 +106,7 @@ def admin_link(*args, **kwargs):
|
||||||
obj = get_field_value(instance, kwargs['field'])
|
obj = get_field_value(instance, kwargs['field'])
|
||||||
if not getattr(obj, 'pk', None):
|
if not getattr(obj, 'pk', None):
|
||||||
return '---'
|
return '---'
|
||||||
url = admin_change_url(obj)
|
url = change_url(obj)
|
||||||
extra = ''
|
extra = ''
|
||||||
if kwargs['popup']:
|
if kwargs['popup']:
|
||||||
extra = 'onclick="return showAddAnotherPopup(this);"'
|
extra = 'onclick="return showAddAnotherPopup(this);"'
|
||||||
|
|
|
@ -9,7 +9,8 @@ from django.utils.six.moves.urllib.parse import parse_qsl
|
||||||
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
|
||||||
from orchestra.admin.utils import wrap_admin_view, admin_link, set_url_query
|
from orchestra.admin.utils import (wrap_admin_view, admin_link, set_url_query,
|
||||||
|
change_url)
|
||||||
from orchestra.core import services, accounts
|
from orchestra.core import services, accounts
|
||||||
|
|
||||||
from .filters import HasMainUserListFilter
|
from .filters import HasMainUserListFilter
|
||||||
|
@ -136,8 +137,7 @@ class AccountAdminMixin(object):
|
||||||
|
|
||||||
def account_link(self, instance):
|
def account_link(self, instance):
|
||||||
account = instance.account if instance.pk else self.account
|
account = instance.account if instance.pk else self.account
|
||||||
url = reverse('admin:accounts_account_change', args=(account.pk,))
|
url = change_url(account)
|
||||||
pk = account.pk
|
|
||||||
return '<a href="%s">%s</a>' % (url, str(account))
|
return '<a href="%s">%s</a>' % (url, str(account))
|
||||||
account_link.short_description = _("account")
|
account_link.short_description = _("account")
|
||||||
account_link.allow_tags = True
|
account_link.allow_tags = True
|
||||||
|
|
|
@ -42,7 +42,7 @@ view_bill.url_name = 'view'
|
||||||
|
|
||||||
|
|
||||||
def close_bills(modeladmin, request, queryset):
|
def close_bills(modeladmin, request, queryset):
|
||||||
queryset = queryset.filter(status=queryset.model.OPEN)
|
queryset = queryset.filter(is_open=True)
|
||||||
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
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
#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.shortcuts import redirect
|
|
||||||
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
|
||||||
|
@ -17,23 +15,30 @@ from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForm
|
||||||
BillLine)
|
BillLine)
|
||||||
|
|
||||||
|
|
||||||
|
PAYMENT_STATE_COLORS = {
|
||||||
|
Bill.PAID: 'green',
|
||||||
|
Bill.PENDING: 'darkorange',
|
||||||
|
Bill.BAD_DEBT: 'red',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class BillLineInline(admin.TabularInline):
|
class BillLineInline(admin.TabularInline):
|
||||||
model = BillLine
|
model = BillLine
|
||||||
fields = ('description', 'rate', 'amount', 'tax', 'total', 'get_total')
|
fields = ('description', 'rate', 'quantity', 'tax', 'subtotal', 'get_total')
|
||||||
readonly_fields = ('get_total',)
|
readonly_fields = ('get_total',)
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
if obj and obj.status != Bill.OPEN:
|
if obj and not obj.is_open:
|
||||||
return self.fields
|
return self.fields
|
||||||
return super(BillLineInline, self).get_readonly_fields(request, obj=obj)
|
return super(BillLineInline, self).get_readonly_fields(request, obj=obj)
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
if request.__bill__ and request.__bill__.status != Bill.OPEN:
|
if request.__bill__ and not request.__bill__.is_open:
|
||||||
return False
|
return False
|
||||||
return super(BillLineInline, self).has_add_permission(request)
|
return super(BillLineInline, self).has_add_permission(request)
|
||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
def has_delete_permission(self, request, obj=None):
|
||||||
if obj and obj.status != Bill.OPEN:
|
if obj and not obj.is_open:
|
||||||
return False
|
return False
|
||||||
return super(BillLineInline, self).has_delete_permission(request, obj=obj)
|
return super(BillLineInline, self).has_delete_permission(request, obj=obj)
|
||||||
|
|
||||||
|
@ -48,15 +53,15 @@ class BillLineInline(admin.TabularInline):
|
||||||
|
|
||||||
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
'number', 'status', 'type_link', 'account_link', 'created_on_display',
|
'number', 'is_open', 'type_link', 'account_link', 'created_on_display',
|
||||||
'num_lines', 'display_total'
|
'num_lines', 'display_total', 'display_payment_state'
|
||||||
)
|
)
|
||||||
list_filter = (BillTypeListFilter, 'status',)
|
list_filter = (BillTypeListFilter, 'is_open',)
|
||||||
add_fields = ('account', 'type', 'status', 'due_on', 'comments')
|
add_fields = ('account', 'type', 'is_open', 'due_on', 'comments')
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
'fields': ('number', 'display_total', 'account_link', 'type',
|
'fields': ('number', 'display_total', 'account_link', 'type',
|
||||||
'status', 'due_on', 'comments'),
|
'is_open', 'display_payment_state', 'is_sent', 'due_on', 'comments'),
|
||||||
}),
|
}),
|
||||||
(_("Raw"), {
|
(_("Raw"), {
|
||||||
'classes': ('collapse',),
|
'classes': ('collapse',),
|
||||||
|
@ -65,8 +70,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
)
|
)
|
||||||
actions = [download_bills, close_bills, send_bills]
|
actions = [download_bills, close_bills, send_bills]
|
||||||
change_view_actions = [view_bill, download_bills, send_bills, close_bills]
|
change_view_actions = [view_bill, download_bills, send_bills, close_bills]
|
||||||
change_readonly_fields = ('account_link', 'type', 'status')
|
change_readonly_fields = ('account_link', 'type', 'is_open')
|
||||||
readonly_fields = ('number', 'display_total')
|
readonly_fields = ('number', 'display_total', 'display_payment_state')
|
||||||
inlines = [BillLineInline]
|
inlines = [BillLineInline]
|
||||||
|
|
||||||
created_on_display = admin_date('created_on')
|
created_on_display = admin_date('created_on')
|
||||||
|
@ -90,29 +95,36 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
type_link.short_description = _("type")
|
type_link.short_description = _("type")
|
||||||
type_link.admin_order_field = 'type'
|
type_link.admin_order_field = 'type'
|
||||||
|
|
||||||
|
def display_payment_state(self, bill):
|
||||||
|
topts = bill.transactions.model._meta
|
||||||
|
url = reverse('admin:%s_%s_changelist' % (topts.app_label, topts.module_name))
|
||||||
|
url += '?bill=%i' % bill.pk
|
||||||
|
state = bill.get_payment_state_display().upper()
|
||||||
|
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
|
||||||
|
return '<a href="{url}" style="color:{color}">{name}</a>'.format(
|
||||||
|
url=url, color=color, name=state)
|
||||||
|
display_payment_state.allow_tags = True
|
||||||
|
display_payment_state.short_description = _("Payment")
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
fields = super(BillAdmin, self).get_readonly_fields(request, obj=obj)
|
fields = super(BillAdmin, self).get_readonly_fields(request, obj=obj)
|
||||||
if obj and obj.status != Bill.OPEN:
|
if obj and not obj.is_open:
|
||||||
fields += self.add_fields
|
fields += self.add_fields
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def get_fieldsets(self, request, obj=None):
|
def get_fieldsets(self, request, obj=None):
|
||||||
fieldsets = super(BillAdmin, self).get_fieldsets(request, obj=obj)
|
fieldsets = super(BillAdmin, self).get_fieldsets(request, obj=obj)
|
||||||
if obj and obj.status == obj.OPEN:
|
if obj and obj.is_open:
|
||||||
fieldsets = (fieldsets[0],)
|
fieldsets = (fieldsets[0],)
|
||||||
return fieldsets
|
return fieldsets
|
||||||
|
|
||||||
def get_change_view_actions(self, obj=None):
|
def get_change_view_actions(self, obj=None):
|
||||||
actions = super(BillAdmin, self).get_change_view_actions()
|
actions = super(BillAdmin, self).get_change_view_actions()
|
||||||
discard = []
|
exclude = []
|
||||||
if obj:
|
if obj:
|
||||||
if obj.status != Bill.OPEN:
|
if not obj.is_open:
|
||||||
discard = ['close_bills']
|
exclude.append('close_bills')
|
||||||
if obj.status != Bill.CLOSED:
|
return [action for action in actions if action.__name__ not in exclude]
|
||||||
discard = ['send_bills']
|
|
||||||
if not discard:
|
|
||||||
return actions
|
|
||||||
return [action for action in actions if action.__name__ not in discard]
|
|
||||||
|
|
||||||
def get_inline_instances(self, request, obj=None):
|
def get_inline_instances(self, request, obj=None):
|
||||||
# Make parent object available for inline.has_add_permission()
|
# Make parent object available for inline.has_add_permission()
|
||||||
|
|
|
@ -4,6 +4,7 @@ from dateutil.relativedelta import relativedelta
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.template import loader, Context
|
from django.template import loader, Context
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.encoding import force_text
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
@ -25,16 +26,13 @@ class BillManager(models.Manager):
|
||||||
|
|
||||||
|
|
||||||
class Bill(models.Model):
|
class Bill(models.Model):
|
||||||
OPEN = 'OPEN'
|
OPEN = ''
|
||||||
CLOSED = 'CLOSED'
|
|
||||||
SENT = 'SENT'
|
|
||||||
PAID = 'PAID'
|
PAID = 'PAID'
|
||||||
|
PENDING = 'PENDING'
|
||||||
BAD_DEBT = 'BAD_DEBT'
|
BAD_DEBT = 'BAD_DEBT'
|
||||||
STATUSES = (
|
PAYMENT_STATES = (
|
||||||
(OPEN, _("Open")),
|
|
||||||
(CLOSED, _("Closed")),
|
|
||||||
(SENT, _("Sent")),
|
|
||||||
(PAID, _("Paid")),
|
(PAID, _("Paid")),
|
||||||
|
(PENDING, _("Pending")),
|
||||||
(BAD_DEBT, _("Bad debt")),
|
(BAD_DEBT, _("Bad debt")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,10 +49,10 @@ class Bill(models.Model):
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
||||||
related_name='%(class)s')
|
related_name='%(class)s')
|
||||||
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
||||||
status = models.CharField(_("status"), max_length=16, choices=STATUSES,
|
|
||||||
default=OPEN)
|
|
||||||
created_on = models.DateTimeField(_("created on"), auto_now_add=True)
|
created_on = models.DateTimeField(_("created on"), auto_now_add=True)
|
||||||
closed_on = models.DateTimeField(_("closed on"), blank=True, null=True)
|
closed_on = models.DateTimeField(_("closed on"), blank=True, null=True)
|
||||||
|
is_open = models.BooleanField(_("is open"), default=True)
|
||||||
|
is_sent = models.BooleanField(_("is sent"), default=False)
|
||||||
due_on = models.DateField(_("due on"), null=True, blank=True)
|
due_on = models.DateField(_("due on"), null=True, blank=True)
|
||||||
last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True)
|
last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True)
|
||||||
total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
||||||
|
@ -74,6 +72,21 @@ class Bill(models.Model):
|
||||||
def buyer(self):
|
def buyer(self):
|
||||||
return self.account.invoicecontact
|
return self.account.invoicecontact
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def payment_state(self):
|
||||||
|
if self.is_open:
|
||||||
|
return self.OPEN
|
||||||
|
secured = self.transactions.secured().amount()
|
||||||
|
if secured >= self.total:
|
||||||
|
return self.PAID
|
||||||
|
elif self.transactions.exclude_rejected().exists():
|
||||||
|
return self.PENDING
|
||||||
|
return self.BAD_DEBT
|
||||||
|
|
||||||
|
def get_payment_state_display(self):
|
||||||
|
value = self.payment_state
|
||||||
|
return force_text(dict(self.PAYMENT_STATES).get(value, value))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_class_type(cls):
|
def get_class_type(cls):
|
||||||
return cls.__name__.upper()
|
return cls.__name__.upper()
|
||||||
|
@ -87,7 +100,7 @@ class Bill(models.Model):
|
||||||
if bill_type == 'BILL':
|
if bill_type == 'BILL':
|
||||||
raise TypeError("get_new_number() can not be used on a Bill class")
|
raise TypeError("get_new_number() can not be used on a Bill class")
|
||||||
prefix = getattr(settings, 'BILLS_%s_NUMBER_PREFIX' % bill_type)
|
prefix = getattr(settings, 'BILLS_%s_NUMBER_PREFIX' % bill_type)
|
||||||
if self.status == self.OPEN:
|
if self.is_open:
|
||||||
prefix = 'O{}'.format(prefix)
|
prefix = 'O{}'.format(prefix)
|
||||||
bills = cls.objects.filter(number__regex=r'^%s[1-9]+' % prefix)
|
bills = cls.objects.filter(number__regex=r'^%s[1-9]+' % prefix)
|
||||||
last_number = bills.order_by('-number').values_list('number', flat=True).first()
|
last_number = bills.order_by('-number').values_list('number', flat=True).first()
|
||||||
|
@ -110,7 +123,7 @@ class Bill(models.Model):
|
||||||
return now + relativedelta(months=1)
|
return now + relativedelta(months=1)
|
||||||
|
|
||||||
def close(self, payment=False):
|
def close(self, payment=False):
|
||||||
assert self.status == self.OPEN, "Bill not in Open state"
|
assert self.is_open, "Bill not in Open state"
|
||||||
if payment is False:
|
if payment is False:
|
||||||
payment = self.account.paymentsources.get_default()
|
payment = self.account.paymentsources.get_default()
|
||||||
if not self.due_on:
|
if not self.due_on:
|
||||||
|
@ -119,7 +132,8 @@ class Bill(models.Model):
|
||||||
self.html = self.render(payment=payment)
|
self.html = self.render(payment=payment)
|
||||||
self.transactions.create(bill=self, source=payment, amount=self.total)
|
self.transactions.create(bill=self, source=payment, amount=self.total)
|
||||||
self.closed_on = timezone.now()
|
self.closed_on = timezone.now()
|
||||||
self.status = self.CLOSED
|
self.is_open = False
|
||||||
|
self.is_sent = False
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def send(self):
|
def send(self):
|
||||||
|
@ -134,7 +148,7 @@ class Bill(models.Model):
|
||||||
('%s.pdf' % self.number, html_to_pdf(self.html), 'application/pdf')
|
('%s.pdf' % self.number, html_to_pdf(self.html), 'application/pdf')
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
self.status = self.SENT
|
self.is_sent = True
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def render(self, payment=False):
|
def render(self, payment=False):
|
||||||
|
@ -166,7 +180,7 @@ class Bill(models.Model):
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.type:
|
if not self.type:
|
||||||
self.type = self.get_type()
|
self.type = self.get_type()
|
||||||
if not self.number or (self.number.startswith('O') and self.status != self.OPEN):
|
if not self.number or (self.number.startswith('O') and not self.is_open):
|
||||||
self.set_number()
|
self.set_number()
|
||||||
super(Bill, self).save(*args, **kwargs)
|
super(Bill, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -217,8 +231,8 @@ class BillLine(models.Model):
|
||||||
description = models.CharField(_("description"), max_length=256)
|
description = models.CharField(_("description"), max_length=256)
|
||||||
rate = models.DecimalField(_("rate"), blank=True, null=True,
|
rate = models.DecimalField(_("rate"), blank=True, null=True,
|
||||||
max_digits=12, decimal_places=2)
|
max_digits=12, decimal_places=2)
|
||||||
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
|
quantity = models.DecimalField(_("quantity"), max_digits=12, decimal_places=2)
|
||||||
total = models.DecimalField(_("total"), max_digits=12, decimal_places=2)
|
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
|
||||||
tax = models.PositiveIntegerField(_("tax"))
|
tax = models.PositiveIntegerField(_("tax"))
|
||||||
# TODO
|
# TODO
|
||||||
# order_id = models.ForeignKey('orders.Order', null=True, blank=True,
|
# order_id = models.ForeignKey('orders.Order', null=True, blank=True,
|
||||||
|
@ -236,15 +250,15 @@ class BillLine(models.Model):
|
||||||
|
|
||||||
def get_total(self):
|
def get_total(self):
|
||||||
""" Computes subline discounts """
|
""" Computes subline discounts """
|
||||||
subtotal = self.total
|
total = self.subtotal
|
||||||
for subline in self.sublines.all():
|
for subline in self.sublines.all():
|
||||||
subtotal += subline.total
|
total += subline.total
|
||||||
return subtotal
|
return total
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# TODO cost of this shit
|
# TODO cost of this shit
|
||||||
super(BillLine, self).save(*args, **kwargs)
|
super(BillLine, self).save(*args, **kwargs)
|
||||||
if self.bill.status == self.bill.OPEN:
|
if self.bill.is_open:
|
||||||
self.bill.total = self.bill.get_total()
|
self.bill.total = self.bill.get_total()
|
||||||
self.bill.save()
|
self.bill.save()
|
||||||
|
|
||||||
|
@ -260,7 +274,7 @@ class BillSubline(models.Model):
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# TODO cost of this shit
|
# TODO cost of this shit
|
||||||
super(BillSubline, self).save(*args, **kwargs)
|
super(BillSubline, self).save(*args, **kwargs)
|
||||||
if self.line.bill.status == self.line.bill.OPEN:
|
if self.line.bill.is_open:
|
||||||
self.line.bill.total = self.line.bill.get_total()
|
self.line.bill.total = self.line.bill.get_total()
|
||||||
self.line.bill.save()
|
self.line.bill.save()
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.template.response import TemplateResponse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin
|
from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin
|
||||||
from orchestra.admin.utils import wrap_admin_view, admin_link
|
from orchestra.admin.utils import wrap_admin_view, admin_link, change_url
|
||||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||||
from orchestra.utils import apps
|
from orchestra.utils import apps
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin
|
||||||
if webs:
|
if webs:
|
||||||
links = []
|
links = []
|
||||||
for web in webs:
|
for web in webs:
|
||||||
url = reverse('admin:websites_website_change', args=(web.pk,))
|
url = change_url(web)
|
||||||
links.append('<a href="%s">%s</a>' % (url, web.name))
|
links.append('<a href="%s">%s</a>' % (url, web.name))
|
||||||
return '<br>'.join(links)
|
return '<br>'.join(links)
|
||||||
return _("No website")
|
return _("No website")
|
||||||
|
|
|
@ -5,6 +5,7 @@ from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
|
|
||||||
|
from orchestra.admin.utils import change_url
|
||||||
from orchestra.apps.users.models import User
|
from orchestra.apps.users.models import User
|
||||||
from orchestra.forms.widgets import ReadOnlyWidget
|
from orchestra.forms.widgets import ReadOnlyWidget
|
||||||
|
|
||||||
|
@ -41,7 +42,7 @@ class MessageInlineForm(forms.ModelForm):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(MessageInlineForm, self).__init__(*args, **kwargs)
|
super(MessageInlineForm, self).__init__(*args, **kwargs)
|
||||||
admin_link = reverse('admin:users_user_change', args=(self.user.pk,))
|
admin_link = change_url(self.user)
|
||||||
self.fields['created_on'].widget = ReadOnlyWidget('')
|
self.fields['created_on'].widget = ReadOnlyWidget('')
|
||||||
|
|
||||||
def clean_content(self):
|
def clean_content(self):
|
||||||
|
|
|
@ -7,7 +7,7 @@ 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
|
||||||
from orchestra.admin.utils import insertattr, admin_link
|
from orchestra.admin.utils import insertattr, admin_link, change_url
|
||||||
from orchestra.apps.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin
|
from orchestra.apps.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin
|
||||||
from orchestra.apps.domains.forms import DomainIterator
|
from orchestra.apps.domains.forms import DomainIterator
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ class MailboxAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
def display_addresses(self, mailbox):
|
def display_addresses(self, mailbox):
|
||||||
addresses = []
|
addresses = []
|
||||||
for addr in mailbox.addresses.all():
|
for addr in mailbox.addresses.all():
|
||||||
url = reverse('admin:mails_address_change', args=(addr.pk,))
|
url = change_url(addr)
|
||||||
addresses.append('<a href="%s">%s</a>' % (url, addr.email))
|
addresses.append('<a href="%s">%s</a>' % (url, addr.email))
|
||||||
return '<br>'.join(addresses)
|
return '<br>'.join(addresses)
|
||||||
display_addresses.short_description = _("Addresses")
|
display_addresses.short_description = _("Addresses")
|
||||||
|
@ -106,7 +106,7 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
def display_mailboxes(self, address):
|
def display_mailboxes(self, address):
|
||||||
boxes = []
|
boxes = []
|
||||||
for mailbox in address.mailboxes.all():
|
for mailbox in address.mailboxes.all():
|
||||||
url = reverse('admin:mails_mailbox_change', args=(mailbox.pk,))
|
url = change_url(mailbox)
|
||||||
boxes.append('<a href="%s">%s</a>' % (url, mailbox.name))
|
boxes.append('<a href="%s">%s</a>' % (url, mailbox.name))
|
||||||
return '<br>'.join(boxes)
|
return '<br>'.join(boxes)
|
||||||
display_mailboxes.short_description = _("Mailboxes")
|
display_mailboxes.short_description = _("Mailboxes")
|
||||||
|
|
|
@ -33,8 +33,8 @@ class BillsBackend(object):
|
||||||
# Create bill line
|
# Create bill line
|
||||||
billine = bill.lines.create(
|
billine = bill.lines.create(
|
||||||
rate=service.nominal_price,
|
rate=service.nominal_price,
|
||||||
amount=line.size,
|
quantity=line.size,
|
||||||
total=line.subtotal,
|
subtotal=line.subtotal,
|
||||||
tax=service.tax,
|
tax=service.tax,
|
||||||
description=self.get_line_description(line),
|
description=self.get_line_description(line),
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,7 @@ 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 AdminFormMixin
|
from orchestra.admin.forms import AdminFormMixin
|
||||||
from orchestra.admin.utils import admin_change_url
|
from orchestra.admin.utils import change_url
|
||||||
|
|
||||||
from .models import Order
|
from .models import Order
|
||||||
|
|
||||||
|
@ -32,8 +32,8 @@ def selected_related_choices(queryset):
|
||||||
verbose = '<a href="{order_url}">{description}</a> '
|
verbose = '<a href="{order_url}">{description}</a> '
|
||||||
verbose += '<a class="account" href="{account_url}">{account}</a>'
|
verbose += '<a class="account" href="{account_url}">{account}</a>'
|
||||||
verbose = verbose.format(
|
verbose = verbose.format(
|
||||||
order_url=admin_change_url(order), description=order.description,
|
order_url=change_url(order), description=order.description,
|
||||||
account_url=admin_change_url(order.account), account=str(order.account)
|
account_url=change_url(order.account), account=str(order.account)
|
||||||
)
|
)
|
||||||
yield (order.pk, mark_safe(verbose))
|
yield (order.pk, mark_safe(verbose))
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.migrations.recorder import MigrationRecorder
|
from django.db.migrations.recorder import MigrationRecorder
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
|
@ -10,7 +9,6 @@ from django.dispatch import receiver
|
||||||
from django.contrib.admin.models import LogEntry
|
from django.contrib.admin.models import LogEntry
|
||||||
from django.contrib.contenttypes import generic
|
from django.contrib.contenttypes import generic
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.validators import ValidationError
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
|
@ -13,7 +13,7 @@ from orchestra.apps.users.models import User
|
||||||
from orchestra.utils.tests import BaseTestCase, random_ascii
|
from orchestra.utils.tests import BaseTestCase, random_ascii
|
||||||
|
|
||||||
|
|
||||||
class ServiceTests(BaseTestCase):
|
class BillingTests(BaseTestCase):
|
||||||
DEPENDENCIES = (
|
DEPENDENCIES = (
|
||||||
'orchestra.apps.services',
|
'orchestra.apps.services',
|
||||||
'orchestra.apps.users',
|
'orchestra.apps.users',
|
||||||
|
@ -91,3 +91,18 @@ class ServiceTests(BaseTestCase):
|
||||||
error = decimal.Decimal(0.05)
|
error = decimal.Decimal(0.05)
|
||||||
self.assertGreater(10*size+error*(10*size), bills[0].get_total())
|
self.assertGreater(10*size+error*(10*size), bills[0].get_total())
|
||||||
self.assertLess(10*size-error*(10*size), bills[0].get_total())
|
self.assertLess(10*size-error*(10*size), bills[0].get_total())
|
||||||
|
|
||||||
|
def test_ftp_account_with_compensation(self):
|
||||||
|
account = self.create_account()
|
||||||
|
service = self.create_ftp_service()
|
||||||
|
user = self.create_ftp(account=account)
|
||||||
|
bp = timezone.now().date() + relativedelta.relativedelta(years=2)
|
||||||
|
bills = service.orders.bill(billing_point=bp, fixed_point=True)
|
||||||
|
user.delete()
|
||||||
|
user = self.create_ftp(account=account)
|
||||||
|
bp = timezone.now().date() + relativedelta.relativedelta(years=1)
|
||||||
|
bills = service.orders.bill(billing_point=bp, fixed_point=True)
|
||||||
|
for line in bills[0].lines.all():
|
||||||
|
print line
|
||||||
|
print line.sublines.all()
|
||||||
|
# TODO asserts
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.shortcuts import render
|
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 ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin.decorators import action_with_confirmation
|
from orchestra.admin.decorators import action_with_confirmation
|
||||||
|
from orchestra.admin.utils import change_url
|
||||||
|
|
||||||
from .methods import PaymentMethod
|
from .methods import PaymentMethod
|
||||||
from .models import Transaction
|
from .models import Transaction
|
||||||
|
@ -38,8 +43,7 @@ def process_transactions(modeladmin, request, queryset):
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@action_with_confirmation()
|
@action_with_confirmation()
|
||||||
def mark_as_executed(modeladmin, request, queryset):
|
def mark_as_executed(modeladmin, request, queryset, extra_context={}):
|
||||||
""" Mark a tickets as unread """
|
|
||||||
for transaction in queryset:
|
for transaction in queryset:
|
||||||
transaction.mark_as_executed()
|
transaction.mark_as_executed()
|
||||||
modeladmin.log_change(request, transaction, 'Executed')
|
modeladmin.log_change(request, transaction, 'Executed')
|
||||||
|
@ -52,7 +56,6 @@ mark_as_executed.verbose_name = _("Mark as executed")
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@action_with_confirmation()
|
@action_with_confirmation()
|
||||||
def mark_as_secured(modeladmin, request, queryset):
|
def mark_as_secured(modeladmin, request, queryset):
|
||||||
""" Mark a tickets as unread """
|
|
||||||
for transaction in queryset:
|
for transaction in queryset:
|
||||||
transaction.mark_as_secured()
|
transaction.mark_as_secured()
|
||||||
modeladmin.log_change(request, transaction, 'Secured')
|
modeladmin.log_change(request, transaction, 'Secured')
|
||||||
|
@ -65,7 +68,6 @@ mark_as_secured.verbose_name = _("Mark as secured")
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@action_with_confirmation()
|
@action_with_confirmation()
|
||||||
def mark_as_rejected(modeladmin, request, queryset):
|
def mark_as_rejected(modeladmin, request, queryset):
|
||||||
""" Mark a tickets as unread """
|
|
||||||
for transaction in queryset:
|
for transaction in queryset:
|
||||||
transaction.mark_as_rejected()
|
transaction.mark_as_rejected()
|
||||||
modeladmin.log_change(request, transaction, 'Rejected')
|
modeladmin.log_change(request, transaction, 'Rejected')
|
||||||
|
@ -73,3 +75,62 @@ def mark_as_rejected(modeladmin, request, queryset):
|
||||||
modeladmin.message_user(request, msg)
|
modeladmin.message_user(request, msg)
|
||||||
mark_as_rejected.url_name = 'reject'
|
mark_as_rejected.url_name = 'reject'
|
||||||
mark_as_rejected.verbose_name = _("Mark as rejected")
|
mark_as_rejected.verbose_name = _("Mark as rejected")
|
||||||
|
|
||||||
|
|
||||||
|
def _format_display_objects(modeladmin, request, queryset, related):
|
||||||
|
objects = []
|
||||||
|
opts = modeladmin.model._meta
|
||||||
|
for obj in queryset:
|
||||||
|
objects.append(
|
||||||
|
mark_safe('{0}: <a href="{1}">{2}</a>'.format(
|
||||||
|
capfirst(opts.verbose_name), change_url(obj), obj))
|
||||||
|
)
|
||||||
|
subobjects = []
|
||||||
|
attr, verb = related
|
||||||
|
for related in getattr(obj.transactions, attr)():
|
||||||
|
subobjects.append(
|
||||||
|
mark_safe('{0}: <a href="{1}">{2}</a> will be marked as {3}'.format(
|
||||||
|
capfirst(subobj.get_type().lower()), change_url(subobj), subobj, verb))
|
||||||
|
)
|
||||||
|
objects.append(subobjects)
|
||||||
|
return {'display_objects': objects}
|
||||||
|
|
||||||
|
_format_executed = partial(_format_display_objects, related=('all', 'executed'))
|
||||||
|
_format_abort = partial(_format_display_objects, related=('processing', 'aborted'))
|
||||||
|
_format_commit = partial(_format_display_objects, related=('all', 'secured'))
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
@action_with_confirmation(extra_context=_format_executed)
|
||||||
|
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()
|
||||||
|
modeladmin.message_user(request, msg)
|
||||||
|
mark_process_as_executed.url_name = 'executed'
|
||||||
|
mark_process_as_executed.verbose_name = _("Mark as executed")
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
@action_with_confirmation(extra_context=_format_abort)
|
||||||
|
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()
|
||||||
|
modeladmin.message_user(request, msg)
|
||||||
|
abort.url_name = 'abort'
|
||||||
|
abort.verbose_name = _("Abort")
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
@action_with_confirmation(extra_context=_format_commit)
|
||||||
|
def commit(modeladmin, request, queryset):
|
||||||
|
for transaction in queryset:
|
||||||
|
transaction.mark_as_rejected()
|
||||||
|
modeladmin.log_change(request, transaction, 'Rejected')
|
||||||
|
msg = _("%s selected transactions have been marked as rejected.") % queryset.count()
|
||||||
|
modeladmin.message_user(request, msg)
|
||||||
|
commit.url_name = 'commit'
|
||||||
|
commit.verbose_name = _("Commit")
|
||||||
|
|
|
@ -16,76 +16,13 @@ from .models import PaymentSource, Transaction, TransactionProcess
|
||||||
|
|
||||||
STATE_COLORS = {
|
STATE_COLORS = {
|
||||||
Transaction.WAITTING_PROCESSING: 'darkorange',
|
Transaction.WAITTING_PROCESSING: 'darkorange',
|
||||||
Transaction.WAITTING_CONFIRMATION: 'magenta',
|
Transaction.WAITTING_EXECUTION: 'magenta',
|
||||||
Transaction.EXECUTED: 'olive',
|
Transaction.EXECUTED: 'olive',
|
||||||
Transaction.SECURED: 'green',
|
Transaction.SECURED: 'green',
|
||||||
Transaction.REJECTED: 'red',
|
Transaction.REJECTED: 'red',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TransactionInline(admin.TabularInline):
|
|
||||||
model = Transaction
|
|
||||||
can_delete = False
|
|
||||||
extra = 0
|
|
||||||
fields = (
|
|
||||||
'transaction_link', 'bill_link', 'source_link', 'display_state',
|
|
||||||
'amount', 'currency'
|
|
||||||
)
|
|
||||||
readonly_fields = fields
|
|
||||||
|
|
||||||
transaction_link = admin_link('__unicode__', short_description=_("ID"))
|
|
||||||
bill_link = admin_link('bill')
|
|
||||||
source_link = admin_link('source')
|
|
||||||
display_state = admin_colored('state', colors=STATE_COLORS)
|
|
||||||
|
|
||||||
class Media:
|
|
||||||
css = {
|
|
||||||
'all': ('orchestra/css/hide-inline-id.css',)
|
|
||||||
}
|
|
||||||
|
|
||||||
def has_add_permission(self, *args, **kwargs):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionAdmin(ChangeViewActionsMixin, AccountAdminMixin, admin.ModelAdmin):
|
|
||||||
list_display = (
|
|
||||||
'id', 'bill_link', 'account_link', 'source_link', 'display_state',
|
|
||||||
'amount', 'process_link'
|
|
||||||
)
|
|
||||||
list_filter = ('source__method', 'state')
|
|
||||||
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')
|
|
||||||
|
|
||||||
bill_link = admin_link('bill')
|
|
||||||
source_link = admin_link('source')
|
|
||||||
process_link = admin_link('process', short_description=_("proc"))
|
|
||||||
account_link = admin_link('bill__account')
|
|
||||||
display_state = admin_colored('state', colors=STATE_COLORS)
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
|
||||||
qs = super(TransactionAdmin, self).get_queryset(request)
|
|
||||||
return qs.select_related('source', 'bill__account__user')
|
|
||||||
|
|
||||||
def get_change_view_actions(self, obj=None):
|
|
||||||
actions = super(TransactionAdmin, self).get_change_view_actions()
|
|
||||||
discard = []
|
|
||||||
if obj:
|
|
||||||
if obj.state == Transaction.EXECUTED:
|
|
||||||
discard = ['mark_as_executed']
|
|
||||||
elif obj.state == Transaction.REJECTED:
|
|
||||||
discard = ['mark_as_rejected']
|
|
||||||
elif obj.state == Transaction.SECURED:
|
|
||||||
discard = ['mark_as_secured']
|
|
||||||
if not discard:
|
|
||||||
return actions
|
|
||||||
return [action for action in actions if action.__name__ not in discard]
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
|
class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||||
list_display = ('label', 'method', 'number', 'account_link', 'is_active')
|
list_display = ('label', 'method', 'number', 'account_link', 'is_active')
|
||||||
list_filter = ('method', 'is_active')
|
list_filter = ('method', 'is_active')
|
||||||
|
@ -138,11 +75,74 @@ class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
|
|
||||||
class TransactionProcessAdmin(admin.ModelAdmin):
|
class TransactionInline(admin.TabularInline):
|
||||||
|
model = Transaction
|
||||||
|
can_delete = False
|
||||||
|
extra = 0
|
||||||
|
fields = (
|
||||||
|
'transaction_link', 'bill_link', 'source_link', 'display_state',
|
||||||
|
'amount', 'currency'
|
||||||
|
)
|
||||||
|
readonly_fields = fields
|
||||||
|
|
||||||
|
transaction_link = admin_link('__unicode__', short_description=_("ID"))
|
||||||
|
bill_link = admin_link('bill')
|
||||||
|
source_link = admin_link('source')
|
||||||
|
display_state = admin_colored('state', colors=STATE_COLORS)
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = {
|
||||||
|
'all': ('orchestra/css/hide-inline-id.css',)
|
||||||
|
}
|
||||||
|
|
||||||
|
def has_add_permission(self, *args, **kwargs):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionAdmin(ChangeViewActionsMixin, AccountAdminMixin, admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
'id', 'bill_link', 'account_link', 'source_link', 'display_state',
|
||||||
|
'amount', 'process_link'
|
||||||
|
)
|
||||||
|
list_filter = ('source__method', 'state')
|
||||||
|
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')
|
||||||
|
|
||||||
|
bill_link = admin_link('bill')
|
||||||
|
source_link = admin_link('source')
|
||||||
|
process_link = admin_link('process', short_description=_("proc"))
|
||||||
|
account_link = admin_link('bill__account')
|
||||||
|
display_state = admin_colored('state', colors=STATE_COLORS)
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
qs = super(TransactionAdmin, self).get_queryset(request)
|
||||||
|
return qs.select_related('source', 'bill__account__user')
|
||||||
|
|
||||||
|
def get_change_view_actions(self, obj=None):
|
||||||
|
actions = super(TransactionAdmin, self).get_change_view_actions()
|
||||||
|
exclude = []
|
||||||
|
if obj:
|
||||||
|
if obj.state == Transaction.EXECUTED:
|
||||||
|
exclude.append('mark_as_executed')
|
||||||
|
elif obj.state == Transaction.REJECTED:
|
||||||
|
exclude.append('mark_as_rejected')
|
||||||
|
elif obj.state == Transaction.SECURED:
|
||||||
|
exclude.append('mark_as_secured')
|
||||||
|
return [action for action in actions if action.__name__ not in exclude]
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
|
||||||
list_display = ('id', 'file_url', 'display_transactions', 'created_at')
|
list_display = ('id', 'file_url', 'display_transactions', 'created_at')
|
||||||
fields = ('data', 'file_url', 'display_transactions', 'created_at')
|
fields = ('data', 'file_url', 'display_transactions', 'created_at')
|
||||||
readonly_fields = ('file_url', 'display_transactions', 'created_at')
|
readonly_fields = ('file_url', 'display_transactions', 'created_at')
|
||||||
inlines = [TransactionInline]
|
inlines = [TransactionInline]
|
||||||
|
actions = (actions.mark_process_as_executed, actions.abort, actions.commit)
|
||||||
|
change_view_actions = actions
|
||||||
|
|
||||||
def file_url(self, process):
|
def file_url(self, process):
|
||||||
if process.file:
|
if process.file:
|
||||||
|
@ -169,6 +169,18 @@ class TransactionProcessAdmin(admin.ModelAdmin):
|
||||||
return '<a href="%s">%s</a>' % (url, '<br>'.join(lines))
|
return '<a href="%s">%s</a>' % (url, '<br>'.join(lines))
|
||||||
display_transactions.short_description = _("Transactions")
|
display_transactions.short_description = _("Transactions")
|
||||||
display_transactions.allow_tags = True
|
display_transactions.allow_tags = True
|
||||||
|
|
||||||
|
def get_change_view_actions(self, obj=None):
|
||||||
|
actions = super(TransactionProcessAdmin, self).get_change_view_actions()
|
||||||
|
exclude = []
|
||||||
|
if obj:
|
||||||
|
if obj.state == TransactionProcess.EXECUTED:
|
||||||
|
exclude.append('mark_process_as_executed')
|
||||||
|
elif obj.state == TransactionProcess.COMMITED:
|
||||||
|
exclude = ['mark_process_as_executed', 'abort', 'commit']
|
||||||
|
elif obj.state == TransactionProcess.ABORTED:
|
||||||
|
exclude = ['mark_process_as_executed', 'abort', 'commit']
|
||||||
|
return [action for action in actions if action.__name__ not in exclude]
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(PaymentSource, PaymentSourceAdmin)
|
admin.site.register(PaymentSource, PaymentSourceAdmin)
|
||||||
|
|
|
@ -59,27 +59,36 @@ class TransactionQuerySet(models.QuerySet):
|
||||||
source = kwargs.get('source')
|
source = kwargs.get('source')
|
||||||
if source is None or not hasattr(source.method_class, 'process'):
|
if source is None or not hasattr(source.method_class, 'process'):
|
||||||
# Manual payments don't need processing
|
# Manual payments don't need processing
|
||||||
kwargs['state']=self.model.WAITTING_CONFIRMATION
|
kwargs['state']=self.model.WAITTING_EXECUTION
|
||||||
return super(TransactionQuerySet, self).create(**kwargs)
|
return super(TransactionQuerySet, self).create(**kwargs)
|
||||||
|
|
||||||
|
def secured(self):
|
||||||
|
return self.filter(state=Transaction.SECURED)
|
||||||
|
|
||||||
|
def exclude_rejected(self):
|
||||||
|
return self.exclude(state=Transaction.REJECTED)
|
||||||
|
|
||||||
|
def amount(self):
|
||||||
|
return self.aggregate(models.Sum('amount')).values()[0]
|
||||||
|
|
||||||
|
def processing(self):
|
||||||
|
return self.filter(state__in=[Transaction.EXECUTED, Transaction.WAITTING_EXECUTION])
|
||||||
|
|
||||||
|
|
||||||
# TODO lock transaction in waiting confirmation
|
|
||||||
class Transaction(models.Model):
|
class Transaction(models.Model):
|
||||||
WAITTING_PROCESSING = 'WAITTING_PROCESSING' # CREATED
|
WAITTING_PROCESSING = 'WAITTING_PROCESSING' # CREATED
|
||||||
WAITTING_CONFIRMATION = 'WAITTING_CONFIRMATION' # PROCESSED
|
WAITTING_EXECUTION = 'WAITTING_EXECUTION' # PROCESSED
|
||||||
EXECUTED = 'EXECUTED'
|
EXECUTED = 'EXECUTED'
|
||||||
SECURED = 'SECURED'
|
SECURED = 'SECURED'
|
||||||
REJECTED = 'REJECTED'
|
REJECTED = 'REJECTED'
|
||||||
STATES = (
|
STATES = (
|
||||||
(WAITTING_PROCESSING, _("Waitting processing")),
|
(WAITTING_PROCESSING, _("Waitting processing")),
|
||||||
(WAITTING_CONFIRMATION, _("Waitting confirmation")),
|
(WAITTING_EXECUTION, _("Waitting execution")),
|
||||||
(EXECUTED, _("Executed")),
|
(EXECUTED, _("Executed")),
|
||||||
(SECURED, _("Secured")),
|
(SECURED, _("Secured")),
|
||||||
(REJECTED, _("Rejected")),
|
(REJECTED, _("Rejected")),
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = TransactionQuerySet.as_manager()
|
|
||||||
|
|
||||||
bill = models.ForeignKey('bills.bill', verbose_name=_("bill"),
|
bill = models.ForeignKey('bills.bill', verbose_name=_("bill"),
|
||||||
related_name='transactions')
|
related_name='transactions')
|
||||||
source = models.ForeignKey(PaymentSource, null=True, blank=True,
|
source = models.ForeignKey(PaymentSource, null=True, blank=True,
|
||||||
|
@ -93,6 +102,8 @@ class Transaction(models.Model):
|
||||||
created_on = models.DateTimeField(auto_now_add=True)
|
created_on = models.DateTimeField(auto_now_add=True)
|
||||||
modified_on = models.DateTimeField(auto_now=True)
|
modified_on = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
objects = TransactionQuerySet.as_manager()
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return "Transaction {}".format(self.id)
|
return "Transaction {}".format(self.id)
|
||||||
|
|
||||||
|
@ -100,19 +111,26 @@ class Transaction(models.Model):
|
||||||
def account(self):
|
def account(self):
|
||||||
return self.bill.account
|
return self.bill.account
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if not self.pk:
|
||||||
|
amount = self.bill.transactions.exclude(state=self.REJECTED).amount()
|
||||||
|
if amount >= self.bill.total:
|
||||||
|
raise ValidationError(_("New transactions can not be allocated for this bill"))
|
||||||
|
|
||||||
|
def mark_as_processed(self):
|
||||||
|
self.state = self.WAITTING_EXECUTION
|
||||||
|
self.save()
|
||||||
|
|
||||||
def mark_as_executed(self):
|
def mark_as_executed(self):
|
||||||
self.state = self.EXECUTED
|
self.state = self.EXECUTED
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def mark_as_secured(self):
|
def mark_as_secured(self):
|
||||||
self.state = self.SECURED
|
self.state = self.SECURED
|
||||||
# TODO think carefully about bill feedback
|
|
||||||
self.bill.mark_as_paid()
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def mark_as_rejected(self):
|
def mark_as_rejected(self):
|
||||||
self.state = self.REJECTED
|
self.state = self.REJECTED
|
||||||
# TODO bill feedback
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
|
@ -120,15 +138,49 @@ class TransactionProcess(models.Model):
|
||||||
"""
|
"""
|
||||||
Stores arbitrary data generated by payment methods while processing transactions
|
Stores arbitrary data generated by payment methods while processing transactions
|
||||||
"""
|
"""
|
||||||
|
CREATED = 'CREATED'
|
||||||
|
EXECUTED = 'EXECUTED'
|
||||||
|
ABORTED = 'ABORTED'
|
||||||
|
COMMITED = 'COMMITED'
|
||||||
|
STATES = (
|
||||||
|
(CREATED, _("Created")),
|
||||||
|
(EXECUTED, _("Executed")),
|
||||||
|
(ABORTED, _("Aborted")),
|
||||||
|
(COMMITED, _("Commited")),
|
||||||
|
)
|
||||||
|
|
||||||
data = JSONField(_("data"), blank=True)
|
data = JSONField(_("data"), blank=True)
|
||||||
file = models.FileField(_("file"), blank=True)
|
file = models.FileField(_("file"), blank=True)
|
||||||
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
state = models.CharField(_("state"), max_length=16, choices=STATES, default=CREATED)
|
||||||
|
created_at = models.DateTimeField(_("created"), auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(_("updated"), auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = _("Transaction processes")
|
verbose_name_plural = _("Transaction processes")
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return str(self.id)
|
return str(self.id)
|
||||||
|
|
||||||
|
def mark_as_executed(self):
|
||||||
|
assert self.state == self.CREATED
|
||||||
|
self.state = self.EXECUTED
|
||||||
|
for transaction in self.transactions.all():
|
||||||
|
transaction.mark_as_executed()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
assert self.state in [self.CREATED, self.EXCECUTED]
|
||||||
|
self.state = self.ABORTED
|
||||||
|
for transaction in self.transaction.all():
|
||||||
|
transaction.mark_as_aborted()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
assert self.state in [self.CREATED, self.EXECUTED]
|
||||||
|
self.state = self.COMMITED
|
||||||
|
for transaction in self.transactions.processing():
|
||||||
|
transaction.mark_as_secured()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
accounts.register(PaymentSource)
|
accounts.register(PaymentSource)
|
||||||
|
|
|
@ -164,7 +164,7 @@ class ServiceHandler(plugins.Plugin):
|
||||||
for order in givers:
|
for order in givers:
|
||||||
if order.billed_until and order.cancelled_on and order.cancelled_on < order.billed_until:
|
if order.billed_until and order.cancelled_on and order.cancelled_on < order.billed_until:
|
||||||
interval = helpers.Interval(order.cancelled_on, order.billed_until, order)
|
interval = helpers.Interval(order.cancelled_on, order.billed_until, order)
|
||||||
compensations.append[interval]
|
compensations.append(interval)
|
||||||
for order in receivers:
|
for order in receivers:
|
||||||
if not order.billed_until or order.billed_until < order.new_billed_until:
|
if not order.billed_until or order.billed_until < order.new_billed_until:
|
||||||
# receiver
|
# receiver
|
||||||
|
@ -277,9 +277,10 @@ class ServiceHandler(plugins.Plugin):
|
||||||
ini = min(ini, cini)
|
ini = min(ini, cini)
|
||||||
end = max(end, bp)
|
end = max(end, bp)
|
||||||
related_orders = account.orders.filter(service=self.service)
|
related_orders = account.orders.filter(service=self.service)
|
||||||
if self.on_cancel == self.COMPENSATE:
|
if self.on_cancel == self.DISCOUNT:
|
||||||
# Get orders pending for compensation
|
# Get orders pending for compensation
|
||||||
givers = related_orders.filter_givers(ini, end)
|
givers = list(related_orders.filter_givers(ini, end))
|
||||||
|
print givers
|
||||||
givers.sort(cmp=helpers.cmp_billed_until_or_registered_on)
|
givers.sort(cmp=helpers.cmp_billed_until_or_registered_on)
|
||||||
orders.sort(cmp=helpers.cmp_billed_until_or_registered_on)
|
orders.sort(cmp=helpers.cmp_billed_until_or_registered_on)
|
||||||
self.compensate(givers, orders, commit=commit)
|
self.compensate(givers, orders, commit=commit)
|
||||||
|
@ -341,6 +342,7 @@ class ServiceHandler(plugins.Plugin):
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def generate_bill_lines(self, orders, account, **options):
|
def generate_bill_lines(self, orders, account, **options):
|
||||||
|
# TODO filter out orders with cancelled_on < billed_until ?
|
||||||
if not self.metric:
|
if not self.metric:
|
||||||
lines = self.bill_with_orders(orders, account, **options)
|
lines = self.bill_with_orders(orders, account, **options)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.db.models.signals import pre_delete, post_delete, post_save
|
from django.db.models.signals import pre_delete, post_delete, post_save
|
||||||
|
@ -90,7 +89,6 @@ class Service(models.Model):
|
||||||
NOTHING = 'NOTHING'
|
NOTHING = 'NOTHING'
|
||||||
DISCOUNT = 'DISCOUNT'
|
DISCOUNT = 'DISCOUNT'
|
||||||
REFOUND = 'REFOUND'
|
REFOUND = 'REFOUND'
|
||||||
COMPENSATE = 'COMPENSATE'
|
|
||||||
PREPAY = 'PREPAY'
|
PREPAY = 'PREPAY'
|
||||||
POSTPAY = 'POSTPAY'
|
POSTPAY = 'POSTPAY'
|
||||||
STEP_PRICE = 'STEP_PRICE'
|
STEP_PRICE = 'STEP_PRICE'
|
||||||
|
@ -174,7 +172,6 @@ class Service(models.Model):
|
||||||
choices=(
|
choices=(
|
||||||
(NOTHING, _("Nothing")),
|
(NOTHING, _("Nothing")),
|
||||||
(DISCOUNT, _("Discount")),
|
(DISCOUNT, _("Discount")),
|
||||||
(COMPENSATE, _("Discount and compensate")),
|
|
||||||
),
|
),
|
||||||
default=DISCOUNT)
|
default=DISCOUNT)
|
||||||
payment_style = models.CharField(_("payment style"), max_length=16,
|
payment_style = models.CharField(_("payment style"), max_length=16,
|
||||||
|
@ -229,11 +226,10 @@ class Service(models.Model):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
content_type = self.handler.get_content_type()
|
content_type = self.handler.get_content_type()
|
||||||
if self.content_type != content_type:
|
if self.content_type != content_type:
|
||||||
msg =_("Content type must be equal to '%s'.") % str(content_type)
|
ct = str(content_type)
|
||||||
raise ValidationError(msg)
|
raise ValidationError(_("Content type must be equal to '%s'.") % ct)
|
||||||
if not self.match:
|
if not self.match:
|
||||||
msg =_("Match should be provided")
|
raise ValidationError(_("Match should be provided"))
|
||||||
raise ValidationError(msg)
|
|
||||||
try:
|
try:
|
||||||
obj = content_type.model_class().objects.all()[0]
|
obj = content_type.model_class().objects.all()[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
|
|
|
@ -136,7 +136,7 @@ class HandlerTests(BaseTestCase):
|
||||||
self.assertIn([order4.billed_until, end, [order2, order3]], chunks)
|
self.assertIn([order4.billed_until, end, [order2, order3]], chunks)
|
||||||
|
|
||||||
def test_sort_billed_until_or_registered_on(self):
|
def test_sort_billed_until_or_registered_on(self):
|
||||||
now = timezone.now()
|
now = timezone.now().date()
|
||||||
order = Order(
|
order = Order(
|
||||||
billed_until=now+datetime.timedelta(days=200))
|
billed_until=now+datetime.timedelta(days=200))
|
||||||
order1 = Order(
|
order1 = Order(
|
||||||
|
@ -158,7 +158,7 @@ class HandlerTests(BaseTestCase):
|
||||||
self.assertEqual(orders, sorted(orders, cmp=helpers.cmp_billed_until_or_registered_on))
|
self.assertEqual(orders, sorted(orders, cmp=helpers.cmp_billed_until_or_registered_on))
|
||||||
|
|
||||||
def test_compensation(self):
|
def test_compensation(self):
|
||||||
now = timezone.now()
|
now = timezone.now().date()
|
||||||
order = Order(
|
order = Order(
|
||||||
description='0',
|
description='0',
|
||||||
billed_until=now+datetime.timedelta(days=220),
|
billed_until=now+datetime.timedelta(days=220),
|
||||||
|
@ -353,5 +353,16 @@ class HandlerTests(BaseTestCase):
|
||||||
self.assertEqual(rate['price'], result.price)
|
self.assertEqual(rate['price'], result.price)
|
||||||
self.assertEqual(rate['quantity'], result.quantity)
|
self.assertEqual(rate['quantity'], result.quantity)
|
||||||
|
|
||||||
def test_compensations(self):
|
def test_generate_bill_lines_with_compensation(self):
|
||||||
pass
|
service = self.create_ftp_service()
|
||||||
|
account = self.create_account()
|
||||||
|
now = timezone.now().date()
|
||||||
|
order = Order(
|
||||||
|
cancelled_on=now,
|
||||||
|
billed_until=now+relativedelta.relativedelta(years=2)
|
||||||
|
)
|
||||||
|
order1 = Order()
|
||||||
|
orders = [order, order1]
|
||||||
|
lines = service.handler.generate_bill_lines(orders, account, commit=False)
|
||||||
|
print lines
|
||||||
|
print len(lines)
|
||||||
|
|
|
@ -10,7 +10,7 @@ from django.utils.encoding import force_text
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin.utils import get_modeladmin
|
from orchestra.admin.utils import get_modeladmin, change_url
|
||||||
|
|
||||||
from .forms import role_form_factory
|
from .forms import role_form_factory
|
||||||
from ..models import User
|
from ..models import User
|
||||||
|
@ -71,9 +71,8 @@ class RoleAdmin(object):
|
||||||
modeladmin.log_change(request, request.user, "%s saved" % self.name.capitalize())
|
modeladmin.log_change(request, request.user, "%s saved" % self.name.capitalize())
|
||||||
msg = _('The role %(name)s for user "%(obj)s" was %(action)s successfully.') % context
|
msg = _('The role %(name)s for user "%(obj)s" was %(action)s successfully.') % context
|
||||||
modeladmin.message_user(request, msg, messages.SUCCESS)
|
modeladmin.message_user(request, msg, messages.SUCCESS)
|
||||||
url = 'admin:%s_%s_change' % (opts.app_label, opts.module_name)
|
|
||||||
if not "_continue" in request.POST:
|
if not "_continue" in request.POST:
|
||||||
return redirect(url, object_id)
|
return redirect(change_url(user))
|
||||||
exists = True
|
exists = True
|
||||||
|
|
||||||
if exists:
|
if exists:
|
||||||
|
@ -117,7 +116,7 @@ class RoleAdmin(object):
|
||||||
obj_display = force_text(obj)
|
obj_display = force_text(obj)
|
||||||
modeladmin.log_deletion(request, obj, obj_display)
|
modeladmin.log_deletion(request, obj, obj_display)
|
||||||
modeladmin.delete_model(request, obj)
|
modeladmin.delete_model(request, obj)
|
||||||
post_url = reverse('admin:users_user_change', args=(user.pk,))
|
post_url = change_url(user)
|
||||||
preserved_filters = modeladmin.get_preserved_filters(request)
|
preserved_filters = modeladmin.get_preserved_filters(request)
|
||||||
post_url = add_preserved_filters(
|
post_url = add_preserved_filters(
|
||||||
{'preserved_filters': preserved_filters, 'opts': opts}, post_url
|
{'preserved_filters': preserved_filters, 'opts': opts}, post_url
|
||||||
|
|
|
@ -4,6 +4,7 @@ from django.core.urlresolvers import reverse
|
||||||
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
|
||||||
|
from orchestra.admin.utils import change_url
|
||||||
from orchestra.apps.accounts.admin import SelectAccountAdminMixin
|
from orchestra.apps.accounts.admin import SelectAccountAdminMixin
|
||||||
|
|
||||||
from .models import WebApp, WebAppOption
|
from .models import WebApp, WebAppOption
|
||||||
|
@ -37,7 +38,7 @@ class WebAppAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
websites = []
|
websites = []
|
||||||
for content in webapp.content_set.all().select_related('website'):
|
for content in webapp.content_set.all().select_related('website'):
|
||||||
website = content.website
|
website = content.website
|
||||||
url = reverse('admin:websites_website_change', args=(website.pk,))
|
url = change_url(website)
|
||||||
name = "%s on %s" % (website.name, content.path)
|
name = "%s on %s" % (website.name, content.path)
|
||||||
websites.append('<a href="%s">%s</a>' % (url, name))
|
websites.append('<a href="%s">%s</a>' % (url, name))
|
||||||
return '<br>'.join(websites)
|
return '<br>'.join(websites)
|
||||||
|
|
|
@ -5,7 +5,7 @@ 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
|
||||||
from orchestra.admin.utils import admin_link
|
from orchestra.admin.utils import admin_link, change_url
|
||||||
from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin
|
from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin
|
||||||
from orchestra.apps.accounts.widgets import account_related_field_widget_factory
|
from orchestra.apps.accounts.widgets import account_related_field_widget_factory
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
webapps = []
|
webapps = []
|
||||||
for content in website.content_set.all().select_related('webapp'):
|
for content in website.content_set.all().select_related('webapp'):
|
||||||
webapp = content.webapp
|
webapp = content.webapp
|
||||||
url = reverse('admin:webapps_webapp_change', args=(webapp.pk,))
|
url = change_url(webapp)
|
||||||
name = "%s on %s" % (webapp.get_type_display(), content.path)
|
name = "%s on %s" % (webapp.get_type_display(), content.path)
|
||||||
webapps.append('<a href="%s">%s</a>' % (url, name))
|
webapps.append('<a href="%s">%s</a>' % (url, name))
|
||||||
return '<br>'.join(webapps)
|
return '<br>'.join(webapps)
|
||||||
|
|
|
@ -97,6 +97,7 @@ INSTALLED_APPS = (
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework.authtoken',
|
'rest_framework.authtoken',
|
||||||
'passlib.ext.django',
|
'passlib.ext.django',
|
||||||
|
'django_nose',
|
||||||
|
|
||||||
# Django.contrib
|
# Django.contrib
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
|
@ -248,3 +249,6 @@ PASSLIB_CONFIG = (
|
||||||
"superuser__django_pbkdf2_sha256__default_rounds = 15000\n"
|
"superuser__django_pbkdf2_sha256__default_rounds = 15000\n"
|
||||||
"superuser__sha512_crypt__default_rounds = 120000\n"
|
"superuser__sha512_crypt__default_rounds = 120000\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||||
|
|
|
@ -27,11 +27,7 @@
|
||||||
<div>
|
<div>
|
||||||
<div style="margin:20px;">
|
<div style="margin:20px;">
|
||||||
<p>{{ content_message | safe }}</p>
|
<p>{{ content_message | safe }}</p>
|
||||||
<ul>
|
<ul>{{ display_objects | unordered_list }}</ul>
|
||||||
{% for display_object in display_objects %}
|
|
||||||
<li> <a href="{% url opts|admin_urlname:'change' display_object.pk %}">{{ display_object }} </a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
<form action="" method="post">{% csrf_token %}
|
<form action="" method="post">{% csrf_token %}
|
||||||
{% if form %}
|
{% if form %}
|
||||||
<fieldset class="module aligned">
|
<fieldset class="module aligned">
|
||||||
|
@ -53,7 +49,6 @@
|
||||||
{% if formset %}
|
{% if formset %}
|
||||||
{{ formset.as_admin }}
|
{{ formset.as_admin }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{% for obj in queryset %}
|
{% for obj in queryset %}
|
||||||
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
|
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.core.urlresolvers import reverse, NoReverseMatch
|
||||||
from django.forms import CheckboxInput
|
from django.forms import CheckboxInput
|
||||||
|
|
||||||
from orchestra import get_version
|
from orchestra import get_version
|
||||||
from orchestra.admin.utils import admin_change_url
|
from orchestra.admin.utils import change_url
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
@ -49,4 +49,4 @@ def is_checkbox(field):
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def admin_link(obj):
|
def admin_link(obj):
|
||||||
return admin_change_url(obj)
|
return change_url(obj)
|
||||||
|
|
Loading…
Reference in a new issue