diff --git a/docs/payments.rst b/docs/payments.rst index e6e0d32ed51c85a7c21655bc8abce59903786da4..c0b427792751506c9675d95caeabd9ac6e02b3c2 100644 --- a/docs/payments.rst +++ b/docs/payments.rst @@ -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 ----------------------- diff --git a/website/payments/admin.py b/website/payments/admin.py index 233dddc75e85bb46681c0037daacc0125cb0b9cd..752f90b7201244c89f811bd2bb53afadb92ae58d 100644 --- a/website/payments/admin.py +++ b/website/payments/admin.py @@ -1,9 +1,10 @@ """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('/process/', + self.admin_site.admin_view( + admin_views.PaymentAdminView.as_view()), + name='payments_payment_process'), + ] + return custom_urls + urls diff --git a/website/payments/views.py b/website/payments/admin_views.py similarity index 87% rename from website/payments/views.py rename to website/payments/admin_views.py index 3c71b751d555b7a42973429f872ff69f3ecef34c..a2461104b65c6b445fcad299eaff2f6555d5418b 100644 --- a/website/payments/views.py +++ b/website/payments/admin_views.py @@ -1,4 +1,4 @@ -"""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']) diff --git a/website/payments/locale/nl/LC_MESSAGES/django.mo b/website/payments/locale/nl/LC_MESSAGES/django.mo index 5d0c6d65c0a334778bae6ac9b11ba63a04426b66..7dfc95bc29f7e0dbf15e3d9e37e4ff50e6e2f6d9 100644 Binary files a/website/payments/locale/nl/LC_MESSAGES/django.mo and b/website/payments/locale/nl/LC_MESSAGES/django.mo differ diff --git a/website/payments/locale/nl/LC_MESSAGES/django.po b/website/payments/locale/nl/LC_MESSAGES/django.po index 75cbd0840221314e4abf851969b198de9d819859..aabb0c825352412d6ff7eedfd3d7a9196145b0b7 100644 --- a/website/payments/locale/nl/LC_MESSAGES/django.po +++ b/website/payments/locale/nl/LC_MESSAGES/django.po @@ -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 \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" diff --git a/website/payments/migrations/0002_auto_20181127_1819.py b/website/payments/migrations/0002_auto_20181127_1819.py new file mode 100644 index 0000000000000000000000000000000000000000..f05aabb9e4311cb20d9fef6c45cf525d77ea9784 --- /dev/null +++ b/website/payments/migrations/0002_auto_20181127_1819.py @@ -0,0 +1,39 @@ +# 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'), + ), + ] diff --git a/website/payments/models.py b/website/payments/models.py index 6a94c6f161c7b1f8c89724529d5d80de8c3f437c..047c8d135431e6be2c9cafb9d5bbfbe293d1bcf9 100644 --- a/website/payments/models.py +++ b/website/payments/models.py @@ -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): diff --git a/website/payments/services.py b/website/payments/services.py index 901b82c4d815e3866f45998313707f7fde4a8d1e..a93c92dfda66fcafb283d8ef7c9a42a65188943d 100644 --- a/website/payments/services.py +++ b/website/payments/services.py @@ -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) diff --git a/website/payments/static/admin/payments/js/payments.js b/website/payments/static/admin/payments/js/payments.js index d5f1770a49389d8bed2f68f79af3324ea3c7a2e5..d0bc80f290eb950134a619e893acae234aaa0d50 100644 --- a/website/payments/static/admin/payments/js/payments.js +++ b/website/payments/static/admin/payments/js/payments.js @@ -1,8 +1,9 @@ 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.attr("method", "post"); @@ -14,6 +15,14 @@ django.jQuery(function () { field.attr("value", type); form.append(field); + if (next) { + var redirect = $(''); + redirect.attr("type", "hidden"); + redirect.attr("name", 'next'); + redirect.attr("value", window.location); + form.append(redirect); + } + var csrf = $(''); csrf.attr("type", "hidden"); csrf.attr("name", 'csrfmiddlewaretoken'); diff --git a/website/payments/templates/admin/payments/change_form.html b/website/payments/templates/admin/payments/change_form.html index b5b91afe751a804572f5e5a59481b1e39d455d78..c24599ce78f3b396d23707875615e67ecef91327 100644 --- a/website/payments/templates/admin/payments/change_form.html +++ b/website/payments/templates/admin/payments/change_form.html @@ -14,8 +14,8 @@ {% block submit_buttons_bottom %} {% if payment %} {% endif %} diff --git a/website/payments/templates/payments/widget.html b/website/payments/templates/payments/widget.html index a922eba9d1702159cb11b286b6c61a7283d2ce87..d07fb82a78d0ae1c860bda5c1e00d6410a8ab003 100644 --- a/website/payments/templates/payments/widget.html +++ b/website/payments/templates/payments/widget.html @@ -1,14 +1,27 @@ {% load i18n %} -
- {% if widget.value %} - - {% if processed %} - {% trans "Processed" %} - {% else %} - {% trans "Unprocessed" %} - {% endif %} - - {% else %} - - - {% endif %} +
+ {% if widget.value %} + {% if not payment.processed %} + + {% trans "Unprocessed" %} + + - + € {{ payment.amount }} + - + {% trans "Process (cash payment)" %} + {% trans "Process (card payment)" %} + {% else %} + + {% trans "Processed" %} + + - + {{ payment.processing_date }} - {{ payment.get_type_display }} + {% endif %} + {% else %} + - + {% endif %}
diff --git a/website/payments/tests/test_admin.py b/website/payments/tests/test_admin.py index 5b6a06442d7b959ce25ac4fa98fb75995db14bf7..4a1e296ddf0378b06d9830cca73ba6e4ecb17738 100644 --- a/website/payments/tests/test_admin.py +++ b/website/payments/tests/test_admin.py @@ -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') diff --git a/website/payments/tests/test_views.py b/website/payments/tests/test_admin_views.py similarity index 61% rename from website/payments/tests/test_views.py rename to website/payments/tests/test_admin_views.py index 3af210d6444fa244d3533c57815f0c50972ec1bc..a5ea25bb28362ae154a895cdcc09429ac2459e05 100644 --- a/website/payments/tests/test_views.py +++ b/website/payments/tests/test_admin_views.py @@ -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): content_type = ContentType.objects.get_for_model(Payment) @@ -41,7 +49,7 @@ class PaymentAdminViewTest(TestCase): self.client.force_login(self.user) def test_permissions(self): - url = '/payment/admin/process/{}/'.format( + url = '/admin/payments/payment/{}/process/'.format( self.payment.pk) response = self.client.post(url, { 'type': 'cash_payment', @@ -50,7 +58,7 @@ class PaymentAdminViewTest(TestCase): self._give_user_permissions() - url = '/payment/admin/process/{}/'.format( + url = '/admin/payments/payment/{}/process/'.format( self.payment.pk) response = self.client.post(url, { 'type': 'cash_payment', @@ -74,8 +82,10 @@ class PaymentAdminViewTest(TestCase): self._give_user_permissions() with self.subTest('Send post without payload'): - response = self.client.post('/payment/admin/process/{}/' - .format(self.payment.pk)) + response = self.client.post( + '/admin/payments/payment/{}/process/' + .format(self.payment.pk) + ) self.assertEqual(response.status_code, 302) self.assertEqual( @@ -87,12 +97,14 @@ class PaymentAdminViewTest(TestCase): messages_error.assert_not_called() messages_success.assert_not_called() - with self.subTest('Send post with successful processing'): + with self.subTest('Send post with successful processing, no next'): payment_type = 'cash_payment' - response = self.client.post('/payment/admin/process/{}/' - .format(self.payment.pk), { - 'type': payment_type, - }) + response = self.client.post( + '/admin/payments/payment/{}/process/' + .format(self.payment.pk), { + 'type': payment_type, + } + ) self.assertEqual(response.status_code, 302) self.assertEqual( @@ -101,7 +113,31 @@ class PaymentAdminViewTest(TestCase): ) process_payment.assert_called_once_with( - payment_qs, payment_type) + payment_qs, self.user, payment_type) + + messages_success.assert_called_once_with( + response.wsgi_request, + _('Successfully processed %s.') % + model_ngettext(self.payment, 1) + ) + + process_payment.reset_mock() + messages_success.reset_mock() + + with self.subTest('Send post with successful processing and next'): + payment_type = 'cash_payment' + response = self.client.post( + '/admin/payments/payment/{}/process/' + .format(self.payment.pk), { + 'type': payment_type, + 'next': '/admin/events/' + }) + + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, '/admin/events/') + + process_payment.assert_called_once_with( + payment_qs, self.user, payment_type) messages_success.assert_called_once_with( response.wsgi_request, @@ -111,10 +147,11 @@ class PaymentAdminViewTest(TestCase): with self.subTest('Send post with failed processing'): process_payment.return_value = [] - response = self.client.post('/payment/admin/process/{}/' - .format(self.payment.pk), { - 'type': payment_type, - }) + response = self.client.post( + '/admin/payments/payment/{}/process/' + .format(self.payment.pk), { + 'type': payment_type, + }) messages_error.assert_called_once_with( response.wsgi_request, diff --git a/website/payments/tests/test_models.py b/website/payments/tests/test_models.py index a594f66d51cba160d1106053e410ee7818766507..0ac5b30e7b0c7cc074427e29b506fb8b62df0a21 100644 --- a/website/payments/tests/test_models.py +++ b/website/payments/tests/test_models.py @@ -1,15 +1,21 @@ from django.test import TestCase +from members.models import Member from payments.models import Payment class PaymentTest(TestCase): """Tests Payments""" + fixtures = ['members.json'] + @classmethod def setUpTestData(cls): + cls.member = Member.objects.filter(last_name="Wiggers").first() cls.payment = Payment.objects.create( amount=10, + paid_by=cls.member, + processed_by=cls.member, ) def test_full_clean_works(self): @@ -21,7 +27,7 @@ class PaymentTest(TestCase): def test_change_processed_sets_processing_date(self): self.assertFalse(self.payment.processed) self.assertIsNone(self.payment.processing_date) - self.payment.processed = True + self.payment.type = Payment.CARD self.payment.save() self.assertTrue(self.payment.processed) self.assertIsNotNone(self.payment.processing_date) diff --git a/website/payments/tests/test_widgets.py b/website/payments/tests/test_widgets.py new file mode 100644 index 0000000000000000000000000000000000000000..824721667896cff366febb39984d166841bfd036 --- /dev/null +++ b/website/payments/tests/test_widgets.py @@ -0,0 +1,35 @@ +from django.test import TestCase + +from members.models import Member +from payments.models import Payment +from payments.widgets import PaymentWidget + + +class PaymentWidgetTest(TestCase): + """Tests widgets""" + + fixtures = ['members.json'] + + @classmethod + def setUpTestData(cls): + cls.member = Member.objects.filter(last_name="Wiggers").first() + cls.payment = Payment.objects.create( + amount=10, + paid_by=cls.member, + processed_by=cls.member, + ) + + def test_get_context(self): + widget = PaymentWidget() + + with self.subTest('With payment primary key'): + context = widget.get_context('payment', self.payment.pk, {}) + self.assertEqual(context['url'], + '/admin/payments/payment/{}/change/'.format( + self.payment.pk)) + self.assertEqual(context['payment'], self.payment) + + with self.subTest('Empty value'): + context = widget.get_context('payment', None, {}) + self.assertNotIn('url', context) + self.assertNotIn('payment', context) diff --git a/website/payments/urls.py b/website/payments/urls.py deleted file mode 100644 index 7040c7b3df370fc1473cdcb6218d5a66c44ed1f1..0000000000000000000000000000000000000000 --- a/website/payments/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -"""The routes defined by the payments package""" -from django.urls import path - -from .views import PaymentAdminView - -app_name = 'payments' - -urlpatterns = [ - path('admin/process//', - PaymentAdminView.as_view(), name='admin-process'), -] diff --git a/website/payments/widgets.py b/website/payments/widgets.py index df4e26bc98d64ad62a7b4f7b03d4d5eae887ebfc..a2e28af27a89941ed76b56a690df67e5842c0a48 100644 --- a/website/payments/widgets.py +++ b/website/payments/widgets.py @@ -18,5 +18,8 @@ class PaymentWidget(Widget): if value: payment = Payment.objects.get(pk=value) context['url'] = payment.get_admin_url() - context['processed'] = payment.processed + context['payment'] = payment return context + + class Media: + js = ('admin/payments/js/payments.js',) diff --git a/website/registrations/admin.py b/website/registrations/admin.py index 0ae00ac02d26b132da5309d361c9bfb42737f605..1ba8a92f66f16fb544182c747a192f2370bbb280 100644 --- a/website/registrations/admin.py +++ b/website/registrations/admin.py @@ -27,7 +27,7 @@ class RegistrationAdmin(admin.ModelAdmin): list_display = ('name', 'email', 'status', 'created_at', 'payment_status') - list_filter = ('status', 'programme', 'payment__processed', + list_filter = ('status', 'programme', 'payment__type', 'payment__amount') search_fields = ('first_name', 'last_name', 'email', 'phone_number', 'student_number',) @@ -161,7 +161,7 @@ class RenewalAdmin(RegistrationAdmin): list_display = ('name', 'email', 'status', 'created_at', 'payment_status',) - list_filter = ('status', 'payment__processed', 'payment__amount') + list_filter = ('status', 'payment__type', 'payment__amount') search_fields = ('member__first_name', 'member__last_name', 'member__email', 'member__profile__phone_number', 'member__profile__student_number',) diff --git a/website/registrations/migrations/0014_fill_new_payments_fields.py b/website/registrations/migrations/0014_fill_new_payments_fields.py new file mode 100644 index 0000000000000000000000000000000000000000..2a67b72be04e210bc61902a9bdbdf7d1747171ce --- /dev/null +++ b/website/registrations/migrations/0014_fill_new_payments_fields.py @@ -0,0 +1,34 @@ +from django.db import migrations + + +def forwards_func(apps, schema_editor): + Entry = apps.get_model('registrations', 'entry') + Renewal = apps.get_model('registrations', 'renewal') + db_alias = schema_editor.connection.alias + for entry in Entry.objects.using(db_alias).all(): + if entry.payment and entry.membership: + payment = entry.payment + membership = entry.membership + payment.paid_by = membership.user + payment.notes = 'Membership registration' + try: + renewal = entry.renewal + payment.notes = 'Membership renewal' + except Renewal.DoesNotExist: + pass + payment.save() + + +def reverse_func(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + dependencies = [ + ('registrations', '0013_auto_20181114_2104'), + ('payments', '0002_auto_20181127_1819') + ] + + operations = [ + migrations.RunPython(forwards_func, reverse_func), + ] diff --git a/website/registrations/migrations/0015_auto_20181126_2021.py b/website/registrations/migrations/0015_auto_20181126_2021.py new file mode 100644 index 0000000000000000000000000000000000000000..1c40250557bc7992e6887ec42a6c4176b825724d --- /dev/null +++ b/website/registrations/migrations/0015_auto_20181126_2021.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1.3 on 2018-11-26 19:21 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('registrations', '0014_fill_new_payments_fields'), + ] + + operations = [ + migrations.AlterField( + model_name='entry', + name='payment', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='registrations_entry', to='payments.Payment'), + ), + ] diff --git a/website/registrations/models.py b/website/registrations/models.py index 901fdf7f951f10852999511c96a1ef3778218142..80f39c2e20a574f4c5a64801f776d1075f53e840 100644 --- a/website/registrations/models.py +++ b/website/registrations/models.py @@ -77,7 +77,7 @@ class Entry(models.Model): payment = models.OneToOneField( 'payments.Payment', related_name='registrations_entry', - on_delete=models.SET_NULL, + on_delete=models.PROTECT, blank=True, null=True, ) diff --git a/website/registrations/services.py b/website/registrations/services.py index 6d65fee1497b4ce803b60476a0028c2283786f56..49256daa2ad77d089a8447d846dc501315fed239 100644 --- a/website/registrations/services.py +++ b/website/registrations/services.py @@ -153,11 +153,13 @@ def revert_entry(entry): if not (entry.status in [Entry.STATUS_ACCEPTED, Entry.STATUS_REJECTED]): return + payment = entry.payment entry.status = Entry.STATUS_REVIEW entry.updated_at = timezone.now() + entry.payment = None entry.save() - if entry.payment is not None: - entry.payment.delete() + if payment is not None: + payment.delete() def _create_payment_for_entry(entry): @@ -170,10 +172,12 @@ def _create_payment_for_entry(entry): :rtype: Payment """ amount = settings.MEMBERSHIP_PRICES[entry.length] + notes = 'Membership registration' try: renewal = entry.renewal membership = renewal.member.latest_membership + notes = 'Membership renewal' # Having a latest membership which has an until date implies that this # membership lasts/lasted till the end of the lecture year # This means it's possible to renew the 'year' membership @@ -195,6 +199,7 @@ def _create_payment_for_entry(entry): return Payment.objects.create( amount=amount, + notes=notes ) @@ -352,6 +357,7 @@ def process_payment(payment): # If member was retrieved, then create a new membership if member is not None: + Payment.objects.filter(pk=payment.pk).update(paid_by=member) membership = _create_membership_from_entry(entry, member) entry.membership = membership entry.status = Entry.STATUS_COMPLETED diff --git a/website/registrations/tests/test_admin.py b/website/registrations/tests/test_admin.py index e15bc89d055cfbe24dc4f695a440a8457694d185..e11aa9c79ef54783062a7e06de99199a3dc30dd2 100644 --- a/website/registrations/tests/test_admin.py +++ b/website/registrations/tests/test_admin.py @@ -280,7 +280,6 @@ class RegistrationAdminTest(TestCase): username='johnnytest', payment=Payment( pk='123', - processed=False ) ) @@ -290,7 +289,7 @@ class RegistrationAdminTest(TestCase): '/admin/payments/payment/123/change/', _('Unprocessed'))) - reg.payment.processed = True + reg.payment.type = Payment.CARD self.assertEqual( self.admin.payment_status(reg), diff --git a/website/registrations/tests/test_services.py b/website/registrations/tests/test_services.py index 47a85093c4a68ea4b75d6cb196a012333401f50a..81fff66575032a2b715de45cf4228d93a0319da0 100644 --- a/website/registrations/tests/test_services.py +++ b/website/registrations/tests/test_services.py @@ -486,7 +486,7 @@ class ServicesTest(TestCase): # Check that the DoesNotExist is caught p = Payment( amount=10, - processed=True, + type=Payment.CARD ) services.process_payment(p) @@ -508,7 +508,7 @@ class ServicesTest(TestCase): ).update(status=Entry.STATUS_ACCEPTED) payments = Payment.objects.filter(pk__in=[p0.pk, p2.pk, p3.pk]) - payments.update(processed=True) + payments.update(type=Payment.CARD) for payment in Payment.objects.filter(pk__in=[p0.pk, p1.pk, p2.pk]): services.process_payment(payment) @@ -521,7 +521,7 @@ class ServicesTest(TestCase): self.assertEqual(self.e1.status, Entry.STATUS_ACCEPTED) self.assertEqual(self.e2.status, Entry.STATUS_COMPLETED) - p0.processed = True + p0.type = Payment.CARD p0.save() self.e0.status = Entry.STATUS_ACCEPTED self.e0.save() @@ -545,7 +545,7 @@ class ServicesTest(TestCase): ).update(status=Entry.STATUS_ACCEPTED) payments = Payment.objects.filter(pk__in=[p1.pk, p2.pk]) - payments.update(processed=True) + payments.update(type=Payment.CARD) with mock.patch('registrations.services.' '_create_member_from_registration') as create_member: diff --git a/website/registrations/tests/test_signals.py b/website/registrations/tests/test_signals.py index 060a4ab09e3cbe54f44eb809ea750e6fee4ab17d..fe2c9552c9c5d82c57f50cecdc13393a098ce1fc 100644 --- a/website/registrations/tests/test_signals.py +++ b/website/registrations/tests/test_signals.py @@ -16,7 +16,7 @@ class ServicesTest(TestCase): @mock.patch('registrations.services.process_payment') def test_post_payment_save(self, process_payment): - self.payment.processed = True + self.payment.type = Payment.CARD self.payment.save() process_payment.assert_called_with(self.payment) diff --git a/website/thaliawebsite/urls.py b/website/thaliawebsite/urls.py index 4ecb30151ff1f2a5346c6e4af6352aed32e3b136..90bbc1e1a0fdadfddad92e19dbf22164bfdb56e0 100644 --- a/website/thaliawebsite/urls.py +++ b/website/thaliawebsite/urls.py @@ -77,7 +77,6 @@ urlpatterns = [ # pylint: disable=invalid-name url(r'^alumni/$', AlumniEventsView.as_view(), name='alumni'), url(r'^members/', include('members.urls')), url(r'^registration/', include('registrations.urls')), - url(r'^payment/', include('payments.urls')), url(r'^account/$', members.views.account, name='account'), url(r'^events/', include('events.urls')), url(r'^pizzas/', include('pizzas.urls')),