forked from CGM_Public/pretix_original
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
|
||||
|
||||
Reference in New Issue
Block a user