mirror of
https://github.com/pretix/pretix.git
synced 2026-08-26 13:14:40 +00:00
Deal with cancelling memberships (#2130)
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.3 on 2021-06-17 10:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0193_auto_20210611_1355'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='membership',
|
||||||
|
name='canceled',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -118,6 +118,10 @@ class Membership(models.Model):
|
|||||||
verbose_name=_('Test mode'),
|
verbose_name=_('Test mode'),
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
canceled = models.BooleanField(
|
||||||
|
verbose_name=_('Canceled'),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
customer = models.ForeignKey(
|
customer = models.ForeignKey(
|
||||||
Customer,
|
Customer,
|
||||||
related_name='memberships',
|
related_name='memberships',
|
||||||
|
|||||||
@@ -599,6 +599,8 @@ class Order(LockModel, LoggedModel):
|
|||||||
for gc in op.issued_gift_cards.all():
|
for gc in op.issued_gift_cards.all():
|
||||||
if gc.value != op.price:
|
if gc.value != op.price:
|
||||||
return False
|
return False
|
||||||
|
if op.granted_memberships.with_usages().filter(usages__gt=0):
|
||||||
|
return False
|
||||||
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
|
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
|
||||||
return False
|
return False
|
||||||
if self.status == Order.STATUS_PENDING:
|
if self.status == Order.STATUS_PENDING:
|
||||||
|
|||||||
@@ -141,6 +141,11 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
|
|||||||
_('You selected a membership that is connected to a different customer account.')
|
_('You selected a membership that is connected to a different customer account.')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if m.canceled:
|
||||||
|
raise ValidationError(
|
||||||
|
_('You selected membership that has been canceled.')
|
||||||
|
)
|
||||||
|
|
||||||
if m.testmode != testmode:
|
if m.testmode != testmode:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('You can only use a test mode membership for test mode tickets.')
|
_('You can only use a test mode membership for test mode tickets.')
|
||||||
|
|||||||
@@ -177,6 +177,10 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
|
|||||||
gc = GiftCard.objects.select_for_update().get(pk=gc.pk)
|
gc = GiftCard.objects.select_for_update().get(pk=gc.pk)
|
||||||
gc.transactions.create(value=position.price, order=order)
|
gc.transactions.create(value=position.price, order=order)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
for m in position.granted_memberships.all():
|
||||||
|
m.canceled = False
|
||||||
|
m.save()
|
||||||
else:
|
else:
|
||||||
raise OrderError(is_available)
|
raise OrderError(is_available)
|
||||||
|
|
||||||
@@ -410,6 +414,10 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
|||||||
else:
|
else:
|
||||||
gc.transactions.create(value=-position.price, order=order)
|
gc.transactions.create(value=-position.price, order=order)
|
||||||
|
|
||||||
|
for m in position.granted_memberships.all():
|
||||||
|
m.canceled = True
|
||||||
|
m.save()
|
||||||
|
|
||||||
if cancellation_fee:
|
if cancellation_fee:
|
||||||
with order.event.lock():
|
with order.event.lock():
|
||||||
for position in order.positions.all():
|
for position in order.positions.all():
|
||||||
@@ -1768,7 +1776,26 @@ class OrderChangeManager:
|
|||||||
else:
|
else:
|
||||||
gc.transactions.create(value=-op.position.price, order=self.order)
|
gc.transactions.create(value=-op.position.price, order=self.order)
|
||||||
|
|
||||||
|
for m in op.position.granted_memberships.with_usages().all():
|
||||||
|
m.canceled = True
|
||||||
|
m.save()
|
||||||
|
|
||||||
for opa in op.position.addons.all():
|
for opa in op.position.addons.all():
|
||||||
|
for gc in opa.issued_gift_cards.all():
|
||||||
|
gc = GiftCard.objects.select_for_update().get(pk=gc.pk)
|
||||||
|
if gc.value < opa.position.price:
|
||||||
|
raise OrderError(_(
|
||||||
|
'A position can not be canceled since the gift card {card} purchased in this order has '
|
||||||
|
'already been redeemed.').format(
|
||||||
|
card=gc.secret
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
gc.transactions.create(value=-opa.position.price, order=self.order)
|
||||||
|
|
||||||
|
for m in opa.granted_memberships.with_usages().all():
|
||||||
|
m.canceled = True
|
||||||
|
m.save()
|
||||||
|
|
||||||
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, auth=self.auth, data={
|
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, auth=self.auth, data={
|
||||||
'position': opa.pk,
|
'position': opa.pk,
|
||||||
'positionid': opa.positionid,
|
'positionid': opa.positionid,
|
||||||
|
|||||||
@@ -560,7 +560,7 @@ class MembershipUpdateForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Membership
|
model = Membership
|
||||||
fields = ['testmode', 'membership_type', 'date_start', 'date_end', 'attendee_name_parts']
|
fields = ['testmode', 'membership_type', 'date_start', 'date_end', 'attendee_name_parts', 'canceled']
|
||||||
field_classes = {
|
field_classes = {
|
||||||
'date_start': SplitDateTimeField,
|
'date_start': SplitDateTimeField,
|
||||||
'date_end': SplitDateTimeField,
|
'date_end': SplitDateTimeField,
|
||||||
|
|||||||
@@ -81,7 +81,9 @@
|
|||||||
{% for m in memberships %}
|
{% for m in memberships %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
{% if m.canceled %}<del>{% endif %}
|
||||||
{{ m.membership_type.name }}
|
{{ m.membership_type.name }}
|
||||||
|
{% if m.canceled %}</del>{% endif %}
|
||||||
{% if m.testmode %}<span class="label label-warning">{% trans "TEST MODE" %}</span>{% endif %}
|
{% if m.testmode %}<span class="label label-warning">{% trans "TEST MODE" %}</span>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -1891,7 +1891,6 @@ class MembershipDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequired
|
|||||||
template_name = 'pretixcontrol/organizers/customer_membership_delete.html'
|
template_name = 'pretixcontrol/organizers/customer_membership_delete.html'
|
||||||
permission = 'can_manage_customers'
|
permission = 'can_manage_customers'
|
||||||
context_object_name = 'membership'
|
context_object_name = 'membership'
|
||||||
form_class = MembershipUpdateForm
|
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
return get_object_or_404(
|
return get_object_or_404(
|
||||||
|
|||||||
@@ -376,6 +376,7 @@ class MembershipStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
|||||||
|
|
||||||
memberships = list(self.cart_customer.memberships.with_usages().filter(
|
memberships = list(self.cart_customer.memberships.with_usages().filter(
|
||||||
Q(Q(membership_type__max_usages__isnull=True) | Q(usages__lt=F('membership_type__max_usages'))),
|
Q(Q(membership_type__max_usages__isnull=True) | Q(usages__lt=F('membership_type__max_usages'))),
|
||||||
|
canceled=False
|
||||||
).select_related('membership_type'))
|
).select_related('membership_type'))
|
||||||
|
|
||||||
for p in self.applicable_positions:
|
for p in self.applicable_positions:
|
||||||
|
|||||||
@@ -58,7 +58,9 @@
|
|||||||
{% for m in memberships %}
|
{% for m in memberships %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
{% if m.canceled %}<del>{% endif %}
|
||||||
{{ m.membership_type.name }}
|
{{ m.membership_type.name }}
|
||||||
|
{% if m.canceled %}</del>{% endif %}
|
||||||
{% if m.testmode %}<span class="label label-warning">{% trans "TEST MODE" %}</span>{% endif %}
|
{% if m.testmode %}<span class="label label-warning">{% trans "TEST MODE" %}</span>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -227,6 +227,27 @@ def test_validate_membership_ensure_locking(event, customer, membership, requiri
|
|||||||
assert any('FOR UPDATE' in s['sql'] for s in captured)
|
assert any('FOR UPDATE' in s['sql'] for s in captured)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_validate_membership_canceled(event, customer, membership, requiring_ticket, membership_type):
|
||||||
|
with pytest.raises(ValidationError) as excinfo:
|
||||||
|
membership.canceled = True
|
||||||
|
membership.save()
|
||||||
|
validate_memberships_in_order(
|
||||||
|
customer,
|
||||||
|
[
|
||||||
|
CartPosition(
|
||||||
|
item=requiring_ticket,
|
||||||
|
used_membership=membership
|
||||||
|
)
|
||||||
|
],
|
||||||
|
event,
|
||||||
|
lock=False,
|
||||||
|
ignored_order=None,
|
||||||
|
testmode=False,
|
||||||
|
)
|
||||||
|
assert "canceled" in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_validate_membership_test_mode(event, customer, membership, requiring_ticket, membership_type):
|
def test_validate_membership_test_mode(event, customer, membership, requiring_ticket, membership_type):
|
||||||
with pytest.raises(ValidationError) as excinfo:
|
with pytest.raises(ValidationError) as excinfo:
|
||||||
|
|||||||
@@ -1233,6 +1233,27 @@ class OrderTestCase(BaseQuotaTestCase):
|
|||||||
)
|
)
|
||||||
assert not self.order.user_cancel_allowed
|
assert not self.order.user_cancel_allowed
|
||||||
|
|
||||||
|
@classscope(attr='o')
|
||||||
|
def test_can_cancel_order_with_membership(self):
|
||||||
|
mt = self.event.organizer.membership_types.create(name="foo")
|
||||||
|
customer = self.event.organizer.customers.create()
|
||||||
|
self.order.customer = customer
|
||||||
|
self.order.save()
|
||||||
|
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||||||
|
admission=True, allow_cancel=True, issue_giftcard=True)
|
||||||
|
p = OrderPosition.objects.create(order=self.order, item=item1,
|
||||||
|
variation=None, price=23)
|
||||||
|
m = customer.memberships.create(
|
||||||
|
membership_type=mt,
|
||||||
|
date_start=now(),
|
||||||
|
date_end=now(),
|
||||||
|
granted_in=p,
|
||||||
|
)
|
||||||
|
# yeah, doesn't really make sense on same order, but good enough for the test
|
||||||
|
OrderPosition.objects.create(order=self.order, item=item1,
|
||||||
|
variation=None, price=23, used_membership=m)
|
||||||
|
assert not self.order.user_cancel_allowed
|
||||||
|
|
||||||
@classscope(attr='o')
|
@classscope(attr='o')
|
||||||
def test_can_cancel_order_free(self):
|
def test_can_cancel_order_free(self):
|
||||||
self.order.status = Order.STATUS_PAID
|
self.order.status = Order.STATUS_PAID
|
||||||
|
|||||||
@@ -1166,6 +1166,23 @@ class OrderChangeManagerTests(TestCase):
|
|||||||
self.ocm.commit()
|
self.ocm.commit()
|
||||||
assert gc.value == Decimal('0.00')
|
assert gc.value == Decimal('0.00')
|
||||||
|
|
||||||
|
@classscope(attr='o')
|
||||||
|
def test_cancel_issued_membership(self):
|
||||||
|
mt = self.event.organizer.membership_types.create(name="foo")
|
||||||
|
customer = self.event.organizer.customers.create()
|
||||||
|
self.order.customer = customer
|
||||||
|
self.o.save()
|
||||||
|
m = customer.memberships.create(
|
||||||
|
membership_type=mt,
|
||||||
|
date_start=now(),
|
||||||
|
date_end=now(),
|
||||||
|
granted_in=self.order.positions.first(),
|
||||||
|
)
|
||||||
|
self.ocm.cancel(self.op1)
|
||||||
|
self.ocm.commit()
|
||||||
|
m.refresh_from_db()
|
||||||
|
assert m.canceled
|
||||||
|
|
||||||
@classscope(attr='o')
|
@classscope(attr='o')
|
||||||
def test_cancel_issued_giftcard_used(self):
|
def test_cancel_issued_giftcard_used(self):
|
||||||
gc = self.o.issued_gift_cards.create(currency="EUR", issued_in=self.op1)
|
gc = self.o.issued_gift_cards.create(currency="EUR", issued_in=self.op1)
|
||||||
@@ -2419,6 +2436,21 @@ class OrderChangeManagerTests(TestCase):
|
|||||||
op.refresh_from_db()
|
op.refresh_from_db()
|
||||||
assert op.secret != s
|
assert op.secret != s
|
||||||
|
|
||||||
|
@classscope(attr='o')
|
||||||
|
def test_cancel_order_membership(self):
|
||||||
|
mt = self.event.organizer.membership_types.create(name="foo")
|
||||||
|
customer = self.event.organizer.customers.create()
|
||||||
|
self.order.customer = customer
|
||||||
|
m = customer.memberships.create(
|
||||||
|
membership_type=mt,
|
||||||
|
date_start=now(),
|
||||||
|
date_end=now(),
|
||||||
|
granted_in=self.order.positions.first(),
|
||||||
|
)
|
||||||
|
cancel_order(self.order)
|
||||||
|
m.refresh_from_db()
|
||||||
|
assert m.canceled
|
||||||
|
|
||||||
@classscope(attr='o')
|
@classscope(attr='o')
|
||||||
def test_auto_change_payment_fee(self):
|
def test_auto_change_payment_fee(self):
|
||||||
fee2 = self.order.fees.create(fee_type=OrderFee.FEE_TYPE_SHIPPING, value=Decimal('0.50'))
|
fee2 = self.order.fees.create(fee_type=OrderFee.FEE_TYPE_SHIPPING, value=Decimal('0.50'))
|
||||||
@@ -3019,3 +3051,20 @@ class OrderReactivateTest(TestCase):
|
|||||||
gc = self.o.issued_gift_cards.create(currency="EUR", issued_in=self.op1)
|
gc = self.o.issued_gift_cards.create(currency="EUR", issued_in=self.op1)
|
||||||
reactivate_order(self.order)
|
reactivate_order(self.order)
|
||||||
assert gc.value == 23
|
assert gc.value == 23
|
||||||
|
|
||||||
|
@classscope(attr='o')
|
||||||
|
def test_reactivate_membership(self):
|
||||||
|
mt = self.event.organizer.membership_types.create(name="foo")
|
||||||
|
customer = self.event.organizer.customers.create()
|
||||||
|
self.order.customer = customer
|
||||||
|
self.order.save()
|
||||||
|
m = customer.memberships.create(
|
||||||
|
membership_type=mt,
|
||||||
|
date_start=now(),
|
||||||
|
date_end=now(),
|
||||||
|
granted_in=self.order.positions.first(),
|
||||||
|
canceled=True,
|
||||||
|
)
|
||||||
|
reactivate_order(self.order)
|
||||||
|
m.refresh_from_db()
|
||||||
|
assert not m.canceled
|
||||||
|
|||||||
Reference in New Issue
Block a user