def post(self, *args, **kwargs): was_paid = self.order.status == Order.STATUS_PAID ocm = OrderChangeManager( self.order, user=self.request.user, notify=True, reissue_invoice=True, ) form_valid = self._process_change(ocm) if not form_valid: messages.error(self.request, _('An error occurred. Please see the details below.')) else: try: ocm.commit(check_quotas=True) except OrderError as e: messages.error(self.request, str(e)) else: if self.order.status != Order.STATUS_PAID and was_paid: messages.success(self.request, _('The order has been changed. You can now proceed by paying the open amount of {amount}.').format( amount=money_filter(self.order.pending_sum, self.request.event.currency) )) return redirect(eventreverse(self.request.event, 'presale:event.order.pay.change', kwargs={ 'order': self.order.code, 'secret': self.order.secret })) else: messages.success(self.request, _('The order has been changed.')) return redirect(self.get_order_url()) return self.get(*args, **kwargs)
def setUp(self): super().setUp() o = Organizer.objects.create(name='Dummy', slug='dummy') self.event = Event.objects.create(organizer=o, name='Dummy', slug='dummy', date_from=now(), plugins='pretix.plugins.banktransfer') self.order = Order.objects.create( code='FOO', event=self.event, email='*****@*****.**', status=Order.STATUS_PENDING, locale='en', datetime=now(), expires=now() + timedelta(days=10), total=Decimal('46.00'), payment_provider='banktransfer' ) self.tr7 = self.event.tax_rules.create(rate=Decimal('7.00')) self.tr19 = self.event.tax_rules.create(rate=Decimal('19.00')) self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket', tax_rule=self.tr7, default_price=Decimal('23.00'), admission=True) self.ticket2 = Item.objects.create(event=self.event, name='Other ticket', tax_rule=self.tr7, default_price=Decimal('23.00'), admission=True) self.shirt = Item.objects.create(event=self.event, name='T-Shirt', tax_rule=self.tr19, default_price=Decimal('12.00')) self.op1 = OrderPosition.objects.create( order=self.order, item=self.ticket, variation=None, price=Decimal("23.00"), attendee_name="Peter", positionid=1 ) self.op2 = OrderPosition.objects.create( order=self.order, item=self.ticket, variation=None, price=Decimal("23.00"), attendee_name="Dieter", positionid=2 ) self.ocm = OrderChangeManager(self.order, None) self.quota = self.event.quotas.create(name='Test', size=None) self.quota.items.add(self.ticket) self.quota.items.add(self.ticket2) self.quota.items.add(self.shirt)
def post(self, *args, **kwargs): notify = self.other_form.cleaned_data[ 'notify'] if self.other_form.is_valid() else True ocm = OrderChangeManager(self.order, user=self.request.user, notify=notify) form_valid = self._process_add(ocm) and self._process_change( ocm) and self._process_other(ocm) if not form_valid: messages.error( self.request, _('An error occurred. Please see the details below.')) else: try: ocm.commit() except OrderError as e: messages.error(self.request, str(e)) else: if notify: messages.success( self.request, _('The order has been changed and the user has been notified.' )) else: messages.success(self.request, _('The order has been changed.')) return self._redirect_back() return self.get(*args, **kwargs)
def test_split_reverse_charge(self): ia = self._enable_reverse_charge() # Set payment fees self.event.settings.set('tax_rate_default', self.tr19.pk) prov = self.ocm._get_payment_provider() prov.settings.set('_fee_percent', Decimal('2.00')) prov.settings.set('_fee_reverse_calc', False) self.ocm.recalculate_taxes() self.ocm.commit() self.ocm = OrderChangeManager(self.order, None) self.order.refresh_from_db() # Check if reverse charge is active assert self.order.total == Decimal('43.86') fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT) assert fee.value == Decimal('0.86') assert fee.tax_rate == Decimal('0.00') self.op1.refresh_from_db() self.op2.refresh_from_db() assert self.op1.price == Decimal('21.50') assert self.op2.price == Decimal('21.50') # Split self.ocm.split(self.op2) self.ocm.commit() self.order.refresh_from_db() self.op2.refresh_from_db() # First order assert self.order.total == Decimal('21.93') fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT) assert fee.value == Decimal('0.43') assert fee.tax_rate == Decimal('0.00') assert fee.tax_value == Decimal('0.00') assert self.order.positions.count() == 1 assert self.order.fees.count() == 1 assert self.order.positions.first().price == Decimal('21.50') assert self.order.positions.first().tax_value == Decimal('0.00') # New order assert self.op2.order != self.order o2 = self.op2.order assert o2.total == Decimal('21.93') fee = o2.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT) assert fee.value == Decimal('0.43') assert fee.tax_rate == Decimal('0.00') assert fee.tax_value == Decimal('0.00') assert o2.positions.count() == 1 assert o2.positions.first().price == Decimal('21.50') assert o2.positions.first().tax_value == Decimal('0.00') assert o2.fees.count() == 1 ia = InvoiceAddress.objects.get(pk=ia.pk) assert o2.invoice_address != ia assert o2.invoice_address.vat_id_validated is True
def post(self, *args, **kwargs): ocm = OrderChangeManager(self.order, self.request.user) form_valid = True for p in self.positions: if not p.form.is_valid(): form_valid = False break try: if p.form.cleaned_data['operation'] == 'product': if '-' in p.form.cleaned_data['itemvar']: itemid, varid = p.form.cleaned_data['itemvar'].split( '-') else: itemid, varid = p.form.cleaned_data['itemvar'], None item = Item.objects.get(pk=itemid, event=self.request.event) if varid: variation = ItemVariation.objects.get(pk=varid, item=item) else: variation = None ocm.change_item(p, item, variation) elif p.form.cleaned_data['operation'] == 'price': ocm.change_price(p, p.form.cleaned_data['price']) elif p.form.cleaned_data['operation'] == 'cancel': ocm.cancel(p) except OrderError as e: p.custom_error = str(e) form_valid = False break if not form_valid: messages.error( self.request, _('An error occured. Please see the details below.')) else: try: ocm.commit() except OrderError as e: messages.error(self.request, str(e)) else: messages.success( self.request, _('The order has been changed and the user has been notified.' )) return self._redirect_back() return self.get(*args, **kwargs)
def test_pending_free_order_stays_pending(self): self.event.settings.set('tax_rate_default', self.tr19.pk) self.ocm.change_price(self.op1, Decimal('0.00')) self.ocm.change_price(self.op2, Decimal('0.00')) self.ocm.commit() self.ocm = OrderChangeManager(self.order, None) self.order.refresh_from_db() assert self.order.total == Decimal('0.00') assert self.order.status == Order.STATUS_PAID self.order.status = Order.STATUS_PENDING self.ocm.cancel(self.op2) self.ocm.commit() self.order.refresh_from_db() assert self.order.status == Order.STATUS_PENDING
def perform_destroy(self, instance): try: ocm = OrderChangeManager( instance.order, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth, notify=False ) ocm.cancel(instance) ocm.commit() except OrderError as e: raise ValidationError(str(e)) except Quota.QuotaExceededException as e: raise ValidationError(str(e))
def test_split_to_new_free(self): self.ocm.change_price(self.op2, Decimal('0.00')) self.ocm.commit() self.ocm = OrderChangeManager(self.order, None) self.op2.refresh_from_db() self.ocm.split(self.op2) self.ocm.commit() self.order.refresh_from_db() self.op2.refresh_from_db() o2 = self.op2.order assert self.order.total == Decimal('23.00') assert self.order.status == Order.STATUS_PENDING assert o2.total == Decimal('0.00') assert o2.status == Order.STATUS_PAID
def post(self, *args, **kwargs): ocm = OrderChangeManager(self.order, self.request.user) form_valid = self._process_add(ocm) and self._process_change(ocm) if not form_valid: messages.error(self.request, _('An error occured. Please see the details below.')) else: try: ocm.commit() except OrderError as e: messages.error(self.request, str(e)) else: messages.success(self.request, _('The order has been changed and the user has been notified.')) return self._redirect_back() return self.get(*args, **kwargs)
def test_recalculate_reverse_charge(self): self.event.settings.set('tax_rate_default', self.tr19.pk) prov = self.ocm._get_payment_provider() prov.settings.set('_fee_abs', Decimal('0.30')) self.ocm._recalculate_total_and_payment_fee() assert self.order.total == Decimal('46.30') fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT) assert fee.value == prov.calculate_fee(self.order.total) assert fee.tax_rate == Decimal('19.00') assert fee.tax_value == Decimal('0.05') self.ocm = OrderChangeManager(self.order, None) ia = self._enable_reverse_charge() self.ocm.recalculate_taxes() self.ocm.commit() ops = list(self.order.positions.all()) for op in ops: assert op.price == Decimal('21.50') assert op.tax_value == Decimal('0.00') assert op.tax_rate == Decimal('0.00') assert self.order.total == Decimal('43.30') fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT) assert fee.value == prov.calculate_fee(self.order.total) assert fee.tax_rate == Decimal('0.00') assert fee.tax_value == Decimal('0.00') ia.vat_id_validated = False ia.save() self.ocm = OrderChangeManager(self.order, None) self.ocm.recalculate_taxes() self.ocm.commit() ops = list(self.order.positions.all()) for op in ops: assert op.price == Decimal('23.01') # sic. we can't really avoid it. assert op.tax_value == Decimal('1.51') assert op.tax_rate == Decimal('7.00') assert self.order.total == Decimal('46.32') fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT) assert fee.value == prov.calculate_fee(self.order.total) assert fee.tax_rate == Decimal('19.00') assert fee.tax_value == Decimal('0.05')
def test_split_to_free_invoice(self): self.event.settings.invoice_include_free = False self.ocm.change_price(self.op2, Decimal('0.00')) self.ocm.commit() self.ocm = OrderChangeManager(self.order, None) self.op2.refresh_from_db() self.ocm._invoice_dirty = False generate_invoice(self.order) assert self.order.invoices.count() == 1 assert self.order.invoices.last().lines.count() == 1 self.ocm.split(self.op2) self.ocm.commit() self.order.refresh_from_db() self.op2.refresh_from_db() o2 = self.op2.order assert self.order.invoices.count() == 1 assert self.order.invoices.last().lines.count() == 1 assert o2.invoices.count() == 0
def test_split_paid_payment_fees(self): # Set payment fees self.event.settings.set('tax_rate_default', self.tr19.pk) prov = self.ocm._get_payment_provider() prov.settings.set('_fee_percent', Decimal('2.00')) prov.settings.set('_fee_abs', Decimal('1.00')) prov.settings.set('_fee_reverse_calc', False) self.ocm.change_price(self.op1, Decimal('23.00')) self.ocm.commit() self.ocm = OrderChangeManager(self.order, None) self.order.refresh_from_db() assert self.order.total == Decimal('47.92') fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT) assert fee.value == Decimal('1.92') assert fee.tax_rate == Decimal('19.00') self.order.status = Order.STATUS_PAID self.order.save() payment = self.order.payments.first() payment.state = OrderPayment.PAYMENT_STATE_CONFIRMED payment.save() # Split self.ocm.split(self.op2) self.ocm.commit() self.order.refresh_from_db() self.op2.refresh_from_db() # First order assert self.order.total == Decimal('24.92') fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT) assert fee.value == Decimal('1.92') assert fee.tax_rate == Decimal('19.00') assert self.order.positions.count() == 1 assert self.order.fees.count() == 1 # New order assert self.op2.order != self.order o2 = self.op2.order assert o2.total == Decimal('23.00') assert o2.fees.count() == 0
def test_reverse_charge_foreign_currency_disabvled(env): event, order = env event.settings.invoice_eu_currencies = False tr = event.tax_rules.first() tr.eu_reverse_charge = True tr.home_country = Country('DE') tr.save() event.settings.set('invoice_language', 'en') InvoiceAddress.objects.create(company='Acme Company', street='221B Baker Street', zipcode='12345', city='Warsaw', country=Country('PL'), vat_id='PL123456780', vat_id_validated=True, order=order, is_business=True) ocm = OrderChangeManager(order, None) ocm.recalculate_taxes() ocm.commit() assert not order.positions.filter(tax_value__gt=0).exists() inv = generate_invoice(order) assert "reverse charge" in inv.additional_text.lower() assert inv.foreign_currency_rate is None assert inv.foreign_currency_rate_date is None
def test_custom_tax_note(env): event, order = env tr = event.tax_rules.first() tr.eu_reverse_charge = True tr.home_country = Country('DE') tr.custom_rules = json.dumps([{ 'country': 'PL', 'address_type': '', 'action': 'vat', 'rate': '20', 'invoice_text': { 'de': 'Polnische Steuer anwendbar', 'en': 'Polish tax applies' } }]) tr.save() event.settings.set('invoice_language', 'en') InvoiceAddress.objects.create(company='Acme Company', street='221B Baker Street', zipcode='12345', city='Warsaw', country=Country('PL'), vat_id='PL123456780', vat_id_validated=True, order=order, is_business=True) ocm = OrderChangeManager(order, None) ocm.recalculate_taxes() ocm.commit() inv = generate_invoice(order) assert "Polish tax applies" in inv.additional_text
def post(self, *args, **kwargs): ocm = OrderChangeManager(self.order, self.request.user) form_valid = True for p in self.positions: if not p.form.is_valid(): form_valid = False break try: if p.form.cleaned_data['operation'] == 'product': if '-' in p.form.cleaned_data['itemvar']: itemid, varid = p.form.cleaned_data['itemvar'].split('-') else: itemid, varid = p.form.cleaned_data['itemvar'], None item = Item.objects.get(pk=itemid, event=self.request.event) if varid: variation = ItemVariation.objects.get(pk=varid, item=item) else: variation = None ocm.change_item(p, item, variation) elif p.form.cleaned_data['operation'] == 'price': ocm.change_price(p, p.form.cleaned_data['price']) elif p.form.cleaned_data['operation'] == 'cancel': ocm.cancel(p) except OrderError as e: p.custom_error = str(e) form_valid = False break if not form_valid: messages.error(self.request, _('An error occured. Please see the details below.')) else: try: ocm.commit() except OrderError as e: messages.error(self.request, str(e)) else: messages.success(self.request, _('The order has been changed and the user has been notified.')) return self._redirect_back() return self.get(*args, **kwargs)
def test_reverse_charge_note(env): event, order = env tr = event.tax_rules.first() tr.eu_reverse_charge = True tr.home_country = Country('DE') tr.save() event.settings.set('invoice_language', 'en') InvoiceAddress.objects.create(company='Acme Company', street='221B Baker Street', zipcode='12345', city='Warsaw', country=Country('PL'), vat_id='PL123456780', vat_id_validated=True, order=order, is_business=True) ocm = OrderChangeManager(order, None) ocm.recalculate_taxes() ocm.commit() assert not order.positions.filter(tax_value__gt=0).exists() inv = generate_invoice(order) assert "reverse charge" in inv.additional_text.lower() assert inv.foreign_currency_display == "PLN" assert inv.foreign_currency_rate == Decimal("4.2408") assert inv.foreign_currency_rate_date == date.today()
def perform_destroy(self, instance): try: ocm = OrderChangeManager( instance.order, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth, notify=False) ocm.cancel(instance) ocm.commit() except OrderError as e: raise ValidationError(str(e)) except Quota.QuotaExceededException as e: raise ValidationError(str(e))
def cancel_for(self, other): """Called when an order is marked as paid. Makes sure that item, variation and subevent match before calling this method. """ if not self.event.settings.cancel_orderpositions: raise Exception( "Order position canceling is currently not allowed") if (self.position.subevent != other.subevent or self.position.item != other.item or self.position.variation != other.variation): raise Exception("Cancelation failed, orders are not equal") if not can_be_canceled(self.event, self.position.item, self.position.subevent): raise Exception("Cancelation failed, currently not allowed") # Make sure AGAIN that the state is alright, because timings self.refresh_from_db() if not self.state == self.States.REQUESTED: raise Exception("Not in 'requesting' state.") if self.position.price > other.price: raise Exception("Cannot cancel for a cheaper product.") try: change_manager = OrderChangeManager(order=self.position.order) change_manager.cancel(position=self.position) change_manager.commit() except OrderError: # Let's hope this order error is because we're trying to empty the order cancel_order( self.position.order.pk, cancellation_fee=self.event.settings.swap_cancellation_fee, try_auto_refund=True, ) self.state = self.States.COMPLETED self.target_order = other.order # Should be set already, let's just make sure self.save() self.position.order.log_action( "pretix_swap.cancelation.complete", data={ "position": self.position.pk, "positionid": self.position.positionid, "other_position": other.pk, "other_positionid": other.positionid, "other_order": other.order.code, }, )
def setUp(self): super().setUp() o = Organizer.objects.create(name='Dummy', slug='dummy') self.event = Event.objects.create(organizer=o, name='Dummy', slug='dummy', date_from=now(), plugins='pretix.plugins.banktransfer') self.order = Order.objects.create( code='FOO', event=self.event, email='*****@*****.**', status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10), total=Decimal('46.00'), payment_provider='banktransfer' ) self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket', tax_rate=Decimal('7.00'), default_price=Decimal('23.00'), admission=True) self.shirt = Item.objects.create(event=self.event, name='T-Shirt', tax_rate=Decimal('19.00'), default_price=Decimal('12.00')) self.op1 = OrderPosition.objects.create( order=self.order, item=self.ticket, variation=None, price=Decimal("23.00"), attendee_name="Peter" ) self.op2 = OrderPosition.objects.create( order=self.order, item=self.ticket, variation=None, price=Decimal("23.00"), attendee_name="Dieter" ) self.ocm = OrderChangeManager(self.order, None)
class OrderChangeManagerTests(TestCase): def setUp(self): super().setUp() o = Organizer.objects.create(name='Dummy', slug='dummy') self.event = Event.objects.create(organizer=o, name='Dummy', slug='dummy', date_from=now(), plugins='pretix.plugins.banktransfer') self.order = Order.objects.create( code='FOO', event=self.event, email='*****@*****.**', status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10), total=Decimal('46.00'), payment_provider='banktransfer' ) self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket', tax_rate=Decimal('7.00'), default_price=Decimal('23.00'), admission=True) self.shirt = Item.objects.create(event=self.event, name='T-Shirt', tax_rate=Decimal('19.00'), default_price=Decimal('12.00')) self.op1 = OrderPosition.objects.create( order=self.order, item=self.ticket, variation=None, price=Decimal("23.00"), attendee_name="Peter" ) self.op2 = OrderPosition.objects.create( order=self.order, item=self.ticket, variation=None, price=Decimal("23.00"), attendee_name="Dieter" ) self.ocm = OrderChangeManager(self.order, None) def test_change_item_success(self): self.ocm.change_item(self.op1, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() self.order.refresh_from_db() assert self.op1.item == self.shirt assert self.op1.price == self.shirt.default_price assert self.op1.tax_rate == self.shirt.tax_rate assert round_decimal(self.op1.price * (1 - 100 / (100 + self.op1.tax_rate))) == self.op1.tax_value assert self.order.total == self.op1.price + self.op2.price def test_change_price_success(self): self.ocm.change_price(self.op1, Decimal('24.00')) self.ocm.commit() self.op1.refresh_from_db() self.order.refresh_from_db() assert self.op1.item == self.ticket assert self.op1.price == Decimal('24.00') assert round_decimal(self.op1.price * (1 - 100 / (100 + self.op1.tax_rate))) == self.op1.tax_value assert self.order.total == self.op1.price + self.op2.price def test_cancel_success(self): 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 def test_free_to_paid(self): self.op1.price = Decimal('0.00') self.op1.save() self.op2.delete() self.order.total = Decimal('0.00') self.order.save() self.ocm.change_price(self.op1, Decimal('24.00')) with self.assertRaises(OrderError): self.ocm.commit() self.op1.refresh_from_db() assert self.op1.price == Decimal('0.00') def test_cancel_all_in_order(self): self.ocm.cancel(self.op1) self.ocm.cancel(self.op2) with self.assertRaises(OrderError): self.ocm.commit() assert self.order.positions.count() == 2 def test_empty(self): self.ocm.commit() def test_quota_unlimited(self): q = self.event.quotas.create(name='Test', size=None) q.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.shirt def test_quota_full(self): q = self.event.quotas.create(name='Test', size=0) q.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) with self.assertRaises(OrderError): self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.ticket def test_quota_full_but_in_same(self): q = self.event.quotas.create(name='Test', size=0) q.items.add(self.shirt) q.items.add(self.ticket) self.ocm.change_item(self.op1, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.shirt def test_multiple_quotas_shared_full(self): q1 = self.event.quotas.create(name='Test', size=0) q2 = self.event.quotas.create(name='Test', size=2) q1.items.add(self.shirt) q1.items.add(self.ticket) q2.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.shirt def test_multiple_quotas_unshared_full(self): q1 = self.event.quotas.create(name='Test', size=2) q2 = self.event.quotas.create(name='Test', size=0) q1.items.add(self.shirt) q1.items.add(self.ticket) q2.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) with self.assertRaises(OrderError): self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.ticket def test_multiple_items_success(self): q1 = self.event.quotas.create(name='Test', size=2) q1.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) self.ocm.change_item(self.op2, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() self.op2.refresh_from_db() assert self.op1.item == self.shirt assert self.op2.item == self.shirt def test_multiple_items_quotas_partially_full(self): q1 = self.event.quotas.create(name='Test', size=1) q1.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) self.ocm.change_item(self.op2, self.shirt, None) with self.assertRaises(OrderError): self.ocm.commit() self.op1.refresh_from_db() self.op2.refresh_from_db() assert self.op1.item == self.ticket assert self.op2.item == self.ticket def test_payment_fee_calculation(self): self.event.settings.set('tax_rate_default', Decimal('19.00')) prov = self.ocm._get_payment_provider() prov.settings.set('_fee_abs', Decimal('0.30')) self.ocm.change_price(self.op1, Decimal('24.00')) self.ocm.commit() self.order.refresh_from_db() assert self.order.total == Decimal('47.30') assert self.order.payment_fee == prov.calculate_fee(self.order.total) assert self.order.payment_fee_tax_rate == Decimal('19.00') assert round_decimal(self.order.payment_fee * (1 - 100 / (100 + self.order.payment_fee_tax_rate))) == self.order.payment_fee_tax_value def test_require_pending(self): self.order.status = Order.STATUS_PAID self.order.save() self.ocm.change_item(self.op1, self.shirt, None) with self.assertRaises(OrderError): self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.ticket def test_change_price_to_free_marked_as_paid(self): self.ocm.change_price(self.op1, Decimal('0.00')) self.ocm.change_price(self.op2, Decimal('0.00')) self.ocm.commit() self.order.refresh_from_db() assert self.order.total == 0 assert self.order.status == Order.STATUS_PAID assert self.order.payment_provider == 'free'
class OrderChangeManagerTests(TestCase): def setUp(self): super().setUp() o = Organizer.objects.create(name='Dummy', slug='dummy') self.event = Event.objects.create( organizer=o, name='Dummy', slug='dummy', date_from=now(), plugins='pretix.plugins.banktransfer') self.order = Order.objects.create(code='FOO', event=self.event, email='*****@*****.**', status=Order.STATUS_PENDING, locale='en', datetime=now(), expires=now() + timedelta(days=10), total=Decimal('46.00'), payment_provider='banktransfer') self.tr7 = self.event.tax_rules.create(rate=Decimal('7.00')) self.tr19 = self.event.tax_rules.create(rate=Decimal('19.00')) self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket', tax_rule=self.tr7, default_price=Decimal('23.00'), admission=True) self.ticket2 = Item.objects.create(event=self.event, name='Other ticket', tax_rule=self.tr7, default_price=Decimal('23.00'), admission=True) self.shirt = Item.objects.create(event=self.event, name='T-Shirt', tax_rule=self.tr19, default_price=Decimal('12.00')) self.op1 = OrderPosition.objects.create(order=self.order, item=self.ticket, variation=None, price=Decimal("23.00"), attendee_name="Peter", positionid=1) self.op2 = OrderPosition.objects.create(order=self.order, item=self.ticket, variation=None, price=Decimal("23.00"), attendee_name="Dieter", positionid=2) self.ocm = OrderChangeManager(self.order, None) self.quota = self.event.quotas.create(name='Test', size=None) self.quota.items.add(self.ticket) self.quota.items.add(self.ticket2) self.quota.items.add(self.shirt) def _enable_reverse_charge(self): self.tr7.eu_reverse_charge = True self.tr7.home_country = Country('DE') self.tr7.save() self.tr19.eu_reverse_charge = True self.tr19.home_country = Country('DE') self.tr19.save() return InvoiceAddress.objects.create(order=self.order, is_business=True, vat_id='ATU1234567', vat_id_validated=True, country=Country('AT')) def test_change_subevent_quota_required(self): self.event.has_subevents = True self.event.save() 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.quota.subevent = se1 self.quota.save() with self.assertRaises(OrderError): self.ocm.change_subevent(self.op1, se2) def test_change_subevent_success(self): self.event.has_subevents = True self.event.save() se1 = self.event.subevents.create(name="Foo", date_from=now()) se2 = self.event.subevents.create(name="Bar", date_from=now()) SubEventItem.objects.create(subevent=se2, item=self.ticket, price=12) self.op1.subevent = se1 self.op1.save() self.quota.subevent = se2 self.quota.save() self.ocm.change_subevent(self.op1, se2) self.ocm.commit() self.op1.refresh_from_db() self.order.refresh_from_db() assert self.op1.subevent == se2 assert self.op1.price == 12 assert self.order.total == self.op1.price + self.op2.price def test_change_subevent_reverse_charge(self): self._enable_reverse_charge() self.event.has_subevents = True self.event.save() se1 = self.event.subevents.create(name="Foo", date_from=now()) se2 = self.event.subevents.create(name="Bar", date_from=now()) SubEventItem.objects.create(subevent=se2, item=self.ticket, price=10.7) self.op1.subevent = se1 self.op1.save() self.quota.subevent = se2 self.quota.save() self.ocm.change_subevent(self.op1, se2) self.ocm.commit() self.op1.refresh_from_db() self.order.refresh_from_db() assert self.op1.subevent == se2 assert self.op1.price == Decimal('10.00') assert self.op1.tax_value == Decimal('0.00') assert self.order.total == self.op1.price + self.op2.price def test_change_subevent_net_price(self): self.event.has_subevents = True self.event.save() se1 = self.event.subevents.create(name="Foo", date_from=now()) se2 = self.event.subevents.create(name="Bar", date_from=now()) self.tr7.price_includes_tax = False self.tr7.save() SubEventItem.objects.create(subevent=se2, item=self.ticket, price=10) self.op1.subevent = se1 self.op1.save() self.quota.subevent = se2 self.quota.save() self.ocm.change_subevent(self.op1, se2) self.ocm.commit() self.op1.refresh_from_db() self.order.refresh_from_db() assert self.op1.subevent == se2 assert self.op1.price == Decimal('10.70') assert self.order.total == self.op1.price + self.op2.price def test_change_subevent_sold_out(self): self.event.has_subevents = True self.event.save() 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.quota.subevent = se2 self.quota.size = 0 self.quota.save() self.ocm.change_subevent(self.op1, se2) with self.assertRaises(OrderError): self.ocm.commit() self.op1.refresh_from_db() assert self.op1.subevent == se1 def test_change_item_quota_required(self): self.quota.delete() with self.assertRaises(OrderError): self.ocm.change_item(self.op1, self.shirt, None) def test_change_item_success(self): self.ocm.change_item(self.op1, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() self.order.refresh_from_db() assert self.op1.item == self.shirt assert self.op1.price == self.shirt.default_price assert self.op1.tax_rate == self.shirt.tax_rule.rate assert round_decimal(self.op1.price * (1 - 100 / (100 + self.op1.tax_rate))) == self.op1.tax_value assert self.order.total == self.op1.price + self.op2.price def test_change_item_net_price_success(self): self.tr19.price_includes_tax = False self.tr19.save() self.ocm.change_item(self.op1, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() self.order.refresh_from_db() assert self.op1.item == self.shirt assert self.op1.price == Decimal('14.28') assert self.op1.tax_rate == self.shirt.tax_rule.rate assert round_decimal(self.op1.price * (1 - 100 / (100 + self.op1.tax_rate))) == self.op1.tax_value assert self.order.total == self.op1.price + self.op2.price def test_change_item_reverse_charge(self): self._enable_reverse_charge() self.ocm.change_item(self.op1, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() self.order.refresh_from_db() assert self.op1.item == self.shirt assert self.op1.price == Decimal('10.08') assert self.op1.tax_rate == Decimal('0.00') assert self.op1.tax_value == Decimal('0.00') assert self.order.total == self.op1.price + self.op2.price def test_change_price_success(self): self.ocm.change_price(self.op1, Decimal('24.00')) self.ocm.commit() self.op1.refresh_from_db() self.order.refresh_from_db() assert self.op1.item == self.ticket assert self.op1.price == Decimal('24.00') assert round_decimal(self.op1.price * (1 - 100 / (100 + self.op1.tax_rate))) == self.op1.tax_value assert self.order.total == self.op1.price + self.op2.price def test_change_price_net_success(self): self.tr7.price_includes_tax = False self.tr7.save() self.ocm.change_price(self.op1, Decimal('10.00')) self.ocm.commit() self.op1.refresh_from_db() self.order.refresh_from_db() assert self.op1.item == self.ticket assert self.op1.price == Decimal('10.70') assert round_decimal(self.op1.price * (1 - 100 / (100 + self.op1.tax_rate))) == self.op1.tax_value assert self.order.total == self.op1.price + self.op2.price def test_cancel_success(self): 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 def test_free_to_paid(self): self.op1.price = Decimal('0.00') self.op1.save() self.op2.delete() self.order.total = Decimal('0.00') self.order.save() self.ocm.change_price(self.op1, Decimal('24.00')) with self.assertRaises(OrderError): self.ocm.commit() self.op1.refresh_from_db() assert self.op1.price == Decimal('0.00') def test_cancel_all_in_order(self): self.ocm.cancel(self.op1) self.ocm.cancel(self.op2) with self.assertRaises(OrderError): self.ocm.commit() assert self.order.positions.count() == 2 def test_empty(self): self.ocm.commit() def test_quota_unlimited(self): q = self.event.quotas.create(name='Test', size=None) q.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.shirt def test_quota_full(self): q = self.event.quotas.create(name='Test', size=0) q.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) with self.assertRaises(OrderError): self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.ticket def test_quota_full_but_in_same(self): q = self.event.quotas.create(name='Test', size=0) q.items.add(self.shirt) q.items.add(self.ticket) self.ocm.change_item(self.op1, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.shirt def test_multiple_quotas_shared_full(self): q1 = self.event.quotas.create(name='Test', size=0) q2 = self.event.quotas.create(name='Test', size=2) q1.items.add(self.shirt) q1.items.add(self.ticket) q2.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.shirt def test_multiple_quotas_unshared_full(self): q1 = self.event.quotas.create(name='Test', size=2) q2 = self.event.quotas.create(name='Test', size=0) q1.items.add(self.shirt) q1.items.add(self.ticket) q2.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) with self.assertRaises(OrderError): self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.ticket def test_multiple_items_success(self): q1 = self.event.quotas.create(name='Test', size=2) q1.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) self.ocm.change_item(self.op2, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() self.op2.refresh_from_db() assert self.op1.item == self.shirt assert self.op2.item == self.shirt def test_multiple_items_quotas_partially_full(self): q1 = self.event.quotas.create(name='Test', size=1) q1.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) self.ocm.change_item(self.op2, self.shirt, None) with self.assertRaises(OrderError): self.ocm.commit() self.op1.refresh_from_db() self.op2.refresh_from_db() assert self.op1.item == self.ticket assert self.op2.item == self.ticket def test_payment_fee_calculation(self): self.event.settings.set('tax_rate_default', self.tr19.pk) prov = self.ocm._get_payment_provider() prov.settings.set('_fee_abs', Decimal('0.30')) self.ocm.change_price(self.op1, Decimal('24.00')) self.ocm.commit() self.order.refresh_from_db() assert self.order.total == Decimal('47.30') fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT) assert fee.value == prov.calculate_fee(self.order.total) assert fee.tax_rate == Decimal('19.00') assert round_decimal(fee.value * (1 - 100 / (100 + fee.tax_rate))) == fee.tax_value def test_require_pending(self): self.order.status = Order.STATUS_PAID self.order.save() self.ocm.change_item(self.op1, self.shirt, None) with self.assertRaises(OrderError): self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.ticket def test_change_price_to_free_marked_as_paid(self): self.ocm.change_price(self.op1, Decimal('0.00')) self.ocm.change_price(self.op2, Decimal('0.00')) self.ocm.commit() self.order.refresh_from_db() assert self.order.total == 0 assert self.order.status == Order.STATUS_PAID assert self.order.payment_provider == 'free' def test_change_paid_same_price(self): self.order.status = Order.STATUS_PAID self.order.save() self.ocm.change_item(self.op1, self.ticket2, None) self.ocm.commit() self.order.refresh_from_db() assert self.order.total == 46 assert self.order.status == Order.STATUS_PAID def test_change_paid_different_price(self): self.order.status = Order.STATUS_PAID self.order.save() self.ocm.change_price(self.op1, Decimal('5.00')) with self.assertRaises(OrderError): self.ocm.commit() self.order.refresh_from_db() assert self.order.total == 46 assert self.order.status == Order.STATUS_PAID def test_add_item_quota_required(self): self.quota.delete() with self.assertRaises(OrderError): self.ocm.add_position(self.shirt, None, None, None) def test_add_item_success(self): self.ocm.add_position(self.shirt, None, None, None) self.ocm.commit() self.order.refresh_from_db() assert self.order.positions.count() == 3 nop = self.order.positions.last() assert nop.item == self.shirt assert nop.price == self.shirt.default_price assert nop.tax_rate == self.shirt.tax_rule.rate assert round_decimal( nop.price * (1 - 100 / (100 + self.shirt.tax_rule.rate))) == nop.tax_value assert self.order.total == self.op1.price + self.op2.price + nop.price assert nop.positionid == 3 def test_add_item_net_price_success(self): self.tr19.price_includes_tax = False self.tr19.save() self.ocm.add_position(self.shirt, None, None, None) self.ocm.commit() self.order.refresh_from_db() assert self.order.positions.count() == 3 nop = self.order.positions.last() assert nop.item == self.shirt assert nop.price == Decimal('14.28') assert nop.tax_rate == self.shirt.tax_rule.rate assert round_decimal( nop.price * (1 - 100 / (100 + self.shirt.tax_rule.rate))) == nop.tax_value assert self.order.total == self.op1.price + self.op2.price + nop.price assert nop.positionid == 3 def test_add_item_reverse_charge(self): self._enable_reverse_charge() self.ocm.add_position(self.shirt, None, None, None) self.ocm.commit() self.order.refresh_from_db() assert self.order.positions.count() == 3 nop = self.order.positions.last() assert nop.item == self.shirt assert nop.price == Decimal('10.08') assert nop.tax_rate == Decimal('0.00') assert nop.tax_value == Decimal('0.00') assert self.order.total == self.op1.price + self.op2.price + nop.price assert nop.positionid == 3 def test_add_item_custom_price(self): self.ocm.add_position(self.shirt, None, Decimal('13.00'), None) self.ocm.commit() self.order.refresh_from_db() assert self.order.positions.count() == 3 nop = self.order.positions.last() assert nop.item == self.shirt assert nop.price == Decimal('13.00') assert nop.tax_rate == self.shirt.tax_rule.rate assert round_decimal( nop.price * (1 - 100 / (100 + self.shirt.tax_rule.rate))) == nop.tax_value assert self.order.total == self.op1.price + self.op2.price + nop.price def test_add_item_custom_price_tax_always_included(self): self.tr19.price_includes_tax = False self.tr19.save() self.ocm.add_position(self.shirt, None, Decimal('11.90'), None) self.ocm.commit() self.order.refresh_from_db() assert self.order.positions.count() == 3 nop = self.order.positions.last() assert nop.item == self.shirt assert nop.price == Decimal('11.90') assert nop.tax_rate == self.shirt.tax_rule.rate assert round_decimal( nop.price * (1 - 100 / (100 + self.shirt.tax_rule.rate))) == nop.tax_value assert self.order.total == self.op1.price + self.op2.price + nop.price def test_add_item_quota_full(self): q1 = self.event.quotas.create(name='Test', size=0) q1.items.add(self.shirt) self.ocm.add_position(self.shirt, None, None, None) with self.assertRaises(OrderError): self.ocm.commit() assert self.order.positions.count() == 2 def test_add_item_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() assert self.order.positions.count() == 3 nop = self.order.positions.last() assert nop.item == self.shirt assert nop.addon_to == self.op1 def test_add_item_addon_invalid(self): with self.assertRaises(OrderError): self.ocm.add_position(self.shirt, None, Decimal('13.00'), self.op1) self.shirt.category = self.event.categories.create(name='Add-ons', is_addon=True) with self.assertRaises(OrderError): self.ocm.add_position(self.shirt, None, Decimal('13.00'), None) def test_add_item_subevent_required(self): self.event.has_subevents = True self.event.save() with self.assertRaises(OrderError): self.ocm.add_position(self.ticket, None, None, None) def test_add_item_subevent_price(self): self.event.has_subevents = True self.event.save() se1 = self.event.subevents.create(name="Foo", date_from=now()) SubEventItem.objects.create(subevent=se1, item=self.ticket, price=12) self.quota.subevent = se1 self.quota.save() self.ocm.add_position(self.ticket, None, None, subevent=se1) self.ocm.commit() self.order.refresh_from_db() assert self.order.positions.count() == 3 nop = self.order.positions.last() assert nop.item == self.ticket assert nop.price == Decimal('12.00') assert nop.subevent == se1 def test_reissue_invoice(self): generate_invoice(self.order) assert self.order.invoices.count() == 1 self.ocm.add_position(self.ticket, None, Decimal('0.00')) self.ocm.commit() assert self.order.invoices.count() == 3 def test_dont_reissue_invoice_on_free_product_changes(self): self.event.settings.invoice_include_free = False generate_invoice(self.order) assert self.order.invoices.count() == 1 self.ocm.add_position(self.ticket, None, Decimal('0.00')) self.ocm.commit() assert self.order.invoices.count() == 1 def test_recalculate_reverse_charge(self): self.event.settings.set('tax_rate_default', self.tr19.pk) prov = self.ocm._get_payment_provider() prov.settings.set('_fee_abs', Decimal('0.30')) self.ocm._recalculate_total_and_payment_fee() assert self.order.total == Decimal('46.30') fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT) assert fee.value == prov.calculate_fee(self.order.total) assert fee.tax_rate == Decimal('19.00') assert fee.tax_value == Decimal('0.05') ia = self._enable_reverse_charge() self.ocm.recalculate_taxes() self.ocm.commit() ops = list(self.order.positions.all()) for op in ops: assert op.price == Decimal('21.50') assert op.tax_value == Decimal('0.00') assert op.tax_rate == Decimal('0.00') assert self.order.total == Decimal('43.30') fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT) assert fee.value == prov.calculate_fee(self.order.total) assert fee.tax_rate == Decimal('0.00') assert fee.tax_value == Decimal('0.00') ia.vat_id_validated = False ia.save() self.ocm.recalculate_taxes() self.ocm.commit() ops = list(self.order.positions.all()) for op in ops: assert op.price == Decimal( '23.01') # sic. we can't really avoid it. assert op.tax_value == Decimal('1.51') assert op.tax_rate == Decimal('7.00') assert self.order.total == Decimal('46.32') fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT) assert fee.value == prov.calculate_fee(self.order.total) assert fee.tax_rate == Decimal('19.00') assert fee.tax_value == Decimal('0.05')
def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_fixed: str, keep_fee_per_ticket: str, keep_fee_percentage: str, keep_fees: list = None, manual_refund: bool = False, send: bool = False, send_subject: dict = None, send_message: dict = None, send_waitinglist: bool = False, send_waitinglist_subject: dict = {}, send_waitinglist_message: dict = {}, user: int = None, refund_as_giftcard: bool = False, giftcard_expires=None, giftcard_conditions=None, subevents_from: str = None, subevents_to: str = None): send_subject = LazyI18nString(send_subject) send_message = LazyI18nString(send_message) send_waitinglist_subject = LazyI18nString(send_waitinglist_subject) send_waitinglist_message = LazyI18nString(send_waitinglist_message) if user: user = User.objects.get(pk=user) s = OrderPosition.objects.filter( order=OuterRef('pk')).order_by().values('order').annotate( k=Count('id')).values('k') orders_to_cancel = event.orders.annotate( pcnt=Subquery(s, output_field=IntegerField())).filter( status__in=[ Order.STATUS_PAID, Order.STATUS_PENDING, Order.STATUS_EXPIRED ], pcnt__gt=0).all() if subevent or subevents_from: if subevent: subevents = event.subevents.filter(pk=subevent) subevent = subevents.first() subevent_ids = {subevent.pk} else: subevents = event.subevents.filter(date_from__gte=subevents_from, date_from__lt=subevents_to) subevent_ids = set(subevents.values_list('id', flat=True)) has_subevent = OrderPosition.objects.filter( order_id=OuterRef('pk')).filter(subevent__in=subevents) has_other_subevent = OrderPosition.objects.filter( order_id=OuterRef('pk')).exclude(subevent__in=subevents) orders_to_change = orders_to_cancel.annotate( has_subevent=Exists(has_subevent), has_other_subevent=Exists(has_other_subevent), ).filter(has_subevent=True, has_other_subevent=True) orders_to_cancel = orders_to_cancel.annotate( has_subevent=Exists(has_subevent), has_other_subevent=Exists(has_other_subevent), ).filter(has_subevent=True, has_other_subevent=False) for se in subevents: se.log_action( 'pretix.subevent.canceled', user=user, ) se.active = False se.save(update_fields=['active']) se.log_action('pretix.subevent.changed', user=user, data={ 'active': False, '_source': 'cancel_event' }) else: subevents = None subevent_ids = set() orders_to_change = event.orders.none() event.log_action( 'pretix.event.canceled', user=user, ) for i in event.items.filter(active=True): i.active = False i.save(update_fields=['active']) i.log_action('pretix.event.item.changed', user=user, data={ 'active': False, '_source': 'cancel_event' }) failed = 0 total = orders_to_cancel.count() + orders_to_change.count() qs_wl = event.waitinglistentries.filter( voucher__isnull=True).select_related('subevent') if subevents: qs_wl = qs_wl.filter(subevent__in=subevents) if send_waitinglist: total += qs_wl.count() counter = 0 self.update_state(state='PROGRESS', meta={'value': 0}) for o in orders_to_cancel.only('id', 'total').iterator(): try: fee = Decimal('0.00') fee_sum = Decimal('0.00') keep_fee_objects = [] if keep_fees: for f in o.fees.all(): if f.fee_type in keep_fees: fee += f.value keep_fee_objects.append(f) fee_sum += f.value if keep_fee_percentage: fee += Decimal(keep_fee_percentage) / Decimal('100.00') * ( o.total - fee_sum) if keep_fee_fixed: fee += Decimal(keep_fee_fixed) if keep_fee_per_ticket: for p in o.positions.all(): if p.addon_to_id is None: fee += min(p.price, Decimal(keep_fee_per_ticket)) fee = round_decimal(min(fee, o.payment_refund_sum), event.currency) _cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects) refund_amount = o.payment_refund_sum try: if auto_refund: _try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard, giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions, comment=gettext('Event canceled')) finally: if send: _send_mail(o, send_subject, send_message, subevent, refund_amount, user, o.positions.all()) counter += 1 if not self.request.called_directly and counter % max( 10, total // 100) == 0: self.update_state( state='PROGRESS', meta={ 'value': round(counter / total * 100 if total else 0, 2) }) except LockTimeoutException: logger.exception("Could not cancel order") failed += 1 except OrderError: logger.exception("Could not cancel order") failed += 1 for o in orders_to_change.values_list('id', flat=True).iterator(): with transaction.atomic(): o = event.orders.select_for_update().get(pk=o) total = Decimal('0.00') fee = Decimal('0.00') positions = [] ocm = OrderChangeManager(o, user=user, notify=False) for p in o.positions.all(): if p.subevent_id in subevent_ids: total += p.price ocm.cancel(p) positions.append(p) if keep_fee_per_ticket: if p.addon_to_id is None: fee += min(p.price, Decimal(keep_fee_per_ticket)) if keep_fee_fixed: fee += Decimal(keep_fee_fixed) if keep_fee_percentage: fee += Decimal(keep_fee_percentage) / Decimal('100.00') * total fee = round_decimal(min(fee, o.payment_refund_sum), event.currency) if fee: f = OrderFee( fee_type=OrderFee.FEE_TYPE_CANCELLATION, value=fee, order=o, tax_rule=o.event.settings.tax_rate_default, ) f._calculate_tax() ocm.add_fee(f) ocm.commit() refund_amount = o.payment_refund_sum - o.total if auto_refund: _try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard, giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions, comment=gettext('Event canceled')) if send: _send_mail(o, send_subject, send_message, subevent, refund_amount, user, positions) counter += 1 if not self.request.called_directly and counter % max( 10, total // 100) == 0: self.update_state( state='PROGRESS', meta={ 'value': round(counter / total * 100 if total else 0, 2) }) if send_waitinglist: for wle in qs_wl: _send_wle_mail(wle, send_waitinglist_subject, send_waitinglist_message, wle.subevent) counter += 1 if not self.request.called_directly and counter % max( 10, total // 100) == 0: self.update_state( state='PROGRESS', meta={ 'value': round(counter / total * 100 if total else 0, 2) }) return failed
def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_fixed: str, keep_fee_percentage: str, keep_fees: list = None, manual_refund: bool = False, send: bool = False, send_subject: dict = None, send_message: dict = None, send_waitinglist: bool = False, send_waitinglist_subject: dict = {}, send_waitinglist_message: dict = {}, user: int = None, refund_as_giftcard: bool = False): send_subject = LazyI18nString(send_subject) send_message = LazyI18nString(send_message) send_waitinglist_subject = LazyI18nString(send_waitinglist_subject) send_waitinglist_message = LazyI18nString(send_waitinglist_message) if user: user = User.objects.get(pk=user) s = OrderPosition.objects.filter( order=OuterRef('pk')).order_by().values('order').annotate( k=Count('id')).values('k') orders_to_cancel = event.orders.annotate( pcnt=Subquery(s, output_field=IntegerField())).filter( status__in=[ Order.STATUS_PAID, Order.STATUS_PENDING, Order.STATUS_EXPIRED ], pcnt__gt=0).all() if subevent: subevent = event.subevents.get(pk=subevent) has_subevent = OrderPosition.objects.filter( order_id=OuterRef('pk')).filter(subevent=subevent) has_other_subevent = OrderPosition.objects.filter( order_id=OuterRef('pk')).exclude(subevent=subevent) orders_to_change = orders_to_cancel.annotate( has_subevent=Exists(has_subevent), has_other_subevent=Exists(has_other_subevent), ).filter(has_subevent=True, has_other_subevent=True) orders_to_cancel = orders_to_cancel.annotate( has_subevent=Exists(has_subevent), has_other_subevent=Exists(has_other_subevent), ).filter(has_subevent=True, has_other_subevent=False) subevent.log_action( 'pretix.subevent.canceled', user=user, ) subevent.active = False subevent.save(update_fields=['active']) subevent.log_action('pretix.subevent.changed', user=user, data={ 'active': False, '_source': 'cancel_event' }) else: orders_to_change = event.orders.none() event.log_action( 'pretix.event.canceled', user=user, ) for i in event.items.filter(active=True): i.active = False i.save(update_fields=['active']) i.log_action('pretix.event.item.changed', user=user, data={ 'active': False, '_source': 'cancel_event' }) failed = 0 for o in orders_to_cancel.only('id', 'total'): try: fee = Decimal('0.00') fee_sum = Decimal('0.00') keep_fee_objects = [] if keep_fees: for f in o.fees.all(): if f.fee_type in keep_fees: fee += f.value keep_fee_objects.append(f) fee_sum += f.value if keep_fee_percentage: fee += Decimal(keep_fee_percentage) / Decimal('100.00') * ( o.total - fee_sum) if keep_fee_fixed: fee += Decimal(keep_fee_fixed) fee = round_decimal(min(fee, o.payment_refund_sum), event.currency) _cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects) refund_amount = o.payment_refund_sum try: if auto_refund: _try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard) finally: if send: _send_mail(o, send_subject, send_message, subevent, refund_amount, user, o.positions.all()) except LockTimeoutException: logger.exception("Could not cancel order") failed += 1 except OrderError: logger.exception("Could not cancel order") failed += 1 for o in orders_to_change.values_list('id', flat=True): with transaction.atomic(): o = event.orders.select_for_update().get(pk=o) total = Decimal('0.00') positions = [] ocm = OrderChangeManager(o, user=user, notify=False) for p in o.positions.all(): if p.subevent == subevent: total += p.price ocm.cancel(p) positions.append(p) fee = Decimal('0.00') if keep_fee_fixed: fee += Decimal(keep_fee_fixed) if keep_fee_percentage: fee += Decimal(keep_fee_percentage) / Decimal('100.00') * total fee = round_decimal(min(fee, o.payment_refund_sum), event.currency) if fee: f = OrderFee( fee_type=OrderFee.FEE_TYPE_CANCELLATION, value=fee, order=o, tax_rule=o.event.settings.tax_rate_default, ) f._calculate_tax() ocm.add_fee(f) ocm.commit() refund_amount = o.payment_refund_sum - o.total if auto_refund: _try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, source=OrderRefund.REFUND_SOURCE_ADMIN) if send: _send_mail(o, send_subject, send_message, subevent, refund_amount, user, positions) for wle in event.waitinglistentries.filter(subevent=subevent, voucher__isnull=True): _send_wle_mail(wle, send_waitinglist_subject, send_waitinglist_message, subevent) return failed
def swap_with(self, other): if not self.event.settings.swap_orderpositions: raise Exception("Order position swapping is currently not allowed") my_item = self.position.item my_variation = self.position.variation my_subevent = self.position.subevent other_item = other.position.item other_variation = other.position.variation other_subevent = other.position.subevent if not (my_item == other_item): raise Exception(f"Items do not match: {my_item} vs {other_item}.") if not (my_variation == other_variation): raise Exception( f"Item variations do not match: {my_variation} vs {other_variation}." ) if my_subevent == other_subevent: raise Exception("Can't swap within the same subevent.") if not can_be_swapped(self.event, my_item, my_subevent, other_subevent): raise Exception("This swap is currently not allowed.") my_change_manager = OrderChangeManager(order=self.position.order) other_change_manager = OrderChangeManager(order=other.position.order) # Make sure AGAIN that the state is alright, because timings self.refresh_from_db() other.refresh_from_db() if self.state != self.States.REQUESTED or other.state != self.States.REQUESTED: raise Exception( "Both requests have to be in the 'requesting' state.") if not self.position.price == other.position.price: raise Exception("Both requests have to have the same price.") my_change_manager.change_item_and_subevent( position=self.position, item=my_item, variation=my_variation, subevent=other_subevent, ) other_change_manager.change_item_and_subevent( position=other.position, item=my_item, variation=my_variation, subevent=my_subevent, ) my_change_manager.commit() other_change_manager.commit() self.state = self.States.COMPLETED self.partner = other self.save() other.state = self.States.COMPLETED other.partner = self other.save() self.position.order.log_action( "pretix_swap.swap.complete", data={ "position": self.position.pk, "positionid": self.position.positionid, "other_position": other.position, "other_positionid": other.position.positionid, "other_order": other.position.order.code, }, ) other.position.order.log_action( "pretix_swap.swap.complete", data={ "position": other.position.pk, "positionid": other.position.positionid, "other_position": self.position, "other_positionid": self.position.positionid, "other_order": self.position.order.code, }, )
class OrderChangeManagerTests(TestCase): def setUp(self): super().setUp() o = Organizer.objects.create(name='Dummy', slug='dummy') self.event = Event.objects.create(organizer=o, name='Dummy', slug='dummy', date_from=now(), plugins='pretix.plugins.banktransfer') self.order = Order.objects.create( code='FOO', event=self.event, email='*****@*****.**', status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10), total=Decimal('46.00'), payment_provider='banktransfer' ) self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket', tax_rate=Decimal('7.00'), default_price=Decimal('23.00'), admission=True) self.ticket2 = Item.objects.create(event=self.event, name='Other ticket', tax_rate=Decimal('7.00'), default_price=Decimal('23.00'), admission=True) self.shirt = Item.objects.create(event=self.event, name='T-Shirt', tax_rate=Decimal('19.00'), default_price=Decimal('12.00')) self.op1 = OrderPosition.objects.create( order=self.order, item=self.ticket, variation=None, price=Decimal("23.00"), attendee_name="Peter", positionid=1 ) self.op2 = OrderPosition.objects.create( order=self.order, item=self.ticket, variation=None, price=Decimal("23.00"), attendee_name="Dieter", positionid=2 ) self.ocm = OrderChangeManager(self.order, None) def test_change_item_success(self): self.ocm.change_item(self.op1, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() self.order.refresh_from_db() assert self.op1.item == self.shirt assert self.op1.price == self.shirt.default_price assert self.op1.tax_rate == self.shirt.tax_rate assert round_decimal(self.op1.price * (1 - 100 / (100 + self.op1.tax_rate))) == self.op1.tax_value assert self.order.total == self.op1.price + self.op2.price def test_change_price_success(self): self.ocm.change_price(self.op1, Decimal('24.00')) self.ocm.commit() self.op1.refresh_from_db() self.order.refresh_from_db() assert self.op1.item == self.ticket assert self.op1.price == Decimal('24.00') assert round_decimal(self.op1.price * (1 - 100 / (100 + self.op1.tax_rate))) == self.op1.tax_value assert self.order.total == self.op1.price + self.op2.price def test_cancel_success(self): 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 def test_free_to_paid(self): self.op1.price = Decimal('0.00') self.op1.save() self.op2.delete() self.order.total = Decimal('0.00') self.order.save() self.ocm.change_price(self.op1, Decimal('24.00')) with self.assertRaises(OrderError): self.ocm.commit() self.op1.refresh_from_db() assert self.op1.price == Decimal('0.00') def test_cancel_all_in_order(self): self.ocm.cancel(self.op1) self.ocm.cancel(self.op2) with self.assertRaises(OrderError): self.ocm.commit() assert self.order.positions.count() == 2 def test_empty(self): self.ocm.commit() def test_quota_unlimited(self): q = self.event.quotas.create(name='Test', size=None) q.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.shirt def test_quota_full(self): q = self.event.quotas.create(name='Test', size=0) q.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) with self.assertRaises(OrderError): self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.ticket def test_quota_full_but_in_same(self): q = self.event.quotas.create(name='Test', size=0) q.items.add(self.shirt) q.items.add(self.ticket) self.ocm.change_item(self.op1, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.shirt def test_multiple_quotas_shared_full(self): q1 = self.event.quotas.create(name='Test', size=0) q2 = self.event.quotas.create(name='Test', size=2) q1.items.add(self.shirt) q1.items.add(self.ticket) q2.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.shirt def test_multiple_quotas_unshared_full(self): q1 = self.event.quotas.create(name='Test', size=2) q2 = self.event.quotas.create(name='Test', size=0) q1.items.add(self.shirt) q1.items.add(self.ticket) q2.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) with self.assertRaises(OrderError): self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.ticket def test_multiple_items_success(self): q1 = self.event.quotas.create(name='Test', size=2) q1.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) self.ocm.change_item(self.op2, self.shirt, None) self.ocm.commit() self.op1.refresh_from_db() self.op2.refresh_from_db() assert self.op1.item == self.shirt assert self.op2.item == self.shirt def test_multiple_items_quotas_partially_full(self): q1 = self.event.quotas.create(name='Test', size=1) q1.items.add(self.shirt) self.ocm.change_item(self.op1, self.shirt, None) self.ocm.change_item(self.op2, self.shirt, None) with self.assertRaises(OrderError): self.ocm.commit() self.op1.refresh_from_db() self.op2.refresh_from_db() assert self.op1.item == self.ticket assert self.op2.item == self.ticket def test_payment_fee_calculation(self): self.event.settings.set('tax_rate_default', Decimal('19.00')) prov = self.ocm._get_payment_provider() prov.settings.set('_fee_abs', Decimal('0.30')) self.ocm.change_price(self.op1, Decimal('24.00')) self.ocm.commit() self.order.refresh_from_db() assert self.order.total == Decimal('47.30') assert self.order.payment_fee == prov.calculate_fee(self.order.total) assert self.order.payment_fee_tax_rate == Decimal('19.00') assert round_decimal(self.order.payment_fee * (1 - 100 / (100 + self.order.payment_fee_tax_rate))) == self.order.payment_fee_tax_value def test_require_pending(self): self.order.status = Order.STATUS_PAID self.order.save() self.ocm.change_item(self.op1, self.shirt, None) with self.assertRaises(OrderError): self.ocm.commit() self.op1.refresh_from_db() assert self.op1.item == self.ticket def test_change_price_to_free_marked_as_paid(self): self.ocm.change_price(self.op1, Decimal('0.00')) self.ocm.change_price(self.op2, Decimal('0.00')) self.ocm.commit() self.order.refresh_from_db() assert self.order.total == 0 assert self.order.status == Order.STATUS_PAID assert self.order.payment_provider == 'free' def test_change_paid_same_price(self): self.order.status = Order.STATUS_PAID self.order.save() self.ocm.change_item(self.op1, self.ticket2, None) self.ocm.commit() self.order.refresh_from_db() assert self.order.total == 46 assert self.order.status == Order.STATUS_PAID def test_change_paid_different_price(self): self.order.status = Order.STATUS_PAID self.order.save() self.ocm.change_price(self.op1, Decimal('5.00')) with self.assertRaises(OrderError): self.ocm.commit() self.order.refresh_from_db() assert self.order.total == 46 assert self.order.status == Order.STATUS_PAID def test_add_item_success(self): self.ocm.add_position(self.shirt, None, None, None) self.ocm.commit() self.order.refresh_from_db() assert self.order.positions.count() == 3 nop = self.order.positions.last() assert nop.item == self.shirt assert nop.price == self.shirt.default_price assert nop.tax_rate == self.shirt.tax_rate assert round_decimal(nop.price * (1 - 100 / (100 + self.shirt.tax_rate))) == nop.tax_value assert self.order.total == self.op1.price + self.op2.price + nop.price assert nop.positionid == 3 def test_add_item_custom_price(self): self.ocm.add_position(self.shirt, None, Decimal('13.00'), None) self.ocm.commit() self.order.refresh_from_db() assert self.order.positions.count() == 3 nop = self.order.positions.last() assert nop.item == self.shirt assert nop.price == Decimal('13.00') assert nop.tax_rate == self.shirt.tax_rate assert round_decimal(nop.price * (1 - 100 / (100 + self.shirt.tax_rate))) == nop.tax_value assert self.order.total == self.op1.price + self.op2.price + nop.price def test_add_item_quota_full(self): q1 = self.event.quotas.create(name='Test', size=0) q1.items.add(self.shirt) self.ocm.add_position(self.shirt, None, None, None) with self.assertRaises(OrderError): self.ocm.commit() assert self.order.positions.count() == 2 def test_add_item_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() assert self.order.positions.count() == 3 nop = self.order.positions.last() assert nop.item == self.shirt assert nop.addon_to == self.op1 def test_add_item_addon_invalid(self): with self.assertRaises(OrderError): self.ocm.add_position(self.shirt, None, Decimal('13.00'), self.op1) self.shirt.category = self.event.categories.create(name='Add-ons', is_addon=True) with self.assertRaises(OrderError): self.ocm.add_position(self.shirt, None, Decimal('13.00'), None)