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
created before this field was introduced as well as for
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
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).
├ event_date_to datetime End date of the (sub)event this line was created for as it
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.
period_start datetime Start date of the service or delivery period of the invoice line.
Can be ``null`` if not known.
├ period_end datetime End date of the service or delivery period of the invoice line.
Can be ``null`` if not known.
├ event_date_from datetime Deprecated alias of ``period_start``.
├ event_date_to datetime Deprecated alias of ``period_end``.
├ 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
lines created before this was introduced as well as for lines in
@@ -274,6 +269,8 @@ List of all invoices
"fee_internal_type": null,
"event_date_from": "2017-12-27T10:00:00Z",
"event_date_to": null,
"period_start": "2017-12-27T10:00:00Z",
"period_end": "2017-12-27T10:00:00Z",
"event_location": "Heidelberg",
"attendee_name": null,
"gross_value": "23.00",
@@ -420,6 +417,8 @@ Fetching individual invoices
"fee_internal_type": null,
"event_date_from": "2017-12-27T10:00:00Z",
"event_date_to": null,
"period_start": "2017-12-27T10:00:00Z",
"period_end": "2017-12-27T10:00:00Z",
"event_location": "Heidelberg",
"attendee_name": null,
"gross_value": "23.00",

View File

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

View File

@@ -1757,12 +1757,14 @@ class LinePositionField(serializers.IntegerField):
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
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:
model = InvoiceLine
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',
'fee_internal_type', 'event_location')
'event_date_to', 'period_start', 'period_end', 'gross_value', 'tax_value', 'tax_rate', 'tax_code',
'tax_name', 'fee_type', 'fee_internal_type', 'event_location')
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.
import string
import warnings
from decimal import Decimal
import pycountry
@@ -387,10 +388,10 @@ class InvoiceLine(models.Model):
:type tax_name: str
:param subevent: The subevent this line refers to
:type subevent: SubEvent
:param event_date_from: Event date of the (sub)event at the time the invoice was created
:type event_date_from: datetime
:param event_date_to: Event end date of the (sub)event at the time the invoice was created
:type event_date_to: datetime
:param period_start: Start if service period invoiced
:type period_start: datetime
:param period_end: End of service period invoiced
:type period_end: datetime
:param event_location: Event location of the (sub)event at the time the invoice was created
:type event_location: str
:param item: The item this line refers to
@@ -409,8 +410,8 @@ class InvoiceLine(models.Model):
tax_name = models.CharField(max_length=190)
tax_code = models.CharField(max_length=190, null=True, blank=True)
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
event_date_from = models.DateTimeField(null=True)
event_date_to = models.DateTimeField(null=True)
period_start = models.DateTimeField(null=True)
period_end = models.DateTimeField(null=True)
event_location = models.TextField(null=True, blank=True)
item = models.ForeignKey('Item', 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):
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,
variation=p.variation,
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,
event_date_to=p.subevent.date_to if invoice.event.has_subevents else invoice.event.date_to,
period_start=p.subevent.date_from if invoice.event.has_subevents else invoice.event.date_from,
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,
tax_rate=p.tax_rate,
tax_code=p.tax_code,
@@ -306,8 +306,8 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice=invoice,
description=fee_title,
gross_value=fee.value,
event_date_from=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_start=None if invoice.event.has_subevents else invoice.event.date_from,
period_end=None if invoice.event.has_subevents else invoice.event.date_to,
event_location=(
None if invoice.event.has_subevents
else (str(invoice.event.location)
@@ -506,8 +506,8 @@ def build_preview_invoice_pdf(event):
invoice=invoice, description=_("Sample product {}").format(i + 1),
gross_value=tax.gross, tax_value=tax.tax,
tax_rate=tax.rate, tax_name=tax.name, tax_code=tax.code,
event_date_from=event.date_from,
event_date_to=event.date_to,
period_start=event.date_from,
period_end=event.date_to,
event_location=event.settings.invoice_event_location,
)
else:
@@ -515,8 +515,8 @@ def build_preview_invoice_pdf(event):
InvoiceLine.objects.create(
invoice=invoice, description=_("Sample product A"),
gross_value=100, tax_value=0, tax_rate=0, tax_code=None,
event_date_from=event.date_from,
event_date_to=event.date_to,
period_start=event.date_from,
period_end=event.date_to,
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.")
)
},
'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': {
'default': 'False',
'type': bool,

View File

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

View File

@@ -15,6 +15,7 @@
{% 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_period layout="control" %}
{% bootstrap_field form.invoice_include_free layout="control" %}
{% bootstrap_field form.invoice_show_payments layout="control" %}
{% bootstrap_field form.invoice_reissue_after_modify layout="control" %}

View File

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

View File

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