mirror of
https://github.com/pretix/pretix.git
synced 2026-05-08 15:44:02 +00:00
Pluggable invoice transmission methods (#5020)
* Flexible invoice transmission
* UI work
* Add peppol and output
* API support
* Profile integration
* Simplify form for individuals
* Remove sent_to_customer usage
* more steps
* Revert "Bank transfer: Allow to send the invoice direclty to the accounting department (#2975)"
This reverts commit cea6c340be.
* minor fixes
* Fixes after rebase
* update stati
* Backend view
* Transmit and show status
* status, retransmission
* API retransmission
* More fields
* API docs
* Plugin docs
* Update migration
* Add missing license headers
* Remove dead code, fix current tests
* Run isort
* Update regex
* Rebase migration
* Fix migration
* Add tests, fix bugs
* Rebase migration
* Apply suggestion from @luelista
Co-authored-by: luelista <weller@rami.io>
* Apply suggestion from @luelista
Co-authored-by: luelista <weller@rami.io>
* Apply suggestion from @luelista
Co-authored-by: luelista <weller@rami.io>
* Apply suggestion from @luelista
Co-authored-by: luelista <weller@rami.io>
* Apply suggestion from @luelista
Co-authored-by: luelista <weller@rami.io>
* Make migration reversible
* Add TransmissionType.enforce_transmission
* Fix registries API usage after rebase
* Remove code I forgot to delete
* Update transmission status display depending on type
* Add testmode_supported
* Update src/pretix/static/pretixbase/js/addressform.js
Co-authored-by: luelista <weller@rami.io>
* Update src/pretix/static/pretixbase/js/addressform.js
Co-authored-by: luelista <weller@rami.io>
* Update src/pretix/static/pretixbase/js/addressform.js
Co-authored-by: luelista <weller@rami.io>
* New mechanism for non-required invoice forms
* Update src/pretix/base/invoicing/transmission.py
Co-authored-by: luelista <weller@rami.io>
* Declare testmode_supported for email
* Make transmission_email_other an implementation detail
* Fix failing tests and add new ones
* Update src/pretix/base/services/invoices.py
Co-authored-by: luelista <weller@rami.io>
* Add emails to email history
* Fix comma error
* More generic default email text
* Cleanup
* Remove "email invoices" button and refine logic
* Rebase migration
* Fix edge case
---------
Co-authored-by: luelista <weller@rami.io>
This commit is contained in:
21
src/pretix/base/invoicing/__init__.py
Normal file
21
src/pretix/base/invoicing/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
173
src/pretix/base/invoicing/email.py
Normal file
173
src/pretix/base/invoicing/email.py
Normal file
@@ -0,0 +1,173 @@
|
||||
#
|
||||
# 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 django import forms
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_countries.fields import Country
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.invoicing.transmission import (
|
||||
TransmissionProvider, TransmissionType, transmission_providers,
|
||||
transmission_types,
|
||||
)
|
||||
from pretix.base.models import Invoice, InvoiceAddress
|
||||
from pretix.base.services.mail import SendMailException, mail, render_mail
|
||||
from pretix.helpers.format import format_map
|
||||
|
||||
|
||||
@transmission_types.new()
|
||||
class EmailTransmissionType(TransmissionType):
|
||||
identifier = "email"
|
||||
verbose_name = _("Email")
|
||||
priority = 1000
|
||||
|
||||
@property
|
||||
def invoice_address_form_fields(self) -> dict:
|
||||
return {
|
||||
"transmission_email_other": forms.BooleanField(
|
||||
label=_("Email invoice directly to accounting department"),
|
||||
help_text=_("If not selected, the invoice will be sent to you using the email address listed above."),
|
||||
required=False,
|
||||
),
|
||||
"transmission_email_address": forms.EmailField(
|
||||
label=_("Email address for invoice"),
|
||||
widget=forms.EmailInput(
|
||||
attrs={"data-display-dependency": "#id_transmission_email_other"}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def invoice_address_form_fields_visible(self, country: Country, is_business: bool):
|
||||
if is_business:
|
||||
# We don't want ask non-business users if they have an accounting department ;)
|
||||
return {"transmission_email_other", "transmission_email_address"}
|
||||
return set()
|
||||
|
||||
def is_available(self, event, country: Country, is_business: bool):
|
||||
# Skip availability check since provider is always available and we do not want to end up without invoice
|
||||
# transmission type
|
||||
return True
|
||||
|
||||
def transmission_info_to_form_data(self, transmission_info: dict) -> dict:
|
||||
return {
|
||||
"transmission_email_other": bool(transmission_info.get("transmission_email_address")),
|
||||
"transmission_email_address": transmission_info.get("transmission_email_address"),
|
||||
}
|
||||
|
||||
def form_data_to_transmission_info(self, form_data: dict) -> dict:
|
||||
if form_data.get("transmission_email_other") and form_data.get("transmission_email_address"):
|
||||
return {
|
||||
"transmission_email_address": form_data["transmission_email_address"],
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
@transmission_providers.new()
|
||||
class EmailTransmissionProvider(TransmissionProvider):
|
||||
identifier = "email_pdf"
|
||||
type = "email"
|
||||
verbose_name = _("PDF via email")
|
||||
priority = 1000
|
||||
testmode_supported = True
|
||||
|
||||
def is_ready(self, event) -> bool:
|
||||
return True
|
||||
|
||||
def is_available(self, event, country: Country, is_business: bool) -> bool:
|
||||
return True
|
||||
|
||||
def transmit(self, invoice: Invoice):
|
||||
info = (invoice.invoice_to_transmission_info or {})
|
||||
if info.get("transmission_email_address"):
|
||||
recipient = info["transmission_email_address"]
|
||||
else:
|
||||
recipient = invoice.order.email
|
||||
|
||||
if not recipient:
|
||||
invoice.transmission_status = Invoice.TRANSMISSION_STATUS_FAILED
|
||||
invoice.transmission_date = now()
|
||||
invoice.save(update_fields=["transmission_status", "transmission_date"])
|
||||
invoice.order.log_action(
|
||||
"pretix.event.order.invoice.sending_failed",
|
||||
data={
|
||||
"full_invoice_no": invoice.full_invoice_no,
|
||||
"transmission_provider": "email_pdf",
|
||||
"transmission_type": "email",
|
||||
"data": {
|
||||
"reason": "no_recipient",
|
||||
},
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
with language(invoice.order.locale, invoice.order.event.settings.region):
|
||||
context = get_email_context(
|
||||
event=invoice.order.event,
|
||||
order=invoice.order,
|
||||
invoice=invoice,
|
||||
event_or_subevent=invoice.order.event,
|
||||
invoice_address=getattr(invoice.order, 'invoice_address', None) or InvoiceAddress()
|
||||
)
|
||||
template = invoice.order.event.settings.get('mail_text_order_invoice', as_type=LazyI18nString)
|
||||
subject = invoice.order.event.settings.get('mail_subject_order_invoice', as_type=LazyI18nString)
|
||||
|
||||
try:
|
||||
# Do not set to completed because that is done by the email sending task
|
||||
subject = format_map(subject, context)
|
||||
email_content = render_mail(template, context)
|
||||
mail(
|
||||
[recipient],
|
||||
subject,
|
||||
template,
|
||||
context=context,
|
||||
event=invoice.order.event,
|
||||
locale=invoice.order.locale,
|
||||
order=invoice.order,
|
||||
invoices=[invoice],
|
||||
attach_tickets=False,
|
||||
auto_email=True,
|
||||
attach_ical=False,
|
||||
plain_text_only=True,
|
||||
no_order_links=True,
|
||||
)
|
||||
except SendMailException:
|
||||
raise
|
||||
else:
|
||||
invoice.order.log_action(
|
||||
'pretix.event.order.email.invoice',
|
||||
user=None,
|
||||
auth=None,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'position': None,
|
||||
'recipient': recipient,
|
||||
'invoices': [invoice.pk],
|
||||
'attach_tickets': False,
|
||||
'attach_ical': False,
|
||||
'attach_other_files': [],
|
||||
'attach_cached_files': [],
|
||||
}
|
||||
)
|
||||
75
src/pretix/base/invoicing/national.py
Normal file
75
src/pretix/base/invoicing/national.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#
|
||||
# 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 django import forms
|
||||
from django.core.validators import RegexValidator
|
||||
from django.utils.translation import pgettext_lazy
|
||||
from django_countries.fields import Country
|
||||
from localflavor.it.forms import ITSocialSecurityNumberField
|
||||
|
||||
from pretix.base.invoicing.transmission import (
|
||||
TransmissionType, transmission_types,
|
||||
)
|
||||
|
||||
|
||||
@transmission_types.new()
|
||||
class ItalianSdITransmissionType(TransmissionType):
|
||||
identifier = "it_sdi"
|
||||
verbose_name = pgettext_lazy("italian_invoice", "Italian Exchange System (SdI)")
|
||||
public_name = pgettext_lazy("italian_invoice", "Exchange System (SdI)")
|
||||
exclusive = True
|
||||
enforce_transmission = True
|
||||
|
||||
def is_available(self, event, country: Country, is_business: bool):
|
||||
return str(country) == "IT" and super().is_available(event, country, is_business)
|
||||
|
||||
@property
|
||||
def invoice_address_form_fields(self) -> dict:
|
||||
return {
|
||||
"transmission_it_sdi_codice_fiscale": ITSocialSecurityNumberField(
|
||||
label=pgettext_lazy("italian_invoice", "Fiscal code"),
|
||||
required=False,
|
||||
),
|
||||
"transmission_it_sdi_pec": forms.EmailField(
|
||||
label=pgettext_lazy("italian_invoice", "Address for certified electronical mail"),
|
||||
widget=forms.EmailInput()
|
||||
),
|
||||
"transmission_it_sdi_recipient_code": forms.CharField(
|
||||
label=pgettext_lazy("italian_invoice", "Recipient code"),
|
||||
validators=[
|
||||
RegexValidator("^[A-Z0-9]{6,7}$")
|
||||
]
|
||||
),
|
||||
}
|
||||
|
||||
def invoice_address_form_fields_visible(self, country: Country, is_business: bool):
|
||||
if is_business:
|
||||
return {"transmission_it_sdi_codice_fiscale", "transmission_it_sdi_pec", "transmission_it_sdi_recipient_code"}
|
||||
return {"transmission_it_sdi_codice_fiscale", "transmission_it_sdi_pec"}
|
||||
|
||||
def invoice_address_form_fields_required(self, country: Country, is_business: bool):
|
||||
base = {
|
||||
"street", "zipcode", "city", "state", "country",
|
||||
}
|
||||
if is_business:
|
||||
return base | {"company", "vat_id", "transmission_it_sdi_pec", "transmission_it_sdi_recipient_code"}
|
||||
return base | {"transmission_it_sdi_codice_fiscale"}
|
||||
1107
src/pretix/base/invoicing/pdf.py
Normal file
1107
src/pretix/base/invoicing/pdf.py
Normal file
File diff suppressed because it is too large
Load Diff
167
src/pretix/base/invoicing/peppol.py
Normal file
167
src/pretix/base/invoicing/peppol.py
Normal file
@@ -0,0 +1,167 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_countries.fields import Country
|
||||
|
||||
from pretix.base.invoicing.transmission import (
|
||||
TransmissionType, transmission_types,
|
||||
)
|
||||
|
||||
|
||||
class PeppolIdValidator:
|
||||
regex_rules = {
|
||||
# Source: https://docs.peppol.eu/edelivery/codelists/old/v8.5/Peppol%20Code%20Lists%20-%20Participant%20identifier%20schemes%20v8.5.html
|
||||
"0002": "[0-9]{9}([0-9]{5})?",
|
||||
"0007": "[0-9]{10}",
|
||||
"0009": "[0-9]{14}",
|
||||
"0037": "(0037)?[0-9]{7}-?[0-9][0-9A-Z]{0,5}",
|
||||
"0060": "[0-9]{9}",
|
||||
"0088": "[0-9]{13}",
|
||||
"0096": "[0-9]{17}",
|
||||
"0097": "[0-9]{11,16}",
|
||||
"0106": "[0-9]{17}",
|
||||
"0130": ".*",
|
||||
"0135": ".*",
|
||||
"0142": ".*",
|
||||
"0151": "[0-9]{11}",
|
||||
"0183": "CHE[0-9]{9}",
|
||||
"0184": "DK[0-9]{8}([0-9]{2})?",
|
||||
"0188": ".*",
|
||||
"0190": "[0-9]{20}",
|
||||
"0191": "[1789][0-9]{7}",
|
||||
"0192": "[0-9]{9}",
|
||||
"0193": ".{4,50}",
|
||||
"0195": "[a-z]{2}[a-z]{3}([0-9]{8}|[0-9]{9}|[RST][0-9]{2}[a-z]{2}[0-9]{4})[0-9a-z]",
|
||||
"0196": "[0-9]{10}",
|
||||
"0198": "DK[0-9]{8}",
|
||||
"0199": "[A-Z0-9]{18}[0-9]{2}",
|
||||
"0020": "[0-9]{9}",
|
||||
"0201": "[0-9a-zA-Z]{6}",
|
||||
"0204": "[0-9]{2,12}(-[0-9A-Z]{0,30})?-[0-9]{2}",
|
||||
"0208": "0[0-9]{9}",
|
||||
"0209": ".*",
|
||||
"0210": "[A-Z0-9]+",
|
||||
"0211": "IT[0-9]{11}",
|
||||
"0212": "[0-9]{7}-[0-9]",
|
||||
"0213": "FI[0-9]{8}",
|
||||
"0205": "[A-Z0-9]+",
|
||||
"0221": "T[0-9]{13}",
|
||||
"0230": ".*",
|
||||
"9901": ".*",
|
||||
"9902": "[1-9][0-9]{7}",
|
||||
"9904": "DK[0-9]{8}",
|
||||
"9909": "NO[0-9]{9}MVA",
|
||||
"9910": "HU[0-9]{8}",
|
||||
"9912": "[A-Z]{2}[A-Z0-9]{,20}",
|
||||
"9913": ".*",
|
||||
"9914": "ATU[0-9]*",
|
||||
"9915": "[A-Z][A-Z0-9]*",
|
||||
"9916": ".*",
|
||||
"9917": "[0-9]{10}",
|
||||
"9918": "[A-Z]{2}[0-9]{2}[A-Z-0-9]{11,30}",
|
||||
"9919": "[A-Z][0-9]{3}[A-Z][0-9]{3}[A-Z]",
|
||||
"9920": ".*",
|
||||
"9921": ".*",
|
||||
"9922": ".*",
|
||||
"9923": ".*",
|
||||
"9924": ".*",
|
||||
"9925": ".*",
|
||||
"9926": ".*",
|
||||
"9927": ".*",
|
||||
"9928": ".*",
|
||||
"9929": ".*",
|
||||
"9930": ".*",
|
||||
"9931": ".*",
|
||||
"9932": ".*",
|
||||
"9933": ".*",
|
||||
"9934": ".*",
|
||||
"9935": ".*",
|
||||
"9936": ".*",
|
||||
"9937": ".*",
|
||||
"9938": ".*",
|
||||
"9939": ".*",
|
||||
"9940": ".*",
|
||||
"9941": ".*",
|
||||
"9942": ".*",
|
||||
"9943": ".*",
|
||||
"9944": ".*",
|
||||
"9945": ".*",
|
||||
"9946": ".*",
|
||||
"9947": ".*",
|
||||
"9948": ".*",
|
||||
"9949": ".*",
|
||||
"9950": ".*",
|
||||
"9951": ".*",
|
||||
"9952": ".*",
|
||||
"9953": ".*",
|
||||
"9954": ".*",
|
||||
"9956": "0[0-9]{9}",
|
||||
"9957": ".*",
|
||||
"9959": ".*",
|
||||
}
|
||||
|
||||
def __call__(self, value):
|
||||
if ":" not in value:
|
||||
raise ValidationError(_("A PEPPOL participant ID always starts with a prefix, followed by a colon (:)."))
|
||||
|
||||
prefix, second = value.split(":", 1)
|
||||
if prefix not in self.regex_rules:
|
||||
raise ValidationError(_("The PEPPOL participant ID prefix %(number)s is not known to our system. Please "
|
||||
"reach out to us if you are sure this ID is correct."), params={"number": prefix})
|
||||
|
||||
if not re.match(self.regex_rules[prefix], second):
|
||||
raise ValidationError(_("The PEPPOL participant ID does not match the validation rules for the prefix "
|
||||
"%(number)s. Please reach out to us if you are sure this ID is correct."),
|
||||
params={"number": prefix})
|
||||
return value
|
||||
|
||||
|
||||
@transmission_types.new()
|
||||
class PeppolTransmissionType(TransmissionType):
|
||||
identifier = "peppol"
|
||||
verbose_name = "PEPPOL"
|
||||
priority = 250
|
||||
enforce_transmission = True
|
||||
|
||||
def is_available(self, event, country: Country, is_business: bool):
|
||||
return is_business and super().is_available(event, country, is_business)
|
||||
|
||||
@property
|
||||
def invoice_address_form_fields(self) -> dict:
|
||||
return {
|
||||
"transmission_peppol_participant_id": forms.CharField(
|
||||
label=_("PEPPOL participant ID"),
|
||||
validators=[
|
||||
PeppolIdValidator(),
|
||||
]
|
||||
),
|
||||
}
|
||||
|
||||
def invoice_address_form_fields_required(self, country: Country, is_business: bool):
|
||||
base = {
|
||||
"company", "street", "zipcode", "city", "country",
|
||||
}
|
||||
return base | {"transmission_peppol_participant_id"}
|
||||
246
src/pretix/base/invoicing/transmission.py
Normal file
246
src/pretix/base/invoicing/transmission.py
Normal file
@@ -0,0 +1,246 @@
|
||||
#
|
||||
# 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 typing import Optional
|
||||
|
||||
from django_countries.fields import Country
|
||||
|
||||
from pretix.base.models import Invoice, InvoiceAddress
|
||||
from pretix.base.signals import EventPluginRegistry, Registry
|
||||
|
||||
|
||||
class TransmissionType:
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
"""
|
||||
A short and unique identifier for this transmission type.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def verbose_name(self) -> str:
|
||||
"""
|
||||
A human-readable name for this transmission type to be shown internally in the backend.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def public_name(self) -> str:
|
||||
"""
|
||||
A human-readable name for this transmission type to be shown to the public.
|
||||
By default, this is the same as ``verbose_name``
|
||||
"""
|
||||
return self.verbose_name
|
||||
|
||||
@property
|
||||
def priority(self) -> int:
|
||||
"""
|
||||
Returns a priority that is used for sorting transmission type. Higher priority means higher up in the list.
|
||||
Default to 100. Providers with same priority are sorted alphabetically.
|
||||
"""
|
||||
return 100
|
||||
|
||||
@property
|
||||
def exclusive(self) -> bool:
|
||||
"""
|
||||
If a transmission type is exclusive, no other type can be chosen if this type is
|
||||
available. Use e.g. if a certain transmission type is legally required in a certain
|
||||
jurisdiction.
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def enforce_transmission(self) -> bool:
|
||||
"""
|
||||
If a transmission type enforces transmission, every invoice created with this type will be transferred.
|
||||
If not, the backend user is in some cases trusted to decide whether or not to transmit it.
|
||||
"""
|
||||
return False
|
||||
|
||||
def is_available(self, event, country: Country, is_business: bool) -> bool:
|
||||
providers = transmission_providers.filter(type=self.identifier, active_in=event)
|
||||
return any(
|
||||
provider.is_available(event, country, is_business)
|
||||
for provider, _ in providers
|
||||
)
|
||||
|
||||
def invoice_address_form_fields_required(self, country: Country, is_business: bool):
|
||||
return set()
|
||||
|
||||
def invoice_address_form_fields_visible(self, country: Country, is_business: bool) -> set:
|
||||
return set(self.invoice_address_form_fields.keys())
|
||||
|
||||
def validate_address(self, ia: InvoiceAddress):
|
||||
pass
|
||||
|
||||
@property
|
||||
def invoice_address_form_fields(self) -> dict:
|
||||
"""
|
||||
Return a set of form fields that **must** be prefixed with ``transmission_<identifier>_``.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def form_data_to_transmission_info(self, form_data: dict) -> dict:
|
||||
return form_data
|
||||
|
||||
def transmission_info_to_form_data(self, transmission_info: dict) -> dict:
|
||||
return transmission_info
|
||||
|
||||
|
||||
class TransmissionProvider:
|
||||
"""
|
||||
Base class for a transmission provider. Should NOT hold internal state as the class is only
|
||||
instantiated once and then shared between events and organizers.
|
||||
"""
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
"""
|
||||
A short and unique identifier for this transmission provider.
|
||||
This should only contain lowercase letters and underscores.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""
|
||||
Identifier of the transmission type this provider provides.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
"""
|
||||
A human-readable name for this transmission provider (can be localized).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def testmode_supported(self) -> bool:
|
||||
"""
|
||||
Whether testmode invoices may be passed to this provider.
|
||||
"""
|
||||
return False
|
||||
|
||||
def is_ready(self, event) -> bool:
|
||||
"""
|
||||
Return whether this provider has all required configuration to be used in this event.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_available(self, event, country: Country, is_business: bool) -> bool:
|
||||
"""
|
||||
Return whether this provider may be used for an invoice for the given recipient country and address type.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def transmit(self, invoice: Invoice):
|
||||
"""
|
||||
Transmit the invoice. The invoice passed as a parameter will be in status ``TRANSMISSION_STATUS_INFLIGHT``.
|
||||
Invoices that stay in this state for more than 24h will be retried automatically. Implementations are expected to:
|
||||
|
||||
- Send the invoice.
|
||||
|
||||
- Update the ``transmission_status`` to `TRANSMISSION_STATUS_COMPLETED` or `TRANSMISSION_STATUS_FAILED`
|
||||
after sending, as well as ``transmission_info`` with provider-specific data, and ``transmission_date`` to
|
||||
the date and time of completion.
|
||||
|
||||
- Create a log entry of action type ``pretix.event.order.invoice.sent`` or
|
||||
``pretix.event.order.invoice.sending_failed`` with the fields ``full_invoice_no``, ``transmission_provider``,
|
||||
``transmission_type`` and a provider-specific ``data`` field.
|
||||
|
||||
Make sure to either handle ``invoice.order.testmode`` properly or set ``testmode_supported`` to ``False``.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def priority(self) -> int:
|
||||
"""
|
||||
Returns a priority that is used for sorting transmission providers. Higher priority will be chosen over
|
||||
lower priority for transmission. Default to 100.
|
||||
"""
|
||||
return 100
|
||||
|
||||
def settings_url(self, event) -> Optional[str]:
|
||||
"""
|
||||
Return a URL to the settings page of this provider (if any).
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
class TransmissionProviderRegistry(EventPluginRegistry):
|
||||
def __init__(self):
|
||||
super().__init__({
|
||||
'identifier': lambda o: getattr(o, 'identifier'),
|
||||
'type': lambda o: getattr(o, 'type'),
|
||||
})
|
||||
|
||||
def register(self, *objs):
|
||||
for obj in objs:
|
||||
if not isinstance(obj, TransmissionProvider):
|
||||
raise TypeError('Entries must be derived from TransmissionProvider')
|
||||
|
||||
if obj.type == "email" and not obj.__module__.startswith('pretix.base.'):
|
||||
raise TypeError('No custom providers for email allowed')
|
||||
|
||||
return super().register(*objs)
|
||||
|
||||
|
||||
class TransmissionTypeRegistry(Registry):
|
||||
def __init__(self):
|
||||
super().__init__({
|
||||
'identifier': lambda o: getattr(o, 'identifier'),
|
||||
})
|
||||
|
||||
def register(self, *objs):
|
||||
for obj in objs:
|
||||
if not isinstance(obj, TransmissionType):
|
||||
raise TypeError('Entries must be derived from TransmissionType')
|
||||
|
||||
if not obj.__module__.startswith('pretix.base.'):
|
||||
raise TypeError('Plugins are currently not allowed to add transmission types')
|
||||
|
||||
return super().register(*objs)
|
||||
|
||||
|
||||
"""
|
||||
Registry for transmission providers.
|
||||
|
||||
Each entry in this registry should be an instance of a subclass of ``TransmissionProvider``.
|
||||
They are annotated with their ``identifier``, ``type``, and the defining ``plugin``.
|
||||
"""
|
||||
transmission_providers = TransmissionProviderRegistry()
|
||||
|
||||
|
||||
"""
|
||||
Registry for transmission types.
|
||||
|
||||
Each entry in this registry should be an instance of a subclass of ``TransmissionType``.
|
||||
They are annotated with their ``identifier``.
|
||||
"""
|
||||
transmission_types = TransmissionTypeRegistry()
|
||||
|
||||
|
||||
def get_transmission_types():
|
||||
return sorted(
|
||||
transmission_types.registered_entries.keys(),
|
||||
key=lambda t: (-t.priority, str(t.public_name)),
|
||||
)
|
||||
Reference in New Issue
Block a user