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 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'): template='admin/orchestra/generic_confirmation.html'):
""" """
Generic pattern for actions that needs confirmation step Generic pattern for actions that needs confirmation step
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): 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): def inner(modeladmin, request, queryset, action_name=action_name):
# 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)
@ -48,7 +48,8 @@ def action_with_confirmation(action_name, extra_context={},
objects_name = force_text(opts.verbose_name) objects_name = force_text(opts.verbose_name)
else: else:
objects_name = force_text(opts.verbose_name_plural) objects_name = force_text(opts.verbose_name_plural)
if not action_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 %s the selected %s?" %

View File

@ -62,7 +62,8 @@ class ChangeViewActionsMixin(object):
action.url_name))) action.url_name)))
return new_urls + urls return new_urls + urls
def get_change_view_actions(self): def get_change_view_actions(self, obj=None):
""" allow customization on modelamdin """
views = [] views = []
for action in self.change_view_actions: for action in self.change_view_actions:
if isinstance(action, basestring): if isinstance(action, basestring):
@ -79,8 +80,9 @@ class ChangeViewActionsMixin(object):
def change_view(self, request, object_id, **kwargs): def change_view(self, request, object_id, **kwargs):
if not 'extra_context' in kwargs: if not 'extra_context' in kwargs:
kwargs['extra_context'] = {} kwargs['extra_context'] = {}
obj = self.get_object(request, unquote(object_id))
kwargs['extra_context']['object_tools_items'] = [ 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) 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: 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
SelectSourceFormSet = adminmodelformset_factory(modeladmin, SelectSourceForm, SelectSourceFormSet = adminmodelformset_factory(modeladmin, SelectSourceForm, extra=0)
extra=0)
formset = SelectSourceFormSet(queryset=queryset) formset = SelectSourceFormSet(queryset=queryset)
if request.POST.get('post') == 'generic_confirmation': if request.POST.get('post') == 'generic_confirmation':
formset = SelectSourceFormSet(request.POST, request.FILES, queryset=queryset) formset = SelectSourceFormSet(request.POST, request.FILES, queryset=queryset)
@ -55,6 +54,8 @@ def close_bills(modeladmin, request, queryset):
for form in formset.forms: for form in formset.forms:
source = form.cleaned_data['source'] source = form.cleaned_data['source']
form.instance.close(payment=source) form.instance.close(payment=source)
for bill in queryset:
modeladmin.log_change(request, bill, 'Closed')
messages.success(request, _("Selected bills have been closed")) messages.success(request, _("Selected bills have been closed"))
return return
opts = modeladmin.model._meta opts = modeladmin.model._meta
@ -80,5 +81,6 @@ close_bills.url_name = 'close'
def send_bills(modeladmin, request, queryset): def send_bills(modeladmin, request, queryset):
for bill in queryset: for bill in queryset:
bill.send() bill.send()
modeladmin.log_change(request, bill, 'Sent')
send_bills.verbose_name = _("Send") send_bills.verbose_name = _("Send")
send_bills.url_name = 'send' send_bills.url_name = 'send'

View File

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

View File

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

View File

@ -1,5 +1,6 @@
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
@ -39,6 +40,11 @@ class ContractedPlan(models.Model):
def __unicode__(self): def __unicode__(self):
return str(self.plan) 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): class RateQuerySet(models.QuerySet):

View File

@ -1,10 +1,15 @@
from django.contrib import messages from django.contrib import messages
from django.db import transaction
from django.shortcuts import render from django.shortcuts import render
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 .methods import PaymentMethod from .methods import PaymentMethod
from .models import Transaction from .models import Transaction
@transaction.atomic
def process_transactions(modeladmin, request, queryset): def process_transactions(modeladmin, request, queryset):
processes = [] processes = []
if queryset.exclude(state=Transaction.WAITTING_PROCESSING).exists(): if queryset.exclude(state=Transaction.WAITTING_PROCESSING).exists():
@ -16,6 +21,8 @@ def process_transactions(modeladmin, request, queryset):
method = PaymentMethod.get_plugin(method) method = PaymentMethod.get_plugin(method)
procs = method.process(transactions) procs = method.process(transactions)
processes += procs processes += procs
for transaction in transactions:
modeladmin.log_change(request, transaction, 'Processed')
if not processes: if not processes:
return return
opts = modeladmin.model._meta opts = modeladmin.model._meta
@ -27,3 +34,42 @@ def process_transactions(modeladmin, request, queryset):
'app_label': opts.app_label, 'app_label': opts.app_label,
} }
return render(request, 'admin/payments/transaction/get_processes.html', context) 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.shortcuts import render, redirect
from django.utils.translation import ugettext_lazy as _ 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.admin.utils import admin_colored, admin_link, wrap_admin_view
from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.apps.accounts.admin import AccountAdminMixin
from .actions import process_transactions from . import actions
from .methods import PaymentMethod from .methods import PaymentMethod
from .models import PaymentSource, Transaction, TransactionProcess from .models import PaymentSource, Transaction, TransactionProcess
@ -16,10 +17,9 @@ from .models import PaymentSource, Transaction, TransactionProcess
STATE_COLORS = { STATE_COLORS = {
Transaction.WAITTING_PROCESSING: 'darkorange', Transaction.WAITTING_PROCESSING: 'darkorange',
Transaction.WAITTING_CONFIRMATION: 'magenta', Transaction.WAITTING_CONFIRMATION: 'magenta',
Transaction.CONFIRMED: 'olive', Transaction.EXECUTED: 'olive',
Transaction.SECURED: 'green', Transaction.SECURED: 'green',
Transaction.REJECTED: 'red', Transaction.REJECTED: 'red',
Transaction.DISCARTED: 'blue',
} }
@ -27,7 +27,10 @@ class TransactionInline(admin.TabularInline):
model = Transaction model = Transaction
can_delete = False can_delete = False
extra = 0 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 readonly_fields = fields
transaction_link = admin_link('__unicode__', short_description=_("ID")) transaction_link = admin_link('__unicode__', short_description=_("ID"))
@ -44,14 +47,19 @@ class TransactionInline(admin.TabularInline):
return False return False
class TransactionAdmin(AccountAdminMixin, admin.ModelAdmin): class TransactionAdmin(ChangeViewActionsMixin, AccountAdminMixin, admin.ModelAdmin):
list_display = ( 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') 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'] 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') bill_link = admin_link('bill')
source_link = admin_link('source') source_link = admin_link('source')
@ -62,6 +70,20 @@ class TransactionAdmin(AccountAdminMixin, admin.ModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
qs = super(TransactionAdmin, self).get_queryset(request) qs = super(TransactionAdmin, self).get_queryset(request)
return qs.select_related('source', 'bill__account__user') 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):
@ -89,10 +111,14 @@ class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
return select_urls + urls return select_urls + urls
def select_method_view(self, request): def select_method_view(self, request):
opts = self.model._meta
context = { context = {
'opts': opts,
'app_label': opts.app_label,
'methods': PaymentMethod.get_plugin_choices(), '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): def add_view(self, request, form_url='', extra_context=None):
""" Redirects to select account view if required """ """ Redirects to select account view if required """

View File

@ -67,17 +67,15 @@ class TransactionQuerySet(models.QuerySet):
class Transaction(models.Model): class Transaction(models.Model):
WAITTING_PROCESSING = 'WAITTING_PROCESSING' # CREATED WAITTING_PROCESSING = 'WAITTING_PROCESSING' # CREATED
WAITTING_CONFIRMATION = 'WAITTING_CONFIRMATION' # PROCESSED WAITTING_CONFIRMATION = 'WAITTING_CONFIRMATION' # PROCESSED
CONFIRMED = 'CONFIRMED' EXECUTED = 'EXECUTED'
REJECTED = 'REJECTED'
DISCARTED = 'DISCARTED'
SECURED = 'SECURED' SECURED = 'SECURED'
REJECTED = 'REJECTED'
STATES = ( STATES = (
(WAITTING_PROCESSING, _("Waitting processing")), (WAITTING_PROCESSING, _("Waitting processing")),
(WAITTING_CONFIRMATION, _("Waitting confirmation")), (WAITTING_CONFIRMATION, _("Waitting confirmation")),
(CONFIRMED, _("Confirmed")), (EXECUTED, _("Executed")),
(REJECTED, _("Rejected")),
(SECURED, _("Secured")), (SECURED, _("Secured")),
(DISCARTED, _("Discarted")), (REJECTED, _("Rejected")),
) )
objects = TransactionQuerySet.as_manager() objects = TransactionQuerySet.as_manager()
@ -101,6 +99,21 @@ class Transaction(models.Model):
@property @property
def account(self): def account(self):
return self.bill.account 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): 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> <p>{{ content_message | safe }}</p>
<ul> <ul>
{% for display_object in display_objects %} {% 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 %} {% endfor %}
</ul> </ul>
<form action="" method="post">{% csrf_token %} <form action="" method="post">{% csrf_token %}