Invoicing: Configurable service date

This commit is contained in:
Raphael Michel
2025-09-08 10:57:57 +02:00
parent 9401fbb1bc
commit ae5c0a5537
11 changed files with 163 additions and 27 deletions

View File

@@ -80,17 +80,12 @@ lines list of objects The actual invo
for all invoice lines for all invoice lines
created before this field was introduced as well as for created before this field was introduced as well as for
all lines not created by a fee (e.g. a product). all lines not created by a fee (e.g. a product).
event_date_from datetime Start date of the (sub)event this line was created for as it period_start datetime Start date of the service or delivery period of the invoice line.
was set during invoice creation. Can be ``null`` for all invoice Can be ``null`` if not known.
lines created before this was introduced as well as for lines in ├ period_end datetime End date of the service or delivery period of the invoice line.
an event series not created by a product (e.g. shipping or Can be ``null`` if not known.
cancellation fees). ├ event_date_from datetime Deprecated alias of ``period_start``.
├ event_date_to datetime End date of the (sub)event this line was created for as it ├ event_date_to datetime Deprecated alias of ``period_end``.
was set during invoice creation. Can be ``null`` for all invoice
lines created before this was introduced as well as for lines in
an event series not created by a product (e.g. shipping or
cancellation fees) as well as whenever the respective (sub)event
has no end date set.
├ event_location string Location of the (sub)event this line was created for as it ├ event_location string Location of the (sub)event this line was created for as it
was set during invoice creation. Can be ``null`` for all invoice was set during invoice creation. Can be ``null`` for all invoice
lines created before this was introduced as well as for lines in lines created before this was introduced as well as for lines in
@@ -274,6 +269,8 @@ List of all invoices
"fee_internal_type": null, "fee_internal_type": null,
"event_date_from": "2017-12-27T10:00:00Z", "event_date_from": "2017-12-27T10:00:00Z",
"event_date_to": null, "event_date_to": null,
"period_start": "2017-12-27T10:00:00Z",
"period_end": "2017-12-27T10:00:00Z",
"event_location": "Heidelberg", "event_location": "Heidelberg",
"attendee_name": null, "attendee_name": null,
"gross_value": "23.00", "gross_value": "23.00",
@@ -420,6 +417,8 @@ Fetching individual invoices
"fee_internal_type": null, "fee_internal_type": null,
"event_date_from": "2017-12-27T10:00:00Z", "event_date_from": "2017-12-27T10:00:00Z",
"event_date_to": null, "event_date_to": null,
"period_start": "2017-12-27T10:00:00Z",
"period_end": "2017-12-27T10:00:00Z",
"event_location": "Heidelberg", "event_location": "Heidelberg",
"attendee_name": null, "attendee_name": null,
"gross_value": "23.00", "gross_value": "23.00",

View File

@@ -805,6 +805,7 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_reissue_after_modify', 'invoice_reissue_after_modify',
'invoice_include_free', 'invoice_include_free',
'invoice_generate', 'invoice_generate',
'invoice_period',
'invoice_numbers_consecutive', 'invoice_numbers_consecutive',
'invoice_numbers_prefix', 'invoice_numbers_prefix',
'invoice_numbers_prefix_cancellations', 'invoice_numbers_prefix_cancellations',

View File

@@ -1757,12 +1757,14 @@ class LinePositionField(serializers.IntegerField):
class InlineInvoiceLineSerializer(I18nAwareModelSerializer): class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
position = LinePositionField(read_only=True) position = LinePositionField(read_only=True)
event_date_from = serializers.DateTimeField(read_only=True, source="period_start")
event_date_to = serializers.DateTimeField(read_only=True, source="period_end")
class Meta: class Meta:
model = InvoiceLine model = InvoiceLine
fields = ('position', 'description', 'item', 'variation', 'subevent', 'attendee_name', 'event_date_from', fields = ('position', 'description', 'item', 'variation', 'subevent', 'attendee_name', 'event_date_from',
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_code', 'tax_name', 'fee_type', 'event_date_to', 'period_start', 'period_end', 'gross_value', 'tax_value', 'tax_rate', 'tax_code',
'fee_internal_type', 'event_location') 'tax_name', 'fee_type', 'fee_internal_type', 'event_location')
class InvoiceSerializer(I18nAwareModelSerializer): class InvoiceSerializer(I18nAwareModelSerializer):

View File

@@ -0,0 +1,65 @@
# Generated by Django 4.2.17 on 2025-09-08 08:14
from django.db import migrations
def set_default_cardtypes(apps, schema_editor):
EventSettingsStore = apps.get_model("pretixbase", "Event_SettingsStore")
ev_seen = set()
insert_queue = []
# Existing events that use pretix-zugferd and have explicitly disabled delivery dates
for ev in EventSettingsStore.objects.filter(key="zugferd_include_delivery_date", value="False"):
insert_queue.append(
EventSettingsStore(
object_id=ev.object_id,
key="invoice_period",
value="invoice_date",
)
)
ev_seen.add(ev.object_id)
if len(insert_queue) > 1000:
EventSettingsStore.objects.bulk_create(insert_queue, ignore_conflicts=True)
insert_queue.clear()
# Existing events that previously hid their date on invoices
# Ignore series as it doesn't make sense for them
for ev in EventSettingsStore.objects.filter(key="show_dates_on_frontpage", value="False",
object__has_subevents=False):
if ev.object_id in ev_seen:
continue
insert_queue.append(
EventSettingsStore(
object_id=ev.object_id,
key="invoice_period",
value="auto_no_event",
)
)
ev_seen.add(ev.object_id)
if len(insert_queue) > 1000:
EventSettingsStore.objects.bulk_create(insert_queue, ignore_conflicts=True)
insert_queue.clear()
EventSettingsStore.objects.bulk_create(insert_queue, ignore_conflicts=True)
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0288_invoice_transmission"),
]
operations = [
migrations.RenameField(
model_name="invoiceline",
old_name="event_date_to",
new_name="period_end",
),
migrations.RenameField(
model_name="invoiceline",
old_name="event_date_from",
new_name="period_start",
),
]

View File

@@ -33,6 +33,7 @@
# License for the specific language governing permissions and limitations under the License. # License for the specific language governing permissions and limitations under the License.
import string import string
import warnings
from decimal import Decimal from decimal import Decimal
import pycountry import pycountry
@@ -387,10 +388,10 @@ class InvoiceLine(models.Model):
:type tax_name: str :type tax_name: str
:param subevent: The subevent this line refers to :param subevent: The subevent this line refers to
:type subevent: SubEvent :type subevent: SubEvent
:param event_date_from: Event date of the (sub)event at the time the invoice was created :param period_start: Start if service period invoiced
:type event_date_from: datetime :type period_start: datetime
:param event_date_to: Event end date of the (sub)event at the time the invoice was created :param period_end: End of service period invoiced
:type event_date_to: datetime :type period_end: datetime
:param event_location: Event location of the (sub)event at the time the invoice was created :param event_location: Event location of the (sub)event at the time the invoice was created
:type event_location: str :type event_location: str
:param item: The item this line refers to :param item: The item this line refers to
@@ -409,8 +410,8 @@ class InvoiceLine(models.Model):
tax_name = models.CharField(max_length=190) tax_name = models.CharField(max_length=190)
tax_code = models.CharField(max_length=190, null=True, blank=True) tax_code = models.CharField(max_length=190, null=True, blank=True)
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT) subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
event_date_from = models.DateTimeField(null=True) period_start = models.DateTimeField(null=True)
event_date_to = models.DateTimeField(null=True) period_end = models.DateTimeField(null=True)
event_location = models.TextField(null=True, blank=True) event_location = models.TextField(null=True, blank=True)
item = models.ForeignKey('Item', null=True, blank=True, on_delete=models.PROTECT) item = models.ForeignKey('Item', null=True, blank=True, on_delete=models.PROTECT)
variation = models.ForeignKey('ItemVariation', null=True, blank=True, on_delete=models.PROTECT) variation = models.ForeignKey('ItemVariation', null=True, blank=True, on_delete=models.PROTECT)
@@ -427,3 +428,35 @@ class InvoiceLine(models.Model):
def __str__(self): def __str__(self):
return 'Line {} of invoice {}'.format(self.position, self.invoice) return 'Line {} of invoice {}'.format(self.position, self.invoice)
@property
def event_date_from(self):
warnings.warn(
'InvoiceLine.event_date_from is deprecated, use period_start instead,',
category=DeprecationWarning,
)
return self.period_start
@event_date_from.setter
def event_date_from(self, value):
warnings.warn(
'InvoiceLine.event_date_from is deprecated, use period_start instead,',
category=DeprecationWarning,
)
self.period_start = value
@property
def event_date_to(self):
warnings.warn(
'InvoiceLine.event_date_to is deprecated, use period_end instead,',
category=DeprecationWarning,
)
return self.period_end
@event_date_to.setter
def event_date_to(self, value):
warnings.warn(
'InvoiceLine.event_date_to is deprecated, use period_end instead,',
category=DeprecationWarning,
)
self.period_to = value

View File

@@ -277,8 +277,8 @@ def build_invoice(invoice: Invoice) -> Invoice:
item=p.item, item=p.item,
variation=p.variation, variation=p.variation,
attendee_name=p.attendee_name if invoice.event.settings.invoice_attendee_name else None, attendee_name=p.attendee_name if invoice.event.settings.invoice_attendee_name else None,
event_date_from=p.subevent.date_from if invoice.event.has_subevents else invoice.event.date_from, period_start=p.subevent.date_from if invoice.event.has_subevents else invoice.event.date_from,
event_date_to=p.subevent.date_to if invoice.event.has_subevents else invoice.event.date_to, period_end=p.subevent.date_to if invoice.event.has_subevents else invoice.event.date_to,
event_location=location if invoice.event.settings.invoice_event_location else None, event_location=location if invoice.event.settings.invoice_event_location else None,
tax_rate=p.tax_rate, tax_rate=p.tax_rate,
tax_code=p.tax_code, tax_code=p.tax_code,
@@ -306,8 +306,8 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice=invoice, invoice=invoice,
description=fee_title, description=fee_title,
gross_value=fee.value, gross_value=fee.value,
event_date_from=None if invoice.event.has_subevents else invoice.event.date_from, period_start=None if invoice.event.has_subevents else invoice.event.date_from,
event_date_to=None if invoice.event.has_subevents else invoice.event.date_to, period_end=None if invoice.event.has_subevents else invoice.event.date_to,
event_location=( event_location=(
None if invoice.event.has_subevents None if invoice.event.has_subevents
else (str(invoice.event.location) else (str(invoice.event.location)
@@ -506,8 +506,8 @@ def build_preview_invoice_pdf(event):
invoice=invoice, description=_("Sample product {}").format(i + 1), invoice=invoice, description=_("Sample product {}").format(i + 1),
gross_value=tax.gross, tax_value=tax.tax, gross_value=tax.gross, tax_value=tax.tax,
tax_rate=tax.rate, tax_name=tax.name, tax_code=tax.code, tax_rate=tax.rate, tax_name=tax.name, tax_code=tax.code,
event_date_from=event.date_from, period_start=event.date_from,
event_date_to=event.date_to, period_end=event.date_to,
event_location=event.settings.invoice_event_location, event_location=event.settings.invoice_event_location,
) )
else: else:
@@ -515,8 +515,8 @@ def build_preview_invoice_pdf(event):
InvoiceLine.objects.create( InvoiceLine.objects.create(
invoice=invoice, description=_("Sample product A"), invoice=invoice, description=_("Sample product A"),
gross_value=100, tax_value=0, tax_rate=0, tax_code=None, gross_value=100, tax_value=0, tax_rate=0, tax_code=None,
event_date_from=event.date_from, period_start=event.date_from,
event_date_to=event.date_to, period_end=event.date_to,
event_location=event.settings.invoice_event_location, event_location=event.settings.invoice_event_location,
) )

View File

@@ -1098,6 +1098,32 @@ DEFAULTS = {
help_text=_("Invoices will never be automatically generated for free orders.") help_text=_("Invoices will never be automatically generated for free orders.")
) )
}, },
'invoice_period': {
'default': 'auto',
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': dict(
choices=(
('auto', _('Ticket-specific validity or event series date or event date')),
('auto_no_event', _('Ticket-specific validity or event series date or invoice date')),
('order_date', _('Order date')),
('invoice_date', _('Invoice date')),
),
),
'form_kwargs': dict(
label=_("Date of service"),
widget=forms.RadioSelect,
choices=(
('auto', _('Ticket-specific validity or event series date or event date')),
('auto_no_event', _('Ticket-specific validity or event series date or invoice date')),
('order_date', _('Order date')),
('invoice_date', _('Invoice date')),
),
help_text=_("This controls what dates are shown on the invoice, but is especially important for "
"electronic invoicing."),
)
},
'invoice_reissue_after_modify': { 'invoice_reissue_after_modify': {
'default': 'False', 'default': 'False',
'type': bool, 'type': bool,

View File

@@ -857,6 +857,7 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
'invoice_show_payments', 'invoice_show_payments',
'invoice_reissue_after_modify', 'invoice_reissue_after_modify',
'invoice_generate', 'invoice_generate',
'invoice_period',
'invoice_attendee_name', 'invoice_attendee_name',
'invoice_event_location', 'invoice_event_location',
'invoice_include_expire_date', 'invoice_include_expire_date',

View File

@@ -15,6 +15,7 @@
{% bootstrap_field form.invoice_email_attachment layout="control" %} {% bootstrap_field form.invoice_email_attachment layout="control" %}
{% bootstrap_field form.invoice_email_organizer layout="control" %} {% bootstrap_field form.invoice_email_organizer layout="control" %}
{% bootstrap_field form.invoice_language layout="control" %} {% bootstrap_field form.invoice_language layout="control" %}
{% bootstrap_field form.invoice_period layout="control" %}
{% bootstrap_field form.invoice_include_free layout="control" %} {% bootstrap_field form.invoice_include_free layout="control" %}
{% bootstrap_field form.invoice_show_payments layout="control" %} {% bootstrap_field form.invoice_show_payments layout="control" %}
{% bootstrap_field form.invoice_reissue_after_modify layout="control" %} {% bootstrap_field form.invoice_reissue_after_modify layout="control" %}

View File

@@ -232,6 +232,8 @@ TEST_INVOICE_RES = {
'subevent': None, 'subevent': None,
'event_date_from': '2017-12-27T10:00:00Z', 'event_date_from': '2017-12-27T10:00:00Z',
'event_date_to': None, 'event_date_to': None,
'period_start': '2017-12-27T10:00:00Z',
'period_end': None,
'event_location': None, 'event_location': None,
'attendee_name': 'Peter', 'attendee_name': 'Peter',
'item': None, 'item': None,
@@ -250,6 +252,8 @@ TEST_INVOICE_RES = {
'subevent': None, 'subevent': None,
'event_date_from': '2017-12-27T10:00:00Z', 'event_date_from': '2017-12-27T10:00:00Z',
'event_date_to': None, 'event_date_to': None,
'period_start': '2017-12-27T10:00:00Z',
'period_end': None,
'event_location': None, 'event_location': None,
'attendee_name': None, 'attendee_name': None,
'fee_type': "payment", 'fee_type': "payment",

View File

@@ -609,6 +609,8 @@ def test_order_create_invoice(token_client, organizer, event, order):
'subevent': None, 'subevent': None,
'event_date_from': '2017-12-27T10:00:00Z', 'event_date_from': '2017-12-27T10:00:00Z',
'event_date_to': None, 'event_date_to': None,
'period_start': '2017-12-27T10:00:00Z',
'period_end': None,
'event_location': None, 'event_location': None,
'fee_type': None, 'fee_type': None,
'fee_internal_type': None, 'fee_internal_type': None,
@@ -627,6 +629,8 @@ def test_order_create_invoice(token_client, organizer, event, order):
'subevent': None, 'subevent': None,
'event_date_from': '2017-12-27T10:00:00Z', 'event_date_from': '2017-12-27T10:00:00Z',
'event_date_to': None, 'event_date_to': None,
'period_start': '2017-12-27T10:00:00Z',
'period_end': None,
'event_location': None, 'event_location': None,
'fee_type': "payment", 'fee_type': "payment",
'fee_internal_type': None, 'fee_internal_type': None,