mirror of
https://github.com/pretix/pretix.git
synced 2026-05-08 15:44:02 +00:00
Invoicing: Configurable service date
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
65
src/pretix/base/migrations/0289_invoiceline_period.py
Normal file
65
src/pretix/base/migrations/0289_invoiceline_period.py
Normal 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",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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" %}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user