Commit 9f55bb1a authored by Sébastiaan Versteeg's avatar Sébastiaan Versteeg Committed by Luko van der Maas
Browse files

Payments package additions and changes

parent 0520c415
......@@ -17,6 +17,14 @@ payments.admin module
:undoc-members:
:show-inheritance:
payments.admin\_views module
----------------------------
.. automodule:: payments.admin_views
:members:
:undoc-members:
:show-inheritance:
payments.apps module
--------------------
......@@ -41,22 +49,6 @@ payments.services module
:undoc-members:
:show-inheritance:
payments.urls module
--------------------
.. automodule:: payments.urls
:members:
:undoc-members:
:show-inheritance:
payments.views module
---------------------
.. automodule:: payments.views
:members:
:undoc-members:
:show-inheritance:
payments.widgets module
-----------------------
......
"""Registers admin interfaces for the payments module"""
from django.contrib import admin, messages
from django.contrib.admin.utils import model_ngettext
from django.urls import path
from django.utils.translation import ugettext_lazy as _
from payments import services
from payments import services, admin_views
from .models import Payment
......@@ -21,14 +22,16 @@ def _show_message(admin, request, n, message, error):
class PaymentAdmin(admin.ModelAdmin):
"""Manage the payments"""
list_display = ('created_at', 'amount',
'processed', 'processing_date', 'type')
list_filter = ('processed', 'amount',)
list_display = ('created_at', 'amount', 'processing_date', 'type')
list_filter = ('type', 'amount',)
date_hierarchy = 'created_at'
fields = ('created_at', 'amount',
'type', 'processed', 'processing_date')
readonly_fields = ('created_at', 'amount', 'processed',
'type', 'processing_date')
fields = ('created_at', 'amount', 'type', 'processing_date',
'paid_by', 'processed_by', 'notes')
readonly_fields = ('created_at', 'amount', 'type',
'processing_date', 'paid_by', 'processed_by',
'notes')
ordering = ('-created_at', 'processing_date')
autocomplete_fields = ('paid_by', 'processed_by')
actions = ['process_cash_selected', 'process_card_selected']
def changeform_view(self, request, object_id=None, form_url='',
......@@ -46,6 +49,11 @@ class PaymentAdmin(admin.ModelAdmin):
return super().changeform_view(
request, object_id, form_url, {'payment': obj})
def get_readonly_fields(self, request, obj=None):
if not obj:
return ('created_at', 'type', 'processing_date', 'processed_by')
return super().get_readonly_fields(request, obj)
def get_actions(self, request):
"""Get the actions for the payments"""
"""Hide the processing actions if the right permissions are missing"""
......@@ -59,7 +67,7 @@ class PaymentAdmin(admin.ModelAdmin):
"""Process the selected payment as cash"""
if request.user.has_perm('payments.process_payments'):
updated_payments = services.process_payment(
queryset, Payment.CASH
queryset, request.member, Payment.CASH
)
self._process_feedback(request, updated_payments)
process_cash_selected.short_description = _(
......@@ -69,7 +77,7 @@ class PaymentAdmin(admin.ModelAdmin):
"""Process the selected payment as card"""
if request.user.has_perm('payments.process_payments'):
updated_payments = services.process_payment(
queryset, Payment.CARD
queryset, request.member, Payment.CARD
)
self._process_feedback(request, updated_payments)
process_card_selected.short_description = _(
......@@ -83,3 +91,13 @@ class PaymentAdmin(admin.ModelAdmin):
message=_("Successfully processed %(count)d %(items)s."),
error=_('The selected payment(s) could not be processed.')
)
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('<uuid:pk>/process/',
self.admin_site.admin_view(
admin_views.PaymentAdminView.as_view()),
name='payments_payment_process'),
]
return custom_urls + urls
"""Views provided by the payments package"""
"""Admin views provided by the payments package"""
from django.contrib import messages
from django.contrib.admin.utils import model_ngettext
from django.contrib.admin.views.decorators import staff_member_required
......@@ -26,7 +26,7 @@ class PaymentAdminView(View):
return redirect('admin:payments_payment_change', kwargs['pk'])
result = services.process_payment(
payment, request.POST['type']
payment, request.member, request.POST['type']
)
if len(result) > 0:
......@@ -36,4 +36,7 @@ class PaymentAdminView(View):
messages.error(request, _('Could not process %s.') %
model_ngettext(payment, 1))
if 'next' in request.POST:
return redirect(request.POST['next'])
return redirect('admin:payments_payment_change', kwargs['pk'])
......@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-11 20:07+0200\n"
"PO-Revision-Date: 2018-02-12 14:08+0100\n"
"POT-Creation-Date: 2018-11-27 18:25+0100\n"
"PO-Revision-Date: 2018-11-27 18:26+0100\n"
"Last-Translator: Thom Wiggers <thom@thomwiggers.nl>\n"
"Language-Team: \n"
"Language: nl\n"
......@@ -16,79 +16,87 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.0.6\n"
"X-Generator: Poedit 2.2\n"
#: payments/admin.py
#: admin.py
msgid "Process selected payments (cash)"
msgstr "Verwerk geselecteerde betalingen (contant)"
#: payments/admin.py
#: admin.py
msgid "Process selected payments (card)"
msgstr "Verwerk geselecteerde betalingen (pin)"
#: payments/admin.py payments/tests/test_admin.py
#: admin.py tests/test_admin.py
#, python-format
msgid "Successfully processed %(count)d %(items)s."
msgstr "%(count)d %(items)s succesvol verwerkt."
#: payments/admin.py
#: admin.py
msgid "The selected payment(s) could not be processed."
msgstr "De geselecteerde betaling(en) konden niet worden verwerkt."
#: payments/apps.py
#: admin_views.py tests/test_admin_views.py
#, python-format
msgid "Successfully processed %s."
msgstr "%s succesvol verwerkt."
#: admin_views.py tests/test_admin_views.py
#, python-format
msgid "Could not process %s."
msgstr "%s kon niet worden verwerkt."
#: apps.py
msgid "Payments"
msgstr "Betalingen"
#: payments/models.py
#: models.py
msgid "created at"
msgstr "aangemaakt op"
#: payments/models.py
#: models.py
msgid "No payment"
msgstr "Geen betaling"
#: models.py
msgid "Cash payment"
msgstr "Contante betaling"
#: payments/models.py
#: models.py
msgid "Card payment"
msgstr "Pin betaling"
#: payments/models.py
#: models.py
msgid "type"
msgstr "type"
#: payments/models.py
msgid "processed"
msgstr "verwerkt"
#: payments/models.py
#: models.py
msgid "processing date"
msgstr "verwerkingsdatum"
#: payments/models.py
#: models.py
msgid "payment"
msgstr "betaling"
#: payments/models.py
#: models.py
msgid "payments"
msgstr "betalingen"
#: payments/models.py
#: models.py
msgid "Process payments"
msgstr "Verwerk betalingen"
#: payments/templates/admin/payments/change_form.html
#: templates/admin/payments/change_form.html templates/payments/widget.html
msgid "Process (cash payment)"
msgstr "Verwerk (contant)"
#: payments/templates/admin/payments/change_form.html
#: templates/admin/payments/change_form.html templates/payments/widget.html
msgid "Process (card payment)"
msgstr "Verwerk (pin)"
#: payments/tests/test_views.py payments/views.py
#, python-format
msgid "Successfully processed %s."
msgstr "%s succesvol verwerkt."
#: templates/payments/widget.html
msgid "Unprocessed"
msgstr "Onverwerkt"
#: payments/tests/test_views.py payments/views.py
#, python-format
msgid "Could not process %s."
msgstr "%s kon niet worden verwerkt."
#: templates/payments/widget.html
msgid "Processed"
msgstr "Verwerkt"
# Generated by Django 2.1.3 on 2018-11-27 17:19
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('members', '0027_auto_20181024_2000'),
('payments', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='payment',
name='processed',
),
migrations.AddField(
model_name='payment',
name='notes',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='payment',
name='paid_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='paid_payment_set', to='members.Member'),
),
migrations.AddField(
model_name='payment',
name='processed_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='processed_payment_set', to='members.Member'),
),
migrations.AlterField(
model_name='payment',
name='type',
field=models.CharField(choices=[('no_payment', 'No payment'), ('cash_payment', 'Cash payment'), ('card_payment', 'Card payment')], default='no_payment', max_length=20, verbose_name='type'),
),
]
......@@ -17,10 +17,12 @@ class Payment(models.Model):
created_at = models.DateTimeField(_('created at'), default=timezone.now)
NONE = 'no_payment'
CASH = 'cash_payment'
CARD = 'card_payment'
PAYMENT_TYPE = (
(NONE, _('No payment')),
(CASH, _('Cash payment')),
(CARD, _('Card payment')),
)
......@@ -29,8 +31,7 @@ class Payment(models.Model):
choices=PAYMENT_TYPE,
verbose_name=_('type'),
max_length=20,
blank=True,
null=True,
default=NONE
)
amount = models.DecimalField(
......@@ -40,21 +41,40 @@ class Payment(models.Model):
decimal_places=2
)
processed = models.BooleanField(
_('processed'),
default=False,
)
processing_date = models.DateTimeField(
_('processing date'),
blank=True,
null=True,
)
paid_by = models.ForeignKey(
'members.Member',
models.CASCADE,
related_name='paid_payment_set',
blank=False,
null=True,
)
processed_by = models.ForeignKey(
'members.Member',
models.CASCADE,
related_name='processed_payment_set',
blank=False,
null=True,
)
notes = models.TextField(blank=True, null=True)
@property
def processed(self):
return self.type != self.NONE
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
if self.processed and not self.processing_date:
if self.type != self.NONE and not self.processing_date:
self.processing_date = timezone.now()
elif self.type == self.NONE:
self.processing_date = None
super().save(force_insert, force_update, using, update_fields)
def get_admin_url(self):
......
......@@ -2,25 +2,27 @@
from .models import Payment
def process_payment(queryset, pay_type=Payment.CARD):
def process_payment(queryset, processed_by, pay_type=Payment.CARD):
"""
Process the payment
:param queryset: Queryset of payments that should be processed
:type queryset: QuerySet[Payment]
:param processed_by: Member that processed this payment
:type processed_by: Member
:param pay_type: Type of the payment
:type pay_type: String
"""
queryset = queryset.filter(processed=False)
queryset = queryset.filter(type=Payment.NONE)
data = []
# This should trigger post_save signals, thus a queryset update
# is not appropriate, moreover save() automatically sets
# the processing date
for payment in queryset:
payment.processed = True
payment.type = pay_type
payment.processed_by = processed_by
payment.save()
data.append(payment)
......
django.jQuery(function () {
var $ = django.jQuery;
$(".payments-row a").click(function(e) {
$(".payments-row a.process").click(function(e) {
e.preventDefault();
var type = $(e.target).data('type');
var next = $(e.target).data('next');
var href = $(e.target).data('href');
var form = $('<form></form>');
form.attr("method", "post");
......@@ -14,6 +15,14 @@ django.jQuery(function () {
field.attr("value", type);
form.append(field);
if (next) {
var redirect = $('<input/>');
redirect.attr("type", "hidden");
redirect.attr("name", 'next');
redirect.attr("value", window.location);
form.append(redirect);
}
var csrf = $('<input/>');
csrf.attr("type", "hidden");
csrf.attr("name", 'csrfmiddlewaretoken');
......
......@@ -14,8 +14,8 @@
{% block submit_buttons_bottom %}
{% if payment %}
<div class="submit-row payments-row">
<a data-href="{% url 'payments:admin-process' pk=payment.pk %}" data-type="cash_payment" class="button process">{% trans "Process (cash payment)" %}</a>
<a data-href="{% url 'payments:admin-process' pk=payment.pk %}" data-type="card_payment" class="button process">{% trans "Process (card payment)" %}</a>
<a data-href="{% url 'admin:payments_payment_process' pk=payment.pk %}" data-type="cash_payment" class="button process">{% trans "Process (cash payment)" %}</a>
<a data-href="{% url 'admin:payments_payment_process' pk=payment.pk %}" data-type="card_payment" class="button process">{% trans "Process (card payment)" %}</a>
</div>
{% endif %}
......
{% load i18n %}
<div class="readonly">
{% if widget.value %}
<a href="{{ url }}">
{% if processed %}
{% trans "Processed" %}
{% else %}
{% trans "Unprocessed" %}
{% endif %}
</a>
{% else %}
-
{% endif %}
<div class="readonly payments-row">
{% if widget.value %}
{% if not payment.processed %}
<a href="{{ url }}">
{% trans "Unprocessed" %}
</a>
-
€ {{ payment.amount }}
-
<a data-href="{% url 'admin:payments_payment_process' pk=payment.pk %}"
data-next="true" data-type="cash_payment"
class="button process">{% trans "Process (cash payment)" %}</a>
<a data-href="{% url 'admin:payments_payment_process' pk=payment.pk %}"
data-next="true" data-type="card_payment"
class="button process">{% trans "Process (card payment)" %}</a>
{% else %}
<a href="{{ url }}">
{% trans "Processed" %}
</a>
-
{{ payment.processing_date }} - {{ payment.get_type_display }}
{% endif %}
{% else %}
-
{% endif %}
</div>
......@@ -4,7 +4,6 @@ from unittest.mock import Mock
from django.contrib import messages
from django.contrib.admin import AdminSite
from django.contrib.admin.utils import model_ngettext
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.http import HttpRequest
......@@ -12,6 +11,7 @@ from django.test import TestCase, SimpleTestCase, Client, RequestFactory
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from members.models import Member, Profile
from payments import admin
from payments.models import Payment
......@@ -36,10 +36,14 @@ class PaymentAdminTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = get_user_model().objects.create_user(
username='username',
is_staff=True,
)
cls.user = Member.objects.create(
username='test1',
first_name='Test1',
last_name='Example',
email='test1@example.org',
is_staff=True,
)
Profile.objects.create(user=cls.user)
def setUp(self):
self.client = Client()
......@@ -72,7 +76,6 @@ class PaymentAdminTest(TestCase):
def test_changeform_view(self, payment_get):
object_id = 'c85ea333-3508-46f1-8cbb-254f8c138020'
payment = Payment.objects.create(pk=object_id,
processed=False,
amount=7.5)
payment_get.return_value = payment
......@@ -94,7 +97,7 @@ class PaymentAdminTest(TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['payment'], payment)
payment.processed = True
payment.type = Payment.CARD
response = self.client.get('/admin/payments/payment/{}/change/'
.format(object_id))
......@@ -107,7 +110,6 @@ class PaymentAdminTest(TestCase):
def test_process_cash(self, process_payment, message_user):
object_id = 'c85ea333-3508-46f1-8cbb-254f8c138020'
payment = Payment.objects.create(pk=object_id,
processed=False,
amount=7.5)
queryset = Payment.objects.filter(pk=object_id)
process_payment.return_value = [payment]
......@@ -132,7 +134,8 @@ class PaymentAdminTest(TestCase):
process_payment.assert_not_called()
self.admin.process_cash_selected(request_hasperms, queryset)
process_payment.assert_called_once_with(queryset, Payment.CASH)
process_payment.assert_called_once_with(queryset,
self.user, Payment.CASH)
message_user.assert_called_once_with(
request_hasperms,
_('Successfully processed %(count)d %(items)s.')
......@@ -147,7 +150,6 @@ class PaymentAdminTest(TestCase):
def test_process_card(self, process_payment, message_user):
object_id = 'c85ea333-3508-46f1-8cbb-254f8c138020'
payment = Payment.objects.create(pk=object_id,
processed=False,
amount=7.5)
queryset = Payment.objects.filter(pk=object_id)
process_payment.return_value = [payment]
......@@ -172,7 +174,8 @@ class PaymentAdminTest(TestCase):
process_payment.assert_not_called()
self.admin.process_card_selected(request_hasperms, queryset)
process_payment.assert_called_once_with(queryset, Payment.CARD)
process_payment.assert_called_once_with(queryset,
self.user, Payment.CARD)
message_user.assert_called_once_with(
request_hasperms,
_('Successfully processed %(count)d %(items)s.')
......@@ -197,3 +200,7 @@ class PaymentAdminTest(TestCase):
self.assertCountEqual(actions, ['delete_selected',
'process_cash_selected',
'process_card_selected'])
def test_get_urls(self):
urls = self.admin.get_urls()
self.assertEqual(urls[0].name, 'payments_payment_process')
......@@ -2,13 +2,13 @@ from unittest import mock
from unittest.mock import Mock
from django.contrib.admin.utils import model_ngettext
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.test import Client, TestCase
from django.utils.translation import ugettext_lazy as _
from payments import views
from members.models import Member, Profile
from payments import admin_views
from payments.models import Payment
......@@ -17,15 +17,23 @@ class PaymentAdminViewTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.payment = Payment.objects.create(
processed=False,
amount=7.5
)
cls.user = get_user_model().objects.create_user(username='username')
cls.user = Member.objects.create(
username='test1',
first_name='Test1',
last_name='Example',
email='test1@example.org'
)
Profile.objects.create(
user=cls.user,
language='nl',
)
def setUp(self):
self.client = Client()
self.client.force_login(self.user)
self.view = views.PaymentAdminView()
self.view = admin_views.PaymentAdminView()
def _give_user_permissions(self):