Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
thalia
concrexit
Commits
7255f425
Commit
7255f425
authored
Mar 04, 2020
by
Job Doesburg
Browse files
Merge branch 'add-create-payment-function' into 'master'
Add create_payment helper function Closes
#1010
See merge request
!1508
parents
aec97de8
5c09f224
Changes
12
Hide whitespace changes
Inline
Side-by-side
docs/payments.rst
View file @
7255f425
...
...
@@ -40,6 +40,14 @@ payments.apps module
:undoc-members:
:show-inheritance:
payments.exceptions module
--------------------------
.. automodule:: payments.exceptions
:members:
:undoc-members:
:show-inheritance:
payments.forms module
---------------------
...
...
website/events/models.py
View file @
7255f425
...
...
@@ -9,10 +9,12 @@ from django.urls import reverse
from
django.utils
import
timezone
from
django.utils.crypto
import
get_random_string
from
django.utils.text
import
format_lazy
from
django.template.defaulttags
import
date
from
django.utils.translation
import
gettext_lazy
as
_
from
tinymce.models
import
HTMLField
from
members.models
import
Member
from
payments.models
import
Payable
from
pushnotifications.models
import
ScheduledMessage
,
Category
from
utils.translation
import
ModelTranslateMeta
,
MultilingualField
from
announcements.models
import
Slide
...
...
@@ -513,7 +515,7 @@ def registration_member_choices_limit():
)
class
Registration
(
models
.
Model
):
class
Registration
(
models
.
Model
,
Payable
):
"""Describes a registration for an Event"""
event
=
models
.
ForeignKey
(
Event
,
models
.
CASCADE
)
...
...
@@ -636,6 +638,24 @@ class Registration(models.Model):
else
:
return
"{}: {}"
.
format
(
self
.
name
,
self
.
event
)
@
property
def
payment_amount
(
self
):
return
self
.
event
.
price
@
property
def
payment_topic
(
self
):
return
f
'
{
self
.
event
.
title_en
}
[
{
date
(
self
.
event
.
start
,
"Y-m-d"
)
}
]'
@
property
def
payment_notes
(
self
):
notes
=
f
"Event registration
{
self
.
event
.
title_en
}
. "
notes
+=
f
"
{
self
.
event
.
start
}
. "
f
"Registration date:
{
self
.
date
}
."
return
notes
@
property
def
payment_payer
(
self
):
return
self
.
member
class
Meta
:
ordering
=
(
"date"
,)
unique_together
=
((
"member"
,
"event"
),)
...
...
website/events/services.py
View file @
7255f425
...
...
@@ -7,7 +7,9 @@ from django.utils.translation import gettext_lazy as _, get_language
from
events
import
emails
from
events.exceptions
import
RegistrationError
from
events.models
import
Registration
,
RegistrationInformationField
,
Event
from
payments.exceptions
import
PaymentError
from
payments.models
import
Payment
from
payments.services
import
create_payment
,
delete_payment
from
utils.snippets
import
datetime_to_lectureyear
...
...
@@ -160,37 +162,18 @@ def pay_with_tpay(member, event):
:param member: the user
:param event: the event
"""
registration
=
None
try
:
registration
=
Registration
.
objects
.
get
(
event
=
event
,
member
=
member
)
except
Registration
.
DoesNotExist
:
raise
RegistrationError
(
_
(
"You are not registered for this event."
))
if
member
.
tpay_enabled
:
if
registration
.
payment
is
None
:
note
=
f
"Event registration
{
registration
.
event
.
title_en
}
. "
if
registration
.
name
:
note
+=
f
"Paid by
{
registration
.
name
}
. "
note
+=
(
f
"
{
registration
.
event
.
start
}
. "
f
"Registration date:
{
registration
.
date
}
."
)
registration
.
payment
=
Payment
.
objects
.
create
(
amount
=
registration
.
event
.
price
,
paid_by
=
member
,
notes
=
note
,
processed_by
=
member
,
type
=
Payment
.
TPAY
,
)
registration
.
save
()
elif
registration
.
payment
.
type
==
Payment
.
NONE
:
registration
.
payment
.
type
=
Payment
.
TPAY
registration
.
save
()
else
:
raise
RegistrationError
(
_
(
"You have already paid for this "
"event."
))
if
registration
.
payment
is
None
:
registration
.
payment
=
create_payment
(
payable
=
registration
,
processed_by
=
member
,
pay_type
=
Payment
.
TPAY
)
registration
.
save
()
else
:
raise
RegistrationError
(
_
(
"You
do not have Thalia Pay enabled
."
))
raise
RegistrationError
(
_
(
"You
have already paid for this event
."
))
def
update_registration
(
...
...
@@ -311,41 +294,13 @@ def update_registration_by_organiser(registration, member, data):
if
"payment"
in
data
:
if
data
[
"payment"
][
"type"
]
==
Payment
.
NONE
and
registration
.
payment
is
not
None
:
p
=
registration
.
payment
registration
.
payment
=
None
registration
.
save
()
p
.
delete
()
elif
(
data
[
"payment"
][
"type"
]
!=
Payment
.
NONE
and
registration
.
payment
is
not
None
):
if
data
[
"payment"
][
"type"
]
!=
Payment
.
TPAY
or
(
data
[
"payment"
][
"type"
]
==
Payment
.
TPAY
and
member
.
tpay_enabled
):
registration
.
payment
.
type
=
data
[
"payment"
][
"type"
]
registration
.
payment
.
save
()
else
:
raise
RegistrationError
(
_
(
"This user does not have Thalia Pay enabled"
))
elif
data
[
"payment"
][
"type"
]
!=
Payment
.
NONE
and
registration
.
payment
is
None
:
if
data
[
"payment"
][
"type"
]
!=
Payment
.
TPAY
or
(
data
[
"payment"
][
"type"
]
==
Payment
.
TPAY
and
member
.
tpay_enabled
):
note
=
f
"Event registration
{
registration
.
event
.
title_en
}
. "
if
registration
.
name
:
note
+=
f
"Paid by
{
registration
.
name
}
. "
note
+=
(
f
"
{
registration
.
event
.
start
}
. "
f
"Registration date:
{
registration
.
date
}
."
)
registration
.
payment
=
Payment
.
objects
.
create
(
amount
=
registration
.
event
.
price
,
paid_by
=
registration
.
member
,
notes
=
note
,
processed_by
=
member
,
type
=
data
[
"payment"
][
"type"
],
)
else
:
raise
RegistrationError
(
_
(
"This user does not have Thalia Pay enabled"
))
delete_payment
(
registration
)
else
:
registration
.
payment
=
create_payment
(
payable
=
registration
,
processed_by
=
member
,
pay_type
=
data
[
"payment"
][
"type"
],
)
if
"present"
in
data
:
registration
.
present
=
data
[
"present"
]
...
...
website/payments/admin.py
View file @
7255f425
...
...
@@ -44,7 +44,7 @@ class PaymentAdmin(admin.ModelAdmin):
"type"
,
"paid_by_link"
,
"processed_by_link"
,
"
notes
"
,
"
topic
"
,
)
list_filter
=
(
"type"
,)
list_select_related
=
(
...
...
@@ -59,6 +59,7 @@ class PaymentAdmin(admin.ModelAdmin):
"processing_date"
,
"paid_by"
,
"processed_by"
,
"topic"
,
"notes"
,
)
readonly_fields
=
(
...
...
@@ -68,9 +69,11 @@ class PaymentAdmin(admin.ModelAdmin):
"processing_date"
,
"paid_by"
,
"processed_by"
,
"topic"
,
"notes"
,
)
search_fields
=
(
"topic"
,
"notes"
,
"paid_by__username"
,
"paid_by__first_name"
,
...
...
website/payments/exceptions.py
0 → 100644
View file @
7255f425
class
PaymentError
(
Exception
):
"""Custom error for problems during payment"""
pass
website/payments/migrations/0006_payment_topic.py
0 → 100644
View file @
7255f425
# Generated by Django 3.0.2 on 2020-02-21 16:24
from
django.db
import
migrations
,
models
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'payments'
,
'0005_auto_20191030_2055'
),
]
operations
=
[
migrations
.
AddField
(
model_name
=
'payment'
,
name
=
'topic'
,
field
=
models
.
CharField
(
default
=
'Unknown'
,
max_length
=
255
),
),
]
website/payments/models.py
View file @
7255f425
...
...
@@ -11,24 +11,6 @@ from localflavor.generic.countries.sepa import IBAN_SEPA_COUNTRIES
from
localflavor.generic.models
import
IBANField
,
BICField
class
Payable
:
@
property
def
payment_amount
(
self
):
raise
NotImplementedError
@
property
def
payment_topic
(
self
):
raise
NotImplementedError
@
property
def
payment_notes
(
self
):
raise
NotImplementedError
@
property
def
payment_payer
(
self
):
raise
NotImplementedError
class
Payment
(
models
.
Model
):
"""
Describes a payment
...
...
@@ -79,6 +61,7 @@ class Payment(models.Model):
)
notes
=
models
.
TextField
(
blank
=
True
,
null
=
True
)
topic
=
models
.
CharField
(
max_length
=
255
,
default
=
"Unknown"
)
@
property
def
processed
(
self
):
...
...
@@ -215,3 +198,26 @@ class BankAccount(models.Model):
class
Meta
:
ordering
=
(
"created_at"
,)
class
Payable
:
payment
=
None
@
property
def
payment_amount
(
self
):
raise
NotImplementedError
@
property
def
payment_topic
(
self
):
raise
NotImplementedError
@
property
def
payment_notes
(
self
):
raise
NotImplementedError
@
property
def
payment_payer
(
self
):
raise
NotImplementedError
def
save
(
self
):
raise
NotImplementedError
website/payments/services.py
View file @
7255f425
"""The services defined by the payments package"""
import
datetime
from
typing
import
Union
from
django.db.models
import
QuerySet
,
Q
from
django.utils
import
timezone
from
django.utils.translation
import
gettext_lazy
as
_
from
members.models
import
Member
from
.models
import
Payment
,
BankAccount
from
.exceptions
import
PaymentError
from
.models
import
Payment
,
BankAccount
,
Payable
def
create_payment
(
payable
:
Payable
,
processed_by
:
Member
,
pay_type
:
Union
[
Payment
.
CASH
,
Payment
.
CARD
,
Payment
.
WIRE
,
Payment
.
TPAY
],
)
->
Payment
:
"""
Create a new payment from a payable object
:param payable: Payable object
:param processed_by: Member that processed this payment
:param pay_type: Payment type
:return: Payment object
"""
if
pay_type
==
Payment
.
TPAY
and
not
payable
.
payment_payer
.
tpay_enabled
:
raise
PaymentError
(
_
(
"This user does not have Thalia Pay enabled"
))
if
payable
.
payment
is
not
None
:
payable
.
payment
.
type
=
pay_type
payable
.
payment
.
save
()
else
:
payable
.
payment
=
Payment
.
objects
.
create
(
processed_by
=
processed_by
,
amount
=
payable
.
payment_amount
,
notes
=
payable
.
payment_notes
,
topic
=
payable
.
payment_topic
,
paid_by
=
payable
.
payment_payer
,
processing_date
=
timezone
.
now
(),
type
=
pay_type
,
)
return
payable
.
payment
def
delete_payment
(
payable
:
Payable
):
"""
Removes a payment from a payable object
:param payable: Payable object
:return:
"""
payment
=
payable
.
payment
payable
.
payment
=
None
payable
.
save
()
payment
.
delete
()
def
process_payment
(
...
...
website/payments/tests/test_models.py
View file @
7255f425
...
...
@@ -30,6 +30,11 @@ class PayableTest(TestCase):
with
self
.
assertRaises
(
NotImplementedError
):
_
=
p
.
payment_payer
def
test_save_not_implemented
(
self
):
p
=
Payable
()
with
self
.
assertRaises
(
NotImplementedError
):
p
.
save
()
@
override_settings
(
SUSPEND_SIGNALS
=
True
)
class
PaymentTest
(
TestCase
):
...
...
website/payments/tests/test_services.py
View file @
7255f425
from
unittest.mock
import
MagicMock
from
django.test
import
TestCase
,
override_settings
from
django.utils
import
timezone
from
freezegun
import
freeze_time
from
members.models
import
Member
from
payments
import
services
from
payments.models
import
BankAccount
,
Payment
from
payments.exceptions
import
PaymentError
from
payments.models
import
BankAccount
,
Payment
,
Payable
class
MockPayable
(
Payable
):
save
=
MagicMock
()
def
__init__
(
self
,
payer
,
amount
=
5
,
topic
=
"mock topic"
,
notes
=
"mock notes"
,
payment
=
None
)
->
None
:
super
().
__init__
()
self
.
payer
=
payer
self
.
amount
=
amount
self
.
topic
=
topic
self
.
notes
=
notes
self
.
payment
=
payment
@
property
def
payment_amount
(
self
):
return
self
.
amount
@
property
def
payment_topic
(
self
):
return
self
.
topic
@
property
def
payment_notes
(
self
):
return
self
.
notes
@
property
def
payment_payer
(
self
):
return
self
.
payer
@
freeze_time
(
"2019-01-01"
)
...
...
@@ -20,6 +53,41 @@ class ServicesTest(TestCase):
def
setUpTestData
(
cls
):
cls
.
member
=
Member
.
objects
.
filter
(
last_name
=
"Wiggers"
).
first
()
def
test_create_payment
(
self
):
with
self
.
subTest
(
"Creates new payment with right payment type"
):
p
=
services
.
create_payment
(
MockPayable
(
self
.
member
),
self
.
member
,
Payment
.
CASH
)
self
.
assertEqual
(
p
.
processing_date
,
timezone
.
now
())
self
.
assertEqual
(
p
.
amount
,
5
)
self
.
assertEqual
(
p
.
topic
,
"mock topic"
)
self
.
assertEqual
(
p
.
notes
,
"mock notes"
)
self
.
assertEqual
(
p
.
paid_by
,
self
.
member
)
self
.
assertEqual
(
p
.
processed_by
,
self
.
member
)
self
.
assertEqual
(
p
.
type
,
Payment
.
CASH
)
with
self
.
subTest
(
"Does not create new payment if one already exists"
):
existing_payment
=
Payment
(
amount
=
2
)
p
=
services
.
create_payment
(
MockPayable
(
payer
=
self
.
member
,
payment
=
existing_payment
),
self
.
member
,
Payment
.
CASH
,
)
self
.
assertEqual
(
p
,
existing_payment
)
self
.
assertEqual
(
p
.
amount
,
2
)
with
self
.
subTest
(
"Does not allow Thalia Pay when not enabled"
):
with
self
.
assertRaises
(
PaymentError
):
services
.
create_payment
(
MockPayable
(
payer
=
self
.
member
),
self
.
member
,
Payment
.
TPAY
)
def
test_delete_payment
(
self
):
existing_payment
=
MagicMock
()
payable
=
MockPayable
(
payer
=
self
.
member
,
payment
=
existing_payment
)
services
.
delete_payment
(
payable
)
self
.
assertIsNone
(
payable
.
payment
)
payable
.
save
.
assert_called_once
()
existing_payment
.
delete
.
assert_called_once
()
def
test_process_payment
(
self
):
BankAccount
.
objects
.
create
(
owner
=
self
.
member
,
...
...
website/pizzas/models.py
View file @
7255f425
...
...
@@ -4,6 +4,7 @@ from django.db import models
from
django.db.models
import
Q
from
django.utils
import
timezone
from
django.utils.translation
import
gettext_lazy
as
_
from
django.template.defaulttags
import
date
from
events.models
import
Event
import
members
...
...
@@ -223,7 +224,11 @@ class Order(models.Model):
self
.
payment
.
save
()
except
ObjectDoesNotExist
:
self
.
payment
=
Payment
.
objects
.
create
(
amount
=
self
.
product
.
price
,
notes
=
notes
,
paid_by
=
self
.
member
amount
=
self
.
product
.
price
,
notes
=
notes
,
paid_by
=
self
.
member
,
topic
=
f
"Pizzas
{
self
.
pizza_event
.
event
.
title_en
}
"
f
'[
{
date
(
self
.
pizza_event
.
start
,
"Y-m-d"
)
}
]'
,
)
super
().
save
(
*
args
,
**
kwargs
)
...
...
website/registrations/services.py
View file @
7255f425
...
...
@@ -233,11 +233,13 @@ def _create_payment_for_entry(entry: Entry) -> Payment:
if
entry
.
contribution
and
entry
.
membership_type
==
Membership
.
BENEFACTOR
:
amount
=
entry
.
contribution
notes
=
f
"Membership registration.
{
entry
.
get_membership_type_display
()
}
."
topic
=
f
"Member registration [
{
entry
.
membership_type
.
upper
()
}
]"
try
:
renewal
=
entry
.
renewal
membership
=
renewal
.
member
.
latest_membership
notes
=
f
"Membership renewal.
{
entry
.
get_membership_type_display
()
}
."
topic
=
f
"Member renewal [
{
entry
.
membership_type
.
upper
()
}
]"
# 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
...
...
@@ -262,7 +264,7 @@ def _create_payment_for_entry(entry: Entry) -> Payment:
except
Renewal
.
DoesNotExist
:
pass
return
Payment
.
objects
.
create
(
amount
=
amount
,
notes
=
notes
)
return
Payment
.
objects
.
create
(
amount
=
amount
,
notes
=
notes
,
topic
=
topic
)
def
_create_member_from_registration
(
registration
:
Registration
)
->
Member
:
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment