forked from CGM_Public/pretix_original
wip
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Callable, Dict, List, Literal, Set
|
from functools import reduce
|
||||||
|
from typing import Callable, Dict, List, Literal, Optional, Set
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
@@ -15,19 +16,6 @@ from pretix.base.reldate import ModelRelativeDateTimeField
|
|||||||
from pretix.base.timemachine import time_machine_now
|
from pretix.base.timemachine import time_machine_now
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class OrderDiff:
|
|
||||||
order: Order
|
|
||||||
keep: Set[OrderPosition]
|
|
||||||
|
|
||||||
def cancellations(self):
|
|
||||||
return self.prev.difference(self.next)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def cancel_all(order: Order) -> "OrderDiff":
|
|
||||||
return OrderDiff(order=order, prev=set(order.positions.all()), next=set())
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CancellationCheckResult:
|
class CancellationCheckResult:
|
||||||
cancellation_possible: bool
|
cancellation_possible: bool
|
||||||
@@ -38,7 +26,7 @@ class CancellationCheckResult:
|
|||||||
CancellationCheckResultsById = Dict[str, CancellationCheckResult]
|
CancellationCheckResultsById = Dict[str, CancellationCheckResult]
|
||||||
|
|
||||||
|
|
||||||
CheckFn = Callable[[Order, Set[OrderPosition], OrderPosition], CancellationCheckResultsById]
|
CheckFn = Callable[[Order, Set[OrderPosition], Optional[OrderPosition]], CancellationCheckResultsById]
|
||||||
|
|
||||||
FeeType = Literal['position_fee', 'process_fee']
|
FeeType = Literal['position_fee', 'process_fee']
|
||||||
|
|
||||||
@@ -127,16 +115,6 @@ class Ruling:
|
|||||||
return self.cancellation_possible and not other.cancellation_possible
|
return self.cancellation_possible and not other.cancellation_possible
|
||||||
|
|
||||||
|
|
||||||
def validate_status_chars(value):
|
|
||||||
invalid = set(value) - Order.ALLOWED_STATUS_CHARS
|
|
||||||
if invalid:
|
|
||||||
raise ValidationError(
|
|
||||||
f"Invalid characters: {invalid}. Allowed: {Order.ALLOWED_STATUS_CHARS}"
|
|
||||||
)
|
|
||||||
if len(value) != len(set(value)):
|
|
||||||
raise ValidationError("Duplicate characters are not allowed.")
|
|
||||||
|
|
||||||
|
|
||||||
class CancellationRule(models.Model):
|
class CancellationRule(models.Model):
|
||||||
event = models.ForeignKey(
|
event = models.ForeignKey(
|
||||||
Event,
|
Event,
|
||||||
@@ -154,13 +132,7 @@ class CancellationRule(models.Model):
|
|||||||
ItemVariation, blank=True, verbose_name=_("Variations")
|
ItemVariation, blank=True, verbose_name=_("Variations")
|
||||||
)
|
)
|
||||||
|
|
||||||
allowed_if_in_order_status = models.CharField(
|
|
||||||
max_length=4,
|
|
||||||
choices=Order.STATUS_CHOICE,
|
|
||||||
verbose_name=_("Cancellation possible if order is in status"),
|
|
||||||
validators=[validate_status_chars],
|
|
||||||
default="".join(Order.ALLOWED_STATUS_CHARS),
|
|
||||||
)
|
|
||||||
allowed_until = ModelRelativeDateTimeField(null=True, blank=True)
|
allowed_until = ModelRelativeDateTimeField(null=True, blank=True)
|
||||||
except_after = ModelRelativeDateTimeField(null=True, blank=True)
|
except_after = ModelRelativeDateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
@@ -187,9 +159,12 @@ class CancellationRule(models.Model):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.checks: List[CheckFn] = [self._check_order_status, self._check_time_window]
|
self.checks: List[CheckFn] = [self._check_time_window]
|
||||||
|
|
||||||
def _check_time_window(self, diff: OrderDiff, order_position: OrderPosition) -> CancellationCheckResultsById:
|
|
||||||
|
# TODO implement order status check
|
||||||
|
|
||||||
|
def _check_time_window(self, order: Order, keep: Set[OrderPosition], position: OrderPosition) -> CancellationCheckResultsById:
|
||||||
check_id = "TIME_WINDOW"
|
check_id = "TIME_WINDOW"
|
||||||
|
|
||||||
if not self.allowed_until and not self.allowed_until:
|
if not self.allowed_until and not self.allowed_until:
|
||||||
@@ -198,7 +173,7 @@ class CancellationRule(models.Model):
|
|||||||
reason="No time window specified",
|
reason="No time window specified",
|
||||||
)}
|
)}
|
||||||
|
|
||||||
relevant_event = order_position.subevent or order_position.event
|
relevant_event = position.subevent or position.event
|
||||||
in_allowed_until = time_machine_now() < self.allowed_until.datetime(
|
in_allowed_until = time_machine_now() < self.allowed_until.datetime(
|
||||||
relevant_event) if self.allowed_until else False
|
relevant_event) if self.allowed_until else False
|
||||||
in_exemption = time_machine_now() > self.except_after.datetime(
|
in_exemption = time_machine_now() > self.except_after.datetime(
|
||||||
@@ -221,24 +196,33 @@ class CancellationRule(models.Model):
|
|||||||
reason=f"Cancellation after time window ending on {self.allowed_until.datetime(relevant_event)}",
|
reason=f"Cancellation after time window ending on {self.allowed_until.datetime(relevant_event)}",
|
||||||
)}
|
)}
|
||||||
|
|
||||||
def _check_order_status(self, diff: OrderDiff, order_position: OrderPosition) -> CancellationCheckResultsById:
|
def check(self, system_check_results: List[CancellationCheckResult], order: Order, keep: Set[OrderPosition], position: OrderPosition) -> Optional[Ruling]:
|
||||||
check_id = "ORDER_STATUS"
|
if not self.all_products and position.item_id not in self.limit_products.values_list('pk', flat=True):
|
||||||
|
return None
|
||||||
|
|
||||||
if diff.order.status == "".join([]):
|
if not self.all_products and position.variation_id not in self.limit_variations.values_list('pk', flat=True):
|
||||||
return {check_id: CancellationCheckResult(
|
return None
|
||||||
cancellation_possible=True,
|
|
||||||
reason="Orders in every status can be cancelled",
|
check_results = [check(order, keep, position) for check in self.checks]
|
||||||
)}
|
|
||||||
elif diff.order.status in self.allowed_if_in_order_status:
|
if self.fee_percentage_per_item and self.fee_absolute_per_item:
|
||||||
return {check_id: CancellationCheckResult(
|
raise NotImplementedError("Should never be reached")
|
||||||
cancellation_possible=True,
|
elif self.fee_absolute_per_item != Decimal(0.00):
|
||||||
reason=f"Order in required status: '{diff.order.status}'",
|
return Ruling.from_absolute_fee(
|
||||||
)}
|
rule_id=self.id,
|
||||||
|
results=reduce(lambda a, b: a | b, [*system_check_results, *check_results], {}),
|
||||||
|
fee_type='position_fee',
|
||||||
|
absolute_fee=self.fee_absolute_per_item
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return {check_id: CancellationCheckResult(
|
return Ruling.from_relative_fee(
|
||||||
cancellation_possible=False,
|
rule_id=self.id,
|
||||||
reason=f"Order in status '{diff.order.status}' cannot be canceled",
|
results=reduce(lambda a, b: a | b, [*system_check_results, *check_results], {}),
|
||||||
)}
|
fee_type='position_fee',
|
||||||
|
reference_price=position.price,
|
||||||
|
percentage=self.fee_absolute_per_item,
|
||||||
|
currency=order.event.currency
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CancellationCheck:
|
class CancellationCheck:
|
||||||
|
|||||||
@@ -43,9 +43,7 @@ from decimal import Decimal
|
|||||||
from functools import reduce
|
from functools import reduce
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Dict
|
from typing import Dict, List, Optional, Set
|
||||||
from typing import List, Optional
|
|
||||||
from typing import Set
|
|
||||||
|
|
||||||
from celery.exceptions import MaxRetriesExceededError
|
from celery.exceptions import MaxRetriesExceededError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -53,8 +51,8 @@ from django.core.cache import cache
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Sum,
|
Count, Exists, F, IntegerField, Max, Min, OuterRef, Prefetch, Q, QuerySet,
|
||||||
Value,
|
Sum, Value,
|
||||||
)
|
)
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
from django.db.models.functions import Coalesce, Greatest
|
from django.db.models.functions import Coalesce, Greatest
|
||||||
@@ -71,20 +69,14 @@ from pretix.base.email import get_email_context
|
|||||||
from pretix.base.i18n import get_language_without_region, language
|
from pretix.base.i18n import get_language_without_region, language
|
||||||
from pretix.base.media import MEDIA_TYPES
|
from pretix.base.media import MEDIA_TYPES
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Checkin, Device, Event, GiftCard, Item, ItemVariation, LogEntry,
|
CartPosition, Checkin, Device, Event, GiftCard, Item, ItemVariation,
|
||||||
Membership, Order, OrderPayment, OrderPosition, Quota, Seat,
|
Membership, Order, OrderPayment, OrderPosition, Quota, Seat,
|
||||||
SeatCategoryMapping, User, Voucher,
|
SeatCategoryMapping, User, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.models import Checkin
|
from pretix.base.models.cancellation import (
|
||||||
from pretix.base.models.cancellation import AbsoluteFee, CancellationCheckResult
|
CancellationCheckResult, CancellationCheckResultsById, CancellationRule,
|
||||||
from pretix.base.models.cancellation import CancellationRule
|
Ruling, CancellationCheck
|
||||||
from pretix.base.models.cancellation import CheckFn
|
)
|
||||||
from pretix.base.models.cancellation import CheckRes
|
|
||||||
from pretix.base.models.cancellation import OrderDiff
|
|
||||||
from pretix.base.models.cancellation import RelativeFee
|
|
||||||
from pretix.base.models.cancellation import Ruling
|
|
||||||
from pretix.base.models.cancellation import assert_no_queries
|
|
||||||
from pretix.base.models.cancellation import merge_check_results
|
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund,
|
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund,
|
||||||
@@ -117,9 +109,8 @@ from pretix.base.signals import (
|
|||||||
order_approved, order_canceled, order_changed, order_denied, order_expired,
|
order_approved, order_canceled, order_changed, order_denied, order_expired,
|
||||||
order_expiry_changed, order_fee_calculation, order_paid, order_placed,
|
order_expiry_changed, order_fee_calculation, order_paid, order_placed,
|
||||||
order_reactivated, order_split, order_valid_if_pending, periodic_task,
|
order_reactivated, order_split, order_valid_if_pending, periodic_task,
|
||||||
validate_order,
|
self_service_cancellation_checks, validate_order,
|
||||||
)
|
)
|
||||||
from pretix.base.signals import self_service_cancellation_checks
|
|
||||||
from pretix.base.timemachine import time_machine_now, time_machine_now_assigned
|
from pretix.base.timemachine import time_machine_now, time_machine_now_assigned
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
from pretix.helpers import OF_SELF, ensure_no_queries
|
from pretix.helpers import OF_SELF, ensure_no_queries
|
||||||
@@ -3523,15 +3514,6 @@ def signal_listener_issue_media(sender: Event, order: Order, **kwargs):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CancellationCheck:
|
|
||||||
id: str
|
|
||||||
prefetches: List[Prefetch] = []
|
|
||||||
related_selects: List[str] = []
|
|
||||||
|
|
||||||
def check(self, order: Order, keep: Set[OrderPosition], order_position: OrderPosition) -> CancellationCheckResult:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
class OrderPositionNotUsedCheck(CancellationCheck):
|
class OrderPositionNotUsedCheck(CancellationCheck):
|
||||||
id = "SYSTEM_TICKET_NOT_USED"
|
id = "SYSTEM_TICKET_NOT_USED"
|
||||||
prefetches = [
|
prefetches = [
|
||||||
@@ -3547,18 +3529,20 @@ class CancellationCheck:
|
|||||||
if order_position.checkins.filter(list__consider_tickets_used=True).exists():
|
if order_position.checkins.filter(list__consider_tickets_used=True).exists():
|
||||||
return {self.id: CancellationCheckResult(
|
return {self.id: CancellationCheckResult(
|
||||||
cancellation_possible=False,
|
cancellation_possible=False,
|
||||||
reason=f"Order position was used",
|
reason="Order position was used",
|
||||||
)}
|
)}
|
||||||
else:
|
else:
|
||||||
return {self.id: CancellationCheckResult(
|
return {self.id: CancellationCheckResult(
|
||||||
cancellation_possible=True,
|
cancellation_possible=True,
|
||||||
reason=f"Order position not yet used",
|
reason="Order position not yet used",
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
@receiver(self_service_cancellation_checks, dispatch_uid="pretixbase_not_used")
|
@receiver(self_service_cancellation_checks, dispatch_uid="pretixbase_not_used")
|
||||||
def cancellation_checks_not_used(sender: Event):
|
def cancellation_checks_not_used(sender: Event):
|
||||||
return OrderPositionNotUsedCheck()
|
return OrderPositionNotUsedCheck()
|
||||||
|
|
||||||
|
|
||||||
class NotDiscountedCheck(CancellationCheck):
|
class NotDiscountedCheck(CancellationCheck):
|
||||||
"""
|
"""
|
||||||
Check that ensures that orders containing discounted order_positions cannot
|
Check that ensures that orders containing discounted order_positions cannot
|
||||||
@@ -3593,14 +3577,11 @@ class NotDiscountedCheck(CancellationCheck):
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(self_service_cancellation_checks, dispatch_uid="pretixbase_not_discountend")
|
@receiver(self_service_cancellation_checks, dispatch_uid="pretixbase_not_discountend")
|
||||||
def cancellation_checks_not_discounted(sender: Event):
|
def cancellation_checks_not_discounted(sender: Event):
|
||||||
return NotDiscountedCheck()
|
return NotDiscountedCheck()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# TODO weitere System Checks
|
# TODO weitere System Checks
|
||||||
# OrderPositions mit Item.min_per_order dürfen nur storniert werden, wenn genug übrig bleiben oder alle des gleichen Items storniert werden
|
# OrderPositions mit Item.min_per_order dürfen nur storniert werden, wenn genug übrig bleiben oder alle des gleichen Items storniert werden
|
||||||
# OrderPositions mit addon_to != None dürfen nur über den bestehenden Add-On-Flow storniert werden
|
# OrderPositions mit addon_to != None dürfen nur über den bestehenden Add-On-Flow storniert werden
|
||||||
@@ -3679,13 +3660,6 @@ def self_service_cancel(order: Order, keep: Set[OrderPosition], dry_run: bool):
|
|||||||
|
|
||||||
effective_process_ruling = process_rulings[0]
|
effective_process_ruling = process_rulings[0]
|
||||||
|
|
||||||
|
cancellation_possible = all([r.cancellation_possible for r in effective_position_rulings])
|
||||||
|
|
||||||
cancellation_possible = all([r.cancellation_possible for r in effective_rulings])
|
|
||||||
|
|
||||||
# TODO zusammenführen der Rulings
|
# TODO zusammenführen der Rulings
|
||||||
|
|
||||||
if dry_run:
|
|
||||||
return cancellation_possible, rulings
|
|
||||||
else:
|
|
||||||
...
|
|
||||||
|
|||||||
Reference in New Issue
Block a user