Improvements on transactions

This commit is contained in:
Marc 2014-09-16 17:14:24 +00:00
parent 2b91495657
commit 821463eb33
11 changed files with 145 additions and 25 deletions

View file

@ -24,16 +24,16 @@ def admin_field(method):
return admin_field_wrapper
def action_with_confirmation(action_name, extra_context={},
def action_with_confirmation(action_name=None, extra_context={},
template='admin/orchestra/generic_confirmation.html'):
"""
Generic pattern for actions that needs confirmation step
If custom template is provided the form must contain:
<input type="hidden" name="post" value="generic_confirmation" />
"""
def decorator(func, extra_context=extra_context, template=template):
def decorator(func, extra_context=extra_context, template=template, action_name=action_name):
@wraps(func, assigned=available_attrs(func))
def inner(modeladmin, request, queryset):
def inner(modeladmin, request, queryset, action_name=action_name):
# The user has already confirmed the action.
if request.POST.get('post') == "generic_confirmation":
stay = func(modeladmin, request, queryset)
@ -48,7 +48,8 @@ def action_with_confirmation(action_name, extra_context={},
objects_name = force_text(opts.verbose_name)
else:
objects_name = force_text(opts.verbose_name_plural)
if not action_name:
action_name = func.__name__
context = {
"title": "Are you sure?",
"content_message": "Are you sure you want to %s the selected %s?" %

View file

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

View file

@ -46,8 +46,7 @@ def close_bills(modeladmin, request, queryset):
if not queryset:
messages.warning(request, _("Selected bills should be in open state"))
return
SelectSourceFormSet = adminmodelformset_factory(modeladmin, SelectSourceForm,
extra=0)
SelectSourceFormSet = adminmodelformset_factory(modeladmin, SelectSourceForm, extra=0)
formset = SelectSourceFormSet(queryset=queryset)
if request.POST.get('post') == 'generic_confirmation':
formset = SelectSourceFormSet(request.POST, request.FILES, queryset=queryset)
@ -55,6 +54,8 @@ def close_bills(modeladmin, request, queryset):
for form in formset.forms:
source = form.cleaned_data['source']
form.instance.close(payment=source)
for bill in queryset:
modeladmin.log_change(request, bill, 'Closed')
messages.success(request, _("Selected bills have been closed"))
return
opts = modeladmin.model._meta
@ -80,5 +81,6 @@ close_bills.url_name = 'close'
def send_bills(modeladmin, request, queryset):
for bill in queryset:
bill.send()
modeladmin.log_change(request, bill, 'Sent')
send_bills.verbose_name = _("Send")
send_bills.url_name = 'send'

View file

@ -17,7 +17,7 @@ def change_ticket_state_factory(action, final_state):
'form': ChangeReasonForm()
}
@transaction.atomic
@action_with_confirmation(action, extra_context=context)
@action_with_confirmation(action_name=action, extra_context=context)
def change_ticket_state(modeladmin, request, queryset, action=action, final_state=final_state):
form = ChangeReasonForm(request.POST)
if form.is_valid():
@ -81,6 +81,7 @@ def take_tickets(modeladmin, request, queryset):
ticket.messages.create(content=content, author=request.user)
if is_read and not ticket.is_read_by(request.user):
ticket.mark_as_read_by(request.user)
modeladmin.log_change(request, ticket, 'Taken')
context = {
'count': queryset.count(),
'user': request.user
@ -97,6 +98,7 @@ def mark_as_unread(modeladmin, request, queryset):
""" Mark a tickets as unread """
for ticket in queryset:
ticket.mark_as_unread_by(request.user)
modeladmin.log_change(request, ticket, 'Marked as unread')
msg = _("%s selected tickets have been marked as unread.") % queryset.count()
modeladmin.message_user(request, msg)
@ -106,6 +108,7 @@ def mark_as_read(modeladmin, request, queryset):
""" Mark a tickets as unread """
for ticket in queryset:
ticket.mark_as_read_by(request.user)
modeladmin.log_change(request, ticket, 'Marked as read')
msg = _("%s selected tickets have been marked as read.") % queryset.count()
modeladmin.message_user(request, msg)

View file

@ -1,5 +1,6 @@
from django.contrib import admin, messages
from django.core.urlresolvers import reverse
from django.db import transaction
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext
@ -71,10 +72,13 @@ class BillSelectedOrders(object):
})
return render(request, self.template, self.context)
@transaction.atomic
def confirmation(self, request):
form = BillSelectConfirmationForm(initial=self.options)
if int(request.POST.get('step')) >= 3:
bills = self.queryset.bill(commit=True, **self.options)
for order in self.queryset:
modeladmin.log_change(request, order, 'Billed')
if not bills:
msg = _("Selected orders do not have pending billing")
self.modeladmin.message_user(request, msg, messages.WARNING)

View file

@ -1,5 +1,6 @@
import sys
from django.core.exceptions import ValidationError
from django.db import models
from django.db.migrations.recorder import MigrationRecorder
from django.db.models import F, Q
@ -39,6 +40,11 @@ class ContractedPlan(models.Model):
def __unicode__(self):
return str(self.plan)
def clean(self):
if not self.pk and not self.plan.allow_multipls:
if ContractedPlan.objects.filter(plan=self.plan, account=self.account).exists():
raise ValidationError("A contracted plan for this account already exists")
class RateQuerySet(models.QuerySet):

View file

@ -1,10 +1,15 @@
from django.contrib import messages
from django.db import transaction
from django.shortcuts import render
from django.utils.translation import ugettext_lazy as _
from orchestra.admin.decorators import action_with_confirmation
from .methods import PaymentMethod
from .models import Transaction
@transaction.atomic
def process_transactions(modeladmin, request, queryset):
processes = []
if queryset.exclude(state=Transaction.WAITTING_PROCESSING).exists():
@ -16,6 +21,8 @@ def process_transactions(modeladmin, request, queryset):
method = PaymentMethod.get_plugin(method)
procs = method.process(transactions)
processes += procs
for transaction in transactions:
modeladmin.log_change(request, transaction, 'Processed')
if not processes:
return
opts = modeladmin.model._meta
@ -27,3 +34,42 @@ def process_transactions(modeladmin, request, queryset):
'app_label': opts.app_label,
}
return render(request, 'admin/payments/transaction/get_processes.html', context)
@transaction.atomic
@action_with_confirmation()
def mark_as_executed(modeladmin, request, queryset):
""" Mark a tickets as unread """
for transaction in queryset:
transaction.mark_as_executed()
modeladmin.log_change(request, transaction, 'Executed')
msg = _("%s selected transactions have been marked as executed.") % queryset.count()
modeladmin.message_user(request, msg)
mark_as_executed.url_name = 'execute'
mark_as_executed.verbose_name = _("Mark as executed")
@transaction.atomic
@action_with_confirmation()
def mark_as_secured(modeladmin, request, queryset):
""" Mark a tickets as unread """
for transaction in queryset:
transaction.mark_as_secured()
modeladmin.log_change(request, transaction, 'Secured')
msg = _("%s selected transactions have been marked as secured.") % queryset.count()
modeladmin.message_user(request, msg)
mark_as_secured.url_name = 'secure'
mark_as_secured.verbose_name = _("Mark as secured")
@transaction.atomic
@action_with_confirmation()
def mark_as_rejected(modeladmin, request, queryset):
""" Mark a tickets as unread """
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)
mark_as_rejected.url_name = 'reject'
mark_as_rejected.verbose_name = _("Mark as rejected")

View file

@ -5,10 +5,11 @@ from django.core.urlresolvers import reverse
from django.shortcuts import render, redirect
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ChangeViewActionsMixin
from orchestra.admin.utils import admin_colored, admin_link, wrap_admin_view
from orchestra.apps.accounts.admin import AccountAdminMixin
from .actions import process_transactions
from . import actions
from .methods import PaymentMethod
from .models import PaymentSource, Transaction, TransactionProcess
@ -16,10 +17,9 @@ from .models import PaymentSource, Transaction, TransactionProcess
STATE_COLORS = {
Transaction.WAITTING_PROCESSING: 'darkorange',
Transaction.WAITTING_CONFIRMATION: 'magenta',
Transaction.CONFIRMED: 'olive',
Transaction.EXECUTED: 'olive',
Transaction.SECURED: 'green',
Transaction.REJECTED: 'red',
Transaction.DISCARTED: 'blue',
}
@ -27,7 +27,10 @@ class TransactionInline(admin.TabularInline):
model = Transaction
can_delete = False
extra = 0
fields = ('transaction_link', 'bill_link', 'source_link', 'display_state', 'amount', 'currency')
fields = (
'transaction_link', 'bill_link', 'source_link', 'display_state',
'amount', 'currency'
)
readonly_fields = fields
transaction_link = admin_link('__unicode__', short_description=_("ID"))
@ -44,14 +47,19 @@ class TransactionInline(admin.TabularInline):
return False
class TransactionAdmin(AccountAdminMixin, admin.ModelAdmin):
class TransactionAdmin(ChangeViewActionsMixin, AccountAdminMixin, admin.ModelAdmin):
list_display = (
'id', 'bill_link', 'account_link', 'source_link', 'display_state', 'amount', 'process_link'
'id', 'bill_link', 'account_link', 'source_link', 'display_state',
'amount', 'process_link'
)
list_filter = ('source__method', 'state')
actions = (process_transactions,)
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 = ('process_link', 'account_link')
readonly_fields = ('bill_link', 'display_state', 'process_link', 'account_link')
bill_link = admin_link('bill')
source_link = admin_link('source')
@ -62,6 +70,20 @@ class TransactionAdmin(AccountAdminMixin, admin.ModelAdmin):
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):
@ -89,10 +111,14 @@ class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
return select_urls + urls
def select_method_view(self, request):
opts = self.model._meta
context = {
'opts': opts,
'app_label': opts.app_label,
'methods': PaymentMethod.get_plugin_choices(),
}
return render(request, 'admin/payments/payment_source/select_method.html', context)
template = 'admin/payments/payment_source/select_method.html'
return render(request, template, context)
def add_view(self, request, form_url='', extra_context=None):
""" Redirects to select account view if required """

View file

@ -67,17 +67,15 @@ class TransactionQuerySet(models.QuerySet):
class Transaction(models.Model):
WAITTING_PROCESSING = 'WAITTING_PROCESSING' # CREATED
WAITTING_CONFIRMATION = 'WAITTING_CONFIRMATION' # PROCESSED
CONFIRMED = 'CONFIRMED'
REJECTED = 'REJECTED'
DISCARTED = 'DISCARTED'
EXECUTED = 'EXECUTED'
SECURED = 'SECURED'
REJECTED = 'REJECTED'
STATES = (
(WAITTING_PROCESSING, _("Waitting processing")),
(WAITTING_CONFIRMATION, _("Waitting confirmation")),
(CONFIRMED, _("Confirmed")),
(REJECTED, _("Rejected")),
(EXECUTED, _("Executed")),
(SECURED, _("Secured")),
(DISCARTED, _("Discarted")),
(REJECTED, _("Rejected")),
)
objects = TransactionQuerySet.as_manager()
@ -101,6 +99,21 @@ class Transaction(models.Model):
@property
def account(self):
return self.bill.account
def mark_as_executed(self):
self.state = self.EXECUTED
self.save()
def mark_as_secured(self):
self.state = self.SECURED
# TODO think carefully about bill feedback
self.bill.mark_as_paid()
self.save()
def mark_as_rejected(self):
self.state = self.REJECTED
# TODO bill feedback
self.save()
class TransactionProcess(models.Model):

View file

@ -0,0 +1,17 @@
{% extends "admin/orchestra/generic_confirmation.html" %}
{% load i18n l10n staticfiles admin_urls %}
{% block content %}
<h1>Select a method for the new payment source</h1>
<form action="" method="post">{% csrf_token %}
<div>
<div style="margin:20px;">
<ul>
{% for name, verbose in methods %}
<li><a href="../?method={{ name }}&{{ request.META.QUERY_STRING }}">{{ verbose }}</<a></li>
{% endfor %}
</ul>
</div>
{% endblock %}

View file

@ -29,7 +29,7 @@
<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>
<li> <a href="{% url opts|admin_urlname:'change' display_object.pk %}">{{ display_object }} </a></li>
{% endfor %}
</ul>
<form action="" method="post">{% csrf_token %}