mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Fix #571 -- Partial payments and refunds
This commit is contained in:
@@ -7,8 +7,8 @@ from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import (
|
||||
Checkin, Event, Invoice, Item, Order, OrderPosition, Organizer, Question,
|
||||
QuestionAnswer, SubEvent,
|
||||
Checkin, Event, Invoice, Item, Order, OrderPayment, OrderPosition,
|
||||
OrderRefund, Organizer, Question, QuestionAnswer, SubEvent,
|
||||
)
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.control.forms.widgets import Select2
|
||||
@@ -152,14 +152,21 @@ class OrderFilterForm(FilterForm):
|
||||
qs = qs.filter(status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
|
||||
elif s == 'ne':
|
||||
qs = qs.filter(status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED])
|
||||
else:
|
||||
elif s in ('p', 'n', 'e', 'c', 'r'):
|
||||
qs = qs.filter(status=s)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(self.get_order_by())
|
||||
|
||||
if fdata.get('provider'):
|
||||
qs = qs.filter(payment_provider=fdata.get('provider'))
|
||||
qs = qs.annotate(
|
||||
has_payment_with_provider=Exists(
|
||||
OrderPayment.objects.filter(
|
||||
Q(order=OuterRef('pk')) & Q(provider=fdata.get('provider'))
|
||||
)
|
||||
)
|
||||
)
|
||||
qs = qs.filter(has_payment_with_provider=1)
|
||||
|
||||
return qs
|
||||
|
||||
@@ -187,6 +194,23 @@ class EventOrderFilterForm(OrderFilterForm):
|
||||
answer = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
label=_('Order status'),
|
||||
choices=(
|
||||
('', _('All orders')),
|
||||
('p', _('Paid')),
|
||||
('n', _('Pending')),
|
||||
('o', _('Pending (overdue)')),
|
||||
('np', _('Pending or paid')),
|
||||
('e', _('Expired')),
|
||||
('ne', _('Pending or expired')),
|
||||
('c', _('Canceled')),
|
||||
('r', _('Refunded')),
|
||||
('overpaid', _('Overpaid')),
|
||||
('underpaid', _('Underpaid')),
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
@@ -247,6 +271,18 @@ class EventOrderFilterForm(OrderFilterForm):
|
||||
)
|
||||
qs = qs.annotate(has_answer=Exists(answers)).filter(has_answer=True)
|
||||
|
||||
if fdata.get('status') == 'overpaid':
|
||||
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_rc__lte=0))
|
||||
)
|
||||
elif fdata.get('status') == 'underpaid':
|
||||
qs = qs.filter(
|
||||
status=Order.STATUS_PAID,
|
||||
pending_sum_t__gt=0
|
||||
)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
@@ -793,3 +829,43 @@ class VoucherFilterForm(FilterForm):
|
||||
qs = qs.order_by(self.get_order_by())
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class RefundFilterForm(FilterForm):
|
||||
provider = forms.ChoiceField(
|
||||
label=_('Payment provider'),
|
||||
choices=[
|
||||
('', _('All payment providers')),
|
||||
],
|
||||
required=False,
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
label=_('Refund status'),
|
||||
choices=(
|
||||
('', _('All open refunds')),
|
||||
('all', _('All refunds')),
|
||||
) + OrderRefund.REFUND_STATES,
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['provider'].choices += [(k, v.verbose_name) for k, v
|
||||
in self.event.get_payment_providers().items()]
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
qs = super().filter_qs(qs)
|
||||
|
||||
if fdata.get('provider'):
|
||||
qs = qs.filter(provider=fdata.get('provider'))
|
||||
|
||||
if fdata.get('status'):
|
||||
if fdata.get('status') != 'all':
|
||||
qs = qs.filter(state=fdata.get('status'))
|
||||
else:
|
||||
qs = qs.filter(state__in=[OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT,
|
||||
OrderRefund.REFUND_STATE_EXTERNAL])
|
||||
|
||||
return qs
|
||||
|
||||
@@ -344,3 +344,47 @@ class OrderMailForm(forms.Form):
|
||||
validators=[PlaceholderValidator(['{expire_date}', '{event}', '{code}', '{date}', '{url}',
|
||||
'{invoice_name}', '{invoice_company}'])]
|
||||
)
|
||||
|
||||
|
||||
class OrderRefundForm(forms.Form):
|
||||
action = forms.ChoiceField(
|
||||
required=False,
|
||||
widget=forms.RadioSelect,
|
||||
choices=(
|
||||
('mark_refunded', _('Mark the complete order as refunded. The order will be canceled and all tickets will '
|
||||
'no longer work. This can not be reverted.')),
|
||||
('mark_pending', _('Mark the order as pending and allow the user to pay the open amount with another '
|
||||
'payment method.')),
|
||||
('do_nothing', _('Do nothing and keep the order as it is.')),
|
||||
)
|
||||
)
|
||||
mode = forms.ChoiceField(
|
||||
required=False,
|
||||
widget=forms.RadioSelect,
|
||||
choices=(
|
||||
('full', 'Full refund'),
|
||||
('partial', 'Partial refund'),
|
||||
)
|
||||
)
|
||||
partial_amount = forms.DecimalField(
|
||||
required=False, max_digits=10, decimal_places=2,
|
||||
localize=True
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.order = kwargs.pop('order')
|
||||
super().__init__(*args, **kwargs)
|
||||
change_decimal_field(self.fields['partial_amount'], self.order.event.currency)
|
||||
|
||||
def clean_partial_amount(self):
|
||||
max_amount = self.order.total - self.order.pending_sum
|
||||
val = self.cleaned_data.get('partial_amount')
|
||||
if val is not None and (val > max_amount or val <= 0):
|
||||
raise ValidationError(_('The refund amount needs to be positive and less than {}.').format(max_amount))
|
||||
return val
|
||||
|
||||
def clean(self):
|
||||
data = self.cleaned_data
|
||||
if data.get('mode') == 'partial' and not data.get('partial_amount'):
|
||||
raise ValidationError(_('You need to specify an amount for a partial refund.'))
|
||||
return data
|
||||
|
||||
@@ -173,7 +173,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.order.comment': _('The order\'s internal comment has been updated.'),
|
||||
'pretix.event.order.checkin_attention': _('The order\'s flag to require attention at check-in has been '
|
||||
'toggled.'),
|
||||
'pretix.event.order.payment.changed': _('The payment method has been changed.'),
|
||||
'pretix.event.order.payment.changed': _('A new payment {local_id} has been started instead of the previous one.'),
|
||||
'pretix.event.order.email.sent': _('An unidentified type email has been sent.'),
|
||||
'pretix.event.order.email.custom_sent': _('A custom email has been sent.'),
|
||||
'pretix.event.order.email.download_reminder_sent': _('An email has been sent with a reminder that the ticket '
|
||||
@@ -186,6 +186,13 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'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_placed': _('An email has been sent to notify the user that the order has been received and requires payment.'),
|
||||
'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.'),
|
||||
'pretix.event.order.payment.started': _('Payment {local_id} has been started.'),
|
||||
'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.done': _('Refund {local_id} has been completed.'),
|
||||
'pretix.event.order.refund.canceled': _('Refund {local_id} has been canceled.'),
|
||||
'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.'),
|
||||
|
||||
@@ -89,6 +89,12 @@
|
||||
{% trans "Overview" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'control:event.orders.refunds' organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
{% if url_name == "event.orders.refunds" %}class="active"{% endif %}>
|
||||
{% trans "Refunds" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'control:event.orders.export' organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
{% if url_name == "event.orders.export" %}class="active"{% endif %}>
|
||||
|
||||
@@ -15,6 +15,25 @@
|
||||
{% endif %}
|
||||
</small>
|
||||
</h1>
|
||||
{% if has_overpaid_orders %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>overpaid orders</strong>, for example due to duplicate payment attempts.
|
||||
You should review the cases and consider refunding the overpaid amount to the user.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=overpaid"
|
||||
class="btn btn-primary">{% trans "Show overpaid orders" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_pending_refunds %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>pending refunds</strong> that you should take care of.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders.refunds" event=request.event.slug organizer=request.event.organizer.slug %}"
|
||||
class="btn btn-primary">{% trans "Show pending refunds" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if actions|length > 0 %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading">
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
<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 btn-default">
|
||||
<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">
|
||||
@@ -36,10 +37,13 @@
|
||||
<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>
|
||||
{% elif order.status == 'p' %}
|
||||
{% endif %}
|
||||
{% if order.status == 'p' %}
|
||||
<button name="status" value="n" class="btn btn-default">{% trans "Mark as not paid" %}</button>
|
||||
<a href="{% url "control:event.order.transition" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}?status=r" class="btn btn-default">
|
||||
{% trans "Refund order" %}
|
||||
{% 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 %}
|
||||
|
||||
@@ -66,6 +70,26 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if overpaid > 0 %}
|
||||
<form action="{% url "control:event.order.refunds.start" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"
|
||||
method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="start-action" value="do_nothing">
|
||||
<input type="hidden" name="start-mode" value="partial">
|
||||
<input type="hidden" name="start-partial_amount" value="{{ overpaid }}">
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed with amount=overpaid|money:request.event.currency %}
|
||||
This order is currently overpaid by {{ amount }}.
|
||||
{% endblocktrans %}
|
||||
<button class="btn btn-primary" type="submit">
|
||||
{% blocktrans trimmed with amount=overpaid|money:request.event.currency %}
|
||||
Initiate a refund of {{ amount }}
|
||||
{% endblocktrans %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-lg-10">
|
||||
<div class="panel panel-primary items">
|
||||
@@ -87,10 +111,7 @@
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
</dd>
|
||||
{% if order.status == "p" %}
|
||||
<dt>{% trans "Payment date" %}</dt>
|
||||
<dd>{{ order.payment_date }}</dd>
|
||||
{% elif order.status == "n" %}
|
||||
{% if order.status == "n" %}
|
||||
<dt>{% trans "Expiry date" %}</dt>
|
||||
<dd>{{ order.expires }}</dd>
|
||||
{% endif %}
|
||||
@@ -207,7 +228,8 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if line.subevent %}
|
||||
<br /><span class="fa fa-calendar"></span> {{ line.subevent.name }} · {{ line.subevent.get_date_range_display }}
|
||||
<br/>
|
||||
<span class="fa fa-calendar"></span> {{ line.subevent.name }} · {{ line.subevent.get_date_range_display }}
|
||||
{% endif %}
|
||||
{% if line.has_questions %}
|
||||
<dl>
|
||||
@@ -239,7 +261,7 @@
|
||||
{{ q.answer.file_name }}
|
||||
</a>
|
||||
<span class="label label-danger" data-toggle="tooltip"
|
||||
title="{% trans "This file has been uploaded by a user and could contain viruses or other malicious content." %}">
|
||||
title="{% trans "This file has been uploaded by a user and could contain viruses or other malicious content." %}">
|
||||
{% trans "UNSAFE" %}
|
||||
</span>
|
||||
{% else %}
|
||||
@@ -262,7 +284,7 @@
|
||||
{% if event.settings.display_net_prices %}
|
||||
<strong>{{ line.net_price|money:event.currency }}</strong>
|
||||
{% if line.tax_rate %}
|
||||
<br />
|
||||
<br/>
|
||||
<small>
|
||||
{% blocktrans trimmed with rate=line.tax_rate taxname=line.tax_rule.name|default:s_taxes %}
|
||||
<strong>plus</strong> {{ rate }}% {{ taxname }}
|
||||
@@ -272,7 +294,7 @@
|
||||
{% else %}
|
||||
<strong>{{ line.price|money:event.currency }}</strong>
|
||||
{% if line.tax_rate and line.price %}
|
||||
<br />
|
||||
<br/>
|
||||
<small>
|
||||
{% blocktrans trimmed with rate=line.tax_rate taxname=line.tax_rule.name|default:s_taxes %}
|
||||
incl. {{ rate }}% {{ taxname }}
|
||||
@@ -350,28 +372,154 @@
|
||||
</div>
|
||||
</div>
|
||||
{% eventsignal event "pretix.control.signals.order_info" order=order request=request %}
|
||||
<div class="row">
|
||||
<div class="row payments">
|
||||
<div class="{% if request.event.settings.invoice_address_asked or order.invoice_address %}col-md-6{% else %}col-md-12{% endif %}">
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Payment information" %}
|
||||
{% trans "Payments" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if order.payment_manual %}
|
||||
<div class="alert alert-info">
|
||||
{% trans "The payment state of this order was manually modified." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ payment }}
|
||||
{% if order.status == 'n' %}
|
||||
<p>{% blocktrans trimmed with date=order.expires %}
|
||||
The payment has to be completed before {{ date }}.
|
||||
{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>{% trans "Start date" %}</th>
|
||||
<th>{% trans "Confirmation date" %}</th>
|
||||
<th>{% trans "Payment method" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th class="text-right">{% trans "Amount" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in payments %}
|
||||
<tr>
|
||||
<td>{{ p.full_id }}</td>
|
||||
<td>
|
||||
{% if p.migrated %}
|
||||
<span class="label label-default" data-toggle="tooltip"
|
||||
title="{% trans "This payment was created with an older version of pretix, therefore accurate data might not be available." %}">
|
||||
{% trans "MIGRATED" %}
|
||||
</span>
|
||||
{% else %}
|
||||
{{ p.created|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ p.payment_date|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td>
|
||||
{{ p.payment_provider.verbose_name }}
|
||||
{% if p.state == "pending" or p.state == "created" %}
|
||||
<a href="{% url "control:event.order.payments.cancel" event=request.event.slug organizer=request.event.organizer.slug code=order.code payment=p.pk %}"
|
||||
class="btn btn-danger btn-xs" data-toggle="tooltip"
|
||||
title="{% trans "Cancel payment" %}">
|
||||
<span class="fa fa-times"></span>
|
||||
</a>
|
||||
<a href="{% url "control:event.order.payments.confirm" event=request.event.slug organizer=request.event.organizer.slug code=order.code payment=p.pk %}"
|
||||
class="btn btn-primary btn-xs" data-toggle="tooltip"
|
||||
title="{% trans "Confirm as paid" %}">
|
||||
<span class="fa fa-check"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="label label-{% if p.state == "created" or p.state == "pending" %}warning{% elif p.state == "confirmed" %}success{% else %}danger{% endif %}">
|
||||
{{ p.get_state_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right">{{ p.amount|money:request.event.currency }}</td>
|
||||
</tr>
|
||||
{% if p.html_info %}
|
||||
<tr>
|
||||
<td colspan="1"></td>
|
||||
<td colspan="5">{{ p.html_info|safe }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if refunds %}
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Refunds" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>{% trans "Start date" %}</th>
|
||||
<th>{% trans "Completion date" %}</th>
|
||||
<th>{% trans "Source" %}</th>
|
||||
<th>{% trans "Payment method" %}</th>
|
||||
<th>{% trans "Payment" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th class="text-right">{% trans "Amount" %}</th>
|
||||
<th class="text-right"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in refunds %}
|
||||
<tr>
|
||||
<td>{{ r.full_id }}</td>
|
||||
<td>{{ r.created|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td>{{ r.execution_date|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td>{{ r.get_source_display }}</td>
|
||||
<td>
|
||||
{{ r.payment_provider.verbose_name }}
|
||||
</td>
|
||||
<td>
|
||||
{% if r.payment %}
|
||||
{{ r.payment.full_id }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="label label-{% if r.state == "external" or r.state == "transit" or r.state == "created" %}warning{% elif r.state == "done" %}success{% else %}danger{% endif %}">
|
||||
{{ r.get_state_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right">{{ r.amount|money:request.event.currency }}</td>
|
||||
<td class="text-right">
|
||||
{% if r.state == "transit" or r.state == "created" %}
|
||||
<a href="{% url "control:event.order.refunds.cancel" event=request.event.slug organizer=request.event.organizer.slug code=order.code refund=r.pk %}"
|
||||
class="btn btn-danger btn-xs" data-toggle="tooltip"
|
||||
title="{% trans "Cancel transfer" %}">
|
||||
<span class="fa fa-times"></span>
|
||||
</a>
|
||||
<a href="{% url "control:event.order.refunds.done" event=request.event.slug organizer=request.event.organizer.slug code=order.code refund=r.pk %}"
|
||||
class="btn btn-primary btn-xs" data-toggle="tooltip"
|
||||
title="{% trans "Confirm as done" %}">
|
||||
<span class="fa fa-check"></span>
|
||||
</a>
|
||||
{% elif r.state == "external" %}
|
||||
<a href="{% url "control:event.order.refunds.cancel" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
|
||||
class="btn btn-default btn-xs" data-toggle="tooltip">
|
||||
<span class="fa fa-times"></span>
|
||||
{% trans "Ignore" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.order.refunds.process" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
|
||||
class="btn btn-primary btn-xs"
|
||||
data-toggle="tooltip">
|
||||
<span class="fa fa-check"></span>
|
||||
{% trans "Process refund" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if request.event.settings.invoice_address_asked or order.invoice_address %}
|
||||
<div class="col-md-6">
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}
|
||||
{% trans "Refund order" %}
|
||||
{% trans "Cancel payment" %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% trans "Refund order" %}
|
||||
<a class="btn btn-link btn-lg"
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% blocktrans trimmed with order=order.code %}
|
||||
Back to order {{ order }}
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
{% trans "Cancel payment" %}
|
||||
</h1>
|
||||
<p>{% blocktrans trimmed %}
|
||||
Do you really want to cancel this payment? You cannot revert this action.
|
||||
{% endblocktrans %}</p>
|
||||
|
||||
<form method="post" href="">
|
||||
<p>{% blocktrans trimmed %}
|
||||
Do you really want to refund this order? You cannot revert this action.
|
||||
{% endblocktrans %}</p>
|
||||
|
||||
{{ payment|safe }}
|
||||
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="status" value="r" />
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
@@ -31,7 +22,7 @@
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<button class="btn btn-block btn-danger btn-lg" type="submit">
|
||||
{% trans "Yes, refund order" %}
|
||||
{% trans "Yes, cancel payment" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}
|
||||
{% trans "Mark payment as complete" %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% trans "Mark payment as complete" %}
|
||||
<a class="btn btn-link btn-lg"
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% blocktrans trimmed with order=order.code %}
|
||||
Back to order {{ order }}
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<form method="post" class="form-horizontal" href="">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
Do you really want to mark this payment as complete?
|
||||
{% endblocktrans %}</p>
|
||||
<input type="hidden" name="status" value="p" />
|
||||
{% bootstrap_form form layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
|
||||
<div class="form-group submit-group">
|
||||
<a class="btn btn-default btn-lg"
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button class="btn btn-primary btn-save btn-lg" type="submit">
|
||||
{% trans "Mark as paid" %}
|
||||
</button>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,38 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}
|
||||
{% trans "Cancel refund" %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% trans "Cancel refund" %}
|
||||
</h1>
|
||||
<p>{% blocktrans trimmed %}
|
||||
Do you really want to cancel this refund? You cannot revert this action.
|
||||
{% endblocktrans %}</p>
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
If the money is already on the way back, this will not stop the money, it will just mark this transfer as
|
||||
aborted in pretix. This will also not reactivate the order, it will just allow you to choose a new refund
|
||||
method.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
|
||||
<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-danger btn-lg" type="submit">
|
||||
{% trans "Yes, cancel refund" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,145 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% block title %}
|
||||
{% trans "Refund order" %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% trans "Refund order" %}
|
||||
<a class="btn btn-link btn-lg"
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% blocktrans trimmed with order=order.code %}
|
||||
Back to order {{ order }}
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</h1>
|
||||
<form method="post" href="">
|
||||
{% csrf_token %}
|
||||
<fieldset class="form-inline form-order-change">
|
||||
<legend>{% trans "How should the refund be sent?" %}</legend>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>{% trans "Payment confirmation date" %}</th>
|
||||
<th>{% trans "Payment method" %}</th>
|
||||
<th>{% trans "Amount not refunded" %}</th>
|
||||
<th>{% trans "Refund" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in payments %}
|
||||
<tr>
|
||||
<td>{{ p.full_id }}</td>
|
||||
<td>{{ p.payment_date|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td>
|
||||
{{ p.payment_provider.verbose_name }}
|
||||
</td>
|
||||
<td>{{ p.available_amount|money:request.event.currency }}</td>
|
||||
<td>
|
||||
{% if p.partial_refund_possible %}
|
||||
{% trans "Automatically refund" context "amount_label" %}
|
||||
<div class="input-group">
|
||||
<input type="text" name="refund-{{ p.pk }}"
|
||||
value="{{ p.propose_refund|floatformat:2 }}"
|
||||
title="" class="form-control">
|
||||
<span class="input-group-addon">
|
||||
{{ request.event.currency }}
|
||||
</span>
|
||||
</div>
|
||||
{% elif p.full_refund_possible %}
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="refund-{{ p.pk }}"
|
||||
value="{{ p.amount|floatformat:2 }}"
|
||||
{% if p.propose_refund == p.amount %}checked{% endif %}>
|
||||
{% trans "Automatically refund full amount" %}
|
||||
</label>
|
||||
{% else %}
|
||||
<em>{% trans "This payment method does not support automatic refunds." %}</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td><strong>{% trans "Transfer to other order" %}</strong></td>
|
||||
<td></td>
|
||||
<td>
|
||||
{% trans "Transfer" context "amount_label" %}
|
||||
<div class="input-group">
|
||||
<input type="text" name="refund-offsetting"
|
||||
title="" class="form-control" value="{{ 0|floatformat:2 }}">
|
||||
<span class="input-group-addon">
|
||||
{{ request.event.currency }}
|
||||
</span>
|
||||
</div>
|
||||
{% trans "to" context "order_label" %}
|
||||
<input type="text" name="order-offsetting"
|
||||
value="" title="" class="form-control">
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td><strong>{% trans "Manual refund" %}</strong></td>
|
||||
<td></td>
|
||||
<td>
|
||||
{% trans "Manually refund" context "amount_label" %}
|
||||
<div class="input-group">
|
||||
<input type="text" name="refund-manual"
|
||||
value="{{ remainder|floatformat:2 }}"
|
||||
title="" class="form-control">
|
||||
<span class="input-group-addon">
|
||||
{{ request.event.currency }}
|
||||
</span>
|
||||
</div>
|
||||
<label class="radio">
|
||||
<input type="radio" name="manual_state" value="created" checked>
|
||||
{% trans "Keep transfer as to do" %}
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="manual_state" value="done">
|
||||
{% trans "Mark refund as done" %}
|
||||
</label>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</fieldset>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Any payments that you selected for automatical refunds will be immediately communicate the refund
|
||||
request to the respective payment provider. Manual refunds will be created as pending refunds, you
|
||||
can then later mark them as done once you actually transferred the money back to the customer.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p> </p>
|
||||
|
||||
<input type="hidden" name="start-action" value="{{ start_form.cleaned_data.action }}">
|
||||
<input type="hidden" name="start-mode" value="{{ start_form.cleaned_data.mode }}">
|
||||
<input type="hidden" name="start-partial_amount" value="{{ partial_amount }}">
|
||||
|
||||
<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 "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<button class="btn btn-block btn-danger btn-lg" type="submit" name="perform" value="true">
|
||||
{% trans "Perform refund" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}
|
||||
{% trans "Mark refund as done" %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% trans "Mark refund as done" %}
|
||||
<a class="btn btn-link btn-lg"
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% blocktrans trimmed with order=order.code %}
|
||||
Back to order {{ order }}
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<form method="post" class="form-horizontal" href="">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
Do you really want to mark this refund as complete?
|
||||
{% endblocktrans %}</p>
|
||||
<div class="form-group submit-group">
|
||||
<a class="btn btn-default btn-lg"
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button class="btn btn-primary btn-save btn-lg" type="submit">
|
||||
{% trans "Mark as done" %}
|
||||
</button>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% block title %}
|
||||
{% trans "Mark refund as done" %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% trans "Mark refund as done" %}
|
||||
<a class="btn btn-link btn-lg"
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% blocktrans trimmed with order=order.code %}
|
||||
Back to order {{ order }}
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<form method="post" class="" action="">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
{% blocktrans trimmed with amount=refund.amount|money:request.event.currency method=refund.payment_provider.verbose_name pending=pending_sum|money:request.event.currency total=order.total|money:request.event.currency %}
|
||||
We recevied notice that <strong>{{ amount }}</strong> have been refunded via
|
||||
<strong>{{ method }}</strong>. If this refund is processed, the order will be underpaid by
|
||||
<strong>{{ pending }}</strong>. The order total is <strong>{{ total }}</strong>.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans trimmed with amount=refund.amount|money:request.event.currency method=refund.payment_provider.verbose_name %}
|
||||
What should happen to the ticket order?
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div class="form-inline">
|
||||
<label class="radio">
|
||||
<input type="radio" name="action" value="n" {% if not propose_cancel %}checked{% endif %}>
|
||||
{% trans "Mark the order as unpaid and allow the customer to pay again with another payment method." %}
|
||||
</label>
|
||||
<br>
|
||||
<label class="radio">
|
||||
<input type="radio" name="action" value="r" {% if propose_cancel %}checked{% endif %}>
|
||||
{% trans "Cancel the order irrevocably." %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
<a class="btn btn-default btn-lg"
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button class="btn btn-primary btn-save btn-lg" type="submit">
|
||||
{% trans "Process refund" %}
|
||||
</button>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,60 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}
|
||||
{% trans "Refund order" %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% trans "Refund order" %}
|
||||
<a class="btn btn-link btn-lg"
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% blocktrans trimmed with order=order.code %}
|
||||
Back to order {{ order }}
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</h1>
|
||||
<form method="post" href="">
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset class="form-inline form-order-change">
|
||||
<legend>{% trans "How much do you want to refund?" %}</legend>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ form.prefix }}-mode" type="radio" value="full"
|
||||
{% if not form.mode.value or form.mode.value == "full" %}checked="checked"{% endif %}>
|
||||
{% trans "Refund full paid amount" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ form.prefix }}-mode" type="radio" value="partial"
|
||||
{% if form.mode.value == "partial" %}checked="checked"{% endif %}>
|
||||
{% trans "Refund only" %}
|
||||
{% bootstrap_field form.partial_amount addon_after=request.event.currency layout='inline' %}
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<p> </p>
|
||||
<fieldset class="form-inline form-order-change">
|
||||
<legend>{% trans "What should happen to the order?" %}</legend>
|
||||
{% bootstrap_field form.action layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='' %}
|
||||
</fieldset>
|
||||
<p> </p>
|
||||
|
||||
{% 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 "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<button class="btn btn-block btn-primary btn-lg" type="submit">
|
||||
{% trans "Continue" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -127,7 +127,19 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td class="text-right">{{ o.total|money:request.event.currency }}</td>
|
||||
<td class="text-right">
|
||||
{% if o.has_external_refund or o.has_pending_refund %}
|
||||
<span class="label label-danger">{% trans "REFUND PENDING" %}</span>
|
||||
{% elif o.has_pending_refund %}
|
||||
<span class="label label-warning">{% trans "REFUND PENDING" %}</span>
|
||||
{% endif %}
|
||||
{% if o.is_overpaid %}
|
||||
<span class="label label-warning">{% trans "OVERPAID" %}</span>
|
||||
{% elif o.is_underpaid %}
|
||||
<span class="label label-danger">{% trans "UNDERPAID" %}</span>
|
||||
{% endif %}
|
||||
{{ o.total|money:request.event.currency }}
|
||||
</td>
|
||||
<td class="text-right">{{ o.pcnt }}</td>
|
||||
<td class="text-right">{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}</td>
|
||||
</tr>
|
||||
|
||||
104
src/pretix/control/templates/pretixcontrol/orders/refunds.html
Normal file
104
src/pretix/control/templates/pretixcontrol/orders/refunds.html
Normal file
@@ -0,0 +1,104 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% load money %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Refunds" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Refunds" %}</h1>
|
||||
<div class="row filter-form">
|
||||
<form class="" action="" method="get">
|
||||
<div class="col-md-5 col-xs-6">
|
||||
{% bootstrap_field filter_form.status layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-5 col-xs-6">
|
||||
{% bootstrap_field filter_form.provider layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-12">
|
||||
<button class="btn btn-primary btn-block" type="submit">
|
||||
<span class="fa fa-filter"></span>
|
||||
<span class="hidden-md">
|
||||
{% trans "Filter" %}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% if not filter_form.filtered and refunds|length == 0 %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
No refunds are currently open.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>{% trans "Payment provider" %}</th>
|
||||
<th>{% trans "Start date" %}</th>
|
||||
<th>{% trans "Source" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th class="text-right">{% trans "Amount" %}</th>
|
||||
<th class="text-right">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in refunds %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code %}">
|
||||
{{ r.order.code }}</a>-R-{{ r.local_id }}
|
||||
</strong>
|
||||
</td>
|
||||
<td>
|
||||
{{ r.payment_provider.verbose_name }}
|
||||
</td>
|
||||
<td>{{ o.created|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td>{{ r.get_source_display }}</td>
|
||||
<td>
|
||||
<span class="label label-{% if r.state == "external" or r.state == "transit" or r.state == "created" %}warning{% elif r.state == "done" %}success{% else %}danger{% endif %}">
|
||||
{{ r.get_state_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ r.amount|money:request.event.currency }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if r.state == "transit" or r.state == "created" %}
|
||||
<a href="{% url "control:event.order.refunds.cancel" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
|
||||
class="btn btn-danger btn-xs" data-toggle="tooltip">
|
||||
<span class="fa fa-times"></span>
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.order.refunds.done" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
|
||||
class="btn btn-primary btn-xs" data-toggle="tooltip">
|
||||
<span class="fa fa-check"></span>
|
||||
{% trans "Confirm as done" %}
|
||||
</a>
|
||||
{% elif r.state == "external" %}
|
||||
<a href="{% url "control:event.order.refunds.cancel" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
|
||||
class="btn btn-default btn-xs" data-toggle="tooltip">
|
||||
<span class="fa fa-times"></span>
|
||||
{% trans "Ignore" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.order.refunds.process" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
|
||||
class="btn btn-primary btn-xs" data-toggle="tooltip">
|
||||
<span class="fa fa-check"></span>
|
||||
{% trans "Process refund" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -198,12 +198,25 @@ urlpatterns = [
|
||||
name='event.order.sendmail'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/mail_history$', orders.OrderEmailHistory.as_view(),
|
||||
name='event.order.mail_history'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/payments/(?P<payment>\d+)/cancel$', orders.OrderPaymentCancel.as_view(),
|
||||
name='event.order.payments.cancel'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/payments/(?P<payment>\d+)/confirm$', orders.OrderPaymentConfirm.as_view(),
|
||||
name='event.order.payments.confirm'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/refund$', orders.OrderRefundView.as_view(),
|
||||
name='event.order.refunds.start'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/refunds/(?P<refund>\d+)/cancel$', orders.OrderRefundCancel.as_view(),
|
||||
name='event.order.refunds.cancel'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/refunds/(?P<refund>\d+)/process$', orders.OrderRefundProcess.as_view(),
|
||||
name='event.order.refunds.process'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/refunds/(?P<refund>\d+)/done$', orders.OrderRefundDone.as_view(),
|
||||
name='event.order.refunds.done'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/$', orders.OrderDetail.as_view(), name='event.order'),
|
||||
url(r'^invoice/(?P<invoice>[^/]+)$', orders.InvoiceDownload.as_view(),
|
||||
name='event.invoice.download'),
|
||||
url(r'^orders/overview/$', orders.OverView.as_view(), name='event.orders.overview'),
|
||||
url(r'^orders/export/$', orders.ExportView.as_view(), name='event.orders.export'),
|
||||
url(r'^orders/export/do$', orders.ExportDoView.as_view(), name='event.orders.export.do'),
|
||||
url(r'^orders/refunds/$', orders.RefundList.as_view(), name='event.orders.refunds'),
|
||||
url(r'^orders/go$', orders.OrderGo.as_view(), name='event.orders.go'),
|
||||
url(r'^orders/$', orders.OrderList.as_view(), name='event.orders'),
|
||||
url(r'^shredder/$', shredder.StartShredView.as_view(), name='event.shredder.start'),
|
||||
|
||||
@@ -17,7 +17,7 @@ from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext, ugettext_lazy as _, ungettext
|
||||
|
||||
from pretix.base.models import (
|
||||
Item, Order, OrderPosition, RequiredAction, SubEvent, Voucher,
|
||||
Item, Order, OrderPosition, OrderRefund, RequiredAction, SubEvent, Voucher,
|
||||
WaitingListEntry,
|
||||
)
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
@@ -266,6 +266,16 @@ def event_index(request, organizer, event):
|
||||
'comment_form': CommentForm(initial={'comment': request.event.comment})
|
||||
}
|
||||
|
||||
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_rc__lte=0))
|
||||
).exists()
|
||||
ctx['has_pending_refunds'] = OrderRefund.objects.filter(
|
||||
order__event=request.event,
|
||||
state__in=(OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_EXTERNAL)
|
||||
)
|
||||
|
||||
for a in ctx['actions']:
|
||||
a.display = a.display(request)
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal, DecimalException
|
||||
|
||||
import pytz
|
||||
import vat_moss.id
|
||||
@@ -9,12 +11,14 @@ from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.files import File
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import transaction
|
||||
from django.db.models import Count
|
||||
from django.http import FileResponse, Http404, HttpResponseNotAllowed
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils import formats
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.http import is_safe_url
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import (
|
||||
@@ -29,8 +33,9 @@ from pretix.base.models import (
|
||||
generate_position_secret, generate_secret,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.orders import OrderFee
|
||||
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
|
||||
from pretix.base.models.tax import EU_COUNTRIES
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.services.export import export
|
||||
from pretix.base.services.invoices import (
|
||||
generate_cancellation, generate_invoice, invoice_pdf, invoice_pdf_task,
|
||||
@@ -40,17 +45,18 @@ 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_paid,
|
||||
mark_order_expired, mark_order_refunded,
|
||||
)
|
||||
from pretix.base.services.stats import order_overview
|
||||
from pretix.base.signals import register_data_exporters
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.views.async import AsyncAction
|
||||
from pretix.base.views.mixins import OrderQuestionsViewMixin
|
||||
from pretix.control.forms.filter import EventOrderFilterForm
|
||||
from pretix.control.forms.filter import EventOrderFilterForm, RefundFilterForm
|
||||
from pretix.control.forms.orders import (
|
||||
CommentForm, ExporterForm, ExtendForm, MarkPaidForm, OrderContactForm,
|
||||
OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
||||
OrderPositionChangeForm, OtherOperationsForm,
|
||||
OrderPositionChangeForm, OrderRefundForm, OtherOperationsForm,
|
||||
)
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views import PaginationMixin
|
||||
@@ -71,6 +77,9 @@ class OrderList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
qs = Order.objects.filter(
|
||||
event=self.request.event
|
||||
).annotate(pcnt=Count('positions', distinct=True)).select_related('invoice_address')
|
||||
|
||||
qs = Order.annotate_overpayments(qs)
|
||||
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
|
||||
@@ -116,10 +125,6 @@ class OrderView(EventPermissionRequiredMixin, DetailView):
|
||||
)
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def payment_provider(self):
|
||||
return self.request.event.get_payment_providers().get(self.order.payment_provider)
|
||||
|
||||
def get_order_url(self):
|
||||
return reverse('control:event.order', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
@@ -136,19 +141,19 @@ class OrderDetail(OrderView):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['items'] = self.get_items()
|
||||
ctx['event'] = self.request.event
|
||||
ctx['payment_provider'] = self.payment_provider
|
||||
if self.payment_provider:
|
||||
ctx['payment'] = self.payment_provider.order_control_render(self.request, self.object)
|
||||
else:
|
||||
ctx['payment'] = mark_safe('<div class="alert alert-danger">{}</div>'.format(
|
||||
_('This order was paid using a payment provider plugin that is now disabled or uninstalled.')
|
||||
))
|
||||
ctx['payments'] = self.order.payments.order_by('-created')
|
||||
ctx['refunds'] = self.order.refunds.select_related('payment').order_by('-created')
|
||||
for p in ctx['payments']:
|
||||
if p.payment_provider:
|
||||
p.html_info = (p.payment_provider.payment_control_render(self.request, p) or "").strip()
|
||||
ctx['invoices'] = list(self.order.invoices.all().select_related('event'))
|
||||
ctx['comment_form'] = CommentForm(initial={
|
||||
'comment': self.order.comment,
|
||||
'checkin_attention': self.order.checkin_attention
|
||||
})
|
||||
ctx['display_locale'] = dict(settings.LANGUAGES)[self.object.locale or self.request.event.settings.locale]
|
||||
|
||||
ctx['overpaid'] = self.order.pending_sum * -1
|
||||
return ctx
|
||||
|
||||
def get_items(self):
|
||||
@@ -223,6 +228,390 @@ class OrderComment(OrderView):
|
||||
return HttpResponseNotAllowed(['POST'])
|
||||
|
||||
|
||||
class OrderPaymentCancel(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
@cached_property
|
||||
def payment(self):
|
||||
return get_object_or_404(self.order.payments, pk=self.kwargs['payment'])
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
if self.payment.state in (OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING):
|
||||
with transaction.atomic():
|
||||
self.payment.state = OrderPayment.PAYMENT_STATE_CANCELED
|
||||
self.payment.save()
|
||||
self.order.log_action('pretix.event.order.payment.canceled', {
|
||||
'local_id': self.payment.local_id,
|
||||
'provider': self.payment.provider,
|
||||
}, user=self.request.user)
|
||||
messages.success(self.request, _('This payment has been canceled.'))
|
||||
else:
|
||||
messages.error(self.request, _('This payment can not be canceled at the moment.'))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return render(self.request, 'pretixcontrol/order/pay_cancel.html', {
|
||||
'order': self.order,
|
||||
})
|
||||
|
||||
|
||||
class OrderRefundCancel(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
@cached_property
|
||||
def refund(self):
|
||||
return get_object_or_404(self.order.refunds, pk=self.kwargs['refund'])
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
if self.refund.state in (OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT,
|
||||
OrderRefund.REFUND_STATE_EXTERNAL):
|
||||
with transaction.atomic():
|
||||
self.refund.state = OrderRefund.REFUND_STATE_CANCELED
|
||||
self.refund.save()
|
||||
self.order.log_action('pretix.event.order.refund.canceled', {
|
||||
'local_id': self.refund.local_id,
|
||||
'provider': self.refund.provider,
|
||||
}, user=self.request.user)
|
||||
messages.success(self.request, _('The refund has been canceled.'))
|
||||
else:
|
||||
messages.error(self.request, _('This refund can not be canceled at the moment.'))
|
||||
if "next" in self.request.GET and is_safe_url(self.request.GET.get("next")):
|
||||
return redirect(self.request.GET.get("next"))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return render(self.request, 'pretixcontrol/order/refund_cancel.html', {
|
||||
'order': self.order,
|
||||
})
|
||||
|
||||
|
||||
class OrderRefundProcess(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
@cached_property
|
||||
def refund(self):
|
||||
return get_object_or_404(self.order.refunds, pk=self.kwargs['refund'])
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
if self.refund.state == OrderRefund.REFUND_STATE_EXTERNAL:
|
||||
self.refund.done(user=self.request.user)
|
||||
|
||||
if self.request.POST.get("action") == "r":
|
||||
mark_order_refunded(self.order, user=self.request.user)
|
||||
else:
|
||||
self.order.status = Order.STATUS_PENDING
|
||||
self.order.set_expires(
|
||||
now(),
|
||||
self.order.event.subevents.filter(
|
||||
id__in=self.order.positions.values_list('subevent_id', flat=True))
|
||||
)
|
||||
self.order.save()
|
||||
|
||||
messages.success(self.request, _('The refund has been processed.'))
|
||||
else:
|
||||
messages.error(self.request, _('This refund can not be processed at the moment.'))
|
||||
if "next" in self.request.GET and is_safe_url(self.request.GET.get("next")):
|
||||
return redirect(self.request.GET.get("next"))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return render(self.request, 'pretixcontrol/order/refund_process.html', {
|
||||
'order': self.order,
|
||||
'refund': self.refund,
|
||||
'pending_sum': self.order.pending_sum + self.refund.amount,
|
||||
'propose_cancel': self.order.pending_sum + self.refund.amount >= self.order.total
|
||||
})
|
||||
|
||||
|
||||
class OrderRefundDone(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
@cached_property
|
||||
def refund(self):
|
||||
return get_object_or_404(self.order.refunds, pk=self.kwargs['refund'])
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
if self.refund.state in (OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT):
|
||||
self.refund.done(user=self.request.user)
|
||||
messages.success(self.request, _('The refund has been marked as done.'))
|
||||
else:
|
||||
messages.error(self.request, _('This refund can not be processed at the moment.'))
|
||||
if "next" in self.request.GET and is_safe_url(self.request.GET.get("next")):
|
||||
return redirect(self.request.GET.get("next"))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return render(self.request, 'pretixcontrol/order/refund_done.html', {
|
||||
'order': self.order,
|
||||
})
|
||||
|
||||
|
||||
class OrderPaymentConfirm(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
@cached_property
|
||||
def payment(self):
|
||||
return get_object_or_404(self.order.payments, pk=self.kwargs['payment'])
|
||||
|
||||
@cached_property
|
||||
def mark_paid_form(self):
|
||||
return MarkPaidForm(
|
||||
instance=self.order,
|
||||
data=self.request.POST if self.request.method == "POST" else None,
|
||||
)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
if self.payment.state in (OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING):
|
||||
if not self.mark_paid_form.is_valid():
|
||||
return render(self.request, 'pretixcontrol/order/pay_complete.html', {
|
||||
'form': self.mark_paid_form,
|
||||
'order': self.order,
|
||||
})
|
||||
try:
|
||||
self.payment.confirm(user=self.request.user,
|
||||
count_waitinglist=False,
|
||||
force=self.mark_paid_form.cleaned_data.get('force', False))
|
||||
except Quota.QuotaExceededException as e:
|
||||
messages.error(self.request, str(e))
|
||||
except PaymentException as e:
|
||||
messages.error(self.request, str(e))
|
||||
except SendMailException:
|
||||
messages.warning(self.request,
|
||||
_('The payment has been marked as complete, but we were unable to send a '
|
||||
'confirmation mail.'))
|
||||
else:
|
||||
messages.success(self.request, _('The payment has been marked as complete.'))
|
||||
else:
|
||||
messages.error(self.request, _('This payment can not be confirmed at the moment.'))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return render(self.request, 'pretixcontrol/order/pay_complete.html', {
|
||||
'form': self.mark_paid_form,
|
||||
'order': self.order,
|
||||
})
|
||||
|
||||
|
||||
class OrderRefundView(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
@cached_property
|
||||
def start_form(self):
|
||||
return OrderRefundForm(
|
||||
order=self.order,
|
||||
data=self.request.POST if self.request.method == "POST" else None,
|
||||
prefix='start',
|
||||
initial={
|
||||
'partial_amount': self.order.total - self.order.pending_sum,
|
||||
'action': (
|
||||
'mark_pending' if self.order.status == Order.STATUS_PAID
|
||||
else 'do_nothing'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
if self.start_form.is_valid():
|
||||
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
|
||||
if self.start_form.cleaned_data.get('mode') == 'full':
|
||||
to_refund = full_refund = self.order.total - self.order.pending_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
|
||||
|
||||
if 'perform' in self.request.POST:
|
||||
refund_selected = Decimal('0.00')
|
||||
refunds = []
|
||||
|
||||
is_valid = True
|
||||
manual_value = self.request.POST.get('refund-manual', '0') or '0'
|
||||
manual_value = formats.sanitize_separators(manual_value)
|
||||
try:
|
||||
manual_value = Decimal(manual_value)
|
||||
except (DecimalException, TypeError) as e:
|
||||
messages.error(self.request, _('You entered an invalid number.'))
|
||||
is_valid = False
|
||||
else:
|
||||
refund_selected += manual_value
|
||||
if manual_value:
|
||||
refunds.append(OrderRefund(
|
||||
order=self.order,
|
||||
payment=None,
|
||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||
state=(
|
||||
OrderRefund.REFUND_STATE_DONE
|
||||
if self.request.POST.get('manual_state') == 'done'
|
||||
else OrderRefund.REFUND_STATE_CREATED
|
||||
),
|
||||
amount=manual_value,
|
||||
provider='manual'
|
||||
))
|
||||
|
||||
offsetting_value = self.request.POST.get('refund-offsetting', '0') or '0'
|
||||
offsetting_value = formats.sanitize_separators(offsetting_value)
|
||||
try:
|
||||
offsetting_value = Decimal(offsetting_value)
|
||||
except (DecimalException, TypeError) as e:
|
||||
messages.error(self.request, _('You entered an invalid number.'))
|
||||
is_valid = False
|
||||
else:
|
||||
if offsetting_value:
|
||||
refund_selected += offsetting_value
|
||||
try:
|
||||
order = Order.objects.get(code=self.request.POST.get('order-offsetting'))
|
||||
except Order.DoesNotExist:
|
||||
messages.error(self.request, _('You entered an order that could not be found.'))
|
||||
is_valid = False
|
||||
else:
|
||||
refunds.append(OrderRefund(
|
||||
order=self.order,
|
||||
payment=None,
|
||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||
state=OrderRefund.REFUND_STATE_DONE,
|
||||
execution_date=now(),
|
||||
amount=offsetting_value,
|
||||
provider='offsetting',
|
||||
info=json.dumps({
|
||||
'orders': [order.code]
|
||||
})
|
||||
))
|
||||
|
||||
for p in payments:
|
||||
value = self.request.POST.get('refund-{}'.format(p.pk), '0') or '0'
|
||||
value = formats.sanitize_separators(value)
|
||||
try:
|
||||
value = Decimal(value)
|
||||
except (DecimalException, TypeError) as e:
|
||||
messages.error(self.request, _('You entered an invalid number.'))
|
||||
is_valid = False
|
||||
else:
|
||||
if value == 0:
|
||||
continue
|
||||
elif value > p.available_amount:
|
||||
messages.error(self.request, _('You can not refund more than the amount of a '
|
||||
'payment that is not yet refunded.'))
|
||||
is_valid = False
|
||||
break
|
||||
elif value != p.amount and not p.partial_refund_possible:
|
||||
messages.error(self.request, _('You selected a partial refund for a payment method that '
|
||||
'only supports full refunds.'))
|
||||
is_valid = False
|
||||
break
|
||||
elif (p.partial_refund_possible or p.full_refund_possible) and value > 0:
|
||||
refund_selected += value
|
||||
refunds.append(OrderRefund(
|
||||
order=self.order,
|
||||
payment=p,
|
||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||
state=OrderRefund.REFUND_STATE_CREATED,
|
||||
amount=value,
|
||||
provider=p.provider
|
||||
))
|
||||
|
||||
any_success = False
|
||||
if refund_selected == full_refund and is_valid:
|
||||
for r in refunds:
|
||||
r.save()
|
||||
if r.payment or r.provider == "offsetting":
|
||||
try:
|
||||
r.payment_provider.execute_refund(r)
|
||||
except PaymentException as e:
|
||||
r.state = OrderRefund.REFUND_STATE_FAILED
|
||||
r.save()
|
||||
messages.error(self.request, _('One of the refunds failed to be processed. You should '
|
||||
'retry to refund in a different way. The error message '
|
||||
'was: {}').format(str(e)))
|
||||
else:
|
||||
any_success = True
|
||||
if r.state == OrderRefund.REFUND_STATE_DONE:
|
||||
messages.success(self.request, _('A refund of {} has been processed.').format(
|
||||
money_filter(r.amount, self.request.event.currency)
|
||||
))
|
||||
elif r.state == OrderRefund.REFUND_STATE_CREATED:
|
||||
messages.info(self.request, _('A refund of {} has been saved, but not yet '
|
||||
'fully executed. You can mark it as complete '
|
||||
'below.').format(
|
||||
money_filter(r.amount, self.request.event.currency)
|
||||
))
|
||||
else:
|
||||
any_success = True
|
||||
|
||||
self.order.log_action('pretix.event.order.refund.created', {
|
||||
'local_id': r.local_id,
|
||||
'provider': r.provider,
|
||||
}, user=self.request.user)
|
||||
if any_success:
|
||||
if self.start_form.cleaned_data.get('action') == 'mark_refunded':
|
||||
mark_order_refunded(self.order, user=self.request.user)
|
||||
elif self.start_form.cleaned_data.get('action') == 'mark_pending':
|
||||
self.order.status = Order.STATUS_PENDING
|
||||
self.order.set_expires(
|
||||
now(),
|
||||
self.order.event.subevents.filter(
|
||||
id__in=self.order.positions.values_list('subevent_id', flat=True))
|
||||
)
|
||||
self.order.save()
|
||||
|
||||
return redirect(self.get_order_url())
|
||||
else:
|
||||
messages.error(self.request, _('The refunds you selected do not match the selected total refund '
|
||||
'amount.'))
|
||||
|
||||
return render(self.request, 'pretixcontrol/order/refund_choose.html', {
|
||||
'payments': payments,
|
||||
'remainder': to_refund,
|
||||
'order': self.order,
|
||||
'partial_amount': self.request.POST.get('start-partial_amount'),
|
||||
'start_form': self.start_form
|
||||
})
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return render(self.request, 'pretixcontrol/order/refund_start.html', {
|
||||
'form': self.start_form,
|
||||
'order': self.order,
|
||||
})
|
||||
|
||||
|
||||
class OrderTransition(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
@@ -235,19 +624,35 @@ class OrderTransition(OrderView):
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
to = self.request.POST.get('status', '')
|
||||
if self.order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and to == 'p':
|
||||
if not self.mark_paid_form.is_valid():
|
||||
return render(self.request, 'pretixcontrol/order/pay.html', {
|
||||
'form': self.mark_paid_form,
|
||||
'order': self.order,
|
||||
})
|
||||
if self.order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and to == 'p' and self.mark_paid_form.is_valid():
|
||||
ps = self.order.pending_sum
|
||||
try:
|
||||
mark_order_paid(self.order, manual=True, user=self.request.user,
|
||||
count_waitinglist=False, force=self.mark_paid_form.cleaned_data.get('force', False))
|
||||
p = self.order.payments.get(
|
||||
state__in=(OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
|
||||
provider='manual',
|
||||
amount=ps
|
||||
)
|
||||
except OrderPayment.DoesNotExist:
|
||||
self.order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING,
|
||||
OrderPayment.PAYMENT_STATE_CREATED)) \
|
||||
.update(state=OrderPayment.PAYMENT_STATE_CANCELED)
|
||||
p = self.order.payments.create(
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||
provider='manual',
|
||||
amount=ps,
|
||||
fee=None
|
||||
)
|
||||
|
||||
try:
|
||||
p.confirm(user=self.request.user, count_waitinglist=False,
|
||||
force=self.mark_paid_form.cleaned_data.get('force', False))
|
||||
except Quota.QuotaExceededException as e:
|
||||
messages.error(self.request, str(e))
|
||||
except PaymentException as e:
|
||||
messages.error(self.request, str(e))
|
||||
except SendMailException:
|
||||
messages.warning(self.request, _('The order has been marked as paid, but we were unable to send a confirmation mail.'))
|
||||
messages.warning(self.request, _('The order has been marked as paid, but we were unable to send a '
|
||||
'confirmation mail.'))
|
||||
else:
|
||||
messages.success(self.request, _('The order has been marked as paid.'))
|
||||
elif self.order.cancel_allowed() and to == 'c':
|
||||
@@ -255,20 +660,12 @@ class OrderTransition(OrderView):
|
||||
messages.success(self.request, _('The order has been canceled.'))
|
||||
elif self.order.status == Order.STATUS_PAID and to == 'n':
|
||||
self.order.status = Order.STATUS_PENDING
|
||||
self.order.payment_manual = True
|
||||
self.order.save()
|
||||
self.order.log_action('pretix.event.order.unpaid', user=self.request.user)
|
||||
messages.success(self.request, _('The order has been marked as not paid.'))
|
||||
elif self.order.status == Order.STATUS_PENDING and to == 'e':
|
||||
mark_order_expired(self.order, user=self.request.user)
|
||||
messages.success(self.request, _('The order has been marked as expired.'))
|
||||
elif self.order.status == Order.STATUS_PAID and to == 'r':
|
||||
if not self.payment_provider:
|
||||
messages.error(self.request, _('This order is not assigned to a known payment provider.'))
|
||||
else:
|
||||
ret = self.payment_provider.order_control_refund_perform(self.request, self.order)
|
||||
if ret:
|
||||
return redirect(ret)
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
@@ -282,20 +679,6 @@ class OrderTransition(OrderView):
|
||||
return render(self.request, 'pretixcontrol/order/cancel.html', {
|
||||
'order': self.order,
|
||||
})
|
||||
elif self.order.status == Order.STATUS_PAID and to == 'r':
|
||||
if not self.payment_provider:
|
||||
messages.error(self.request, _('This order is not assigned to a known payment provider.'))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
try:
|
||||
cr = self.payment_provider.order_control_refund_render(self.order, self.request)
|
||||
except TypeError:
|
||||
cr = self.payment_provider.order_control_refund_render(self.order)
|
||||
|
||||
return render(self.request, 'pretixcontrol/order/refund.html', {
|
||||
'order': self.order,
|
||||
'payment': cr,
|
||||
})
|
||||
else:
|
||||
return HttpResponseNotAllowed(['POST'])
|
||||
|
||||
@@ -700,9 +1083,7 @@ class OrderModifyInformation(OrderQuestionsViewMixin, OrderView):
|
||||
self.order.log_action('pretix.event.order.modified', {
|
||||
'invoice_data': self.invoice_form.cleaned_data,
|
||||
'data': [{
|
||||
k: (f.cleaned_data.get(k).name
|
||||
if isinstance(f.cleaned_data.get(k), File)
|
||||
else f.cleaned_data.get(k))
|
||||
k: (f.cleaned_data.get(k).name if isinstance(f.cleaned_data.get(k), File) else f.cleaned_data.get(k))
|
||||
for k in f.changed_data
|
||||
} for f in self.forms]
|
||||
}, user=request.user)
|
||||
@@ -878,7 +1259,8 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
|
||||
email_context, 'pretix.event.order.email.custom_sent',
|
||||
self.request.user
|
||||
)
|
||||
messages.success(self.request, _('Your message has been queued and will be sent to {}.'.format(order.email)))
|
||||
messages.success(self.request,
|
||||
_('Your message has been queued and will be sent to {}.'.format(order.email)))
|
||||
except SendMailException:
|
||||
messages.error(
|
||||
self.request,
|
||||
@@ -890,8 +1272,8 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
|
||||
return reverse('control:event.order', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'code': self.kwargs['code']}
|
||||
)
|
||||
'code': self.kwargs['code']
|
||||
})
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
ctx = super().get_context_data(*args, **kwargs)
|
||||
@@ -988,7 +1370,6 @@ class OrderGo(EventPermissionRequiredMixin, View):
|
||||
|
||||
|
||||
class ExportMixin:
|
||||
|
||||
@cached_property
|
||||
def exporters(self):
|
||||
exporters = []
|
||||
@@ -1065,3 +1446,30 @@ class ExportView(EventPermissionRequiredMixin, ExportMixin, TemplateView):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['exporters'] = self.exporters
|
||||
return ctx
|
||||
|
||||
|
||||
class RefundList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
model = OrderRefund
|
||||
context_object_name = 'refunds'
|
||||
template_name = 'pretixcontrol/orders/refunds.html'
|
||||
permission = 'can_view_orders'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = OrderRefund.objects.filter(
|
||||
order__event=self.request.event
|
||||
).select_related('order')
|
||||
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
|
||||
return qs.distinct()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['filter_form'] = self.filter_form
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return RefundFilterForm(data=self.request.GET, event=self.request.event,
|
||||
initial={'status': 'open'})
|
||||
|
||||
Reference in New Issue
Block a user