forked from CGM_Public/pretix_original
Refactor cancelling positions and orders in the data model (#1088)
- [x] Data model - [x] display in order view in backend - [x] review all usages of OrderPositions.objects - [x] review all usages of order.positions - [x] review all other model usages - [x] review plugins - [x] plugins backwards-compatible API? - [x] decide on way forward for REST API - [x] need to cancel fees - [x] tests - [ ] plugins - [ ] gdpr - [ ] reports - [x] docs
This commit is contained in:
@@ -104,6 +104,16 @@ def order(event, item, taxrule, question):
|
||||
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
pseudonymization_id="ABCDEFGHKL",
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o,
|
||||
item=item,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name_parts={"full_name": "Peter", "_scheme": "full"},
|
||||
secret="YBiYJrmF5ufiTLdV1iDf",
|
||||
pseudonymization_id="JKLM",
|
||||
canceled=True
|
||||
)
|
||||
op.answers.create(question=question, answer='S')
|
||||
return o
|
||||
|
||||
@@ -509,7 +519,7 @@ def test_refund_process_mark_refunded(token_client, organizer, event, order):
|
||||
assert resp.status_code == 200
|
||||
assert r.state == OrderRefund.REFUND_STATE_DONE
|
||||
order.refresh_from_db()
|
||||
assert order.status == Order.STATUS_REFUNDED
|
||||
assert order.status == Order.STATUS_CANCELED
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/refunds/2/process/'.format(
|
||||
organizer.slug, event.slug, order.code
|
||||
@@ -692,6 +702,14 @@ def test_orderposition_detail(token_client, organizer, event, order, item, quest
|
||||
assert len(resp.data['downloads']) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_orderposition_detail_no_canceled(token_client, organizer, event, order, item, question):
|
||||
op = order.all_positions.filter(canceled=True).first()
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(organizer.slug, event.slug,
|
||||
op.pk))
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_orderposition_delete(token_client, organizer, event, order, item, question):
|
||||
op = order.positions.first()
|
||||
@@ -720,6 +738,7 @@ def test_orderposition_delete(token_client, organizer, event, order, item, quest
|
||||
))
|
||||
assert resp.status_code == 204
|
||||
assert order.positions.count() == 1
|
||||
assert order.all_positions.count() == 3
|
||||
order.refresh_from_db()
|
||||
assert order.total == Decimal('23.25')
|
||||
|
||||
@@ -952,8 +971,8 @@ def test_order_mark_canceled_pending_no_email(token_client, organizer, event, or
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_mark_canceled_paid(token_client, organizer, event, order):
|
||||
order.status = Order.STATUS_PAID
|
||||
def test_order_mark_canceled_expired(token_client, organizer, event, order):
|
||||
order.status = Order.STATUS_EXPIRED
|
||||
order.save()
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/{}/mark_canceled/'.format(
|
||||
@@ -962,7 +981,7 @@ def test_order_mark_canceled_paid(token_client, organizer, event, order):
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
order.refresh_from_db()
|
||||
assert order.status == Order.STATUS_PAID
|
||||
assert order.status == Order.STATUS_EXPIRED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -975,7 +994,7 @@ def test_order_mark_paid_refunded(token_client, organizer, event, order):
|
||||
)
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['status'] == Order.STATUS_REFUNDED
|
||||
assert resp.data['status'] == Order.STATUS_CANCELED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -2411,7 +2430,7 @@ def test_refund_create_mark_refunded(token_client, organizer, event, order):
|
||||
assert r.info_data == {"foo": "bar"}
|
||||
assert r.payment.local_id == 2
|
||||
order.refresh_from_db()
|
||||
assert order.status == Order.STATUS_REFUNDED
|
||||
assert order.status == Order.STATUS_CANCELED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -58,6 +58,14 @@ def env():
|
||||
price=Decimal("42.00"),
|
||||
positionid=2,
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o,
|
||||
item=t_shirt,
|
||||
variation=variation,
|
||||
price=Decimal("42.00"),
|
||||
positionid=3,
|
||||
canceled=True
|
||||
)
|
||||
gs = GlobalSettingsObject()
|
||||
gs.settings.ecb_rates_date = date.today()
|
||||
gs.settings.ecb_rates_dict = json.dumps({
|
||||
|
||||
@@ -740,7 +740,7 @@ class OrderTestCase(BaseQuotaTestCase):
|
||||
q = Question.objects.create(question='Foo', type=Question.TYPE_BOOLEAN, event=self.event)
|
||||
self.item1.questions.add(q)
|
||||
assert self.order.can_modify_answers
|
||||
self.order.status = Order.STATUS_REFUNDED
|
||||
self.order.status = Order.STATUS_CANCELED
|
||||
assert not self.order.can_modify_answers
|
||||
self.order.status = Order.STATUS_PAID
|
||||
assert self.order.can_modify_answers
|
||||
@@ -963,7 +963,7 @@ class OrderTestCase(BaseQuotaTestCase):
|
||||
assert o.has_external_refund
|
||||
|
||||
def test_pending_order_pending_refund(self):
|
||||
self.order.status = Order.STATUS_REFUNDED
|
||||
self.order.status = Order.STATUS_CANCELED
|
||||
self.order.save()
|
||||
self.order.payments.create(
|
||||
amount=Decimal('46.00'),
|
||||
@@ -1023,6 +1023,14 @@ class OrderTestCase(BaseQuotaTestCase):
|
||||
assert not o.has_pending_refund
|
||||
assert not o.has_external_refund
|
||||
|
||||
def test_canceled_positions(self):
|
||||
self.op1.canceled = True
|
||||
self.op1.save()
|
||||
assert OrderPosition.objects.count() == 1
|
||||
assert OrderPosition.all.count() == 2
|
||||
assert self.order.positions.count() == 1
|
||||
assert self.order.all_positions.count() == 2
|
||||
|
||||
|
||||
class ItemCategoryTest(TestCase):
|
||||
"""
|
||||
|
||||
@@ -616,6 +616,26 @@ class OrderChangeManagerTests(TestCase):
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.positions.count() == 1
|
||||
assert self.order.total == self.op2.price
|
||||
self.op1.refresh_from_db()
|
||||
assert self.op1.canceled
|
||||
|
||||
def test_cancel_with_addon(self):
|
||||
self.shirt.category = self.event.categories.create(name='Add-ons', is_addon=True)
|
||||
self.ticket.addons.create(addon_category=self.shirt.category)
|
||||
self.ocm.add_position(self.shirt, None, Decimal('13.00'), self.op1)
|
||||
self.ocm.commit()
|
||||
self.order.refresh_from_db()
|
||||
self.ocm = OrderChangeManager(self.order, None)
|
||||
assert self.order.positions.count() == 3
|
||||
|
||||
self.ocm.cancel(self.op1)
|
||||
self.ocm.commit()
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.positions.count() == 1
|
||||
assert self.order.total == self.op2.price
|
||||
self.op1.refresh_from_db()
|
||||
assert self.op1.canceled
|
||||
assert self.op1.addons.first().canceled
|
||||
|
||||
def test_free_to_paid(self):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
|
||||
@@ -53,6 +53,14 @@ def env():
|
||||
price=Decimal("14"),
|
||||
attendee_name_parts={'full_name': "Peter", "_scheme": "full"}
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o,
|
||||
item=ticket,
|
||||
variation=None,
|
||||
price=Decimal("14"),
|
||||
canceled=True,
|
||||
attendee_name_parts={'full_name': "Lukas Gelöscht", "_scheme": "full"}
|
||||
)
|
||||
return event, user, o, ticket
|
||||
|
||||
|
||||
@@ -125,6 +133,7 @@ def test_order_detail(client, env):
|
||||
response = client.get('/control/event/dummy/dummy/orders/FOO/')
|
||||
assert 'Early-bird' in response.rendered_content
|
||||
assert 'Peter' in response.rendered_content
|
||||
assert 'Lukas Gelöscht' in response.rendered_content
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -256,23 +265,15 @@ def test_order_deny(client, env):
|
||||
# (Old status, new status, success expected)
|
||||
(Order.STATUS_CANCELED, Order.STATUS_PAID, False),
|
||||
(Order.STATUS_CANCELED, Order.STATUS_PENDING, False),
|
||||
(Order.STATUS_CANCELED, Order.STATUS_REFUNDED, False),
|
||||
(Order.STATUS_CANCELED, Order.STATUS_EXPIRED, False),
|
||||
|
||||
(Order.STATUS_PAID, Order.STATUS_PENDING, False),
|
||||
(Order.STATUS_PAID, Order.STATUS_CANCELED, False),
|
||||
(Order.STATUS_PAID, Order.STATUS_REFUNDED, False),
|
||||
(Order.STATUS_PAID, Order.STATUS_CANCELED, True),
|
||||
(Order.STATUS_PAID, Order.STATUS_EXPIRED, False),
|
||||
|
||||
(Order.STATUS_PENDING, Order.STATUS_CANCELED, True),
|
||||
(Order.STATUS_PENDING, Order.STATUS_PAID, True),
|
||||
(Order.STATUS_PENDING, Order.STATUS_REFUNDED, False),
|
||||
(Order.STATUS_PENDING, Order.STATUS_EXPIRED, True),
|
||||
|
||||
(Order.STATUS_REFUNDED, Order.STATUS_CANCELED, False),
|
||||
(Order.STATUS_REFUNDED, Order.STATUS_PAID, False),
|
||||
(Order.STATUS_REFUNDED, Order.STATUS_PENDING, False),
|
||||
(Order.STATUS_REFUNDED, Order.STATUS_EXPIRED, False),
|
||||
])
|
||||
def test_order_transition(client, env, process):
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
@@ -784,6 +785,11 @@ class OrderChangeTests(SoupTest):
|
||||
order=self.order, item=self.ticket, variation=None,
|
||||
price=Decimal("23.00"), attendee_name_parts={'full_name': "Dieter", "_scheme": "full"}
|
||||
)
|
||||
self.op3 = OrderPosition.objects.create(
|
||||
order=self.order, item=self.ticket, variation=None,
|
||||
price=Decimal("23.00"), attendee_name_parts={'full_name': "Lukas", "_scheme": "full"},
|
||||
canceled=True
|
||||
)
|
||||
self.quota = self.event.quotas.create(name="All", size=100)
|
||||
self.quota.items.add(self.ticket)
|
||||
self.quota.items.add(self.shirt)
|
||||
@@ -793,6 +799,14 @@ class OrderChangeTests(SoupTest):
|
||||
t.limit_events.add(self.event)
|
||||
self.client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
|
||||
def test_do_not_show_canceled(self):
|
||||
r = self.client.get('/control/event/{}/{}/orders/{}/change'.format(
|
||||
self.event.organizer.slug, self.event.slug, self.order.code
|
||||
))
|
||||
assert self.op1.secret[:5] in r.rendered_content
|
||||
assert self.op2.secret[:5] in r.rendered_content
|
||||
assert self.op3.secret[:5] not in r.rendered_content
|
||||
|
||||
def test_change_item_success(self):
|
||||
self.client.post('/control/event/{}/{}/orders/{}/change'.format(
|
||||
self.event.organizer.slug, self.event.slug, self.order.code
|
||||
@@ -1137,7 +1151,7 @@ def test_process_refund_mark_refunded(client, env):
|
||||
r.refresh_from_db()
|
||||
assert r.state == OrderRefund.REFUND_STATE_DONE
|
||||
env[2].refresh_from_db()
|
||||
assert env[2].status == Order.STATUS_REFUNDED
|
||||
assert env[2].status == Order.STATUS_CANCELED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -1240,7 +1254,7 @@ def test_refund_paid_order_fully_mark_as_refunded(client, env):
|
||||
assert r.provider == "manual"
|
||||
assert r.state == OrderRefund.REFUND_STATE_DONE
|
||||
assert r.amount == Decimal('14.00')
|
||||
assert env[2].status == Order.STATUS_REFUNDED
|
||||
assert env[2].status == Order.STATUS_CANCELED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -162,7 +162,7 @@ def test_retry_refunded(env, client):
|
||||
state=BankTransaction.STATE_ERROR,
|
||||
amount=23, date='unknown', order=env[3])
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
env[3].status = Order.STATUS_REFUNDED
|
||||
env[3].status = Order.STATUS_CANCELED
|
||||
env[3].save()
|
||||
r = json.loads(client.post('/control/event/{}/{}/banktransfer/action/'.format(env[0].organizer.slug, env[0].slug), {
|
||||
'action_{}'.format(trans.pk): 'retry',
|
||||
@@ -171,7 +171,7 @@ def test_retry_refunded(env, client):
|
||||
trans.refresh_from_db()
|
||||
assert trans.state == BankTransaction.STATE_ERROR
|
||||
env[3].refresh_from_db()
|
||||
assert env[3].status == Order.STATUS_REFUNDED
|
||||
assert env[3].status == Order.STATUS_CANCELED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -63,6 +63,14 @@ class OrdersTest(TestCase):
|
||||
price=Decimal("23"),
|
||||
attendee_name_parts={'full_name': "Peter"}
|
||||
)
|
||||
self.deleted_pos = OrderPosition.objects.create(
|
||||
order=self.order,
|
||||
item=self.ticket,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name_parts={'full_name': "Lukas"},
|
||||
canceled=True
|
||||
)
|
||||
self.not_my_order = Order.objects.create(
|
||||
status=Order.STATUS_PENDING,
|
||||
event=self.event,
|
||||
@@ -130,15 +138,17 @@ class OrdersTest(TestCase):
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
assert len(doc.select(".cart-row")) > 0
|
||||
assert "pending" in doc.select(".label-warning")[0].text.lower()
|
||||
assert "Peter" in response.rendered_content
|
||||
assert "Lukas" not in response.rendered_content
|
||||
|
||||
def test_orders_modify_invalid(self):
|
||||
self.order.status = Order.STATUS_REFUNDED
|
||||
self.order.status = Order.STATUS_CANCELED
|
||||
self.order.save()
|
||||
self.client.get(
|
||||
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
|
||||
)
|
||||
self.order = Order.objects.get(id=self.order.id)
|
||||
assert self.order.status == Order.STATUS_REFUNDED
|
||||
assert self.order.status == Order.STATUS_CANCELED
|
||||
|
||||
def test_orders_modify_attendee_optional(self):
|
||||
self.event.settings.set('attendee_names_asked', True)
|
||||
@@ -169,6 +179,8 @@ class OrdersTest(TestCase):
|
||||
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret))
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertEqual(len(doc.select('input[name=%s-attendee_name_parts_0]' % self.ticket_pos.id)), 1)
|
||||
assert "Peter" in response.rendered_content
|
||||
assert "Lukas" not in response.rendered_content
|
||||
|
||||
# Not all required fields filled out, expect failure
|
||||
response = self.client.post(
|
||||
|
||||
Reference in New Issue
Block a user