OrderChangeManager: Re-use same instances of OrderPosition/OrderFee

This commit is contained in:
Raphael Michel
2025-04-29 12:38:41 +02:00
committed by GitHub
parent d3792935ae
commit e1027e3e8c
2 changed files with 174 additions and 130 deletions

View File

@@ -2221,73 +2221,79 @@ class OrderChangeManager:
nextposid = self.order.all_positions.aggregate(m=Max('positionid'))['m'] + 1
split_positions = []
secret_dirty = set()
position_cache = {}
fee_cache = {}
for op in self._operations:
if isinstance(op, self.ItemOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.item', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'old_item': op.position.item.pk,
'old_variation': op.position.variation.pk if op.position.variation else None,
'position': position.pk,
'positionid': position.positionid,
'old_item': position.item.pk,
'old_variation': position.variation.pk if position.variation else None,
'new_item': op.item.pk,
'new_variation': op.variation.pk if op.variation else None,
'old_price': op.position.price,
'addon_to': op.position.addon_to_id,
'new_price': op.position.price
'old_price': position.price,
'addon_to': position.addon_to_id,
'new_price': position.price
})
op.position.item = op.item
op.position.variation = op.variation
op.position._calculate_tax()
position.item = op.item
position.variation = op.variation
position._calculate_tax()
if op.position.voucher_budget_use is not None and op.position.voucher and not op.position.addon_to_id:
listed_price = get_listed_price(op.position.item, op.position.variation, op.position.subevent)
if not op.position.item.tax_rule or op.position.item.tax_rule.price_includes_tax:
price_after_voucher = max(op.position.price, op.position.voucher.calculate_price(listed_price))
if position.voucher_budget_use is not None and position.voucher and not position.addon_to_id:
listed_price = get_listed_price(position.item, position.variation, position.subevent)
if not position.item.tax_rule or position.item.tax_rule.price_includes_tax:
price_after_voucher = max(position.price, position.voucher.calculate_price(listed_price))
else:
price_after_voucher = max(op.position.price - op.position.tax_value, op.position.voucher.calculate_price(listed_price))
op.position.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
secret_dirty.add(op.position)
op.position.save()
price_after_voucher = max(position.price - position.tax_value, position.voucher.calculate_price(listed_price))
position.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
secret_dirty.add(position)
position.save()
elif isinstance(op, self.MembershipOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.membership', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'old_membership_id': op.position.used_membership_id,
'position': position.pk,
'positionid': position.positionid,
'old_membership_id': position.used_membership_id,
'new_membership_id': op.membership.pk if op.membership else None,
})
op.position.used_membership = op.membership
op.position.save()
position.used_membership = op.membership
position.save()
elif isinstance(op, self.SeatOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.seat', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'old_seat': op.position.seat.name if op.position.seat else "-",
'position': position.pk,
'positionid': position.positionid,
'old_seat': position.seat.name if position.seat else "-",
'new_seat': op.seat.name if op.seat else "-",
'old_seat_id': op.position.seat.pk if op.position.seat else None,
'old_seat_id': position.seat.pk if position.seat else None,
'new_seat_id': op.seat.pk if op.seat else None,
})
op.position.seat = op.seat
secret_dirty.add(op.position)
op.position.save()
position.seat = op.seat
secret_dirty.add(position)
position.save()
elif isinstance(op, self.SubeventOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.subevent', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'old_subevent': op.position.subevent.pk,
'position': position.pk,
'positionid': position.positionid,
'old_subevent': position.subevent.pk,
'new_subevent': op.subevent.pk,
'old_price': op.position.price,
'new_price': op.position.price
'old_price': position.price,
'new_price': position.price
})
op.position.subevent = op.subevent
secret_dirty.add(op.position)
if op.position.voucher_budget_use is not None and op.position.voucher and not op.position.addon_to_id:
listed_price = get_listed_price(op.position.item, op.position.variation, op.position.subevent)
if not op.position.item.tax_rule or op.position.item.tax_rule.price_includes_tax:
price_after_voucher = max(op.position.price, op.position.voucher.calculate_price(listed_price))
position.subevent = op.subevent
secret_dirty.add(position)
if position.voucher_budget_use is not None and position.voucher and not position.addon_to_id:
listed_price = get_listed_price(position.item, position.variation, position.subevent)
if not position.item.tax_rule or position.item.tax_rule.price_includes_tax:
price_after_voucher = max(position.price, position.voucher.calculate_price(listed_price))
else:
price_after_voucher = max(op.position.price - op.position.tax_value, op.position.voucher.calculate_price(listed_price))
op.position.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
op.position.save()
price_after_voucher = max(position.price - position.tax_value, position.voucher.calculate_price(listed_price))
position.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
position.save()
elif isinstance(op, self.AddFeeOperation):
self.order.log_action('pretix.event.order.changed.addfee', user=self.user, auth=self.auth, data={
'fee': op.fee.pk,
@@ -2296,70 +2302,79 @@ class OrderChangeManager:
op.fee._calculate_tax()
op.fee.save()
elif isinstance(op, self.FeeValueOperation):
fee = fee_cache.setdefault(op.fee.pk, op.fee)
self.order.log_action('pretix.event.order.changed.feevalue', user=self.user, auth=self.auth, data={
'fee': op.fee.pk,
'old_price': op.fee.value,
'fee': fee.pk,
'old_price': fee.value,
'new_price': op.value.gross
})
op.fee.value = op.value.gross
op.fee._calculate_tax()
op.fee.save()
fee.value = op.value.gross
fee._calculate_tax()
fee.save()
elif isinstance(op, self.PriceOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.price', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'old_price': op.position.price,
'addon_to': op.position.addon_to_id,
'position': position.pk,
'positionid': position.positionid,
'old_price': position.price,
'addon_to': position.addon_to_id,
'new_price': op.price.gross
})
op.position.price = op.price.gross
op.position.tax_rate = op.price.rate
op.position.tax_value = op.price.tax
op.position.tax_code = op.price.code
op.position.save(update_fields=['price', 'tax_rate', 'tax_value', 'tax_code'])
position.price = op.price.gross
position.tax_rate = op.price.rate
position.tax_value = op.price.tax
position.tax_code = op.price.code
position.save(update_fields=['price', 'tax_rate', 'tax_value', 'tax_code'])
elif isinstance(op, self.TaxRuleOperation):
if isinstance(op.position, OrderPosition):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.tax_rule', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'addon_to': op.position.addon_to_id,
'old_taxrule': op.position.tax_rule.pk if op.position.tax_rule else None,
'position': position.pk,
'positionid': position.positionid,
'addon_to': position.addon_to_id,
'old_taxrule': position.tax_rule.pk if position.tax_rule else None,
'new_taxrule': op.tax_rule.pk
})
position._calculate_tax(op.tax_rule)
position.save()
elif isinstance(op.position, OrderFee):
fee = fee_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.tax_rule', user=self.user, auth=self.auth, data={
'fee': op.position.pk,
'fee_type': op.position.fee_type,
'old_taxrule': op.position.tax_rule.pk if op.position.tax_rule else None,
'fee': fee.pk,
'fee_type': fee.fee_type,
'old_taxrule': fee.tax_rule.pk if fee.tax_rule else None,
'new_taxrule': op.tax_rule.pk
})
op.position._calculate_tax(op.tax_rule)
op.position.save()
fee._calculate_tax(op.tax_rule)
fee.save()
elif isinstance(op, self.CancelFeeOperation):
fee = fee_cache.setdefault(op.fee.pk, op.fee)
self.order.log_action('pretix.event.order.changed.cancelfee', user=self.user, auth=self.auth, data={
'fee': op.fee.pk,
'fee_type': op.fee.fee_type,
'old_price': op.fee.value,
'fee': fee.pk,
'fee_type': fee.fee_type,
'old_price': fee.value,
})
op.fee.canceled = True
op.fee.save(update_fields=['canceled'])
fee.canceled = True
fee.save(update_fields=['canceled'])
elif isinstance(op, self.CancelOperation):
for gc in op.position.issued_gift_cards.all():
position = position_cache.setdefault(op.position.pk, op.position)
for gc in position.issued_gift_cards.all():
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
if gc.value < op.position.price:
if gc.value < 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=-op.position.price, order=self.order, acceptor=self.order.event.organizer)
gc.transactions.create(value=-position.price, order=self.order, acceptor=self.order.event.organizer)
for m in op.position.granted_memberships.with_usages().all():
for m in position.granted_memberships.with_usages().all():
m.canceled = True
m.save()
for opa in op.position.addons.all():
for opa in position.addons.all():
opa = position_cache.setdefault(opa.pk, opa)
for gc in opa.issued_gift_cards.all():
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
if gc.value < opa.position.price:
@@ -2393,22 +2408,22 @@ class OrderChangeManager:
)
opa.save(update_fields=['canceled', 'secret'])
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'old_item': op.position.item.pk,
'old_variation': op.position.variation.pk if op.position.variation else None,
'old_price': op.position.price,
'position': position.pk,
'positionid': position.positionid,
'old_item': position.item.pk,
'old_variation': position.variation.pk if position.variation else None,
'old_price': position.price,
'addon_to': None,
})
op.position.canceled = True
if op.position.voucher:
Voucher.objects.filter(pk=op.position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
position.canceled = True
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
assign_ticket_secret(
event=self.event, position=op.position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
event=self.event, position=position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
)
if op.position in secret_dirty:
secret_dirty.remove(op.position)
op.position.save(update_fields=['canceled', 'secret'])
if position in secret_dirty:
secret_dirty.remove(position)
position.save(update_fields=['canceled', 'secret'])
elif isinstance(op, self.AddOperation):
pos = OrderPosition.objects.create(
item=op.item, variation=op.variation, addon_to=op.addon_to,
@@ -2433,20 +2448,22 @@ class OrderChangeManager:
'valid_until': op.valid_until.isoformat() if op.valid_until else None,
})
elif isinstance(op, self.SplitOperation):
split_positions.append(op.position)
position = position_cache.setdefault(op.position.pk, op.position)
split_positions.append(position)
elif isinstance(op, self.RegenerateSecretOperation):
op.position.web_secret = generate_secret()
op.position.save(update_fields=["web_secret"])
position = position_cache.setdefault(op.position.pk, op.position)
position.web_secret = generate_secret()
position.save(update_fields=["web_secret"])
assign_ticket_secret(
event=self.event, position=op.position, force_invalidate=True, save=True
event=self.event, position=position, force_invalidate=True, save=True
)
if op.position in secret_dirty:
secret_dirty.remove(op.position)
if position in secret_dirty:
secret_dirty.remove(position)
tickets.invalidate_cache.apply_async(kwargs={'event': self.event.pk,
'order': self.order.pk})
self.order.log_action('pretix.event.order.changed.secret', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'position': position.pk,
'positionid': position.positionid,
})
elif isinstance(op, self.ChangeSecretOperation):
if OrderPosition.all.filter(order__event=self.event, secret=op.new_secret).exists():
@@ -2462,64 +2479,68 @@ class OrderChangeManager:
'positionid': op.position.positionid,
})
elif isinstance(op, self.ChangeValidFromOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.valid_from', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'position': position.pk,
'positionid': position.positionid,
'new_value': op.valid_from.isoformat() if op.valid_from else None,
'old_value': op.position.valid_from.isoformat() if op.position.valid_from else None,
'old_value': position.valid_from.isoformat() if position.valid_from else None,
})
op.position.valid_from = op.valid_from
op.position.save(update_fields=['valid_from'])
secret_dirty.add(op.position)
position.valid_from = op.valid_from
position.save(update_fields=['valid_from'])
secret_dirty.add(position)
elif isinstance(op, self.ChangeValidUntilOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.valid_until', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'position': position.pk,
'positionid': position.positionid,
'new_value': op.valid_until.isoformat() if op.valid_until else None,
'old_value': op.position.valid_until.isoformat() if op.position.valid_until else None,
'old_value': position.valid_until.isoformat() if position.valid_until else None,
})
op.position.valid_until = op.valid_until
op.position.save(update_fields=['valid_until'])
secret_dirty.add(op.position)
position.valid_until = op.valid_until
position.save(update_fields=['valid_until'])
secret_dirty.add(position)
elif isinstance(op, self.AddBlockOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.add_block', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'position': position.pk,
'positionid': position.positionid,
'block_name': op.block_name,
})
if op.position.blocked:
if op.block_name not in op.position.blocked:
op.position.blocked = op.position.blocked + [op.block_name]
if position.blocked:
if op.block_name not in position.blocked:
position.blocked = position.blocked + [op.block_name]
else:
op.position.blocked = [op.block_name]
position.blocked = [op.block_name]
if op.ignore_from_quota_while_blocked is not None:
op.position.ignore_from_quota_while_blocked = op.ignore_from_quota_while_blocked
op.position.save(update_fields=['blocked', 'ignore_from_quota_while_blocked'])
if op.position.blocked:
op.position.blocked_secrets.update_or_create(
position.ignore_from_quota_while_blocked = op.ignore_from_quota_while_blocked
position.save(update_fields=['blocked', 'ignore_from_quota_while_blocked'])
if position.blocked:
position.blocked_secrets.update_or_create(
event=self.event,
secret=op.position.secret,
secret=position.secret,
defaults={
'blocked': True,
'updated': now(),
}
)
elif isinstance(op, self.RemoveBlockOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.remove_block', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'position': position.pk,
'positionid': position.positionid,
'block_name': op.block_name,
})
if op.position.blocked and op.block_name in op.position.blocked:
op.position.blocked = [b for b in op.position.blocked if b != op.block_name]
if not op.position.blocked:
op.position.blocked = None
if position.blocked and op.block_name in position.blocked:
position.blocked = [b for b in position.blocked if b != op.block_name]
if not position.blocked:
position.blocked = None
if op.ignore_from_quota_while_blocked is not None:
op.position.ignore_from_quota_while_blocked = op.ignore_from_quota_while_blocked
op.position.save(update_fields=['blocked', 'ignore_from_quota_while_blocked'])
if not op.position.blocked:
position.ignore_from_quota_while_blocked = op.ignore_from_quota_while_blocked
position.save(update_fields=['blocked', 'ignore_from_quota_while_blocked'])
if not position.blocked:
try:
bs = op.position.blocked_secrets.get(secret=op.position.secret)
bs = position.blocked_secrets.get(secret=position.secret)
bs.blocked = False
bs.save()
except BlockedTicketSecret.DoesNotExist:

View File

@@ -1734,6 +1734,29 @@ class OrderChangeManagerTests(TestCase):
with self.assertRaises(OrderError):
self.ocm.change_price(self.op1, 25)
@classscope(attr='o')
def test_cancel_and_change_addon(self):
se1 = self.event.subevents.create(name="Foo", date_from=now())
se2 = self.event.subevents.create(name="Bar", date_from=now())
self.op1.subevent = se1
self.op1.save()
self.op2.subevent = se1
self.op2.save()
self.quota.subevent = se2
self.quota.save()
op3 = OrderPosition.objects.create(
order=self.order, item=self.ticket, variation=None, addon_to=self.op1,
price=Decimal("0.00"), positionid=3, subevent=se1,
)
self.ocm.cancel(self.op1)
self.ocm.change_subevent(op3, se2)
self.ocm.commit()
# Expected: the addon is also canceled
# Bug we had: the addon is not canceled
op3.refresh_from_db()
assert op3.canceled
@classscope(attr='o')
def test_cancel_all_in_order(self):
self.ocm.cancel(self.op1)