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:
Raphael Michel
2025-08-19 17:59:45 +02:00
committed by GitHub
parent 37910f6037
commit 05c74b7ad6
65 changed files with 4514 additions and 1825 deletions

View 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"}