Self-service refund form (#1135)

* Auto-refund

* Add missing template

* Notification for requested refund

* Model-level tests

* Add front-end tests

* Default to notify
This commit is contained in:
Raphael Michel
2019-01-18 17:24:42 +01:00
committed by GitHub
parent 80b5750756
commit 06eddb2c6d
24 changed files with 857 additions and 95 deletions

View File

@@ -0,0 +1,31 @@
# Generated by Django 2.1.5 on 2019-01-18 15:27
from django.db import migrations
def enable_notifications_for_everyone(apps, schema_editor):
NotificationSetting = apps.get_model('pretixbase', 'NotificationSetting')
User = apps.get_model('pretixbase', 'User')
create = []
for u in User.objects.iterator():
create.append(NotificationSetting(
user=u,
action_type='pretix.event.order.refund.requested',
event=None,
method='mail',
enabled=True
))
if len(create) > 200:
NotificationSetting.objects.bulk_create(create)
create.clear()
NotificationSetting.objects.bulk_create(create)
create.clear()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0105_auto_20190112_1512'),
]
operations = [
migrations.RunPython(enable_notifications_for_everyone, migrations.RunPython.noop)
]

View File

@@ -114,7 +114,15 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
def save(self, *args, **kwargs):
self.email = self.email.lower()
is_new = not self.pk
super().save(*args, **kwargs)
if is_new:
self.notification_settings.create(
action_type='pretix.event.order.refund.requested',
event=None,
method='mail',
enabled=True
)
def __str__(self):
return self.email

View File

@@ -28,6 +28,7 @@ from django_countries.fields import CountryField
from i18nfield.strings import LazyI18nString
from jsonfallback.fields import FallbackJSONField
from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.models import User
from pretix.base.reldate import RelativeDateWrapper
@@ -356,9 +357,106 @@ class Order(LockModel, LoggedModel):
def cancel_allowed(self):
return (
self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and self.positions.exists()
self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and self.count_positions
)
@cached_property
def user_cancel_deadline(self):
if self.status == Order.STATUS_PAID and self.total != Decimal('0.00'):
until = self.event.settings.get('cancel_allow_user_paid_until', as_type=RelativeDateWrapper)
else:
until = self.event.settings.get('cancel_allow_user_until', as_type=RelativeDateWrapper)
if until:
if self.event.has_subevents:
return min([
until.datetime(se)
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
])
else:
return until.datetime(self.event)
@cached_property
def user_cancel_fee(self):
fee = Decimal('0.00')
if self.event.settings.cancel_allow_user_paid_keep:
fee += self.event.settings.cancel_allow_user_paid_keep
if self.event.settings.cancel_allow_user_paid_keep_percentage:
fee += self.event.settings.cancel_allow_user_paid_keep_percentage / Decimal('100.0') * self.total
if self.event.settings.cancel_allow_user_paid_keep_fees:
fee += self.fees.filter(
fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE)
).aggregate(
s=Sum('value')
)['s'] or 0
return round_decimal(fee, self.event.currency)
@property
def user_cancel_allowed(self) -> bool:
"""
Returns whether or not this order can be canceled by the user.
"""
positions = list(self.positions.all().select_related('item'))
cancelable = all([op.item.allow_cancel for op in positions])
if not cancelable or not positions:
return False
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
return False
if self.status == Order.STATUS_PENDING:
return self.event.settings.cancel_allow_user
elif self.status == Order.STATUS_PAID:
if self.total == Decimal('0.00'):
return self.event.settings.cancel_allow_user
return self.event.settings.cancel_allow_user_paid
return False
def propose_auto_refunds(self, amount: Decimal, payments: list=None):
# Algorithm to choose which payments are to be refunded to create the least hassle
payments = payments or self.payments.filter(state=OrderPayment.PAYMENT_STATE_CONFIRMED)
for p in payments:
p.full_refund_possible = p.payment_provider.payment_refund_supported(p)
p.partial_refund_possible = p.payment_provider.payment_partial_refund_supported(p)
p.propose_refund = Decimal('0.00')
p.available_amount = p.amount - p.refunded_amount
unused_payments = set(p for p in payments if p.full_refund_possible or p.partial_refund_possible)
to_refund = amount
proposals = {}
while to_refund and unused_payments:
bigger = sorted([
p for p in unused_payments
if p.available_amount > to_refund
and p.partial_refund_possible
], key=lambda p: p.available_amount)
same = [
p for p in unused_payments
if p.available_amount == to_refund
and (p.full_refund_possible or p.partial_refund_possible)
]
smaller = sorted([
p for p in unused_payments
if p.available_amount < to_refund
and (p.full_refund_possible or p.partial_refund_possible)
], key=lambda p: p.available_amount, reverse=True)
if same:
payment = same[0]
proposals[payment] = payment.available_amount
to_refund -= payment.available_amount
unused_payments.remove(payment)
elif bigger:
payment = bigger[0]
proposals[payment] = to_refund
to_refund -= to_refund
unused_payments.remove(payment)
elif smaller:
payment = smaller[0]
proposals[payment] = payment.available_amount
to_refund -= payment.available_amount
unused_payments.remove(payment)
else:
break
return proposals
@staticmethod
def normalize_code(code):
tr = str.maketrans({
@@ -411,18 +509,6 @@ class Order(LockModel, LoggedModel):
return False # nothing there to modify
@property
def can_user_cancel(self) -> bool:
"""
Returns whether or not this order can be canceled by the user.
"""
positions = self.positions.all().select_related('item')
cancelable = all([op.item.allow_cancel for op in positions])
return (
self.status == Order.STATUS_PENDING
or (self.status == Order.STATUS_PAID and self.total == Decimal('0.00'))
) and self.event.settings.cancel_allow_user and cancelable and self.positions.exists()
@property
def is_expired_by_time(self):
return (

View File

@@ -241,6 +241,12 @@ def register_default_notification_types(sender, **kwargs):
_('External refund of payment'),
_('An external refund for {order.code} has occurred.')
),
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.refund.requested',
_('Refund requested'),
_('You have been requested to issue a refund for {order.code}.')
),
ActionRequiredNotificationType(
sender,
)

View File

@@ -31,7 +31,7 @@ from pretix.base.models.orders import (
)
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.models.tax import TaxedPrice
from pretix.base.payment import BasePaymentProvider
from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_qualified,
)
@@ -1364,11 +1364,59 @@ def perform_order(self, event: str, payment_provider: str, positions: List[str],
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
device=None, cancellation_fee=None):
device=None, cancellation_fee=None, try_auto_refund=False):
try:
try:
return _cancel_order(order, user, send_mail, api_token, device, oauth_application,
cancellation_fee)
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
cancellation_fee)
if try_auto_refund:
notify_admin = False
error = False
order = Order.objects.get(pk=order)
refund_amount = order.pending_sum * -1
proposals = order.propose_auto_refunds(refund_amount)
can_auto_refund = sum(proposals.values()) == refund_amount
if can_auto_refund:
for p, value in proposals.items():
with transaction.atomic():
r = order.refunds.create(
payment=p,
source=OrderRefund.REFUND_SOURCE_BUYER,
state=OrderRefund.REFUND_STATE_CREATED,
amount=value,
provider=p.provider
)
order.log_action('pretix.event.order.refund.created', {
'local_id': r.local_id,
'provider': r.provider,
})
try:
r.payment_provider.execute_refund(r)
except PaymentException as e:
with transaction.atomic():
r.state = OrderRefund.REFUND_STATE_FAILED
r.save()
order.log_action('pretix.event.order.refund.failed', {
'local_id': r.local_id,
'provider': r.provider,
'error': str(e)
})
error = True
notify_admin = True
else:
if r.state != OrderRefund.REFUND_STATE_DONE:
notify_admin = True
else:
notify_admin = True
if notify_admin:
order.log_action('pretix.event.order.refund.requested')
if error:
raise OrderError(
_('There was an error while trying to send the money back to you. Please contact the event organizer for further information.')
)
return ret
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):

View File

@@ -1,6 +1,7 @@
import json
from collections import OrderedDict
from datetime import datetime
from decimal import Decimal
from typing import Any
from django.conf import settings
@@ -224,6 +225,30 @@ DEFAULTS = {
'default': 'True',
'type': bool
},
'cancel_allow_user_until': {
'default': None,
'type': RelativeDateWrapper,
},
'cancel_allow_user_paid': {
'default': 'False',
'type': bool,
},
'cancel_allow_user_paid_keep': {
'default': '0.00',
'type': Decimal,
},
'cancel_allow_user_paid_keep_fees': {
'default': 'False',
'type': bool,
},
'cancel_allow_user_paid_keep_percentage': {
'default': '0.00',
'type': Decimal,
},
'cancel_allow_user_paid_until': {
'default': None,
'type': RelativeDateWrapper,
},
'contact_mail': {
'default': None,
'type': str

View File

@@ -435,6 +435,39 @@ class EventSettingsForm(SettingsForm):
)
class CancelSettingsForm(SettingsForm):
cancel_allow_user = forms.BooleanField(
label=_("Customers can cancel their unpaid orders"),
required=False
)
cancel_allow_user_until = RelativeDateTimeField(
label=_("Do not allow cancellations after"),
required=False
)
cancel_allow_user_paid = forms.BooleanField(
label=_("Customers can cancel their paid orders"),
help_text=_("Paid money will be automatically paid back if the payment method allows it. "
"Otherwise, a manual refund will be created for you to process manually."),
required=False
)
cancel_allow_user_paid_keep = forms.DecimalField(
label=_("Keep a fixed cancellation fee"),
required=False
)
cancel_allow_user_paid_keep_fees = forms.BooleanField(
label=_("Keep payment, shipping and service fees"),
required=False
)
cancel_allow_user_paid_keep_percentage = forms.DecimalField(
label=_("Keep a percentual cancellation fee"),
required=False
)
cancel_allow_user_paid_until = RelativeDateTimeField(
label=_("Do not allow cancellations after"),
required=False
)
class PaymentSettingsForm(SettingsForm):
payment_term_days = forms.IntegerField(
label=_('Payment term in days'),

View File

@@ -212,8 +212,10 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.quotaexceeded': _('The order could not be marked as paid: {message}'),
'pretix.event.order.refund.created': _('Refund {local_id} has been created.'),
'pretix.event.order.refund.created.externally': _('Refund {local_id} has been created by an external entity.'),
'pretix.event.order.refund.requested': _('The customer requested you to issue a refund.'),
'pretix.event.order.refund.done': _('Refund {local_id} has been completed.'),
'pretix.event.order.refund.canceled': _('Refund {local_id} has been canceled.'),
'pretix.event.order.refund.failed': _('Refund {local_id} has failed.'),
'pretix.control.auth.user.created': _('The user has been created.'),
'pretix.user.settings.2fa.enabled': _('Two-factor authentication has been enabled.'),
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),

View File

@@ -88,6 +88,14 @@ def get_event_navigation(request: HttpRequest):
}),
'active': url.url_name == 'event.settings.invoice',
},
{
'label': pgettext_lazy('action', 'Cancellation'),
'url': reverse('control:event.settings.cancel', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name == 'event.settings.cancel',
},
{
'label': _('Widget'),
'url': reverse('control:event.settings.widget', kwargs={

View File

@@ -0,0 +1,40 @@
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<h1>{% trans "Cancellation settings" %}</h1>
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "Cancellation of unpaid or free orders" %}</legend>
{% bootstrap_field form.cancel_allow_user layout="control" %}
{% bootstrap_field form.cancel_allow_user_until layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Cancellation of paid orders" %}</legend>
{% bootstrap_field form.cancel_allow_user_paid layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_keep layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_keep_percentage layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_keep_fees layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_until layout="control" %}
{% if not gets_notification %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
If a user requests cancels a paid order and the money can not be refunded automatically, e.g.
due to the selected payment method, you will need to take manual action. However, you have
currently turned off notifications for this event.
{% endblocktrans %}
<a href="{% url "control:user.settings.notifications" %}" class="btn btn-default">
{% trans "Change notification settings" %}
</a>
</div>
{% endif %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -150,7 +150,7 @@
<dd>
{% for i in invoices %}
<a href="{% url "control:event.invoice.download" invoice=i.pk event=request.event.slug organizer=request.event.organizer.slug %}">
{% if i.is_cancellation %}{% trans "Cancellation" %}{% else %}{% trans "Invoice" %}{% endif %}
{% if i.is_cancellation %}{% trans "Cancellation" context "invoice" %}{% else %}{% trans "Invoice" %}{% endif %}
{{ i.number }}</a> ({{ i.date|date:"SHORT_DATE_FORMAT" }})
{% if not i.canceled %}
<form class="form-inline helper-display-inline" method="post"

View File

@@ -124,6 +124,7 @@ urlpatterns = [
url(r'^settings/email/preview$', event.MailSettingsPreview.as_view(), name='event.settings.mail.preview'),
url(r'^settings/email/layoutpreview$', event.MailSettingsRendererPreview.as_view(),
name='event.settings.mail.preview.layout'),
url(r'^settings/cancel', event.CancelSettings.as_view(), name='event.settings.cancel'),
url(r'^settings/invoice$', event.InvoiceSettings.as_view(), name='event.settings.invoice'),
url(r'^settings/invoice/preview$', event.InvoicePreview.as_view(), name='event.settings.invoice.preview'),
url(r'^settings/display', event.DisplaySettings.as_view(), name='event.settings.display'),

View File

@@ -41,10 +41,10 @@ from pretix.base.signals import register_ticket_outputs
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import markdown_compile
from pretix.control.forms.event import (
CommentForm, DisplaySettingsForm, EventDeleteForm, EventMetaValueForm,
EventSettingsForm, EventUpdateForm, InvoiceSettingsForm, MailSettingsForm,
PaymentSettingsForm, ProviderForm, QuickSetupForm,
QuickSetupProductFormSet, TaxRuleForm, TaxRuleLineFormSet,
CancelSettingsForm, CommentForm, DisplaySettingsForm, EventDeleteForm,
EventMetaValueForm, EventSettingsForm, EventUpdateForm,
InvoiceSettingsForm, MailSettingsForm, PaymentSettingsForm, ProviderForm,
QuickSetupForm, QuickSetupProductFormSet, TaxRuleForm, TaxRuleLineFormSet,
TicketSettingsForm, WidgetCodeForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
@@ -407,6 +407,43 @@ class InvoiceSettings(EventSettingsViewMixin, EventSettingsFormView):
})
class CancelSettings(EventSettingsViewMixin, EventSettingsFormView):
model = Event
form_class = CancelSettingsForm
template_name = 'pretixcontrol/event/cancel.html'
permission = 'can_change_event_settings'
def get_success_url(self) -> str:
return reverse('control:event.settings.cancel', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug
})
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['gets_notification'] = self.request.user.notifications_send and (
(
self.request.user.notification_settings.filter(
event=self.request.event,
action_type='pretix.event.order.refund.requested',
enabled=True
).exists()
) or (
self.request.user.notification_settings.filter(
event__isnull=True,
action_type='pretix.event.order.refund.requested',
enabled=True
).exists() and not
self.request.user.notification_settings.filter(
event=self.request.event,
action_type='pretix.event.order.refund.requested',
enabled=False
).exists()
)
)
return ctx
class InvoicePreview(EventPermissionRequiredMixin, View):
permission = 'can_change_event_settings'

View File

@@ -583,51 +583,15 @@ class OrderRefundView(OrderView):
)
def choose_form(self):
payments = self.order.payments.filter(
state=OrderPayment.PAYMENT_STATE_CONFIRMED
)
for p in payments:
p.full_refund_possible = p.payment_provider.payment_refund_supported(p)
p.partial_refund_possible = p.payment_provider.payment_partial_refund_supported(p)
p.propose_refund = Decimal('0.00')
p.available_amount = p.amount - p.refunded_amount
unused_payments = set(p for p in payments if p.full_refund_possible or p.partial_refund_possible)
# Algorithm to choose which payments are to be refunded to create the least hassle
payments = list(self.order.payments.filter(state=OrderPayment.PAYMENT_STATE_CONFIRMED))
if self.start_form.cleaned_data.get('mode') == 'full':
to_refund = full_refund = self.order.payment_refund_sum
full_refund = self.order.payment_refund_sum
else:
to_refund = full_refund = self.start_form.cleaned_data.get('partial_amount')
while to_refund and unused_payments:
bigger = sorted([p for p in unused_payments if p.available_amount > to_refund],
key=lambda p: p.available_amount)
same = [p for p in unused_payments if p.available_amount == to_refund]
smaller = sorted([p for p in unused_payments if p.available_amount < to_refund],
key=lambda p: p.available_amount,
reverse=True)
if same:
for payment in same:
if payment.full_refund_possible or payment.partial_refund_possible:
payment.propose_refund = payment.available_amount
to_refund -= payment.available_amount
unused_payments.remove(payment)
break
elif bigger:
for payment in bigger:
if payment.partial_refund_possible:
payment.propose_refund = to_refund
to_refund -= to_refund
unused_payments.remove(payment)
break
elif smaller:
for payment in smaller:
if payment.full_refund_possible or payment.partial_refund_possible:
payment.propose_refund = payment.available_amount
to_refund -= payment.available_amount
unused_payments.remove(payment)
break
full_refund = self.start_form.cleaned_data.get('partial_amount')
proposals = self.order.propose_auto_refunds(full_refund, payments=payments)
to_refund = full_refund - sum(proposals.values())
for p in payments:
p.propose_refund = proposals.get(p, 0)
if 'perform' in self.request.POST:
refund_selected = Decimal('0.00')

View File

@@ -168,7 +168,7 @@
{% for i in invoices %}
<li>
<a href="{% eventurl event "presale:event.invoice.download" invoice=i.pk secret=order.secret order=order.code %}">
{% if i.is_cancellation %}{% trans "Cancellation" %}{% else %}{% trans "Invoice" %}{% endif %}
{% if i.is_cancellation %}{% trans "Cancellation" context "invoice" %}{% else %}{% trans "Invoice" %}{% endif %}
{{ i.number }}</a> ({{ i.date|date:"SHORT_DATE_FORMAT" }})
</li>
{% endfor %}
@@ -245,16 +245,57 @@
{% endif %}
<div class="clearfix"></div>
</div>
{% if order.can_user_cancel %}
<div class="row">
<div class="col-md-12 text-right">
<p>
<a href="{% eventurl event 'presale:event.order.cancel' secret=order.secret order=order.code %}"
{% if order.cancel_allowed %}
<div class="panel panel-primary cart">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Cancellation" context "action" %}
</h3>
</div>
<div class="panel-body">
{% if order.user_cancel_allowed %}
{% if order.status == "p" and order.total != 0 %}
{% if order.user_cancel_fee %}
<p>
{% blocktrans trimmed with fee=order.user_cancel_fee|money:request.event.currency %}
You can cancel this order. In this case, a cancellation fee of <strong>{{ fee }}</strong>
will be kept and you will receive a refund of the remainder to your original payment method.
{% endblocktrans %}
{% trans "This will invalidate all of your tickets." %}
</p>
{% else %}
<p>
{% blocktrans trimmed %}
You can cancel this order and receive a full refund to your original payment method.
{% endblocktrans %}
{% trans "This will invalidate all of your tickets." %}
</p>
{% endif %}
<a href="{% eventurl event 'presale:event.order.cancel' secret=order.secret order=order.code %}"
class="btn btn-danger">
<span class="fa fa-remove"></span>
{% trans "Cancel order" %}
</a>
{% else %}
<p>
{% blocktrans trimmed %}
You can cancel this order using the following button.
{% endblocktrans %}
{% trans "This will invalidate all of your tickets." %}
</p>
<a href="{% eventurl event 'presale:event.order.cancel' secret=order.secret order=order.code %}"
class="btn btn-danger">
<span class="fa fa-remove"></span>
{% trans "Cancel order" %}
</a>
</p>
<span class="fa fa-remove"></span>
{% trans "Cancel order" %}
</a>
{% endif %}
{% else %}
<p>
{% blocktrans trimmed %}
You can not cancel this order yourself. Please contact the event organizer for more information.
{% endblocktrans %}
</p>
{% endif %}
</div>
</div>
{% endif %}

View File

@@ -1,5 +1,6 @@
{% extends "pretixpresale/event/base.html" %}
{% load i18n %}
{% load money %}
{% load eventurl %}
{% block title %}{% trans "Cancel order" %}{% endblock %}
{% block content %}
@@ -8,9 +9,29 @@
Cancel order: {{ code }}
{% endblocktrans %}
</h2>
<p>{% blocktrans trimmed %}
Do you really want to cancel this order? You cannot revert this action.
{% endblocktrans %}</p>
<p>
{% blocktrans trimmed %}
Do you really want to cancel this order? You cannot revert this action.
{% endblocktrans %}
{% trans "This will invalidate all of your tickets." %}
</p>
{% if can_auto_refund %}
<p>
<strong>
{% blocktrans trimmed with amount=refund_amount|money:request.event.currency %}
The refund amount of {{ amount }} will automatically be sent back to your original payment method. Depending on the payment method,
please allow for up to two weeks before this appears on your statement.
{% endblocktrans %}
</strong>
</p>
{% else %}
<div class="alert alert-warning">
{% blocktrans trimmed with amount=refund_amount|money:request.event.currency %}
With to the payment method you used, the refund amount of {{ amount }} <strong>can not be sent back to you automatically</strong>. Instead, the
event organizer will need to initiate the transfer manually. Please be patient as this might take a bit longer.
{% endblocktrans %}
</div>
{% endif %}
<form method="post" action="{% eventurl request.event "presale:event.order.cancel.do" secret=order.secret order=order.code %}" data-asynctask>
{% csrf_token %}

View File

@@ -566,7 +566,7 @@ class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView):
self.kwargs = kwargs
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
if not self.order.can_user_cancel:
if not self.order.user_cancel_allowed:
messages.error(request, _('You cannot cancel this order.'))
return redirect(self.get_order_url())
return super().dispatch(request, *args, **kwargs)
@@ -577,6 +577,10 @@ class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['order'] = self.order
refund_amount = self.order.total - self.order.user_cancel_fee
proposals = self.order.propose_auto_refunds(refund_amount)
ctx['refund_amount'] = refund_amount
ctx['can_auto_refund'] = sum(proposals.values()) == refund_amount
return ctx
@@ -594,10 +598,13 @@ class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View):
def post(self, request, *args, **kwargs):
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
if not self.order.can_user_cancel:
if not self.order.user_cancel_allowed:
messages.error(request, _('You cannot cancel this order.'))
return redirect(self.get_order_url())
return self.do(self.order.pk)
fee = None
if self.order.status == Order.STATUS_PAID and self.order.total != Decimal('0.00'):
fee = self.order.user_cancel_fee
return self.do(self.order.pk, cancellation_fee=fee, try_auto_refund=True)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)