mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Merge pull request #989 from pretix/approvals
Require approval for orders of specific products
This commit is contained in:
@@ -79,7 +79,7 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
'position', 'picture', 'available_from', 'available_until',
|
||||
'require_voucher', 'hide_without_voucher', 'allow_cancel',
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations',
|
||||
'variations', 'addons', 'original_price')
|
||||
'variations', 'addons', 'original_price', 'require_approval')
|
||||
read_only_fields = ('has_variations', 'picture')
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
||||
@@ -213,7 +213,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
model = Order
|
||||
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
||||
'checkin_attention', 'last_modified', 'payments', 'refunds')
|
||||
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -35,8 +35,8 @@ from pretix.base.services.invoices import (
|
||||
)
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, cancel_order, extend_order,
|
||||
mark_order_expired, mark_order_refunded,
|
||||
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
|
||||
extend_order, mark_order_expired, mark_order_refunded,
|
||||
)
|
||||
from pretix.base.services.tickets import (
|
||||
get_cachedticket_for_order, get_cachedticket_for_position,
|
||||
@@ -52,7 +52,7 @@ class OrderFilter(FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ['code', 'status', 'email', 'locale']
|
||||
fields = ['code', 'status', 'email', 'locale', 'require_approval']
|
||||
|
||||
|
||||
class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
@@ -182,6 +182,42 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def approve(self, request, **kwargs):
|
||||
send_mail = request.data.get('send_email', True)
|
||||
|
||||
order = self.get_object()
|
||||
try:
|
||||
approve_order(
|
||||
order,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
auth=request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken)) else None,
|
||||
send_mail=send_mail,
|
||||
)
|
||||
except Quota.QuotaExceededException as e:
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except OrderError as e:
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def deny(self, request, **kwargs):
|
||||
send_mail = request.data.get('send_email', True)
|
||||
comment = request.data.get('comment', '')
|
||||
|
||||
order = self.get_object()
|
||||
try:
|
||||
deny_order(
|
||||
order,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
auth=request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken)) else None,
|
||||
send_mail=send_mail,
|
||||
comment=comment,
|
||||
)
|
||||
except OrderError as e:
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def mark_pending(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
|
||||
23
src/pretix/base/migrations/0100_item_require_approval.py
Normal file
23
src/pretix/base/migrations/0100_item_require_approval.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.1 on 2018-08-09 15:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0099_auto_20180807_0841'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='require_approval',
|
||||
field=models.BooleanField(default=False, help_text='If this product is part of an order, the order will be put into an "approval" state and will need to be confirmed by you before it can be paid and completed. You can use this e.g. for discounted tickets that are only available to specific groups.', verbose_name='Buying this product requires approval.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='require_approval',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -193,6 +193,8 @@ class Item(LoggedModel):
|
||||
:type checkin_attention: bool
|
||||
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
|
||||
:type original_price: decimal.Decimal
|
||||
:param require_approval: If set to ``True``, orders containing this product can only be processed and paid after approved by an administrator
|
||||
:type require_approval: bool
|
||||
"""
|
||||
|
||||
event = models.ForeignKey(
|
||||
@@ -280,6 +282,13 @@ class Item(LoggedModel):
|
||||
help_text=_('To buy this product, the user needs a voucher that applies to this product '
|
||||
'either directly or via a quota.')
|
||||
)
|
||||
require_approval = models.BooleanField(
|
||||
verbose_name=_('Buying this product requires approval'),
|
||||
default=False,
|
||||
help_text=_('If this product is part of an order, the order will be put into an "approval" state and '
|
||||
'will need to be confirmed by you before it can be paid and completed. You can use this e.g. for '
|
||||
'discounted tickets that are only available to specific groups.'),
|
||||
)
|
||||
hide_without_voucher = models.BooleanField(
|
||||
verbose_name=_('This product will only be shown if a voucher matching the product is redeemed.'),
|
||||
default=False,
|
||||
|
||||
@@ -88,6 +88,8 @@ class Order(LoggedModel):
|
||||
:type comment: str
|
||||
:param download_reminder_sent: A field to indicate whether a download reminder has been sent.
|
||||
:type download_reminder_sent: boolean
|
||||
:param require_approval: If set to ``True``, this order is pending approval by an organizer
|
||||
:type require_approval: bool
|
||||
:param meta_info: Additional meta information on the order, JSON-encoded.
|
||||
:type meta_info: str
|
||||
"""
|
||||
@@ -167,6 +169,9 @@ class Order(LoggedModel):
|
||||
last_modified = models.DateTimeField(
|
||||
auto_now=True, db_index=True
|
||||
)
|
||||
require_approval = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Order")
|
||||
@@ -231,7 +236,10 @@ class Order(LoggedModel):
|
||||
then=Value('1')),
|
||||
When(Q(status__in=(Order.STATUS_REFUNDED, Order.STATUS_CANCELED)) & Q(pending_sum_rc__lt=0),
|
||||
then=Value('1')),
|
||||
When(Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0),
|
||||
When(Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lt=0),
|
||||
then=Value('1')),
|
||||
When(Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0)
|
||||
& Q(require_approval=False),
|
||||
then=Value('1')),
|
||||
default=Value('0'),
|
||||
output_field=models.IntegerField()
|
||||
@@ -423,7 +431,10 @@ class Order(LoggedModel):
|
||||
"payment settings is over."),
|
||||
'late': _("The payment can not be accepted as it the order is expired and you configured that no late "
|
||||
"payments should be accepted in the payment settings."),
|
||||
'require_approval': _('This order is not yet approved by the event organizer.')
|
||||
}
|
||||
if self.require_approval:
|
||||
return error_messages['require_approval']
|
||||
term_last = self.payment_term_last
|
||||
if term_last:
|
||||
if now() > term_last:
|
||||
|
||||
@@ -237,7 +237,7 @@ def invoice_pdf_task(invoice: int):
|
||||
|
||||
|
||||
def invoice_qualified(order: Order):
|
||||
if order.total == Decimal('0.00'):
|
||||
if order.total == Decimal('0.00') or order.require_approval:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -169,6 +169,142 @@ def mark_order_expired(order, user=None, auth=None):
|
||||
return order
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def approve_order(order, user=None, send_mail: bool=True, auth=None):
|
||||
"""
|
||||
Mark this order as approved
|
||||
:param order: The order to change
|
||||
:param user: The user that performed the change
|
||||
"""
|
||||
if not order.require_approval or not order.status == Order.STATUS_PENDING:
|
||||
raise OrderError(_('This order is not pending approval.'))
|
||||
|
||||
order.require_approval = False
|
||||
order.set_expires(now(), order.event.subevents.filter(id__in=[p.subevent_id for p in order.positions.all()]))
|
||||
order.save()
|
||||
|
||||
order.log_action('pretix.event.order.approved', user=user, auth=auth)
|
||||
if order.total == Decimal('0.00'):
|
||||
p = order.payments.create(
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||
provider='free',
|
||||
amount=0,
|
||||
fee=None
|
||||
)
|
||||
try:
|
||||
p.confirm(send_mail=False, count_waitinglist=False, user=user, auth=auth)
|
||||
except Quota.QuotaExceededException:
|
||||
raise OrderError(error_messages['unavailable'])
|
||||
|
||||
invoice = order.invoices.last() # Might be generated by plugin already
|
||||
if order.event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
|
||||
if not invoice:
|
||||
generate_invoice(
|
||||
order,
|
||||
trigger_pdf=not order.event.settings.invoice_email_attachment or not order.email
|
||||
)
|
||||
# send_mail will trigger PDF generation later
|
||||
|
||||
if send_mail:
|
||||
try:
|
||||
invoice_name = order.invoice_address.name
|
||||
invoice_company = order.invoice_address.company
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
invoice_name = ""
|
||||
invoice_company = ""
|
||||
|
||||
with language(order.locale):
|
||||
if order.total == Decimal('0.00'):
|
||||
email_template = order.event.settings.mail_text_order_free
|
||||
email_subject = _('Order approved and confirmed: %(code)s') % {'code': order.code}
|
||||
else:
|
||||
email_template = order.event.settings.mail_text_order_approved
|
||||
email_subject = _('Order approved and awaiting payment: %(code)s') % {'code': order.code}
|
||||
|
||||
email_context = {
|
||||
'total': LazyNumber(order.total),
|
||||
'currency': order.event.currency,
|
||||
'total_with_currency': LazyCurrencyNumber(order.total, order.event.currency),
|
||||
'date': LazyDate(order.expires),
|
||||
'event': order.event.name,
|
||||
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
}),
|
||||
'invoice_name': invoice_name,
|
||||
'invoice_company': invoice_company,
|
||||
}
|
||||
try:
|
||||
order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_approved', user
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order approved email could not be sent')
|
||||
|
||||
return order.pk
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
|
||||
"""
|
||||
Mark this order as canceled
|
||||
:param order: The order to change
|
||||
:param user: The user that performed the change
|
||||
"""
|
||||
if not order.require_approval or not order.status == Order.STATUS_PENDING:
|
||||
raise OrderError(_('This order is not pending approval.'))
|
||||
|
||||
with order.event.lock():
|
||||
order.status = Order.STATUS_CANCELED
|
||||
order.save()
|
||||
|
||||
order.log_action('pretix.event.order.denied', user=user, auth=auth, data={
|
||||
'comment': comment
|
||||
})
|
||||
i = order.invoices.filter(is_cancellation=False).last()
|
||||
if i:
|
||||
generate_cancellation(i)
|
||||
|
||||
for position in order.positions.all():
|
||||
if position.voucher:
|
||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=F('redeemed') - 1)
|
||||
|
||||
if send_mail:
|
||||
try:
|
||||
invoice_name = order.invoice_address.name
|
||||
invoice_company = order.invoice_address.company
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
invoice_name = ""
|
||||
invoice_company = ""
|
||||
email_template = order.event.settings.mail_text_order_denied
|
||||
email_context = {
|
||||
'total': LazyNumber(order.total),
|
||||
'currency': order.event.currency,
|
||||
'total_with_currency': LazyCurrencyNumber(order.total, order.event.currency),
|
||||
'date': LazyDate(order.expires),
|
||||
'event': order.event.name,
|
||||
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
}),
|
||||
'comment': comment,
|
||||
'invoice_name': invoice_name,
|
||||
'invoice_company': invoice_company,
|
||||
}
|
||||
with language(order.locale):
|
||||
email_subject = _('Order denied: %(code)s') % {'code': order.code}
|
||||
try:
|
||||
order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_denied', user
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order denied email could not be sent')
|
||||
|
||||
return order.pk
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, oauth_application=None):
|
||||
"""
|
||||
@@ -342,7 +478,10 @@ def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvid
|
||||
meta_info: dict, event: Event):
|
||||
fees = []
|
||||
total = sum([c.price for c in positions])
|
||||
payment_fee = payment_provider.calculate_fee(total)
|
||||
if payment_provider:
|
||||
payment_fee = payment_provider.calculate_fee(total)
|
||||
else:
|
||||
payment_fee = 0
|
||||
pf = None
|
||||
if payment_fee:
|
||||
pf = OrderFee(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=payment_fee,
|
||||
@@ -370,6 +509,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
locale=locale,
|
||||
total=total,
|
||||
meta_info=json.dumps(meta_info or {}),
|
||||
require_approval=any(p.item.require_approval for p in positions)
|
||||
)
|
||||
order.set_expires(now_dt, event.subevents.filter(id__in=[p.subevent_id for p in positions]))
|
||||
order.save()
|
||||
@@ -389,12 +529,13 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
fee.tax_rule = None # TODO: deprecate
|
||||
fee.save()
|
||||
|
||||
order.payments.create(
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||
provider=payment_provider,
|
||||
amount=total,
|
||||
fee=pf
|
||||
)
|
||||
if payment_provider:
|
||||
order.payments.create(
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||
provider=payment_provider,
|
||||
amount=total,
|
||||
fee=pf
|
||||
)
|
||||
|
||||
OrderPosition.transform_cart_positions(positions, order)
|
||||
order.log_action('pretix.event.order.placed')
|
||||
@@ -410,9 +551,12 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
email: str, locale: str, address: int, meta_info: dict=None):
|
||||
|
||||
event = Event.objects.get(id=event)
|
||||
pprov = event.get_payment_providers().get(payment_provider)
|
||||
if not pprov:
|
||||
raise OrderError(error_messages['internal'])
|
||||
if payment_provider:
|
||||
pprov = event.get_payment_providers().get(payment_provider)
|
||||
if not pprov:
|
||||
raise OrderError(error_messages['internal'])
|
||||
else:
|
||||
pprov = None
|
||||
|
||||
if email == settings.PRETIX_EMAIL_NONE_VALUE:
|
||||
email = None
|
||||
@@ -445,7 +589,10 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
# send_mail will trigger PDF generation later
|
||||
|
||||
if order.email:
|
||||
if payment_provider == 'free':
|
||||
if order.require_approval:
|
||||
email_template = event.settings.mail_text_order_placed_require_approval
|
||||
log_entry = 'pretix.event.order.email.order_placed_require_approval'
|
||||
elif payment_provider == 'free':
|
||||
email_template = event.settings.mail_text_order_free
|
||||
log_entry = 'pretix.event.order.email.order_free'
|
||||
else:
|
||||
@@ -458,6 +605,12 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
invoice_name = ""
|
||||
invoice_company = ""
|
||||
|
||||
if pprov:
|
||||
payment_info = str(pprov.order_pending_mail_render(order))
|
||||
else:
|
||||
payment_info = None
|
||||
|
||||
email_context = {
|
||||
'total': LazyNumber(order.total),
|
||||
'currency': event.currency,
|
||||
@@ -468,7 +621,7 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
}),
|
||||
'payment_info': str(pprov.order_pending_mail_render(order)),
|
||||
'payment_info': payment_info,
|
||||
'invoice_name': invoice_name,
|
||||
'invoice_company': invoice_company,
|
||||
}
|
||||
@@ -489,7 +642,8 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
def expire_orders(sender, **kwargs):
|
||||
eventcache = {}
|
||||
|
||||
for o in Order.objects.filter(expires__lt=now(), status=Order.STATUS_PENDING).select_related('event'):
|
||||
for o in Order.objects.filter(expires__lt=now(), status=Order.STATUS_PENDING,
|
||||
require_approval=False).select_related('event'):
|
||||
expire = eventcache.get(o.event.pk, None)
|
||||
if expire is None:
|
||||
expire = o.event.settings.get('payment_term_expire_automatically', as_type=bool)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files import File
|
||||
@@ -7,7 +8,6 @@ from django.db.models import Model
|
||||
from django.utils.translation import ugettext_noop
|
||||
from hierarkey.models import GlobalSettingsBase, Hierarkey
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from typing import Any
|
||||
|
||||
from pretix.base.models.tax import TaxRule
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
@@ -270,8 +270,22 @@ Your {event} team"""))
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
|
||||
we successfully received your order for {event}. As you only ordered
|
||||
free products, no payment is required.
|
||||
your order for {event} was successful. As you only ordered free products,
|
||||
no payment is required.
|
||||
|
||||
You can change your order details and view the status of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_placed_require_approval': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
|
||||
we successfully received your order for {event}. Since you ordered
|
||||
a product that requires approval by the event organizer, we ask you to
|
||||
be patient and wait for our next email.
|
||||
|
||||
You can change your order details and view the status of your order at
|
||||
{url}
|
||||
@@ -370,6 +384,37 @@ your order {code} for {event} has been canceled.
|
||||
You can view the details of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_approved': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
|
||||
we approved your order for {event} and will be happy to welcome you
|
||||
at our event.
|
||||
|
||||
Please continue by paying for your order before {date}.
|
||||
|
||||
You can select a payment method and perform the payment here:
|
||||
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_denied': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
|
||||
unfortunately, we denied your order request for {event}.
|
||||
|
||||
{comment}
|
||||
|
||||
You can view the details of your order here:
|
||||
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
},
|
||||
|
||||
@@ -784,6 +784,34 @@ class MailSettingsForm(SettingsForm):
|
||||
help_text=_("This email will be sent out this many days before the order event starts. If the "
|
||||
"field is empty, the mail will never be sent.")
|
||||
)
|
||||
mail_text_order_placed_require_approval = I18nFormField(
|
||||
label=_("Received order"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {total_with_currency}, {total}, {currency}, {date}, "
|
||||
"{url}, {invoice_name}, {invoice_company}"),
|
||||
validators=[PlaceholderValidator(['{event}', '{total_with_currency}', '{total}', '{currency}', '{date}',
|
||||
'{url}', '{invoice_name}', '{invoice_company}'])]
|
||||
)
|
||||
mail_text_order_approved = I18nFormField(
|
||||
label=_("Approved order"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("This will only be sent out for non-free orders. Free orders will receive the free order "
|
||||
"template from above instead. Available placeholders: {event}, {total_with_currency}, {total}, "
|
||||
"{currency}, {date}, {payment_info}, {url}, {invoice_name}, {invoice_company}"),
|
||||
validators=[PlaceholderValidator(['{event}', '{total_with_currency}', '{total}', '{currency}', '{date}',
|
||||
'{url}', '{invoice_name}', '{invoice_company}'])]
|
||||
)
|
||||
mail_text_order_denied = I18nFormField(
|
||||
label=_("Denied order"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {total_with_currency}, {total}, {currency}, {date}, "
|
||||
"{comment}, {url}, {invoice_name}, {invoice_company}"),
|
||||
validators=[PlaceholderValidator(['{event}', '{total_with_currency}', '{total}', '{currency}', '{date}',
|
||||
'{comment}', '{url}', '{invoice_name}', '{invoice_company}'])]
|
||||
)
|
||||
smtp_use_custom = forms.BooleanField(
|
||||
label=_("Use custom SMTP server"),
|
||||
help_text=_("All mail related to your event will be sent over the smtp server specified by you."),
|
||||
|
||||
@@ -206,6 +206,7 @@ class EventOrderFilterForm(OrderFilterForm):
|
||||
('ne', _('Pending or expired')),
|
||||
('c', _('Canceled')),
|
||||
('r', _('Refunded')),
|
||||
('pa', _('Approval pending')),
|
||||
('overpaid', _('Overpaid')),
|
||||
('underpaid', _('Underpaid')),
|
||||
),
|
||||
@@ -275,13 +276,20 @@ class EventOrderFilterForm(OrderFilterForm):
|
||||
qs = qs.filter(
|
||||
Q(~Q(status__in=(Order.STATUS_REFUNDED, Order.STATUS_CANCELED)) & Q(pending_sum_t__lt=0))
|
||||
| Q(Q(status__in=(Order.STATUS_REFUNDED, Order.STATUS_CANCELED)) & Q(pending_sum_rc__lt=0))
|
||||
| Q(Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0))
|
||||
| Q(Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lt=0))
|
||||
| Q(Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0)
|
||||
& Q(require_approval=False))
|
||||
)
|
||||
elif fdata.get('status') == 'underpaid':
|
||||
qs = qs.filter(
|
||||
status=Order.STATUS_PAID,
|
||||
pending_sum_t__gt=0
|
||||
)
|
||||
elif fdata.get('status') == 'pa':
|
||||
qs = qs.filter(
|
||||
status=Order.STATUS_PENDING,
|
||||
require_approval=True
|
||||
)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
@@ -321,6 +321,7 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'available_from',
|
||||
'available_until',
|
||||
'require_voucher',
|
||||
'require_approval',
|
||||
'hide_without_voucher',
|
||||
'allow_cancel',
|
||||
'max_per_order',
|
||||
|
||||
@@ -165,6 +165,8 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.order.refunded': _('The order has been refunded.'),
|
||||
'pretix.event.order.canceled': _('The order has been canceled.'),
|
||||
'pretix.event.order.placed': _('The order has been created.'),
|
||||
'pretix.event.order.approved': _('The order has been approved.'),
|
||||
'pretix.event.order.denied': _('The order has been denied.'),
|
||||
'pretix.event.order.contact.changed': _('The email address has been changed from "{old_email}" '
|
||||
'to "{new_email}".'),
|
||||
'pretix.event.order.locale.changed': _('The order locale has been changed.'),
|
||||
@@ -185,7 +187,13 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.order.email.order_changed': _('An email has been sent to notify the user that the order has been changed.'),
|
||||
'pretix.event.order.email.order_free': _('An email has been sent to notify the user that the order has been received.'),
|
||||
'pretix.event.order.email.order_paid': _('An email has been sent to notify the user that payment has been received.'),
|
||||
'pretix.event.order.email.order_denied': _('An email has been sent to notify the user that the order has been denied.'),
|
||||
'pretix.event.order.email.order_approved': _('An email has been sent to notify the user that the order has '
|
||||
'been approved.'),
|
||||
'pretix.event.order.email.order_placed': _('An email has been sent to notify the user that the order has been received and requires payment.'),
|
||||
'pretix.event.order.email.order_placed_require_approval': _('An email has been sent to notify the user that '
|
||||
'the order has been received and requires '
|
||||
'approval.'),
|
||||
'pretix.event.order.email.resend': _('An email with a link to the order detail page has been resent to the user.'),
|
||||
'pretix.event.order.payment.confirmed': _('Payment {local_id} has been confirmed.'),
|
||||
'pretix.event.order.payment.canceled': _('Payment {local_id} has been canceled.'),
|
||||
|
||||
@@ -34,6 +34,15 @@
|
||||
class="btn btn-primary">{% trans "Show pending refunds" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_pending_approvals %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>pending approvals</strong> that you should take care of.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=pa"
|
||||
class="btn btn-primary">{% trans "Show orders pending approval" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if actions|length > 0 %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading">
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
|
||||
{% blocktrans asvar title_download_tickets_reminder %}Reminder to download tickets{% endblocktrans %}
|
||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_text_download_reminder" exclude="mail_days_download_reminder" %}
|
||||
|
||||
{% blocktrans asvar title_require_approval %}Order approval process{% endblocktrans %}
|
||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_text_order_placed_require_approval,mail_text_order_approved,mail_text_order_denied" %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Additional settings" %}</legend>
|
||||
{% bootstrap_field form.original_price addon_after=request.event.currency layout="control" %}
|
||||
{% bootstrap_field form.require_approval layout="control" %}
|
||||
{% for f in plugin_forms %}
|
||||
{% bootstrap_form f layout="control" %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}
|
||||
{% trans "Approve order" %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% trans "Approve order" %}
|
||||
</h1>
|
||||
<p>{% blocktrans trimmed %}
|
||||
Do you really want to approve this order?
|
||||
{% endblocktrans %}</p>
|
||||
|
||||
<form method="post" href="">
|
||||
{% csrf_token %}
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% trans "No, take me back" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<button class="btn btn-block btn-primary btn-lg" type="submit">
|
||||
{% trans "Yes, approve order" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
41
src/pretix/control/templates/pretixcontrol/order/deny.html
Normal file
41
src/pretix/control/templates/pretixcontrol/order/deny.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}
|
||||
{% trans "Deny order" %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% trans "Deny order" %}
|
||||
</h1>
|
||||
<p>{% blocktrans trimmed %}
|
||||
Do you really want to cancel this order? You cannot revert this action.
|
||||
{% endblocktrans %}</p>
|
||||
|
||||
<form method="post" href="">
|
||||
{% csrf_token %}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="send_email" value="on" checked="checked">
|
||||
{% trans "Notify user by e-mail" %}
|
||||
</label>
|
||||
</div>
|
||||
<p>
|
||||
<label>{% trans "Comment (will be sent to the user)" %}</label>
|
||||
<textarea name="comment" class="form-control" rows="5"></textarea>
|
||||
</p>
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% trans "No, take me back" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<button class="btn btn-block btn-danger btn-lg" type="submit">
|
||||
{% trans "Yes, deny order" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -24,27 +24,40 @@
|
||||
{% csrf_token %}
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
{% if order.status == 'n' or order.status == 'e' %}
|
||||
<a href="{% url "control:event.order.transition" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}?status=p"
|
||||
class="btn {% if overpaid >= 0 %}btn-primary{% else %}btn-default{% endif %}">
|
||||
{% trans "Mark as paid" %}
|
||||
{% if order.require_approval and order.status == 'n' %}
|
||||
<a href="{% url "control:event.order.approve" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"
|
||||
class="btn btn-primary">
|
||||
<span class="fa fa-thumbs-up"></span>
|
||||
{% trans "Approve" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.order.extend" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default">
|
||||
{% trans "Extend payment term" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if order.cancel_allowed %}
|
||||
<a href="{% url "control:event.order.transition" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}?status=c" class="btn btn-default">
|
||||
{% trans "Cancel order" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if order.status == 'p' %}
|
||||
<button name="status" value="n" class="btn btn-default">{% trans "Mark as not paid" %}</button>
|
||||
{% endif %}
|
||||
{% if overpaid|add:order.total != 0 %}
|
||||
<a href="{% url "control:event.order.refunds.start" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default">
|
||||
{% trans "Create a refund" %}
|
||||
<a href="{% url "control:event.order.deny" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"
|
||||
class="btn btn-danger">
|
||||
<span class="fa fa-thumbs-down"></span>
|
||||
{% trans "Deny" %}
|
||||
</a>
|
||||
{% else %}
|
||||
{% if order.status == 'n' or order.status == 'e' %}
|
||||
<a href="{% url "control:event.order.transition" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}?status=p"
|
||||
class="btn {% if overpaid >= 0 %}btn-primary{% else %}btn-default{% endif %}">
|
||||
{% trans "Mark as paid" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.order.extend" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default">
|
||||
{% trans "Extend payment term" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if order.cancel_allowed %}
|
||||
<a href="{% url "control:event.order.transition" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}?status=c" class="btn btn-default">
|
||||
{% trans "Cancel order" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if order.status == 'p' %}
|
||||
<button name="status" value="n" class="btn btn-default">{% trans "Mark as not paid" %}</button>
|
||||
{% endif %}
|
||||
{% if overpaid|add:order.total != 0 %}
|
||||
<a href="{% url "control:event.order.refunds.start" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default">
|
||||
{% trans "Create a refund" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<a href="{% eventurl request.event "presale:event.order" order=order.code secret=order.secret %}"
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% if order.status == "n" %}
|
||||
<span class="label label-warning {{ class }}">{% trans "Pending" %}</span>
|
||||
{% if order.require_approval %}
|
||||
<span class="label label-warning {{ class }}">{% trans "Approval pending" %}</span>
|
||||
{% else %}
|
||||
<span class="label label-warning {{ class }}">{% trans "Pending" %}</span>
|
||||
{% endif %}
|
||||
{% elif order.status == "p" %}
|
||||
<span class="label label-success {{ class }}">{% trans "Paid" %}</span>
|
||||
{% elif order.status == "e" %} {# expired #}
|
||||
|
||||
@@ -192,6 +192,10 @@ urlpatterns = [
|
||||
name='event.order.comment'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/change$', orders.OrderChange.as_view(),
|
||||
name='event.order.change'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/approve', orders.OrderApprove.as_view(),
|
||||
name='event.order.approve'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/deny$', orders.OrderDeny.as_view(),
|
||||
name='event.order.deny'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/info', orders.OrderModifyInformation.as_view(),
|
||||
name='event.order.info'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/sendmail$', orders.OrderSendMail.as_view(),
|
||||
|
||||
@@ -269,12 +269,18 @@ def event_index(request, organizer, event):
|
||||
ctx['has_overpaid_orders'] = Order.annotate_overpayments(request.event.orders).filter(
|
||||
Q(~Q(status__in=(Order.STATUS_REFUNDED, Order.STATUS_CANCELED)) & Q(pending_sum_t__lt=0))
|
||||
| Q(Q(status__in=(Order.STATUS_REFUNDED, Order.STATUS_CANCELED)) & Q(pending_sum_rc__lt=0))
|
||||
| Q(Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0))
|
||||
| Q(Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lt=0))
|
||||
| Q(Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0)
|
||||
& Q(require_approval=False))
|
||||
).exists()
|
||||
ctx['has_pending_refunds'] = OrderRefund.objects.filter(
|
||||
order__event=request.event,
|
||||
state__in=(OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_EXTERNAL)
|
||||
)
|
||||
).exists()
|
||||
ctx['has_pending_approvals'] = request.event.orders.filter(
|
||||
status=Order.STATUS_PENDING,
|
||||
require_approval=True
|
||||
).exists()
|
||||
|
||||
for a in ctx['actions']:
|
||||
a.display = a.display(request)
|
||||
|
||||
@@ -541,7 +541,13 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
'mail_text_order_canceled': ['code', 'event', 'url'],
|
||||
'mail_text_order_custom_mail': ['expire_date', 'event', 'code', 'date', 'url',
|
||||
'invoice_name', 'invoice_company'],
|
||||
'mail_text_download_reminder': ['event', 'url']
|
||||
'mail_text_download_reminder': ['event', 'url'],
|
||||
'mail_text_order_placed_require_approval': ['total', 'currency', 'date', 'invoice_company',
|
||||
'total_with_currency', 'event', 'url', 'invoice_name'],
|
||||
'mail_text_order_approved': ['total', 'currency', 'date', 'invoice_company',
|
||||
'total_with_currency', 'event', 'url', 'invoice_name'],
|
||||
'mail_text_order_denied': ['total', 'currency', 'date', 'invoice_company',
|
||||
'total_with_currency', 'event', 'url', 'invoice_name'],
|
||||
}
|
||||
|
||||
@cached_property
|
||||
@@ -566,6 +572,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
'code': '68CYU2H6ZTP3WLK5',
|
||||
'invoice_name': _('John Doe'),
|
||||
'invoice_company': _('Sample Corporation'),
|
||||
'common': _('An individial text with a reason can be inserted here.'),
|
||||
'payment_info': _('Please transfer money to this bank account: 9999-9999-9999-9999')
|
||||
}
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ from pretix.base.services.invoices import (
|
||||
from pretix.base.services.locking import LockTimeoutException
|
||||
from pretix.base.services.mail import SendMailException, render_mail
|
||||
from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, cancel_order, extend_order,
|
||||
mark_order_expired, mark_order_refunded,
|
||||
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
|
||||
extend_order, mark_order_expired, mark_order_refunded,
|
||||
)
|
||||
from pretix.base.services.stats import order_overview
|
||||
from pretix.base.signals import register_data_exporters
|
||||
@@ -228,6 +228,46 @@ class OrderComment(OrderView):
|
||||
return HttpResponseNotAllowed(['POST'])
|
||||
|
||||
|
||||
class OrderApprove(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
if self.order.require_approval:
|
||||
try:
|
||||
approve_order(self.order, user=self.request.user)
|
||||
except OrderError as e:
|
||||
messages.error(self.request, str(e))
|
||||
else:
|
||||
messages.success(self.request, _('The order has been approved.'))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return render(self.request, 'pretixcontrol/order/approve.html', {
|
||||
'order': self.order,
|
||||
})
|
||||
|
||||
|
||||
class OrderDeny(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
if self.order.require_approval:
|
||||
try:
|
||||
deny_order(self.order, user=self.request.user,
|
||||
comment=self.request.POST.get('comment'),
|
||||
send_mail=self.request.POST.get('send_email') == 'on')
|
||||
except OrderError as e:
|
||||
messages.error(self.request, str(e))
|
||||
else:
|
||||
messages.success(self.request, _('The order has been denied and is therefore now canceled.'))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return render(self.request, 'pretixcontrol/order/deny.html', {
|
||||
'order': self.order,
|
||||
})
|
||||
|
||||
|
||||
class OrderPaymentCancel(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
|
||||
@@ -503,6 +503,10 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
def is_applicable(self, request):
|
||||
self.request = request
|
||||
|
||||
for cartpos in get_cart(self.request):
|
||||
if cartpos.item.require_approval:
|
||||
return False
|
||||
|
||||
for p in self.request.event.get_payment_providers().values():
|
||||
if p.is_implicit:
|
||||
if self._is_allowed(p, request):
|
||||
@@ -530,8 +534,10 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['cart'] = self.get_cart(answers=True)
|
||||
ctx['payment'] = self.payment_provider.checkout_confirm_render(self.request)
|
||||
ctx['payment_provider'] = self.payment_provider
|
||||
if self.payment_provider:
|
||||
ctx['payment'] = self.payment_provider.checkout_confirm_render(self.request)
|
||||
ctx['payment_provider'] = self.payment_provider
|
||||
ctx['require_approval'] = any(cp.item.require_approval for cp in ctx['cart']['positions'])
|
||||
ctx['addr'] = self.invoice_address
|
||||
ctx['confirm_messages'] = self.confirm_messages
|
||||
ctx['cart_session'] = self.cart_session
|
||||
@@ -566,6 +572,8 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
|
||||
@cached_property
|
||||
def payment_provider(self):
|
||||
if 'payment' not in self.cart_session:
|
||||
return None
|
||||
return self.request.event.get_payment_providers().get(self.cart_session['payment'])
|
||||
|
||||
def get(self, request):
|
||||
@@ -599,7 +607,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
for receiver, response in order_meta_from_request.send(sender=request.event, request=request):
|
||||
meta_info.update(response)
|
||||
|
||||
return self.do(self.request.event.id, self.payment_provider.identifier,
|
||||
return self.do(self.request.event.id, self.payment_provider.identifier if self.payment_provider else None,
|
||||
[p.id for p in self.positions], self.cart_session.get('email'),
|
||||
translation.get_language(), self.invoice_address.pk, meta_info)
|
||||
|
||||
@@ -620,10 +628,16 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
return self.get_step_url(self.request)
|
||||
|
||||
def get_order_url(self, order):
|
||||
payment = order.payments.first()
|
||||
if not payment:
|
||||
return eventreverse(self.request.event, 'presale:event.order', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
}) + '?thanks=1'
|
||||
return eventreverse(self.request.event, 'presale:event.order.pay.complete', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
'payment': order.payments.first().pk
|
||||
'payment': payment.pk
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -49,24 +49,26 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
{% if payment_provider.identifier != "free" %}
|
||||
<div class="pull-right">
|
||||
<a href="{% eventurl request.event "presale:event.checkout" step="payment" cart_namespace=cart_namespace|default_if_none:"" %}">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Modify" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h3 class="panel-title">
|
||||
{% trans "Payment" %}
|
||||
</h3>
|
||||
{% if payment_provider %}
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
{% if payment_provider.identifier != "free" %}
|
||||
<div class="pull-right">
|
||||
<a href="{% eventurl request.event "presale:event.checkout" step="payment" cart_namespace=cart_namespace|default_if_none:"" %}">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Modify" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h3 class="panel-title">
|
||||
{% trans "Payment" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{ payment }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{ payment }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% eventsignal event "pretix.presale.signals.checkout_confirm_page_content" request=request %}
|
||||
<div class="row">
|
||||
{% if request.event.settings.invoice_address_asked %}
|
||||
@@ -155,6 +157,17 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if require_approval %}
|
||||
<div class="alert alert-warning alert-primary">
|
||||
<strong>
|
||||
{% trans "Your order requires approval by the event organizer before it can be confirmed and forms a valid contract." %}
|
||||
</strong>
|
||||
{% blocktrans trimmed %}
|
||||
We will sent you an email as soon as the event organizer approved or rejected your order. If your
|
||||
order was approved, we will send you a link that you can use to pay.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row checkout-button-row clearfix">
|
||||
<div class="col-md-4">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% if order.status == "n" %}
|
||||
<span class="label label-warning {{ class }}">{% trans "Payment pending" %}</span>
|
||||
{% if order.require_approval %}
|
||||
<span class="label label-warning {{ class }}">{% trans "Approval pending" %}</span>
|
||||
{% else %}
|
||||
<span class="label label-warning {{ class }}">{% trans "Payment pending" %}</span>
|
||||
{% endif %}
|
||||
{% elif order.status == "p" %}
|
||||
<span class="label label-success {{ class }}">{% trans "Paid" %}</span>
|
||||
{% elif order.status == "e" %}
|
||||
|
||||
@@ -14,9 +14,15 @@
|
||||
{% if order.status != 'p' %}
|
||||
<p>
|
||||
{% trans "Your order has been placed successfully. See below for details." %}<br>
|
||||
<strong>
|
||||
{% trans "Please note that we still await your payment to complete the process." %}
|
||||
</strong>
|
||||
{% if order.require_approval %}
|
||||
<strong>
|
||||
{% trans "Please note that we still await approval by the event organizer before you can pay and complete this order." %}
|
||||
</strong>
|
||||
{% else %}
|
||||
<strong>
|
||||
{% trans "Please note that we still await your payment to complete the process." %}
|
||||
</strong>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% elif order.total == 0 %}
|
||||
<p>{% trans "Your order has been processed successfully! See below for details." %}</p>
|
||||
@@ -41,7 +47,7 @@
|
||||
{% include "pretixpresale/event/fragment_order_status.html" with order=order class="pull-right" %}
|
||||
<div class="clearfix"></div>
|
||||
</h2>
|
||||
{% if order.status == "n" %}
|
||||
{% if order.status == "n" and not order.require_approval %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
|
||||
@@ -147,7 +147,7 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
|
||||
if lp.state == OrderPayment.PAYMENT_STATE_PENDING and not pp.abort_pending_allowed:
|
||||
ctx['can_pay'] = False
|
||||
|
||||
ctx['can_pay'] = ctx['can_pay'] and self.order._can_be_paid()
|
||||
ctx['can_pay'] = ctx['can_pay'] and self.order._can_be_paid() is True
|
||||
|
||||
elif self.order.status == Order.STATUS_PAID:
|
||||
ctx['can_pay'] = False
|
||||
@@ -168,7 +168,8 @@ class OrderPaymentStart(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||
if (self.order.status not in (Order.STATUS_PENDING, Order.STATUS_EXPIRED)
|
||||
or self.payment.state != OrderPayment.PAYMENT_STATE_CREATED
|
||||
or not self.payment.payment_provider.is_enabled):
|
||||
or not self.payment.payment_provider.is_enabled
|
||||
or self.order._can_be_paid() is not True):
|
||||
messages.error(request, _('The payment for this order cannot be continued.'))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
@@ -229,7 +230,7 @@ class OrderPaymentConfirm(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
self.request = request
|
||||
if not self.order:
|
||||
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||
if self.payment.state != OrderPayment.PAYMENT_STATE_CREATED:
|
||||
if self.payment.state != OrderPayment.PAYMENT_STATE_CREATED or not self.order._can_be_paid():
|
||||
messages.error(request, _('The payment for this order cannot be continued.'))
|
||||
return redirect(self.get_order_url())
|
||||
if (not self.payment.payment_provider.payment_is_valid_session(request) or
|
||||
@@ -286,7 +287,7 @@ class OrderPaymentComplete(EventViewMixin, OrderDetailMixin, View):
|
||||
self.request = request
|
||||
if not self.order:
|
||||
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||
if self.payment.state != OrderPayment.PAYMENT_STATE_CREATED:
|
||||
if self.payment.state != OrderPayment.PAYMENT_STATE_CREATED or not self.order._can_be_paid():
|
||||
messages.error(request, _('The payment for this order cannot be continued.'))
|
||||
return redirect(self.get_order_url())
|
||||
if (not self.payment.payment_provider.payment_is_valid_session(request) or
|
||||
@@ -329,7 +330,7 @@ class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
self.request = request
|
||||
if not self.order:
|
||||
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||
if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_EXPIRED):
|
||||
if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) or not self.order._can_be_paid():
|
||||
messages.error(request, _('The payment method for this order cannot be changed.'))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
|
||||
@@ -137,6 +137,12 @@
|
||||
font-size: 22px;
|
||||
padding-top: 14px;
|
||||
}
|
||||
.alert-primary::before {
|
||||
background: $brand-primary !important;
|
||||
}
|
||||
.alert-primary {
|
||||
border-color: $brand-primary !important;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
box-shadow: none;
|
||||
|
||||
Reference in New Issue
Block a user