New implementation of sales channels (#4111)

Co-authored-by: Martin Gross <gross@rami.io>
This commit is contained in:
Raphael Michel
2024-06-30 19:24:30 +02:00
committed by GitHub
parent 95511b0330
commit 4fb5c6bef0
174 changed files with 2902 additions and 616 deletions

View File

@@ -52,12 +52,11 @@ from django.utils.translation import (
)
from django_scopes import scopes_disabled
from pretix.base.channels import get_all_sales_channels
from pretix.base.i18n import language
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import (
CartPosition, Event, InvoiceAddress, Item, ItemVariation, Seat,
SeatCategoryMapping, Voucher,
CartPosition, Event, InvoiceAddress, Item, ItemVariation, SalesChannel,
Seat, SeatCategoryMapping, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import OrderFee
@@ -275,8 +274,8 @@ class CartManager:
AddOperation: 30
}
def __init__(self, event: Event, cart_id: str, invoice_address: InvoiceAddress=None, widget_data=None,
sales_channel='web'):
def __init__(self, event: Event, cart_id: str, sales_channel: SalesChannel,
invoice_address: InvoiceAddress=None, widget_data=None):
self.event = event
self.cart_id = cart_id
self.real_now_dt = now()
@@ -384,7 +383,7 @@ class CartManager:
})
def _check_max_cart_size(self):
if not get_all_sales_channels()[self._sales_channel].unlimited_items_per_order:
if not self._sales_channel.type_instance.unlimited_items_per_order:
cartsize = self.positions.filter(addon_to__isnull=True).count()
cartsize += sum([op.count for op in self._operations if isinstance(op, self.AddOperation) and not op.addon_to])
cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if
@@ -422,8 +421,13 @@ class CartManager:
elif op.item.media_policy == Item.MEDIA_POLICY_REUSE:
raise CartError(error_messages['media_usage_not_implemented'])
if self._sales_channel not in op.item.sales_channels or (op.variation and self._sales_channel not in op.variation.sales_channels):
raise CartError(error_messages['unavailable'])
if not op.item.all_sales_channels:
if self._sales_channel.identifier not in (s.identifier for s in op.item.limit_sales_channels.all()):
raise CartError(error_messages['unavailable'])
if op.variation and not op.variation.all_sales_channels:
if self._sales_channel.identifier not in (s.identifier for s in op.variation.limit_sales_channels.all()):
raise CartError(error_messages['unavailable'])
if op.subevent and op.item.pk in op.subevent.item_overrides and not op.subevent.item_overrides[op.item.pk].is_available():
raise CartError(error_messages['not_for_sale'])
@@ -457,7 +461,14 @@ class CartManager:
raise CartError(error_messages['ended'])
seated = self._is_seated(op.item, op.subevent)
if seated and (not op.seat or (op.seat.blocked and self._sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel)):
if (
seated and (
not op.seat or (
op.seat.blocked and
self._sales_channel.identifier not in self.event.settings.seating_allow_blocked_seats_for_channel
)
)
):
raise CartError(error_messages['seat_invalid'])
elif op.seat and not seated:
raise CartError(error_messages['seat_forbidden'])
@@ -1371,7 +1382,7 @@ class CartManager:
discount_results = apply_discounts(
self.event,
self._sales_channel,
self._sales_channel.identifier,
[
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
for cp in positions
@@ -1505,6 +1516,11 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
except InvoiceAddress.DoesNotExist:
pass
try:
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
except SalesChannel.DoesNotExist:
raise CartError("Invalid sales channel.")
try:
try:
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, widget_data=widget_data,
@@ -1526,6 +1542,10 @@ def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='e
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
try:
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
except SalesChannel.DoesNotExist:
raise CartError("Invalid sales channel.")
try:
try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
@@ -1546,6 +1566,10 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
try:
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
except SalesChannel.DoesNotExist:
raise CartError("Invalid sales channel.")
try:
try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
@@ -1565,6 +1589,10 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
try:
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
except SalesChannel.DoesNotExist:
raise CartError("Invalid sales channel.")
try:
try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
@@ -1593,6 +1621,10 @@ def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, l
ia = InvoiceAddress.objects.get(pk=invoice_address)
except InvoiceAddress.DoesNotExist:
pass
try:
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
except SalesChannel.DoesNotExist:
raise CartError("Invalid sales channel.")
try:
try:
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, sales_channel=sales_channel)

View File

@@ -1159,7 +1159,7 @@ def order_placed(sender, **kwargs):
order = kwargs['order']
event = sender
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels__contains=order.sales_channel).prefetch_related(
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels=order.sales_channel).prefetch_related(
'limit_products'))
if not cls:
return

View File

@@ -420,7 +420,7 @@ def invoice_pdf_task(invoice: int):
def invoice_qualified(order: Order):
if order.total == Decimal('0.00') or order.require_approval or \
order.sales_channel not in order.event.settings.get('invoice_generate_sales_channels'):
order.sales_channel.identifier not in order.event.settings.get('invoice_generate_sales_channels'):
return False
return True
@@ -443,8 +443,11 @@ def build_preview_invoice_pdf(event):
locale = event.settings.locale
with rolledback_transaction(), language(locale, event.settings.region):
order = event.orders.create(status=Order.STATUS_PENDING, datetime=timezone.now(),
expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count())
order = event.orders.create(
status=Order.STATUS_PENDING, datetime=timezone.now(),
expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count(),
sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
invoice = Invoice(
order=order, event=event, invoice_no="PREVIEW",
date=timezone.now().date(), locale=locale, organizer=event.organizer

View File

@@ -62,7 +62,6 @@ from django.utils.translation import gettext as _, gettext_lazy, ngettext_lazy
from django_scopes import scopes_disabled
from pretix.api.models import OAuthApplication
from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_email_context
from pretix.base.i18n import get_language_without_region, language
from pretix.base.media import MEDIA_TYPES
@@ -76,7 +75,7 @@ from pretix.base.models.orders import (
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund,
generate_secret,
)
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.models.organizer import SalesChannel, TeamAPIToken
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.payment import GiftCardPayment, PaymentException
from pretix.base.reldate import RelativeDateWrapper
@@ -650,8 +649,7 @@ def _check_date(event: Event, now_dt: datetime):
def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: datetime, positions: List[CartPosition],
address: InvoiceAddress = None,
sales_channel='web', customer=None):
sales_channel: SalesChannel, address: InvoiceAddress=None, customer=None):
err = None
_check_date(event, time_machine_now_dt)
@@ -775,7 +773,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
if cp.seat:
# Unlike quotas (which we blindly trust as long as the position is not expired), we check seats every
# time, since we absolutely can not overbook a seat.
if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id, sales_channel=sales_channel):
if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id, sales_channel=sales_channel.identifier):
err = err or error_messages['seat_unavailable']
delete(cp)
continue
@@ -873,7 +871,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions]
discount_results = apply_discounts(
event,
sales_channel,
sales_channel.identifier,
[
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
for cp in sorted_positions
@@ -959,12 +957,11 @@ def _get_fees(positions: List[CartPosition], payment_requests: List[dict], addre
return fees
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
payment_requests: List[dict], locale: str=None, address: InvoiceAddress=None,
meta_info: dict=None, sales_channel: str='web', shown_total=None,
def _create_order(event: Event, *, email: str, positions: List[CartPosition], now_dt: datetime,
payment_requests: List[dict], sales_channel: SalesChannel, locale: str=None,
address: InvoiceAddress=None, meta_info: dict=None, shown_total=None,
customer=None, valid_if_pending=False):
payments = []
sales_channel = get_all_sales_channels()[sales_channel]
try:
validate_memberships_in_order(customer, positions, event, lock=True, testmode=event.testmode)
@@ -986,10 +983,10 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
datetime=now_dt,
locale=get_language_without_region(locale),
total=total,
testmode=True if sales_channel.testmode_supported and event.testmode else False,
testmode=True if sales_channel.type_instance.testmode_supported and event.testmode else False,
meta_info=json.dumps(meta_info or {}),
require_approval=require_approval,
sales_channel=sales_channel.identifier,
sales_channel=sales_channel,
customer=customer,
valid_if_pending=valid_if_pending,
)
@@ -1108,6 +1105,11 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
if customer:
customer = event.organizer.customers.get(pk=customer)
try:
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
except SalesChannel.DoesNotExist:
raise OrderError("Invalid sales channel.")
if email == settings.PRETIX_EMAIL_NONE_VALUE:
email = None
@@ -1186,9 +1188,20 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
if 'sleep-after-quota-check' in debugflags_var.get():
sleep(2)
order, payment_objs = _create_order(event, email, positions, real_now_dt, payment_requests,
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
shown_total=shown_total, customer=customer, valid_if_pending=valid_if_pending)
order, payment_objs = _create_order(
event,
email=email,
positions=positions,
now_dt=real_now_dt,
payment_requests=payment_requests,
locale=locale,
address=addr,
meta_info=meta_info,
sales_channel=sales_channel,
shown_total=shown_total,
customer=customer,
valid_if_pending=valid_if_pending
)
try:
for p in payment_objs:
@@ -1272,7 +1285,7 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
email_attendees_template = event.settings.mail_text_order_placed_attendee
subject_attendees_template = event.settings.mail_subject_order_placed_attendee
if sales_channel in event.settings.mail_sales_channel_placed_paid:
if sales_channel.identifier in event.settings.mail_sales_channel_placed_paid:
_order_placed_email(event, order, email_template, subject_template, log_entry, invoice, payment_objs,
is_free=free_order_flow)
if email_attendees:
@@ -1424,7 +1437,7 @@ def send_download_reminders(sender, **kwargs):
if days is None:
continue
if o.sales_channel not in event.settings.mail_sales_channel_download_reminder:
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder:
continue
reminder_date = (o.first_date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
@@ -1926,9 +1939,13 @@ class OrderChangeManager:
if not item.is_available() or (variation and not variation.is_available()):
raise OrderError(error_messages['unavailable'])
if self.order.sales_channel not in item.sales_channels or (
variation and self.order.sales_channel not in variation.sales_channels):
raise OrderError(error_messages['unavailable'])
if not item.all_sales_channels:
if self.order.sales_channel.identifier not in (s.identifier for s in item.limit_sales_channels.all()):
raise OrderError(error_messages['unavailable'])
if variation and not variation.all_sales_channels:
if self.order.sales_channel.identifier not in (s.identifier for s in variation.limit_sales_channels.all()):
raise OrderError(error_messages['unavailable'])
if subevent and item.pk in subevent.item_overrides and not subevent.item_overrides[item.pk].is_available():
raise OrderError(error_messages['not_for_sale'])
@@ -2027,7 +2044,10 @@ class OrderChangeManager:
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
or (a.variation and self.order.sales_channel not in a.variation.sales_channels)
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
or self.order.sales_channel not in item.sales_channels
or (
not item.all_sales_channels and
not item.limit_sales_channels.contains(self.order.sales_channel)
)
)
if is_unavailable:
continue

View File

@@ -28,7 +28,8 @@ from django.db.models import Q
from pretix.base.decimal import round_decimal
from pretix.base.models import (
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation, Voucher,
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation,
SalesChannel, Voucher,
)
from pretix.base.models.event import Event, SubEvent
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
@@ -164,12 +165,14 @@ def apply_discounts(event: Event, sales_channel: str,
:param positions: Tuple of the form ``(item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
:return: A list of ``(new_gross_price, discount)`` tuples in the same order as the input
"""
if isinstance(sales_channel, SalesChannel):
sales_channel = sales_channel.identifier
new_prices = {}
discount_qs = event.discounts.filter(
Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()),
Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()),
sales_channels__contains=sales_channel,
Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=sales_channel),
active=True,
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
for discount in discount_qs: