Compare commits

..

9 Commits

Author SHA1 Message Date
Mira Weller
6d66a351f0 make price asserts more specific 2024-10-31 11:14:08 +01:00
Mira Weller
b61faf7e29 test for adding a fee 2024-10-31 10:54:52 +01:00
Mira Weller
b1c2b0b827 rename prefix, update tests 2024-10-31 10:47:07 +01:00
Mira Weller
8e47c00167 refactor names, ordering 2024-10-29 18:58:02 +01:00
Mira Weller
cac9a39ae2 formatting 2024-10-29 18:56:52 +01:00
Mira Weller
6f0d813cfc control: allow organizers to manually add fees to an existing order 2024-10-29 18:53:42 +01:00
Mira Weller
839aca6561 add missing word 2024-10-29 17:21:32 +01:00
Richard Schreiber
f9502a3212 Fix testutils redis mock (#4586) 2024-10-29 16:37:06 +01:00
Richard Schreiber
a31f624417 Dev: add support for asyncio_default_fixture_loop_scope in pytest-asyncio (#4589) 2024-10-29 16:36:43 +01:00
14 changed files with 392 additions and 170 deletions

View File

@@ -100,7 +100,3 @@ API
.. automodule:: pretix.base.signals
:no-index:
:members: validate_event_settings, api_event_settings_fields
.. automodule:: pretix.api.signals
:no-index:
:members: register_device_security_profile

View File

@@ -117,7 +117,7 @@ dev = [
"isort==5.13.*",
"pep8-naming==0.14.*",
"potypo",
"pytest-asyncio",
"pytest-asyncio>=0.24",
"pytest-cache",
"pytest-cov",
"pytest-django==4.*",

View File

@@ -27,7 +27,7 @@ from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication
from pretix.api.auth.devicesecurity import (
FullAccessSecurityProfile, get_all_security_profiles,
DEVICE_SECURITY_PROFILES, FullAccessSecurityProfile,
)
from pretix.base.models import Device
@@ -58,8 +58,7 @@ class DeviceTokenAuthentication(TokenAuthentication):
def authenticate(self, request):
r = super().authenticate(request)
if r and isinstance(r[1], Device):
profiles = get_all_security_profiles()
profile = profiles.get(r[1].security_profile, FullAccessSecurityProfile())
profile = DEVICE_SECURITY_PROFILES.get(r[1].security_profile, FullAccessSecurityProfile)
if not profile.is_allowed(request):
raise exceptions.PermissionDenied('Request denied by device security profile.')
return r

View File

@@ -20,40 +20,13 @@
# <https://www.gnu.org/licenses/>.
#
import logging
from collections import OrderedDict
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from pretix.api.signals import register_device_security_profile
logger = logging.getLogger(__name__)
_ALL_PROFILES = None
class BaseSecurityProfile:
@property
def identifier(self) -> str:
"""
Unique identifier for this profile.
"""
raise NotImplementedError()
@property
def verbose_name(self) -> str:
"""
Human-readable name (can be a ``gettext_lazy`` object).
"""
raise NotImplementedError()
def is_allowed(self, request) -> bool:
"""
Return whether a given request should be allowed.
"""
raise NotImplementedError()
class FullAccessSecurityProfile(BaseSecurityProfile):
class FullAccessSecurityProfile:
identifier = 'full'
verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)')
@@ -61,7 +34,7 @@ class FullAccessSecurityProfile(BaseSecurityProfile):
return True
class AllowListSecurityProfile(BaseSecurityProfile):
class AllowListSecurityProfile:
allowlist = ()
def is_allowed(self, request):
@@ -184,28 +157,88 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
)
def get_all_security_profiles():
global _ALL_PROFILES
if _ALL_PROFILES:
return _ALL_PROFILES
types = OrderedDict()
for recv, ret in register_device_security_profile.send(None):
if isinstance(ret, (list, tuple)):
for r in ret:
types[r.identifier] = r
else:
types[ret.identifier] = ret
_ALL_PROFILES = types
return types
@receiver(register_device_security_profile, dispatch_uid="base_register_default_security_profiles")
def register_default_webhook_events(sender, **kwargs):
return (
FullAccessSecurityProfile(),
PretixScanSecurityProfile(),
PretixScanNoSyncSecurityProfile(),
PretixScanNoSyncNoSearchSecurityProfile(),
class PretixPosSecurityProfile(AllowListSecurityProfile):
identifier = 'pretixpos'
verbose_name = _('pretixPOS')
allowlist = (
('GET', 'api-v1:version'),
('GET', 'api-v1:device.eventselection'),
('GET', 'api-v1:idempotency.query'),
('GET', 'api-v1:device.info'),
('POST', 'api-v1:device.update'),
('POST', 'api-v1:device.revoke'),
('POST', 'api-v1:device.roll'),
('GET', 'api-v1:event-list'),
('GET', 'api-v1:event-detail'),
('GET', 'api-v1:subevent-list'),
('GET', 'api-v1:subevent-detail'),
('GET', 'api-v1:itemcategory-list'),
('GET', 'api-v1:item-list'),
('GET', 'api-v1:question-list'),
('GET', 'api-v1:quota-list'),
('GET', 'api-v1:taxrule-list'),
('GET', 'api-v1:ticketlayout-list'),
('GET', 'api-v1:ticketlayoutitem-list'),
('GET', 'api-v1:badgelayout-list'),
('GET', 'api-v1:badgeitem-list'),
('GET', 'api-v1:voucher-list'),
('GET', 'api-v1:voucher-detail'),
('GET', 'api-v1:order-list'),
('POST', 'api-v1:order-list'),
('GET', 'api-v1:order-detail'),
('DELETE', 'api-v1:orderposition-detail'),
('PATCH', 'api-v1:orderposition-detail'),
('GET', 'api-v1:orderposition-list'),
('GET', 'api-v1:orderposition-answer'),
('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:orderposition-printlog'),
('POST', 'api-v1:order-mark-canceled'),
('POST', 'api-v1:orderpayment-list'),
('POST', 'api-v1:orderrefund-list'),
('POST', 'api-v1:orderrefund-done'),
('POST', 'api-v1:cartposition-list'),
('POST', 'api-v1:cartposition-bulk-create'),
('GET', 'api-v1:checkinlist-list'),
('POST', 'api-v1:checkinlistpos-redeem'),
('POST', 'plugins:pretix_posbackend:order.posprintlog'),
('POST', 'plugins:pretix_posbackend:order.poslock'),
('DELETE', 'plugins:pretix_posbackend:order.poslock'),
('DELETE', 'api-v1:cartposition-detail'),
('GET', 'api-v1:giftcard-list'),
('POST', 'api-v1:giftcard-transact'),
('PATCH', 'api-v1:giftcard-detail'),
('GET', 'plugins:pretix_posbackend:posclosing-list'),
('POST', 'plugins:pretix_posbackend:posreceipt-list'),
('POST', 'plugins:pretix_posbackend:posclosing-list'),
('POST', 'plugins:pretix_posbackend:posdebugdump-list'),
('POST', 'plugins:pretix_posbackend:posdebuglogentry-list'),
('POST', 'plugins:pretix_posbackend:posdebuglogentry-bulk-create'),
('GET', 'plugins:pretix_posbackend:poscashier-list'),
('POST', 'plugins:pretix_posbackend:stripeterminal.token'),
('POST', 'plugins:pretix_posbackend:stripeterminal.paymentintent'),
('PUT', 'plugins:pretix_posbackend:file.upload'),
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:blockedsecrets-list'),
('GET', 'api-v1:event.settings'),
('GET', 'plugins:pretix_seating:event.event'),
('GET', 'plugins:pretix_seating:event.event.subevent'),
('GET', 'plugins:pretix_seating:event.plan'),
('GET', 'plugins:pretix_seating:selection.simple'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
('POST', 'api-v1:reusablemedium-lookup'),
('GET', 'api-v1:reusablemedium-list'),
('POST', 'api-v1:reusablemedium-list'),
)
DEVICE_SECURITY_PROFILES = {
k.identifier: k() for k in (
FullAccessSecurityProfile,
PretixScanSecurityProfile,
PretixScanNoSyncSecurityProfile,
PretixScanNoSyncNoSearchSecurityProfile,
PretixPosSecurityProfile,
)
}

View File

@@ -29,7 +29,6 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.auth.devicesecurity import get_all_security_profiles
from pretix.api.serializers import AsymmetricField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField
@@ -298,7 +297,6 @@ class DeviceSerializer(serializers.ModelSerializer):
revoked = serializers.BooleanField(read_only=True)
initialized = serializers.DateTimeField(read_only=True)
initialization_token = serializers.DateTimeField(read_only=True)
security_profile = serializers.ChoiceField(choices=[], required=False, default="full")
class Meta:
model = Device
@@ -308,10 +306,6 @@ class DeviceSerializer(serializers.ModelSerializer):
'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile'
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['security_profile'].choices = [(k, v.verbose_name) for k, v in get_all_security_profiles().items()]
class TeamInviteSerializer(serializers.ModelSerializer):
class Meta:

View File

@@ -32,17 +32,10 @@ from pretix.helpers.periodic import minimum_interval
register_webhook_events = Signal()
"""
This signal is sent out to get all known webhook events. Receivers should return an
instance of a subclass of ``pretix.api.webhooks.WebhookEvent`` or a list of such
instance of a subclass of pretix.api.webhooks.WebhookEvent or a list of such
instances.
"""
register_device_security_profile = Signal()
"""
This signal is sent out to get all known device security_profiles. Receivers should
return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurityProfile``
or a list of such instances.
"""
@receiver(periodic_task)
@scopes_disabled()

View File

@@ -28,6 +28,7 @@ from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
from pretix.api.auth.devicesecurity import DEVICE_SECURITY_PROFILES
from pretix.base.models import LoggedModel
@@ -160,6 +161,7 @@ class Device(LoggedModel):
)
security_profile = models.CharField(
max_length=190,
choices=[(k, v.verbose_name) for k, v in DEVICE_SECURITY_PROFILES.items()],
default='full',
null=True,
blank=False

View File

@@ -609,6 +609,49 @@ class OrderFeeChangeForm(forms.Form):
change_decimal_field(self.fields['value'], instance.order.event.currency)
class OrderFeeAddForm(forms.Form):
fee_type = forms.ChoiceField(choices=OrderFee.FEE_TYPES)
value = forms.DecimalField(
max_digits=13, decimal_places=2,
localize=True,
label=_('Price'),
help_text=_("including all taxes"),
)
tax_rule = forms.ModelChoiceField(
TaxRule.objects.none(),
required=False,
)
description = forms.CharField(required=False)
def __init__(self, *args, **kwargs):
order = kwargs.pop('order')
super().__init__(*args, **kwargs)
self.fields['tax_rule'].queryset = order.event.tax_rules.all()
change_decimal_field(self.fields['value'], order.event.currency)
class OrderFeeAddFormset(forms.BaseFormSet):
def __init__(self, *args, **kwargs):
self.order = kwargs.pop('order', None)
super().__init__(*args, **kwargs)
def _construct_form(self, i, **kwargs):
kwargs['order'] = self.order
return super()._construct_form(i, **kwargs)
@property
def empty_form(self):
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
use_required_attribute=False,
order=self.order,
)
self.add_fields(form, None)
return form
class OrderContactForm(forms.ModelForm):
regenerate_secrets = forms.BooleanField(required=False, label=_('Invalidate secrets'),
help_text=_('Regenerates the order and ticket secrets. You will '

View File

@@ -54,7 +54,6 @@ from i18nfield.strings import LazyI18nString
from phonenumber_field.formfields import PhoneNumberField
from pytz import common_timezones
from pretix.api.auth.devicesecurity import get_all_security_profiles
from pretix.api.models import WebHook
from pretix.api.webhooks import get_all_webhook_events
from pretix.base.customersso.oidc import oidc_validate_and_complete_config
@@ -312,11 +311,6 @@ class DeviceForm(forms.ModelForm):
'-has_subevents', '-date_from'
)
self.fields['gate'].queryset = organizer.gates.all()
self.fields['security_profile'] = forms.ChoiceField(
label=self.fields['security_profile'].label,
help_text=self.fields['security_profile'].help_text,
choices=[(k, v.verbose_name) for k, v in get_all_security_profiles().items()],
)
def clean(self):
d = super().clean()
@@ -350,11 +344,6 @@ class DeviceBulkEditForm(forms.ModelForm):
'-has_subevents', '-date_from'
)
self.fields['gate'].queryset = organizer.gates.all()
self.fields['security_profile'] = forms.ChoiceField(
label=self.fields['security_profile'].label,
help_text=self.fields['security_profile'].help_text,
choices=[(k, v.verbose_name) for k, v in get_all_security_profiles().items()],
)
def clean(self):
d = super().clean()

View File

@@ -296,11 +296,11 @@
{% endfor %}
<div class="formset" data-formset data-formset-prefix="{{ add_formset.prefix }}">
{{ add_formset.management_form }}
{% bootstrap_formset_errors add_formset %}
<div class="formset" data-formset data-formset-prefix="{{ add_position_formset.prefix }}">
{{ add_position_formset.management_form }}
{% bootstrap_formset_errors add_position_formset %}
<div data-formset-body>
{% for add_form in add_formset %}
{% for add_form in add_position_formset %}
<div class="panel panel-default items" data-formset-form data-subevent="0">
<div class="panel-heading">
<h3 class="panel-title">
@@ -351,25 +351,25 @@
</button>
{% trans "Add product" %}
<div class="sr-only">
{{ add_formset.empty_form.id }}
{% bootstrap_field add_formset.empty_form.DELETE form_group_class="" layout="inline" %}
{{ add_position_formset.empty_form.id }}
{% bootstrap_field add_position_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
</h3>
</div>
<div class="panel-body">
<div class="form-horizontal">
{% bootstrap_field add_formset.empty_form.itemvar layout="control" %}
{% bootstrap_field add_formset.empty_form.price addon_after=request.event.currency layout="control" %}
{% if add_formset.empty_form.addon_to %}
{% bootstrap_field add_formset.empty_form.addon_to layout="control" %}
{% bootstrap_field add_position_formset.empty_form.itemvar layout="control" %}
{% bootstrap_field add_position_formset.empty_form.price addon_after=request.event.currency layout="control" %}
{% if add_position_formset.empty_form.addon_to %}
{% bootstrap_field add_position_formset.empty_form.addon_to layout="control" %}
{% endif %}
{% if add_formset.empty_form.subevent %}
{% bootstrap_field add_formset.empty_form.subevent layout="control" %}
{% if add_position_formset.empty_form.subevent %}
{% bootstrap_field add_position_formset.empty_form.subevent layout="control" %}
{% endif %}
{% if add_formset.empty_form.used_membership %}
{% bootstrap_field add_formset.empty_form.used_membership layout="control" %}
{% if add_position_formset.empty_form.used_membership %}
{% bootstrap_field add_position_formset.empty_form.used_membership layout="control" %}
{% endif %}
{% bootstrap_field add_formset.empty_form.seat layout="control" %}
{% bootstrap_field add_position_formset.empty_form.seat layout="control" %}
</div>
</div>
</div>
@@ -431,13 +431,77 @@
{% bootstrap_field fee.form.operation_cancel layout='inline' %}
{% if fee.fee_type == "payment" %}
<em class="text-danger">
{% trans "Manually modifying payment fees is discouraged since they might automatically be on subsequent order changes or when choosing a different payment method." %}
{% trans "Manually modifying payment fees is discouraged since they might automatically be updated on subsequent order changes or when choosing a different payment method." %}
</em>
{% endif %}
</div>
</div>
</div>
{% endfor %}
<div class="formset" data-formset data-formset-prefix="{{ add_fee_formset.prefix }}">
{{ add_fee_formset.management_form }}
{% bootstrap_formset_errors add_fee_formset %}
<div data-formset-body>
{% for add_form in add_fee_formset %}
<div class="panel panel-default items" data-formset-form data-subevent="0">
<div class="panel-heading">
<h3 class="panel-title">
<button type="button" class="btn btn-danger btn-xs pull-right flip"
data-formset-delete-button>
<i class="fa fa-trash"></i>
</button>
{% trans "Add fee" %}
<div class="sr-only">
{{ add_form.id }}
{% bootstrap_field add_form.DELETE form_group_class="" layout="inline" %}
</div>
</h3>
</div>
<div class="panel-body">
<div class="form-horizontal">
{% bootstrap_field add_form.fee_type layout='control' %}
{% bootstrap_field add_form.value addon_after=request.event.currency layout='control' %}
{% bootstrap_field add_form.tax_rule layout='control' %}
{% bootstrap_field add_form.description layout='control' %}
</div>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default items" data-formset-form data-subevent="0">
<div class="panel-heading">
<h3 class="panel-title">
<button type="button" class="btn btn-danger btn-xs pull-right flip"
data-formset-delete-button>
<i class="fa fa-trash"></i>
</button>
{% trans "Add fee" %}
<div class="sr-only">
{{ add_fee_formset.empty_form.id }}
{% bootstrap_field add_fee_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
</h3>
</div>
<div class="panel-body">
<div class="form-horizontal">
{% bootstrap_field add_fee_formset.empty_form.fee_type layout='control' %}
{% bootstrap_field add_fee_formset.empty_form.value addon_after=request.event.currency layout='control' %}
{% bootstrap_field add_fee_formset.empty_form.tax_rule layout='control' %}
{% bootstrap_field add_fee_formset.empty_form.description layout='control' %}
</div>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-primary" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add fee" %}</button>
</p>
</div>
<div class="panel panel-default items">
<div class="panel-heading">
<h3 class="panel-title">

View File

@@ -122,10 +122,11 @@ from pretix.control.forms.filter import (
)
from pretix.control.forms.orders import (
CancelForm, CommentForm, DenyForm, EventCancelForm, ExporterForm,
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeChangeForm,
OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
OrderRefundForm, OtherOperationsForm, ReactivateOrderForm,
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeAddForm,
OrderFeeAddFormset, OrderFeeChangeForm, OrderLocaleForm, OrderMailForm,
OrderPositionAddForm, OrderPositionAddFormset, OrderPositionChangeForm,
OrderPositionMailForm, OrderRefundForm, OtherOperationsForm,
ReactivateOrderForm,
)
from pretix.control.forms.rrule import RRuleForm
from pretix.control.permissions import EventPermissionRequiredMixin
@@ -1874,18 +1875,30 @@ class OrderChange(OrderView):
data=self.request.POST if self.request.method == "POST" else None)
@cached_property
def add_formset(self):
def add_position_formset(self):
ff = formset_factory(
OrderPositionAddForm, formset=OrderPositionAddFormset,
can_order=False, can_delete=True, extra=0
)
return ff(
prefix='add',
prefix='add_position',
order=self.order,
items=self.items,
data=self.request.POST if self.request.method == "POST" else None
)
@cached_property
def add_fee_formset(self):
ff = formset_factory(
OrderFeeAddForm, formset=OrderFeeAddFormset,
can_order=False, can_delete=True, extra=0
)
return ff(
prefix='add_fee',
order=self.order,
data=self.request.POST if self.request.method == "POST" else None
)
@cached_property
def items(self):
return self.request.event.items.prefetch_related('variations', 'tax_rule').all()
@@ -1914,7 +1927,8 @@ class OrderChange(OrderView):
ctx = super().get_context_data(**kwargs)
ctx['positions'] = self.positions
ctx['fees'] = self.fees
ctx['add_formset'] = self.add_formset
ctx['add_position_formset'] = self.add_position_formset
ctx['add_fee_formset'] = self.add_fee_formset
ctx['other_form'] = self.other_form
ctx['use_revocation_list'] = self.request.event.ticket_secret_generator.use_revocation_list
return ctx
@@ -1929,12 +1943,35 @@ class OrderChange(OrderView):
)
return True
def _process_add(self, ocm):
if not self.add_formset.is_valid():
def _process_add_fees(self, ocm):
if not self.add_fee_formset.is_valid():
return False
else:
for f in self.add_formset.forms:
if f in self.add_formset.deleted_forms or not f.has_changed():
for f in self.add_fee_formset.forms:
if f in self.add_fee_formset.deleted_forms or not f.has_changed():
continue
f = OrderFee(
fee_type=f.cleaned_data['fee_type'],
value=f.cleaned_data['value'],
order=ocm.order,
tax_rule=f.cleaned_data['tax_rule'],
description=f.cleaned_data['description'],
)
f._calculate_tax()
try:
ocm.add_fee(f)
except OrderError as e:
f.custom_error = str(e)
return False
return True
def _process_add_positions(self, ocm):
if not self.add_position_formset.is_valid():
return False
else:
for f in self.add_position_formset.forms:
if f in self.add_position_formset.deleted_forms or not f.has_changed():
continue
if '-' in f.cleaned_data['itemvar']:
@@ -1959,7 +1996,7 @@ class OrderChange(OrderView):
return False
return True
def _process_fees(self, ocm):
def _process_change_fees(self, ocm):
for f in self.fees:
if not f.form.is_valid():
return False
@@ -1980,7 +2017,7 @@ class OrderChange(OrderView):
return False
return True
def _process_change(self, ocm):
def _process_change_positions(self, ocm):
for p in self.positions:
if not p.form.is_valid():
return False
@@ -2061,7 +2098,11 @@ class OrderChange(OrderView):
notify=notify,
reissue_invoice=self.other_form.cleaned_data['reissue_invoice'] if self.other_form.is_valid() else True
)
form_valid = self._process_add(ocm) and self._process_fees(ocm) and self._process_change(ocm) and self._process_other(ocm)
form_valid = (self._process_add_fees(ocm) and
self._process_add_positions(ocm) and
self._process_change_fees(ocm) and
self._process_change_positions(ocm) and
self._process_other(ocm))
if not form_valid:
messages.error(self.request, _('An error occurred. Please see the details below.'))

View File

@@ -40,7 +40,7 @@ def mocker_context():
def get_redis_connection(alias="default", write=True):
worker_id = os.environ.get("PYTEST_XDIST_WORKER")
if worker_id.startswith("gw"):
if worker_id and worker_id.startswith("gw"):
redis_port = 1000 + int(worker_id.replace("gw", ""))
else:
redis_port = 1000

View File

@@ -1309,10 +1309,14 @@ class OrderChangeTests(SoupTest):
self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code
), {
'add-TOTAL_FORMS': '0',
'add-INITIAL_FORMS': '0',
'add-MIN_NUM_FORMS': '0',
'add-MAX_NUM_FORMS': '100',
'add_fee-TOTAL_FORMS': '0',
'add_fee-INITIAL_FORMS': '0',
'add_fee-MIN_NUM_FORMS': '0',
'add_fee-MAX_NUM_FORMS': '100',
'add_position-TOTAL_FORMS': '0',
'add_position-INITIAL_FORMS': '0',
'add_position-MIN_NUM_FORMS': '0',
'add_position-MAX_NUM_FORMS': '100',
'op-{}-itemvar'.format(self.op1.pk): str(self.shirt.pk),
'op-{}-price'.format(self.op1.pk): str('12.00'),
})
@@ -1341,10 +1345,14 @@ class OrderChangeTests(SoupTest):
self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code
), {
'add-TOTAL_FORMS': '0',
'add-INITIAL_FORMS': '0',
'add-MIN_NUM_FORMS': '0',
'add-MAX_NUM_FORMS': '100',
'add_fee-TOTAL_FORMS': '0',
'add_fee-INITIAL_FORMS': '0',
'add_fee-MIN_NUM_FORMS': '0',
'add_fee-MAX_NUM_FORMS': '100',
'add_position-TOTAL_FORMS': '0',
'add_position-INITIAL_FORMS': '0',
'add_position-MIN_NUM_FORMS': '0',
'add_position-MAX_NUM_FORMS': '100',
'op-{}-subevent'.format(self.op1.pk): str(se2.pk),
})
self.op1.refresh_from_db()
@@ -1373,10 +1381,14 @@ class OrderChangeTests(SoupTest):
self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code
), {
'add-TOTAL_FORMS': '0',
'add-INITIAL_FORMS': '0',
'add-MIN_NUM_FORMS': '0',
'add-MAX_NUM_FORMS': '100',
'add_fee-TOTAL_FORMS': '0',
'add_fee-INITIAL_FORMS': '0',
'add_fee-MIN_NUM_FORMS': '0',
'add_fee-MAX_NUM_FORMS': '100',
'add_position-TOTAL_FORMS': '0',
'add_position-INITIAL_FORMS': '0',
'add_position-MIN_NUM_FORMS': '0',
'add_position-MAX_NUM_FORMS': '100',
'op-{}-used_membership'.format(self.op1.pk): str(m_correct1.pk),
'op-{}-used_membership'.format(self.op2.pk): str(m_correct1.pk),
'op-{}-used_membership'.format(self.op3.pk): str(m_correct1.pk),
@@ -1389,10 +1401,14 @@ class OrderChangeTests(SoupTest):
self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code
), {
'add-TOTAL_FORMS': '0',
'add-INITIAL_FORMS': '0',
'add-MIN_NUM_FORMS': '0',
'add-MAX_NUM_FORMS': '100',
'add_fee-TOTAL_FORMS': '0',
'add_fee-INITIAL_FORMS': '0',
'add_fee-MIN_NUM_FORMS': '0',
'add_fee-MAX_NUM_FORMS': '100',
'add_position-TOTAL_FORMS': '0',
'add_position-INITIAL_FORMS': '0',
'add_position-MIN_NUM_FORMS': '0',
'add_position-MAX_NUM_FORMS': '100',
'op-{}-operation'.format(self.op1.pk): 'price',
'op-{}-itemvar'.format(self.op1.pk): str(self.ticket.pk),
'op-{}-price'.format(self.op1.pk): '24.00',
@@ -1409,10 +1425,14 @@ class OrderChangeTests(SoupTest):
self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code
), {
'add-TOTAL_FORMS': '0',
'add-INITIAL_FORMS': '0',
'add-MIN_NUM_FORMS': '0',
'add-MAX_NUM_FORMS': '100',
'add_fee-TOTAL_FORMS': '0',
'add_fee-INITIAL_FORMS': '0',
'add_fee-MIN_NUM_FORMS': '0',
'add_fee-MAX_NUM_FORMS': '100',
'add_position-TOTAL_FORMS': '0',
'add_position-INITIAL_FORMS': '0',
'add_position-MIN_NUM_FORMS': '0',
'add_position-MAX_NUM_FORMS': '100',
'op-{}-operation_cancel'.format(self.op1.pk): 'on',
})
self.order.refresh_from_db()
@@ -1424,13 +1444,17 @@ class OrderChangeTests(SoupTest):
self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code
), {
'add-TOTAL_FORMS': '1',
'add-INITIAL_FORMS': '0',
'add-MIN_NUM_FORMS': '0',
'add-MAX_NUM_FORMS': '100',
'add-0-itemvar': str(self.shirt.pk),
'add-0-do': 'on',
'add-0-price': '14.00',
'add_fee-TOTAL_FORMS': '0',
'add_fee-INITIAL_FORMS': '0',
'add_fee-MIN_NUM_FORMS': '0',
'add_fee-MAX_NUM_FORMS': '100',
'add_position-TOTAL_FORMS': '1',
'add_position-INITIAL_FORMS': '0',
'add_position-MIN_NUM_FORMS': '0',
'add_position-MAX_NUM_FORMS': '100',
'add_position-0-itemvar': str(self.shirt.pk),
'add_position-0-do': 'on',
'add_position-0-price': '14.00',
})
with scopes_disabled():
assert self.order.positions.count() == 3
@@ -1453,10 +1477,14 @@ class OrderChangeTests(SoupTest):
self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code
), {
'add-TOTAL_FORMS': '0',
'add-INITIAL_FORMS': '0',
'add-MIN_NUM_FORMS': '0',
'add-MAX_NUM_FORMS': '100',
'add_fee-TOTAL_FORMS': '0',
'add_fee-INITIAL_FORMS': '0',
'add_fee-MIN_NUM_FORMS': '0',
'add_fee-MAX_NUM_FORMS': '100',
'add_position-TOTAL_FORMS': '0',
'add_position-INITIAL_FORMS': '0',
'add_position-MIN_NUM_FORMS': '0',
'add_position-MAX_NUM_FORMS': '100',
'other-recalculate_taxes': 'net',
'op-{}-operation'.format(self.op1.pk): '',
'op-{}-operation'.format(self.op2.pk): '',
@@ -1489,10 +1517,14 @@ class OrderChangeTests(SoupTest):
self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code
), {
'add-TOTAL_FORMS': '0',
'add-INITIAL_FORMS': '0',
'add-MIN_NUM_FORMS': '0',
'add-MAX_NUM_FORMS': '100',
'add_fee-TOTAL_FORMS': '0',
'add_fee-INITIAL_FORMS': '0',
'add_fee-MIN_NUM_FORMS': '0',
'add_fee-MAX_NUM_FORMS': '100',
'add_position-TOTAL_FORMS': '0',
'add_position-INITIAL_FORMS': '0',
'add_position-MIN_NUM_FORMS': '0',
'add_position-MAX_NUM_FORMS': '100',
'other-recalculate_taxes': 'gross',
'op-{}-operation'.format(self.op1.pk): '',
'op-{}-operation'.format(self.op2.pk): '',
@@ -1517,10 +1549,14 @@ class OrderChangeTests(SoupTest):
self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code
), {
'add-TOTAL_FORMS': '0',
'add-INITIAL_FORMS': '0',
'add-MIN_NUM_FORMS': '0',
'add-MAX_NUM_FORMS': '100',
'add_fee-TOTAL_FORMS': '0',
'add_fee-INITIAL_FORMS': '0',
'add_fee-MIN_NUM_FORMS': '0',
'add_fee-MAX_NUM_FORMS': '100',
'add_position-TOTAL_FORMS': '0',
'add_position-INITIAL_FORMS': '0',
'add_position-MIN_NUM_FORMS': '0',
'add_position-MAX_NUM_FORMS': '100',
'op-{}-price'.format(self.op1.pk): '24.00',
'op-{}-operation'.format(self.op2.pk): '',
'op-{}-itemvar'.format(self.op2.pk): str(self.ticket.pk),
@@ -1544,10 +1580,14 @@ class OrderChangeTests(SoupTest):
self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code
), {
'add-TOTAL_FORMS': '0',
'add-INITIAL_FORMS': '0',
'add-MIN_NUM_FORMS': '0',
'add-MAX_NUM_FORMS': '100',
'add_fee-TOTAL_FORMS': '0',
'add_fee-INITIAL_FORMS': '0',
'add_fee-MIN_NUM_FORMS': '0',
'add_fee-MAX_NUM_FORMS': '100',
'add_position-TOTAL_FORMS': '0',
'add_position-INITIAL_FORMS': '0',
'add_position-MIN_NUM_FORMS': '0',
'add_position-MAX_NUM_FORMS': '100',
'op-{}-operation'.format(self.op1.pk): 'price',
'op-{}-itemvar'.format(self.op1.pk): str(self.ticket.pk),
'op-{}-price'.format(self.op1.pk): '24.00',
@@ -1563,6 +1603,34 @@ class OrderChangeTests(SoupTest):
self.op2.refresh_from_db()
assert self.order.total == self.op1.price + self.op2.price
def test_add_fee_success(self):
old_total = self.order.total
r = self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code
), {
'add_fee-TOTAL_FORMS': '1',
'add_fee-INITIAL_FORMS': '0',
'add_fee-MIN_NUM_FORMS': '0',
'add_fee-MAX_NUM_FORMS': '100',
'add_position-TOTAL_FORMS': '0',
'add_position-INITIAL_FORMS': '0',
'add_position-MIN_NUM_FORMS': '0',
'add_position-MAX_NUM_FORMS': '100',
'add_fee-0-do': 'on',
'add_fee-0-fee_type': 'other',
'add_fee-0-description': 'Surprise Fee',
'add_fee-0-value': '5.00',
})
assert r.status_code == 302
self.order.refresh_from_db()
with scopes_disabled():
fee = self.order.fees.get()
assert fee.fee_type == OrderFee.FEE_TYPE_OTHER
assert fee.description == 'Surprise Fee'
assert fee.value == Decimal('5.00')
assert not fee.canceled
assert self.order.total == old_total + 5
@pytest.mark.django_db
def test_check_vatid(client, env):

View File

@@ -405,11 +405,11 @@ class ItemDisplayTest(EventTestMixin, SoupTest):
SubEventItem.objects.create(subevent=se1, item=item, price=12)
resp = self.client.get('/%s/%s/%d/' % (self.orga.slug, self.event.slug, se1.pk))
self.assertIn("12.00", resp.rendered_content)
self.assertNotIn("15.00", resp.rendered_content)
self.assertIn("12.00", resp.rendered_content)
self.assertNotIn("15.00", resp.rendered_content)
resp = self.client.get('/%s/%s/%d/' % (self.orga.slug, self.event.slug, se2.pk))
self.assertIn("15.00", resp.rendered_content)
self.assertNotIn("12.00", resp.rendered_content)
self.assertIn("15.00", resp.rendered_content)
self.assertNotIn("12.00", resp.rendered_content)
def test_subevent_net_prices(self):
self.event.has_subevents = True
@@ -429,14 +429,14 @@ class ItemDisplayTest(EventTestMixin, SoupTest):
resp = self.client.get('/%s/%s/%d/' % (self.orga.slug, self.event.slug, se1.pk))
doc = BeautifulSoup(resp.rendered_content, "lxml")
self.assertIn("10.08", doc.text)
self.assertNotIn("12.00", doc.text)
self.assertNotIn("15.00", doc.text)
self.assertIn("10.08", doc.text)
self.assertNotIn("12.00", doc.text)
self.assertNotIn("15.00", doc.text)
resp = self.client.get('/%s/%s/%d/' % (self.orga.slug, self.event.slug, se2.pk))
doc = BeautifulSoup(resp.rendered_content, "lxml")
self.assertIn("12.61", doc.text)
self.assertNotIn("12.00", doc.text)
self.assertNotIn("15.00", doc.text)
self.assertIn("12.61", doc.text)
self.assertNotIn("12.00", doc.text)
self.assertNotIn("15.00", doc.text)
def test_variations_subevent_disabled(self):
self.event.has_subevents = True