diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py index 6e01de4bd9..dde70c090e 100644 --- a/src/pretix/api/serializers/event.py +++ b/src/pretix/api/serializers/event.py @@ -719,6 +719,7 @@ class EventSettingsSerializer(SettingsSerializer): 'invoice_include_expire_date', 'invoice_address_explanation_text', 'invoice_email_attachment', + 'invoice_email_organizer', 'invoice_address_from_name', 'invoice_address_from', 'invoice_address_from_zipcode', diff --git a/src/pretix/base/migrations/0186_invoice_sent_to_organizer.py b/src/pretix/base/migrations/0186_invoice_sent_to_organizer.py new file mode 100644 index 0000000000..ce47502b19 --- /dev/null +++ b/src/pretix/base/migrations/0186_invoice_sent_to_organizer.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.2 on 2021-05-09 14:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0185_memberships'), + ] + + operations = [ + migrations.AddField( + model_name='invoice', + name='sent_to_organizer', + field=models.BooleanField(null=True, default=False), + preserve_default=False, + ), + ] diff --git a/src/pretix/base/models/invoices.py b/src/pretix/base/models/invoices.py index 0dd4313de4..fe1220608b 100644 --- a/src/pretix/base/models/invoices.py +++ b/src/pretix/base/models/invoices.py @@ -109,11 +109,14 @@ class Invoice(models.Model): order = models.ForeignKey('Order', related_name='invoices', db_index=True, on_delete=models.CASCADE) organizer = models.ForeignKey('Organizer', related_name='invoices', db_index=True, on_delete=models.PROTECT) event = models.ForeignKey('Event', related_name='invoices', db_index=True, on_delete=models.CASCADE) + prefix = models.CharField(max_length=160, db_index=True) invoice_no = models.CharField(max_length=19, db_index=True) full_invoice_no = models.CharField(max_length=190, db_index=True) + is_cancellation = models.BooleanField(default=False) refers = models.ForeignKey('Invoice', related_name='refered', null=True, blank=True, on_delete=models.CASCADE) + invoice_from = models.TextField() invoice_from_name = models.CharField(max_length=190, null=True) invoice_from_zipcode = models.CharField(max_length=190, null=True) @@ -121,6 +124,7 @@ class Invoice(models.Model): invoice_from_country = FastCountryField(null=True) invoice_from_tax_id = models.CharField(max_length=190, null=True) invoice_from_vat_id = models.CharField(max_length=190, null=True) + invoice_to = models.TextField() invoice_to_company = models.TextField(null=True) invoice_to_name = models.TextField(null=True) @@ -131,6 +135,9 @@ class Invoice(models.Model): invoice_to_country = FastCountryField(null=True) invoice_to_vat_id = models.TextField(null=True) invoice_to_beneficiary = models.TextField(null=True) + internal_reference = models.TextField(blank=True) + custom_field = models.CharField(max_length=255, null=True) + date = models.DateField(default=today) locale = models.CharField(max_length=50, default='en') introductory_text = models.TextField(blank=True) @@ -138,14 +145,21 @@ class Invoice(models.Model): reverse_charge = models.BooleanField(default=False) payment_provider_text = models.TextField(blank=True) footer_text = models.TextField(blank=True) + foreign_currency_display = models.CharField(max_length=50, null=True, blank=True) foreign_currency_rate = models.DecimalField(decimal_places=4, max_digits=10, null=True, blank=True) foreign_currency_rate_date = models.DateField(null=True, blank=True) + shredded = models.BooleanField(default=False) + # The field sent_to_organizer records whether this invocie was already sent to the organizer by a configured + # mechanism such as email. + # NULL: The cronjob that handles sending did not yet run. + # True: The invoice was sent. + # False: The invoice wasn't sent and never will, because sending was not configured at the time of the check. + sent_to_organizer = models.BooleanField(null=True, blank=True) + file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255) - internal_reference = models.TextField(blank=True) - custom_field = models.CharField(max_length=255, null=True) objects = ScopedManager(organizer='event__organizer') diff --git a/src/pretix/base/services/invoices.py b/src/pretix/base/services/invoices.py index c633acdd6c..f9c8cd5d6d 100644 --- a/src/pretix/base/services/invoices.py +++ b/src/pretix/base/services/invoices.py @@ -288,6 +288,7 @@ def generate_cancellation(invoice: Invoice, trigger_pdf=True): cancellation.date = timezone.now().date() cancellation.payment_provider_text = '' cancellation.file = None + cancellation.sent_to_organizer = None with language(invoice.locale, invoice.event.settings.region): cancellation.invoice_from = invoice.event.settings.get('invoice_address_from') cancellation.invoice_from_name = invoice.event.settings.get('invoice_address_from_name') @@ -442,3 +443,44 @@ def fetch_ecb_rates(sender, **kwargs): gs.settings.ecb_rates_dict = json.dumps(rates, cls=DjangoJSONEncoder) except urllib.error.URLError: logger.exception('Could not retrieve rates from ECB') + + +@receiver(signal=periodic_task) +@scopes_disabled() +def send_invoices_to_organizer(sender, **kwargs): + from pretix.base.services.mail import mail + + batch_size = 50 + # this adds some rate limiting on the number of invoices to send at the same time. If there's more, the next + # cronjob will handle them + max_number_of_batches = 10 + + for i in range(max_number_of_batches): + with transaction.atomic(): + qs = Invoice.objects.filter( + sent_to_organizer__isnull=True + ).prefetch_related('event').select_for_update(skip_locked=True) + for i in qs[:batch_size]: + if i.event.settings.invoice_email_organizer: + with language(i.event.settings.locale): + mail( + email=i.event.settings.invoice_email_organizer, + subject=_('New invoice: {number}').format(number=i.number), + template=LazyI18nString.from_gettext(_( + 'Hello,\n\n' + 'a new invoice for {event} has been created, see attached.\n\n' + 'We are sending this email because you configured us to do so in your event settings.' + )), + context={ + 'event': str(i.event), + }, + locale=i.event.settings.locale, + event=i.event, + invoices=[i], + auto_email=True, + ) + i.sent_to_organizer = True + i.save(update_fields=['sent_to_organizer']) + else: + i.sent_to_organizer = False + i.save(update_fields=['sent_to_organizer']) diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 42580a21c6..a546fb8e21 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -854,6 +854,18 @@ DEFAULTS = { "to emails."), ) }, + 'invoice_email_organizer': { + 'default': '', + 'type': str, + 'form_class': forms.EmailField, + 'serializer_class': serializers.EmailField, + 'form_kwargs': dict( + label=_("Email address to receive a copy of each invoice"), + help_text=_("Each newly created invoice will be sent to this email address shortly after creation. You can " + "use this for an automated import of invoices to your accounting system. The invoice will be " + "the only attachment of the email."), + ) + }, 'show_items_outside_presale_period': { 'default': 'True', 'type': bool, diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index c3ac6ba17a..e9bb2dba80 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -756,6 +756,7 @@ class InvoiceSettingsForm(SettingsForm): 'invoice_numbers_counter_length', 'invoice_address_explanation_text', 'invoice_email_attachment', + 'invoice_email_organizer', 'invoice_address_from_name', 'invoice_address_from', 'invoice_address_from_zipcode', diff --git a/src/pretix/control/templates/pretixcontrol/event/invoicing.html b/src/pretix/control/templates/pretixcontrol/event/invoicing.html index e8f8b5b448..d05f0112c8 100644 --- a/src/pretix/control/templates/pretixcontrol/event/invoicing.html +++ b/src/pretix/control/templates/pretixcontrol/event/invoicing.html @@ -12,6 +12,7 @@ {% bootstrap_field form.invoice_generate layout="control" %} {% bootstrap_field form.invoice_generate_sales_channels layout="control" %} {% bootstrap_field form.invoice_email_attachment layout="control" %} + {% bootstrap_field form.invoice_email_organizer layout="control" %} {% bootstrap_field form.invoice_language layout="control" %} {% bootstrap_field form.invoice_include_free layout="control" %} {% bootstrap_field form.invoice_show_payments layout="control" %}