mirror of
https://github.com/pretix/pretix.git
synced 2026-05-09 15:54:03 +00:00
Generalize import process from orders to more models (#4002)
* Generalize import process from orders to more models * Add voucher import * Model import: Guess assignments of based on column headers * Fix lock_seats being pointless * Update docs * Update doc/development/api/import.rst Co-authored-by: Richard Schreiber <schreiber@rami.io> * Update src/pretix/base/modelimport_vouchers.py Co-authored-by: Richard Schreiber <schreiber@rami.io> --------- Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
255
src/pretix/base/services/modelimport.py
Normal file
255
src/pretix/base/services/modelimport.py
Normal file
@@ -0,0 +1,255 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from decimal import Decimal
|
||||
from typing import List
|
||||
|
||||
from django.conf import settings as django_settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.modelimport import DataImportError, ImportColumn, parse_csv
|
||||
from pretix.base.modelimport_orders import get_order_import_columns
|
||||
from pretix.base.modelimport_vouchers import get_voucher_import_columns
|
||||
from pretix.base.models import (
|
||||
CachedFile, Event, InvoiceAddress, Order, OrderPayment, OrderPosition,
|
||||
User, Voucher,
|
||||
)
|
||||
from pretix.base.models.orders import Transaction
|
||||
from pretix.base.services.invoices import generate_invoice, invoice_qualified
|
||||
from pretix.base.services.locking import lock_objects
|
||||
from pretix.base.services.tasks import ProfiledEventTask
|
||||
from pretix.base.signals import order_paid, order_placed
|
||||
from pretix.celery_app import app
|
||||
|
||||
|
||||
def _validate(cf: CachedFile, charset: str, cols: List[ImportColumn], settings: dict):
|
||||
try:
|
||||
parsed = parse_csv(cf.file, charset=charset)
|
||||
except UnicodeDecodeError as e:
|
||||
raise DataImportError(
|
||||
_(
|
||||
'Error decoding special characters in your file: {message}').format(
|
||||
message=str(e)
|
||||
)
|
||||
)
|
||||
data = []
|
||||
for i, record in enumerate(parsed):
|
||||
if not any(record.values()):
|
||||
continue
|
||||
values = {}
|
||||
for c in cols:
|
||||
val = c.resolve(settings, record)
|
||||
if isinstance(val, str):
|
||||
val = val.strip()
|
||||
try:
|
||||
values[c.identifier] = c.clean(val, values)
|
||||
except ValidationError as e:
|
||||
raise DataImportError(
|
||||
_(
|
||||
'Error while importing value "{value}" for column "{column}" in line "{line}": {message}').format(
|
||||
value=val if val is not None else '', column=c.verbose_name, line=i + 1, message=e.message
|
||||
)
|
||||
)
|
||||
data.append(values)
|
||||
return data
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, throws=(DataImportError,))
|
||||
def import_orders(event: Event, fileid: str, settings: dict, locale: str, user, charset=None) -> None:
|
||||
cf = CachedFile.objects.get(id=fileid)
|
||||
user = User.objects.get(pk=user)
|
||||
with language(locale, event.settings.region):
|
||||
cols = get_order_import_columns(event)
|
||||
data = _validate(cf, charset, cols, settings)
|
||||
|
||||
if settings['orders'] == 'one' and len(data) > django_settings.PRETIX_MAX_ORDER_SIZE:
|
||||
raise DataImportError(
|
||||
_('Orders cannot have more than %(max)s positions.') % {'max': django_settings.PRETIX_MAX_ORDER_SIZE}
|
||||
)
|
||||
|
||||
orders = []
|
||||
order = None
|
||||
|
||||
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
|
||||
# shorter. We'll see what works better in reality…
|
||||
lock_seats = []
|
||||
for i, record in enumerate(data):
|
||||
try:
|
||||
if order is None or settings['orders'] == 'many':
|
||||
order = Order(
|
||||
event=event,
|
||||
testmode=settings['testmode'],
|
||||
)
|
||||
order.meta_info = {}
|
||||
order._positions = []
|
||||
order._address = InvoiceAddress()
|
||||
order._address.name_parts = {'_scheme': event.settings.name_scheme}
|
||||
orders.append(order)
|
||||
|
||||
position = OrderPosition(positionid=len(order._positions) + 1)
|
||||
position.attendee_name_parts = {'_scheme': event.settings.name_scheme}
|
||||
position.meta_info = {}
|
||||
order._positions.append(position)
|
||||
position.assign_pseudonymization_id()
|
||||
|
||||
for c in cols:
|
||||
c.assign(record.get(c.identifier), order, position, order._address)
|
||||
|
||||
if position.seat is not None:
|
||||
lock_seats.append(position.seat)
|
||||
except (ValidationError, ImportError) as e:
|
||||
raise DataImportError(
|
||||
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
|
||||
)
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# We don't support vouchers, quotas, or memberships here, so we only need to lock if seats are in use
|
||||
if lock_seats:
|
||||
lock_objects(lock_seats, shared_lock_objects=[event])
|
||||
for s in lock_seats:
|
||||
if not s.is_available():
|
||||
raise DataImportError(_('The seat you selected has already been taken. Please select a different seat.'))
|
||||
|
||||
save_transactions = []
|
||||
for o in orders:
|
||||
o.total = sum([c.price for c in o._positions]) # currently no support for fees
|
||||
if o.total == Decimal('0.00'):
|
||||
o.status = Order.STATUS_PAID
|
||||
o.save()
|
||||
OrderPayment.objects.create(
|
||||
local_id=1,
|
||||
order=o,
|
||||
amount=Decimal('0.00'),
|
||||
provider='free',
|
||||
info='{}',
|
||||
payment_date=now(),
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||
)
|
||||
elif settings['status'] == 'paid':
|
||||
o.status = Order.STATUS_PAID
|
||||
o.save()
|
||||
OrderPayment.objects.create(
|
||||
local_id=1,
|
||||
order=o,
|
||||
amount=o.total,
|
||||
provider='manual',
|
||||
info='{}',
|
||||
payment_date=now(),
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||
)
|
||||
else:
|
||||
o.status = Order.STATUS_PENDING
|
||||
o.save()
|
||||
for p in o._positions:
|
||||
p.order = o
|
||||
p.save()
|
||||
o._address.order = o
|
||||
o._address.save()
|
||||
for c in cols:
|
||||
c.save(o)
|
||||
o.log_action(
|
||||
'pretix.event.order.placed',
|
||||
user=user,
|
||||
data={'source': 'import'}
|
||||
)
|
||||
save_transactions += o.create_transactions(is_new=True, fees=[], positions=o._positions, save=False)
|
||||
Transaction.objects.bulk_create(save_transactions)
|
||||
|
||||
for o in orders:
|
||||
with language(o.locale, event.settings.region):
|
||||
order_placed.send(event, order=o)
|
||||
if o.status == Order.STATUS_PAID:
|
||||
order_paid.send(event, order=o)
|
||||
|
||||
gen_invoice = invoice_qualified(o) and (
|
||||
(event.settings.get('invoice_generate') == 'True') or
|
||||
(event.settings.get('invoice_generate') == 'paid' and o.status == Order.STATUS_PAID)
|
||||
) and not o.invoices.last()
|
||||
if gen_invoice:
|
||||
generate_invoice(o, trigger_pdf=True)
|
||||
except DataImportError:
|
||||
raise ValidationError(_('We were not able to process your request completely as the server was too busy. '
|
||||
'Please try again.'))
|
||||
cf.delete()
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, throws=(DataImportError,))
|
||||
def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user, charset=None) -> None:
|
||||
cf = CachedFile.objects.get(id=fileid)
|
||||
user = User.objects.get(pk=user)
|
||||
with language(locale, event.settings.region):
|
||||
cols = get_voucher_import_columns(event)
|
||||
data = _validate(cf, charset, cols, settings)
|
||||
|
||||
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
|
||||
# shorter. We'll see what works better in reality…
|
||||
vouchers = []
|
||||
lock_seats = []
|
||||
for i, record in enumerate(data):
|
||||
try:
|
||||
voucher = Voucher(event=event)
|
||||
vouchers.append(voucher)
|
||||
|
||||
Voucher.clean_item_properties(
|
||||
record,
|
||||
event,
|
||||
record.get('quota'),
|
||||
record.get('item'),
|
||||
record.get('variation'),
|
||||
block_quota=record.get('block_quota')
|
||||
)
|
||||
Voucher.clean_subevent(record, event)
|
||||
Voucher.clean_max_usages(record, 0)
|
||||
|
||||
for c in cols:
|
||||
c.assign(record.get(c.identifier), voucher)
|
||||
|
||||
if voucher.seat is not None:
|
||||
lock_seats.append(voucher.seat)
|
||||
except (ValidationError, ImportError) as e:
|
||||
raise DataImportError(
|
||||
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
# We don't support quotas here, so we only need to lock if seats are in use
|
||||
if lock_seats:
|
||||
lock_objects(lock_seats, shared_lock_objects=[event])
|
||||
for s in lock_seats:
|
||||
if not s.is_available():
|
||||
raise DataImportError(
|
||||
_('The seat you selected has already been taken. Please select a different seat.'))
|
||||
|
||||
for v in vouchers:
|
||||
v.save()
|
||||
v.log_action(
|
||||
'pretix.voucher.added',
|
||||
user=user,
|
||||
data={'source': 'import'}
|
||||
)
|
||||
for c in cols:
|
||||
c.save(v)
|
||||
cf.delete()
|
||||
Reference in New Issue
Block a user