Compare commits

..

11 Commits

Author SHA1 Message Date
Mira Weller
22d8158356 Fix rrules 2025-10-09 17:22:31 +02:00
luelista
bff0f54bf8 Fix formset widths (#5530)
* Remove explicitly specified width for formset-forms
With that style, all formset rows were a fix pixels less wide than surrounding content

* Set select2 width to 100% so they adapt when browser window is resized
2025-10-09 17:02:45 +02:00
luelista
50c1c9c724 Run sync job as soon as possible on clicking "Sync now" (#5526) 2025-10-08 13:15:09 +02:00
Raphael Michel
802268df46 Fix ajax error not being shown 2025-10-08 09:47:09 +02:00
luelista
a823f261f3 Fix unhandled exception in datasync code in case order should not be synced (PRETIXEU-C9H) (#5525)
* Fix unhandled exception in datasync code in case order should not be synced (PRETIXEU-C9H)
* Add test case
2025-10-07 19:58:26 +02:00
luelista
59a754f913 Fix log entry details for datasync logs without external link (Z#23210015) (#5524) 2025-10-07 19:58:13 +02:00
Raphael Michel
82eca01e5c QuotaListExporter: Flip incorrect allow_repeatable_read 2025-10-07 13:20:06 +02:00
Raphael Michel
943f594b6b InvoiceExporter: Set repeatable_read = False 2025-10-07 13:18:03 +02:00
Raphael Michel
15cbb3a416 Do not crash if generate_invoice fails (#5483)
* Do not crash if generate_invoice fails

* Add logging

* Add cancellation to try block

* One last thing…
2025-10-07 11:20:31 +02:00
dependabot[bot]
f447e7b9c4 Update sentry-sdk requirement from ==2.38.* to ==2.40.* (#5521)
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.38.0...2.40.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.40.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 10:40:54 +02:00
Raphael Michel
dcf473c543 Send invoice to organizer in plain text (Z#23210026) (#5518) 2025-10-07 10:40:15 +02:00
22 changed files with 255 additions and 180 deletions

View File

@@ -91,7 +91,7 @@ dependencies = [
"redis==6.4.*", "redis==6.4.*",
"reportlab==4.4.*", "reportlab==4.4.*",
"requests==2.32.*", "requests==2.32.*",
"sentry-sdk==2.38.*", "sentry-sdk==2.40.*",
"sepaxml==2.6.*", "sepaxml==2.6.*",
"stripe==7.9.*", "stripe==7.9.*",
"text-unidecode==1.*", "text-unidecode==1.*",

View File

@@ -764,7 +764,13 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
) and not order.invoices.last() ) and not order.invoices.last()
invoice = None invoice = None
if gen_invoice: if gen_invoice:
invoice = generate_invoice(order, trigger_pdf=True) try:
invoice = generate_invoice(order, trigger_pdf=True)
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
# Refresh serializer only after running signals # Refresh serializer only after running signals
prefetch_related_objects([order], self._positions_prefetch(request)) prefetch_related_objects([order], self._positions_prefetch(request))

View File

@@ -391,7 +391,7 @@ class OutboundSyncProvider:
def sync_order(self, order): def sync_order(self, order):
if not self.should_sync_order(order): if not self.should_sync_order(order):
logger.debug("Skipping order %r", order) logger.debug("Skipping order %r", order)
return return {}
logger.debug("Syncing order %r", order) logger.debug("Syncing order %r", order)
positions = list( positions = list(

View File

@@ -125,6 +125,7 @@ class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
identifier = 'invoices' identifier = 'invoices'
verbose_name = _('All invoices') verbose_name = _('All invoices')
description = _('Download all invoices created by the system as a ZIP file of PDF files.') description = _('Download all invoices created by the system as a ZIP file of PDF files.')
repeatable_read = False
def render(self, form_data: dict, output_file=None): def render(self, form_data: dict, output_file=None):
qs = self.invoices_queryset(form_data).filter(shredded=False) qs = self.invoices_queryset(form_data).filter(shredded=False)

View File

@@ -1162,7 +1162,7 @@ class QuotaListExporter(ListExporter):
yield headers yield headers
quotas = list(self.event.quotas.select_related('subevent')) quotas = list(self.event.quotas.select_related('subevent'))
qa = QuotaAvailability(full_results=True, allow_repeatable_read=False) qa = QuotaAvailability(full_results=True, allow_repeatable_read=True)
qa.queue(*quotas) qa.queue(*quotas)
qa.compute() qa.compute()

View File

@@ -1969,14 +1969,20 @@ class OrderPayment(models.Model):
self.order.invoice_dirty self.order.invoice_dirty
) )
if gen_invoice: if gen_invoice:
if invoices: try:
last_i = self.order.invoices.filter(is_cancellation=False).last() if invoices:
if not last_i.canceled: last_i = self.order.invoices.filter(is_cancellation=False).last()
generate_cancellation(last_i) if not last_i.canceled:
invoice = generate_invoice( generate_cancellation(last_i)
self.order, invoice = generate_invoice(
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment self.order,
) trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment
)
except Exception as e:
logger.exception("Could not generate invoice.")
self.order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
transmit_invoice_task = invoice_transmission_separately(invoice) transmit_invoice_task = invoice_transmission_separately(invoice)
transmit_invoice_mail = not transmit_invoice_task and self.order.event.settings.invoice_email_attachment and self.order.email transmit_invoice_mail = not transmit_invoice_task and self.order.event.settings.invoice_email_attachment and self.order.email

View File

@@ -671,6 +671,7 @@ def send_invoices_to_organizer(sender, **kwargs):
event=i.event, event=i.event,
invoices=[i], invoices=[i],
auto_email=True, auto_email=True,
plain_text_only=True,
) )
i.sent_to_organizer = True i.sent_to_organizer = True
else: else:

View File

@@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # 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/>. # <https://www.gnu.org/licenses/>.
# #
import logging
from decimal import Decimal from decimal import Decimal
from typing import List from typing import List
@@ -43,6 +44,8 @@ from pretix.base.services.tasks import ProfiledEventTask
from pretix.base.signals import order_paid, order_placed from pretix.base.signals import order_paid, order_placed
from pretix.celery_app import app from pretix.celery_app import app
logger = logging.getLogger(__name__)
def _validate(cf: CachedFile, charset: str, cols: List[ImportColumn], settings: dict): def _validate(cf: CachedFile, charset: str, cols: List[ImportColumn], settings: dict):
try: try:
@@ -233,7 +236,13 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
(event.settings.get('invoice_generate') == 'paid' and o.status == Order.STATUS_PAID) (event.settings.get('invoice_generate') == 'paid' and o.status == Order.STATUS_PAID)
) and not o.invoices.last() ) and not o.invoices.last()
if gen_invoice: if gen_invoice:
generate_invoice(o, trigger_pdf=True) try:
generate_invoice(o, trigger_pdf=True)
except Exception as e:
logger.exception("Could not generate invoice.")
o.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
except DataImportError: except DataImportError:
raise ValidationError(_('We were not able to process your request completely as the server was too busy. ' raise ValidationError(_('We were not able to process your request completely as the server was too busy. '
'Please try again.')) 'Please try again.'))

View File

@@ -264,7 +264,13 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
num_invoices = order.invoices.filter(is_cancellation=False).count() num_invoices = order.invoices.filter(is_cancellation=False).count()
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order): if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order):
generate_invoice(order) try:
generate_invoice(order)
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
def extend_order(order: Order, new_date: datetime, force: bool=False, valid_if_pending: bool=None, user: User=None, auth=None): def extend_order(order: Order, new_date: datetime, force: bool=False, valid_if_pending: bool=None, user: User=None, auth=None):
@@ -312,7 +318,13 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, valid_if_p
if was_expired: if was_expired:
num_invoices = order.invoices.filter(is_cancellation=False).count() num_invoices = order.invoices.filter(is_cancellation=False).count()
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order): if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order):
generate_invoice(order) try:
generate_invoice(order)
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
order.create_transactions() order.create_transactions()
with transaction.atomic(): with transaction.atomic():
@@ -397,13 +409,19 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
if order.event.settings.get('invoice_generate') == 'True' and invoice_qualified(order): if order.event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
if not invoice: if not invoice:
invoice = generate_invoice( try:
order, invoice = generate_invoice(
# send_mail will trigger PDF generation later order,
trigger_pdf=not transmit_invoice_mail # send_mail will trigger PDF generation later
) trigger_pdf=not transmit_invoice_mail
if transmit_invoice_task: )
transmit_invoice.apply_async(args=(order.event_id, invoice.pk, False)) if transmit_invoice_task:
transmit_invoice.apply_async(args=(order.event_id, invoice.pk, False))
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
if send_mail: if send_mail:
with language(order.locale, order.event.settings.region): with language(order.locale, order.event.settings.region):
@@ -608,7 +626,13 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
order.save(update_fields=['status', 'cancellation_date', 'total']) order.save(update_fields=['status', 'cancellation_date', 'total'])
if cancel_invoice and i: if cancel_invoice and i:
invoices.append(generate_invoice(order)) try:
invoices.append(generate_invoice(order))
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
else: else:
order.status = Order.STATUS_CANCELED order.status = Order.STATUS_CANCELED
order.cancellation_date = now() order.cancellation_date = now()
@@ -1306,13 +1330,19 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
) )
) )
if invoice_required: if invoice_required:
invoice = generate_invoice( try:
order, invoice = generate_invoice(
# send_mail will trigger PDF generation later order,
trigger_pdf=not transmit_invoice_mail # send_mail will trigger PDF generation later
) trigger_pdf=not transmit_invoice_mail
if transmit_invoice_task: )
transmit_invoice.apply_async(args=(event.pk, invoice.pk, False)) if transmit_invoice_task:
transmit_invoice.apply_async(args=(event.pk, invoice.pk, False))
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
if order.email: if order.email:
if order.require_approval: if order.require_approval:
@@ -2701,7 +2731,13 @@ class OrderChangeManager:
) )
if split_order.total != Decimal('0.00') and self.order.invoices.filter(is_cancellation=False).last(): if split_order.total != Decimal('0.00') and self.order.invoices.filter(is_cancellation=False).last():
generate_invoice(split_order) try:
generate_invoice(split_order)
except Exception as e:
logger.exception("Could not generate invoice.")
split_order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
order_split.send(sender=self.order.event, original=self.order, split_order=split_order) order_split.send(sender=self.order.event, original=self.order, split_order=split_order)
return split_order return split_order
@@ -2812,15 +2848,27 @@ class OrderChangeManager:
if order_now_qualified: if order_now_qualified:
if invoice_should_be_generated_now: if invoice_should_be_generated_now:
if i and not i.canceled: try:
self._invoices.append(generate_cancellation(i)) if i and not i.canceled:
self._invoices.append(generate_invoice(self.order)) self._invoices.append(generate_cancellation(i))
self._invoices.append(generate_invoice(self.order))
except Exception as e:
logger.exception("Could not generate invoice.")
self.order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
elif invoice_should_be_generated_later: elif invoice_should_be_generated_later:
self.order.invoice_dirty = True self.order.invoice_dirty = True
self.order.save(update_fields=["invoice_dirty"]) self.order.save(update_fields=["invoice_dirty"])
else: else:
if i and not i.canceled: try:
self._invoices.append(generate_cancellation(i)) if i and not i.canceled:
self._invoices.append(generate_cancellation(i))
except Exception as e:
logger.exception("Could not generate invoice.")
self.order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
def _check_complete_cancel(self): def _check_complete_cancel(self):
current = self.order.positions.count() current = self.order.positions.count()
@@ -3246,8 +3294,14 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
has_active_invoice = i and not i.canceled has_active_invoice = i and not i.canceled
if has_active_invoice and order.total != oldtotal: if has_active_invoice and order.total != oldtotal:
generate_cancellation(i) try:
generate_invoice(order) generate_cancellation(i)
generate_invoice(order)
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
new_invoice_created = True new_invoice_created = True
elif (not has_active_invoice or order.invoice_dirty) and invoice_qualified(order): elif (not has_active_invoice or order.invoice_dirty) and invoice_qualified(order):
@@ -3255,13 +3309,19 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
order.event.settings.get('invoice_generate') == 'paid' and order.event.settings.get('invoice_generate') == 'paid' and
new_payment.payment_provider.requires_invoice_immediately new_payment.payment_provider.requires_invoice_immediately
): ):
if has_active_invoice: try:
generate_cancellation(i) if has_active_invoice:
i = generate_invoice(order) generate_cancellation(i)
new_invoice_created = True i = generate_invoice(order)
order.log_action('pretix.event.order.invoice.generated', data={ new_invoice_created = True
'invoice': i.pk order.log_action('pretix.event.order.invoice.generated', data={
}) 'invoice': i.pk
})
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
order.create_transactions() order.create_transactions()
return old_fee, new_fee, fee, new_payment, new_invoice_created return old_fee, new_fee, fee, new_payment, new_invoice_created

View File

@@ -390,8 +390,7 @@ class QuotaFormSet(I18nInlineFormSet):
use_required_attribute=False, use_required_attribute=False,
locales=self.locales, locales=self.locales,
event=self.event, event=self.event,
items=self.items, items=self.items
searchable_selection=self.searchable_selection,
) )
self.add_fields(form, None) self.add_fields(form, None)
return form return form

View File

@@ -455,7 +455,7 @@ class OrderDataSyncSuccessLogEntryType(OrderDataSyncLogEntryType):
links.append(", ".join( links.append(", ".join(
prov.get_external_link_html(logentry.event, obj['external_link_href'], obj['external_link_display_name']) prov.get_external_link_html(logentry.event, obj['external_link_href'], obj['external_link_display_name'])
for obj in objs for obj in objs
if obj and 'external_link_href' in obj and 'external_link_display_name' in obj if obj and obj.get('external_link_href') and obj.get('external_link_display_name')
)) ))
return mark_safe(escape(super().display(logentry, data)) + "".join("<p>" + link + "</p>" for link in links)) return mark_safe(escape(super().display(logentry, data)) + "".join("<p>" + link + "</p>" for link in links))
@@ -522,6 +522,7 @@ def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, bl
'pretix.event.order.customer.changed': _('The customer account has been changed.'), 'pretix.event.order.customer.changed': _('The customer account has been changed.'),
'pretix.event.order.locale.changed': _('The order locale has been changed.'), 'pretix.event.order.locale.changed': _('The order locale has been changed.'),
'pretix.event.order.invoice.generated': _('The invoice has been generated.'), 'pretix.event.order.invoice.generated': _('The invoice has been generated.'),
'pretix.event.order.invoice.failed': _('The invoice could not be generated.'),
'pretix.event.order.invoice.regenerated': _('The invoice has been regenerated.'), 'pretix.event.order.invoice.regenerated': _('The invoice has been regenerated.'),
'pretix.event.order.invoice.reissued': _('The invoice has been reissued.'), 'pretix.event.order.invoice.reissued': _('The invoice has been reissued.'),
'pretix.event.order.invoice.sent': _('The invoice {full_invoice_no} has been sent.'), 'pretix.event.order.invoice.sent': _('The invoice {full_invoice_no} has been sent.'),

View File

@@ -487,32 +487,30 @@
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %} {% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
</p> </p>
{% for f in itemvar_forms %} {% for f in itemvar_forms %}
<div data-itemvar="{{ f.item.id }}{% if f.variation %}-{{ f.variation.id }}{% endif %}"> {% bootstrap_form_errors f %}
{% bootstrap_form_errors f %} <div class="form-group subevent-itemvar-group">
<div class="form-group subevent-itemvar-group"> <label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price"> {% if f.variation %}{{ f.item }} {{ f.variation }}{% else %}{{ f.item }}{% endif %}
{% if f.variation %}{{ f.item }} {{ f.variation }}{% else %}{{ f.item }}{% endif %} </label>
</label> <div class="col-md-4">
<div class="col-md-4"> <label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br> {% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
</div>
<div class="col-md-4">
<br>
{% bootstrap_field f.disabled layout="inline" form_group_class="" %}
</div>
</div> </div>
<div class="form-group subevent-itemvar-group"> <div class="col-md-4">
<div class="col-md-4 col-md-offset-3"> <br>
<label for="{{ f.rel_available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label> {% bootstrap_field f.disabled layout="inline" form_group_class="" %}
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br> </div>
{% bootstrap_field f.rel_available_from form_group_class="" layout="inline" %} </div>
</div> <div class="form-group subevent-itemvar-group">
<div class="col-md-4"> <div class="col-md-4 col-md-offset-3">
<label for="{{ f.rel_available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label> <label for="{{ f.rel_available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br> {% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
{% bootstrap_field f.rel_available_until form_group_class="" layout="inline" %} {% bootstrap_field f.rel_available_from form_group_class="" layout="inline" %}
</div> </div>
<div class="col-md-4">
<label for="{{ f.rel_available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
{% bootstrap_field f.rel_available_until form_group_class="" layout="inline" %}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
@@ -627,7 +625,7 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</fieldset> </fieldset>
<div class="form-group submit-group submit-group-sticky"> <div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %} {% trans "Save" %}
</button> </button>

View File

@@ -363,7 +363,7 @@
</div> </div>
{% endif %} {% endif %}
</fieldset> </fieldset>
<div class="form-group submit-group submit-group-sticky"> <div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %} {% trans "Save" %}
</button> </button>

View File

@@ -130,32 +130,30 @@
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %} {% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
</p> </p>
{% for f in itemvar_forms %} {% for f in itemvar_forms %}
<div data-itemvar="{{ f.item.id }}{% if f.variation %}-{{ f.variation.id }}{% endif %}"> {% bootstrap_form_errors f %}
{% bootstrap_form_errors f %} <div class="form-group subevent-itemvar-group">
<div class="form-group subevent-itemvar-group"> <label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price"> {% if f.variation %}{{ f.item }} {{ f.variation }}{% else %}{{ f.item }}{% endif %}
{% if f.variation %}{{ f.item }} {{ f.variation }}{% else %}{{ f.item }}{% endif %} </label>
</label> <div class="col-md-4">
<div class="col-md-4"> <label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br> {% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
</div>
<div class="col-md-4">
<br>
{% bootstrap_field f.disabled layout="inline" form_group_class="" %}
</div>
</div> </div>
<div class="form-group subevent-itemvar-group"> <div class="col-md-4">
<div class="col-md-4 col-md-offset-3"> <br>
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label> {% bootstrap_field f.disabled layout="inline" form_group_class="" %}
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br> </div>
{% bootstrap_field f.available_from form_group_class="foo" layout="inline" %} </div>
</div> <div class="form-group subevent-itemvar-group">
<div class="col-md-4"> <div class="col-md-4 col-md-offset-3">
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label> <label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br> {% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
{% bootstrap_field f.available_until form_group_class="" layout="inline" %} {% bootstrap_field f.available_from form_group_class="foo" layout="inline" %}
</div> </div>
<div class="col-md-4">
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
{% bootstrap_field f.available_until form_group_class="" layout="inline" %}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
@@ -284,7 +282,7 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="form-group submit-group submit-group-sticky"> <div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %} {% trans "Save" %}
</button> </button>

View File

@@ -78,8 +78,8 @@ class ControlSyncJob(OrderView):
prov, meta = datasync_providers.get(active_in=self.request.event, identifier=provider) prov, meta = datasync_providers.get(active_in=self.request.event, identifier=provider)
if self.request.POST.get("queue_sync") == "true": if self.request.POST.get("queue_sync") == "true":
prov.enqueue_order(self.order, 'user') prov.enqueue_order(self.order, 'user', immediate=True)
messages.success(self.request, _('The sync job has been enqueued and will run in the next minutes.')) messages.success(self.request, _('The sync job has been set to run as soon as possible.'))
elif self.request.POST.get("cancel_job"): elif self.request.POST.get("cancel_job"):
with transaction.atomic(): with transaction.atomic():
try: try:

View File

@@ -36,6 +36,7 @@ import copy
import hmac import hmac
import inspect import inspect
import json import json
import logging
import mimetypes import mimetypes
import os import os
import re import re
@@ -98,6 +99,8 @@ from pretix.presale.views import (
from pretix.presale.views.event import get_grouped_items from pretix.presale.views.event import get_grouped_items
from pretix.presale.views.robots import NoSearchIndexViewMixin from pretix.presale.views.robots import NoSearchIndexViewMixin
logger = logging.getLogger(__name__)
class OrderDetailMixin(NoSearchIndexViewMixin): class OrderDetailMixin(NoSearchIndexViewMixin):
@@ -734,11 +737,18 @@ class OrderInvoiceCreate(EventViewMixin, OrderDetailMixin, View):
elif self.order.invoices.exists(): elif self.order.invoices.exists():
messages.error(self.request, _('An invoice for this order already exists.')) messages.error(self.request, _('An invoice for this order already exists.'))
else: else:
i = generate_invoice(self.order) try:
self.order.log_action('pretix.event.order.invoice.generated', data={ i = generate_invoice(self.order)
'invoice': i.pk self.order.log_action('pretix.event.order.invoice.generated', data={
}) 'invoice': i.pk
messages.success(self.request, _('The invoice has been generated.')) })
messages.success(self.request, _('The invoice has been generated.'))
except Exception as e:
logger.exception("Could not generate invoice.")
self.order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
messages.error(self.request, _('Invoice generation has failed, please reach out to the organizer.'))
return redirect(self.get_order_url()) return redirect(self.get_order_url())
@@ -807,24 +817,37 @@ class OrderModify(EventViewMixin, OrderDetailMixin, OrderQuestionsViewMixin, Tem
elif self.order.invoices.exists(): elif self.order.invoices.exists():
messages.error(self.request, _('An invoice for this order already exists.')) messages.error(self.request, _('An invoice for this order already exists.'))
else: else:
i = generate_invoice(self.order) try:
self.order.log_action('pretix.event.order.invoice.generated', data={ i = generate_invoice(self.order)
'invoice': i.pk self.order.log_action('pretix.event.order.invoice.generated', data={
}) 'invoice': i.pk
messages.success(self.request, _('The invoice has been generated.')) })
messages.success(self.request, _('The invoice has been generated.'))
except Exception as e:
logger.exception("Could not generate invoice.")
self.order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
messages.error(self.request, _('Invoice generation has failed, please reach out to the organizer.'))
elif self.request.event.settings.invoice_reissue_after_modify: elif self.request.event.settings.invoice_reissue_after_modify:
if self.invoice_form.changed_data: if self.invoice_form.changed_data:
inv = self.order.invoices.last() try:
if inv and not inv.canceled and not inv.shredded: inv = self.order.invoices.last()
c = generate_cancellation(inv) if inv and not inv.canceled and not inv.shredded:
if self.order.status != Order.STATUS_CANCELED: c = generate_cancellation(inv)
inv = generate_invoice(self.order) if self.order.status != Order.STATUS_CANCELED:
else: inv = generate_invoice(self.order)
inv = c else:
self.order.log_action('pretix.event.order.invoice.reissued', data={ inv = c
'invoice': inv.pk self.order.log_action('pretix.event.order.invoice.reissued', data={
'invoice': inv.pk
})
messages.success(self.request, _('The invoice has been reissued.'))
except Exception as e:
self.order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
}) })
messages.success(self.request, _('The invoice has been reissued.')) logger.exception("Could not generate invoice.")
invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'order': self.order.pk}) invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'order': self.order.pk})
CachedTicket.objects.filter(order_position__order=self.order).delete() CachedTicket.objects.filter(order_position__order=self.order).delete()

View File

@@ -330,9 +330,11 @@ var ajaxErrDialog = {
$("#ajaxerr .links").html("<a class='btn btn-default ajaxerr-close'>" $("#ajaxerr .links").html("<a class='btn btn-default ajaxerr-close'>"
+ gettext("Close message") + "</a>"); + gettext("Close message") + "</a>");
$("body").addClass("ajaxerr has-modal-dialog"); $("body").addClass("ajaxerr has-modal-dialog");
$("#ajaxerr").prop("hidden", false);
}, },
hide: function () { hide: function () {
"use strict"; "use strict";
$("body").removeClass("ajaxerr has-modal-dialog"); $("body").removeClass("ajaxerr has-modal-dialog");
$("#ajaxerr").prop("hidden", true);
}, },
}; };

View File

@@ -60,7 +60,7 @@ var i18nToString = function (i18nstring) {
$(document).ajaxError(function (event, jqXHR, settings, thrownError) { $(document).ajaxError(function (event, jqXHR, settings, thrownError) {
waitingDialog.hide(); waitingDialog.hide();
var c = $(jqXHR.responseText).filter('.container'); var c = $(jqXHR.responseText).filter('.container');
if (jqXHR.responseText.indexOf("<!-- pretix-login-marker -->") !== -1) { if (jqXHR.responseText && jqXHR.responseText.indexOf("<!-- pretix-login-marker -->") !== -1) {
location.href = '/control/login?next=' + encodeURIComponent(location.pathname + location.search + location.hash) location.href = '/control/login?next=' + encodeURIComponent(location.pathname + location.search + location.hash)
} else if (c.length > 0) { } else if (c.length > 0) {
ajaxErrDialog.show(c.first().html()); ajaxErrDialog.show(c.first().html());
@@ -485,6 +485,7 @@ var form_handlers = function (el) {
theme: "bootstrap", theme: "bootstrap",
language: $("body").attr("data-select2-locale"), language: $("body").attr("data-select2-locale"),
data: JSON.parse($(this.getAttribute('data-select2-src')).text()), data: JSON.parse($(this.getAttribute('data-select2-src')).text()),
width: '100%',
}).val(selectedValue).trigger('change'); }).val(selectedValue).trigger('change');
}); });

View File

@@ -26,7 +26,6 @@ $(document).on("pretix:bind-forms", function () {
} }
} }
// RRule editor
function rrule_preview() { function rrule_preview() {
var ruleset = new rrule.RRuleSet(); var ruleset = new rrule.RRuleSet();
@@ -122,14 +121,7 @@ $(document).on("pretix:bind-forms", function () {
}); });
} }
} }
$("#rrule-formset").on("change keydown keyup keypress dp.change", "input, select", function () {
rrule_preview();
});
rrule_preview();
$("#rrule-formset").on("formAdded", "div", function (event) {rrule_bind_form($(event.target)); });
// Timeslot editor
$("#subevent_add_many_slots_go").on("click", function () { $("#subevent_add_many_slots_go").on("click", function () {
$("#time-formset [data-formset-form]").each(function () { $("#time-formset [data-formset-form]").each(function () {
var tf = $(this).find("[name$=time_from]").val() var tf = $(this).find("[name$=time_from]").val()
@@ -175,45 +167,13 @@ $(document).on("pretix:bind-forms", function () {
$(this).addClass("hidden"); $(this).addClass("hidden");
}); });
// Hide config for products that are not for sale $("#rrule-formset").on("change keydown keyup keypress dp.change", "input, select", function () {
function quota_form_handlers(el) { rrule_preview();
// searchable_selection = True });
el.find('[id^="id_quotas-"]').on("select2:select select2:unselect", () => { rrule_preview();
update_item_visibility();
});
// searchable_selection = False
el.find('input[id^="id_quotas-"][id*=itemvars_]').on("change", () => {
update_item_visibility();
});
}
function update_item_visibility() {
const itemvars = [];
// searchable_selection = True $("#rrule-formset").on("formAdded", "div", function (event) { rrule_bind_form($(event.target)); });
$("select[id^=id_quotas-][id$=-itemvars]").filter((idx, el) => {
return !$(el).closest('[data-formset-form]').is('[data-formset-form-deleted]');
}).each((_, e) => itemvars.push(...$(e).val()));
// searchable_selection = False
$("input[id^=id_quotas-][id*=itemvars_]:checked").filter((idx, el) => {
return !$(el).closest('[data-formset-form]').is('[data-formset-form-deleted]');
}).each((_, e) => itemvars.push($(e).val()));
$("div[data-itemvar]").each(function (idx, e) {
const el = $(e);
el.prop("hidden", !itemvars.includes(el.attr("data-itemvar")) && !el.find(".has-error, .alert-danger").length);
});
}
$('[data-formset-prefix="quotas"]').on("formDeleted", "div", () => {
update_item_visibility();
}).on("formAdded", "div", (event) => {
quota_form_handlers($(event.target));
update_item_visibility();
})
quota_form_handlers($("body"));
update_item_visibility();
// Auto-set name of check-in list
var $namef = $("input[id^=id_name]").first(); var $namef = $("input[id^=id_name]").first();
var lastValue = $namef.val(); var lastValue = $namef.val();
$namef.change(function () { $namef.change(function () {

View File

@@ -63,10 +63,6 @@ td > .form-group > .checkbox {
@include box-shadow(none); @include box-shadow(none);
} }
div[data-formset-body], div[data-formset-form], div[data-nested-formset-form], div[data-nested-formset-body], details[data-formset-form] {
width: 100%;
}
.form-plugins .panel-title { .form-plugins .panel-title {
line-height: 34px; line-height: 34px;
} }
@@ -95,12 +91,6 @@ div[data-formset-body], div[data-formset-form], div[data-nested-formset-form], d
} }
} }
.submit-group-sticky {
position: sticky;
bottom: 0;
z-index: 100;
}
.panel .form-group:last-child { .panel .form-group:last-child {
margin-bottom: 0; margin-bottom: 0;
} }

View File

@@ -1934,7 +1934,7 @@ function buildPoslist(bysetpos, timeset, start, end, ii, dayset) {
function iter(iterResult, options) { function iter(iterResult, options) {
var dtstart = options.dtstart, freq = options.freq, interval = options.interval, until = options.until, bysetpos = options.bysetpos; var dtstart = options.dtstart, freq = options.freq, interval = options.interval, until = options.until, bysetpos = options.bysetpos;
var count = options.count; var count = options.count;
if (count <= 0 || interval <= 0) { if (count === 0 || count < 0 || interval === 0 || interval < 0) {
return emitResult(iterResult); return emitResult(iterResult);
} }
var counterDate = datetime_DateTime.fromDate(dtstart); var counterDate = datetime_DateTime.fromDate(dtstart);

View File

@@ -327,6 +327,26 @@ def test_enqueue_order_twice(event):
SimpleOrderSync.enqueue_order(order, 'testcase_2nd') SimpleOrderSync.enqueue_order(order, 'testcase_2nd')
class DoNothingSync(SimpleOrderSync):
def should_sync_order(self, order):
return False
@pytest.mark.django_db
def test_should_not_sync(event):
_register_with_fake_plugin_name(datasync_providers, DoNothingSync, 'testplugin')
DoNothingSync.fake_api_client = FakeSyncAPI()
for order in event.orders.order_by("code").all():
DoNothingSync.enqueue_order(order, 'testcase')
sync_all()
assert DoNothingSync.fake_api_client.fake_database == {}
StaticMappingWithAssociations = namedtuple('StaticMappingWithAssociations', ( StaticMappingWithAssociations = namedtuple('StaticMappingWithAssociations', (
'id', 'pretix_model', 'external_object_type', 'pretix_id_field', 'external_id_field', 'property_mappings', 'association_mappings' 'id', 'pretix_model', 'external_object_type', 'pretix_id_field', 'external_id_field', 'property_mappings', 'association_mappings'
)) ))