mirror of
https://github.com/pretix/pretix.git
synced 2026-05-13 16:33:59 +00:00
wip
This commit is contained in:
@@ -4,9 +4,10 @@ from decimal import Decimal
|
||||
from functools import reduce
|
||||
from typing import Callable, Dict, List, Set, Union
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.utils.functional import _StrPromise
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import ScopedManager
|
||||
|
||||
@@ -31,10 +32,10 @@ class RelativeFee:
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CheckResult:
|
||||
class CheckRes:
|
||||
cancellation_possible: bool
|
||||
reason: str | _StrPromise
|
||||
|
||||
reason: str
|
||||
CheckResult=Dict[str, CheckRes]
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OrderDiff:
|
||||
@@ -46,8 +47,6 @@ class OrderDiff:
|
||||
return self.prev.difference(self.next)
|
||||
|
||||
|
||||
|
||||
CheckResult=Dict[str, CheckResult]
|
||||
Fee=Union[AbsoluteFee, RelativeFee]
|
||||
CheckFn=Callable[[OrderDiff, OrderPosition], CheckResult]
|
||||
|
||||
@@ -64,7 +63,7 @@ def merge_check_results(a: CheckResult, b: CheckResult) -> CheckResult:
|
||||
@dataclass(frozen=True)
|
||||
class Ruling:
|
||||
rule_id: int
|
||||
check_results: CheckResult
|
||||
results: CheckResult
|
||||
order_fee: Decimal=dataclasses.field(default_factory=lambda: Decimal(0))
|
||||
position_fee: Fee=dataclasses.field(default_factory=lambda: AbsoluteFee(Decimal(0)))
|
||||
|
||||
@@ -75,7 +74,7 @@ class Ruling:
|
||||
self,
|
||||
'cancellation_possible',
|
||||
all(ruling.cancellation_possible
|
||||
for ruling in self.check_results.values()
|
||||
for ruling in self.results.values()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -94,26 +93,37 @@ class Ruling:
|
||||
|
||||
|
||||
class CancellationRuleQuerySet(models.QuerySet):
|
||||
def cancellation_possible(self, order: Order):
|
||||
verdicts = [v[0] for v in self._evaluate(order)]
|
||||
def cancellation_possible(self, diff: OrderDiff):
|
||||
verdicts = [v[0] for v in self._evaluate(diff)]
|
||||
return all(v.cancellation_possible for v in verdicts), verdicts
|
||||
|
||||
def _evaluate(self, order: Order) -> List[List[Ruling]]:
|
||||
return [self._evaluate_op(position) for position in order.positions.all()]
|
||||
def _evaluate(self, diff: OrderDiff) -> List[List[Ruling]]:
|
||||
return [self._evaluate_op(diff, position) for position in diff.order.positions.all()]
|
||||
|
||||
def _evaluate_op(self, order_position: OrderPosition) -> List[Ruling]:
|
||||
consequences=[rule.apply(order_position) for rule in self]
|
||||
def _evaluate_op(self, diff: OrderDiff, order_position: OrderPosition) -> List[Ruling]:
|
||||
consequences=[rule.apply(diff, order_position) for rule in self]
|
||||
consequences.sort()
|
||||
return consequences
|
||||
|
||||
|
||||
ALLOWED_STATUS_CHARS={char for char, _ in Order.STATUS_CHOICE} # {'n', 'p', 'e', 'c'}
|
||||
|
||||
def validate_status_chars(value):
|
||||
invalid=set(value) - ALLOWED_STATUS_CHARS
|
||||
if invalid:
|
||||
raise ValidationError(
|
||||
f"Invalid characters: {invalid}. Allowed: {ALLOWED_STATUS_CHARS}"
|
||||
)
|
||||
if len(value) != len(set(value)):
|
||||
raise ValidationError("Duplicate characters are not allowed.")
|
||||
|
||||
|
||||
|
||||
class CancellationRule(models.Model):
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
|
||||
organizer=models.ForeignKey(
|
||||
"Organizer",
|
||||
related_name="orders",
|
||||
@@ -127,14 +137,15 @@ class CancellationRule(models.Model):
|
||||
related_name="orders",
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
item=models.ForeignKey("Item", on_delete=models.CASCADE, null=True, blank=True)
|
||||
item_variation=models.ForeignKey("ItemVariation", on_delete=models.CASCADE, null=True, blank=True)
|
||||
item=models.ForeignKey("Item", on_delete=models.CASCADE, null=True, blank=True) # probably m2m field to avoid duplicating rules
|
||||
item_variation=models.ForeignKey("ItemVariation", on_delete=models.CASCADE, null=True, blank=True) # probably m2m field to avoid duplicating rules
|
||||
|
||||
order_status=models.CharField(
|
||||
max_length=3,
|
||||
|
||||
allowed_if_in_order_status=models.CharField(
|
||||
max_length=4,
|
||||
choices=Order.STATUS_CHOICE,
|
||||
verbose_name=_("Status"),
|
||||
db_index=True
|
||||
verbose_name=_("Cancellation possible if order is in status"),
|
||||
validators=[validate_status_chars]
|
||||
)
|
||||
|
||||
allowed_until=ModelRelativeDateTimeField(null=True, blank=True)
|
||||
@@ -173,21 +184,24 @@ class CancellationRule(models.Model):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# erstmal festgelegte List an Stornoregeln, weitere checks können zukünftig über ein
|
||||
# Signal eingesammelt und an CancellationRule.__init__ übergeben werden
|
||||
# Ermöglicht dann: "Shipping modul kann storno geshippter Items verhindern"
|
||||
self.checks: List[CheckFn]=[self._check_order_status, self._check_time_window,
|
||||
self._system_check_not_checked_in]
|
||||
self.partial_checks: List[CheckFn]=[self._system_check_not_discounted]
|
||||
self._system_check_not_checked_in, self._system_check_not_discounted]
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _system_check_not_checked_in(diff: OrderDiff, order_position: OrderPosition) -> CheckResult:
|
||||
check_id = "SYSTEM_TICKET_NOT_USED"
|
||||
|
||||
if order_position.checkins.filter(list__consider_tickets_used=True).exists():
|
||||
return {check_id: CheckResult(
|
||||
return {check_id: CheckRes(
|
||||
cancellation_possible=False,
|
||||
reason=f"Order position was used",
|
||||
)}
|
||||
else:
|
||||
return {check_id: CheckResult(
|
||||
return {check_id: CheckRes(
|
||||
cancellation_possible=True,
|
||||
reason=f"Order position not yet used",
|
||||
)}
|
||||
@@ -208,17 +222,17 @@ class CancellationRule(models.Model):
|
||||
|
||||
if order_position in diff.cancellations():
|
||||
if order_position.discount is None:
|
||||
return {check_id: CheckResult(
|
||||
return {check_id: CheckRes(
|
||||
cancellation_possible=True,
|
||||
reason=_("Order position was bought without discount"),
|
||||
)}
|
||||
else:
|
||||
return {check_id: CheckResult(
|
||||
return {check_id: CheckRes(
|
||||
cancellation_possible=False,
|
||||
reason=_("Order position was bought with a discount"),
|
||||
)}
|
||||
else:
|
||||
return {check_id: CheckResult(
|
||||
return {check_id: CheckRes(
|
||||
cancellation_possible=False,
|
||||
reason=_("Order position not canceled - check not applicable"),
|
||||
)}
|
||||
@@ -226,53 +240,51 @@ class CancellationRule(models.Model):
|
||||
def _check_time_window(self, diff: OrderDiff, order_position: OrderPosition) -> CheckResult:
|
||||
check_id = "TIME_WINDOW"
|
||||
|
||||
relevant_event = order_position.subevent or order_position.event
|
||||
|
||||
if not self.allowed_until and not self.allowed_until:
|
||||
return {check_id: CheckResult(
|
||||
return {check_id: CheckRes(
|
||||
cancellation_possible=True,
|
||||
reason=f"No time window specified",
|
||||
)}
|
||||
|
||||
relevant_event=order_position.subevent or order_position.event
|
||||
in_allowed_until=time_machine_now() < self.allowed_until.datetime(
|
||||
relevant_event) if self.allowed_until else False
|
||||
in_exemption=time_machine_now() > self.except_after.datetime(
|
||||
relevant_event) if self.except_after else False
|
||||
|
||||
if in_allowed_until and not in_exemption:
|
||||
except_after_message = f" and not after {self.except_after.datetime(relevant_event)}" if self.except_after else ""
|
||||
|
||||
return {check_id: CheckResult(
|
||||
return {check_id: CheckRes(
|
||||
cancellation_possible=True,
|
||||
reason=f"Cancellation in required time window before {self.allowed_until.datetime(relevant_event)}{except_after_message}",
|
||||
)}
|
||||
elif in_allowed_until and in_exemption:
|
||||
return {check_id: CheckResult(
|
||||
return {check_id: CheckRes(
|
||||
cancellation_possible=False,
|
||||
reason=f"Cancellation in exemption period after {self.except_after.datetime(relevant_event)}",
|
||||
)}
|
||||
else:
|
||||
return {check_id: CheckResult(
|
||||
return {check_id: CheckRes(
|
||||
cancellation_possible=False,
|
||||
reason=f"Cancellation after time window ending on {self.allowed_until.datetime(relevant_event)}",
|
||||
)}
|
||||
|
||||
|
||||
|
||||
def _check_order_status(self, diff: OrderDiff, order_position: OrderPosition) -> CheckResult:
|
||||
check_id = "ORDER_STATUS"
|
||||
|
||||
if not self.order_status:
|
||||
return {check_id: CheckResult(
|
||||
if not self.allowed_until and not self.allowed_until:
|
||||
return {check_id: CheckRes(
|
||||
cancellation_possible=True,
|
||||
reason=f"Orders in every status can be cancelled",
|
||||
)}
|
||||
elif order_position.order.status == self.order_status:
|
||||
return {check_id: CheckResult(
|
||||
elif order_position.order.status in self.allowed_if_in_order_status:
|
||||
return {check_id: CheckRes(
|
||||
cancellation_possible=True,
|
||||
reason=f"Order in required status: '{order_position.order.status}'",
|
||||
)}
|
||||
else:
|
||||
return {check_id: CheckResult(
|
||||
return {check_id: CheckRes(
|
||||
cancellation_possible=False,
|
||||
reason=f"Order in status '{order_position.order.status}' cannot be canceled",
|
||||
)}
|
||||
@@ -281,8 +293,8 @@ class CancellationRule(models.Model):
|
||||
# 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 is_bundled dürfen nur mit der Parent-Position zusammen storniert werden
|
||||
# Shipping modul kann storno geshippter Items verhindern
|
||||
# Backend-Anzeige "welche Regel greift da gerade" in der Order
|
||||
|
||||
|
||||
|
||||
def apply(self, diff: OrderDiff, order_position: OrderPosition) -> Ruling:
|
||||
check_results=reduce(merge_check_results,
|
||||
@@ -298,7 +310,7 @@ class CancellationRule(models.Model):
|
||||
|
||||
return Ruling(
|
||||
rule_id=self.id,
|
||||
check_results=check_results,
|
||||
results=check_results,
|
||||
order_fee=self.fee_absolute_per_order,
|
||||
position_fee=fee
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user