diff --git a/orchestra/admin/decorators.py b/orchestra/admin/decorators.py index 80a19074..c7e62039 100644 --- a/orchestra/admin/decorators.py +++ b/orchestra/admin/decorators.py @@ -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: """ - 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?" % diff --git a/orchestra/admin/options.py b/orchestra/admin/options.py index ab99940c..eb470e3f 100644 --- a/orchestra/admin/options.py +++ b/orchestra/admin/options.py @@ -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) diff --git a/orchestra/apps/bills/actions.py b/orchestra/apps/bills/actions.py index 06418dbc..4b004872 100644 --- a/orchestra/apps/bills/actions.py +++ b/orchestra/apps/bills/actions.py @@ -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' diff --git a/orchestra/apps/issues/actions.py b/orchestra/apps/issues/actions.py index 998d9a6a..6e0456ca 100644 --- a/orchestra/apps/issues/actions.py +++ b/orchestra/apps/issues/actions.py @@ -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) diff --git a/orchestra/apps/orders/actions.py b/orchestra/apps/orders/actions.py index 645a3c24..9f1a2aa0 100644 --- a/orchestra/apps/orders/actions.py +++ b/orchestra/apps/orders/actions.py @@ -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) diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py index ea85c403..560d5533 100644 --- a/orchestra/apps/orders/models.py +++ b/orchestra/apps/orders/models.py @@ -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): diff --git a/orchestra/apps/payments/actions.py b/orchestra/apps/payments/actions.py index d696b843..b07cc566 100644 --- a/orchestra/apps/payments/actions.py +++ b/orchestra/apps/payments/actions.py @@ -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") diff --git a/orchestra/apps/payments/admin.py b/orchestra/apps/payments/admin.py index b86abadf..1416f33a 100644 --- a/orchestra/apps/payments/admin.py +++ b/orchestra/apps/payments/admin.py @@ -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 """ diff --git a/orchestra/apps/payments/models.py b/orchestra/apps/payments/models.py index 0a4538b9..b1726ffe 100644 --- a/orchestra/apps/payments/models.py +++ b/orchestra/apps/payments/models.py @@ -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): diff --git a/orchestra/apps/payments/templates/admin/payments/payment_source/select_method.html b/orchestra/apps/payments/templates/admin/payments/payment_source/select_method.html new file mode 100644 index 00000000..b274be31 --- /dev/null +++ b/orchestra/apps/payments/templates/admin/payments/payment_source/select_method.html @@ -0,0 +1,17 @@ +{% extends "admin/orchestra/generic_confirmation.html" %} +{% load i18n l10n staticfiles admin_urls %} + + +{% block content %} +

Select a method for the new payment source

+
{% csrf_token %} +
+
+ +
+{% endblock %} + diff --git a/orchestra/templates/admin/orchestra/generic_confirmation.html b/orchestra/templates/admin/orchestra/generic_confirmation.html index 00cb6f2d..5ba2fa12 100644 --- a/orchestra/templates/admin/orchestra/generic_confirmation.html +++ b/orchestra/templates/admin/orchestra/generic_confirmation.html @@ -29,7 +29,7 @@

{{ content_message | safe }}

{% csrf_token %}