OrderChangeManager: add_position() returns a handle to the newly created position (#5557)

* OrderChangeManager: Add support for custom operations

* OrderChangeManager: Add callback to AddPosition operation

This is also meant as a way to fix #5548

* Refs #5557: Checkstyle fix

* Refs #5557: Added tests

* Refs #5557: Changes requested in the PR review

* Refs #5557: Fix error in previous merge conflict

* Refs #5557: PR review
This commit is contained in:
Luca Sorace "Stranck
2026-01-05 17:34:53 +01:00
committed by GitHub
parent 59c09e27fd
commit 8cc12fa1c7
2 changed files with 29 additions and 3 deletions

View File

@@ -1630,7 +1630,7 @@ class OrderChangeManager:
MembershipOperation = namedtuple('MembershipOperation', ('position', 'membership')) MembershipOperation = namedtuple('MembershipOperation', ('position', 'membership'))
CancelOperation = namedtuple('CancelOperation', ('position', 'price_diff')) CancelOperation = namedtuple('CancelOperation', ('position', 'price_diff'))
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat', 'membership', AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat', 'membership',
'valid_from', 'valid_until', 'is_bundled')) 'valid_from', 'valid_until', 'is_bundled', 'result'))
SplitOperation = namedtuple('SplitOperation', ('position',)) SplitOperation = namedtuple('SplitOperation', ('position',))
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff')) FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff'))
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff')) AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
@@ -1642,6 +1642,18 @@ class OrderChangeManager:
AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked')) AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
RemoveBlockOperation = namedtuple('RemoveBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked')) RemoveBlockOperation = namedtuple('RemoveBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
class AddPositionResult:
_position: Optional[OrderPosition]
def __init__(self):
self._position = None
@property
def position(self) -> OrderPosition:
if self._position is None:
raise RuntimeError("Order position has not been created yet. Call commit() first on OrderChangeManager.")
return self._position
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True, allow_blocked_seats=False): def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True, allow_blocked_seats=False):
self.order = order self.order = order
self.user = user self.user = user
@@ -1846,7 +1858,7 @@ class OrderChangeManager:
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: OrderPosition = None, def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: OrderPosition = None,
subevent: SubEvent = None, seat: Seat = None, membership: Membership = None, subevent: SubEvent = None, seat: Seat = None, membership: Membership = None,
valid_from: datetime = None, valid_until: datetime = None): valid_from: datetime = None, valid_until: datetime = None) -> 'OrderChangeManager.AddPositionResult':
if isinstance(seat, str): if isinstance(seat, str):
if not seat: if not seat:
seat = None seat = None
@@ -1905,8 +1917,11 @@ class OrderChangeManager:
self._quotadiff.update(new_quotas) self._quotadiff.update(new_quotas)
if seat: if seat:
self._seatdiff.update([seat]) self._seatdiff.update([seat])
result = self.AddPositionResult()
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent, seat, membership, self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent, seat, membership,
valid_from, valid_until, is_bundled)) valid_from, valid_until, is_bundled, result))
return result
def split(self, position: OrderPosition): def split(self, position: OrderPosition):
if self.order.event.settings.invoice_include_free or position.price != Decimal('0.00'): if self.order.event.settings.invoice_include_free or position.price != Decimal('0.00'):
@@ -2525,6 +2540,7 @@ class OrderChangeManager:
'valid_from': op.valid_from.isoformat() if op.valid_from else None, 'valid_from': op.valid_from.isoformat() if op.valid_from else None,
'valid_until': op.valid_until.isoformat() if op.valid_until else None, 'valid_until': op.valid_until.isoformat() if op.valid_until else None,
}) })
op.result._position = pos
elif isinstance(op, self.SplitOperation): elif isinstance(op, self.SplitOperation):
position = position_cache.setdefault(op.position.pk, op.position) position = position_cache.setdefault(op.position.pk, op.position)
split_positions.append(position) split_positions.append(position)

View File

@@ -2465,6 +2465,16 @@ class OrderChangeManagerTests(TestCase):
assert nop.price == Decimal('12.00') assert nop.price == Decimal('12.00')
assert nop.subevent == se1 assert nop.subevent == se1
@classscope(attr='o')
def test_add_item_result_value(self):
res_shirt = self.ocm.add_position(self.shirt, None, None, None)
res_ticket2 = self.ocm.add_position(self.ticket2, None, None, None)
with self.assertRaises(RuntimeError):
_ = res_ticket2.position
self.ocm.commit()
assert res_shirt.position.item == self.shirt
assert res_ticket2.position.item == self.ticket2
@classscope(attr='o') @classscope(attr='o')
def test_add_item_with_rounding(self): def test_add_item_with_rounding(self):
self.order.tax_rounding_mode = "sum_by_net" self.order.tax_rounding_mode = "sum_by_net"