diff --git a/src/pretix/base/invoicing/pdf.py b/src/pretix/base/invoicing/pdf.py
index 843e21bcd7..cc279e65a5 100644
--- a/src/pretix/base/invoicing/pdf.py
+++ b/src/pretix/base/invoicing/pdf.py
@@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# .
#
+import datetime
import logging
import re
import unicodedata
@@ -522,6 +523,20 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Event'))))
canvas.drawText(textobject)
+ def _date_range_in_header(self):
+ if self.invoice.event.has_subevents or not self.invoice.event.settings.show_dates_on_frontpage:
+ return None, None
+ tz = self.invoice.event.timezone
+ show_end_date = (
+ self.invoice.event.settings.show_date_to and
+ self.invoice.event.date_to and
+ self.invoice.event.date_to.astimezone(tz).date() != self.invoice.event.date_from.astimezone(tz).date()
+ )
+ if show_end_date:
+ return self.invoice.event.date_from.astimezone(tz).date(), self.invoice.event.date_to.astimezone(tz).date
+ else:
+ return self.invoice.event.date_from.astimezone(tz).date(), None
+
def _draw_event(self, canvas):
def shorten(txt):
txt = str(txt)
@@ -535,25 +550,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
p_size = p.wrap(self.event_width, self.event_height)
return txt
- if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
- tz = self.invoice.event.timezone
- show_end_date = (
- self.invoice.event.settings.show_date_to and
- self.invoice.event.date_to and
- self.invoice.event.date_to.astimezone(tz).date() != self.invoice.event.date_from.astimezone(tz).date()
+ d_from, d_to = self._date_range_in_header()
+ if d_from and d_to:
+ p_str = (
+ shorten(self.invoice.event.name) + '\n' +
+ pgettext('invoice', '{from_date}\nuntil {to_date}').format(
+ from_date=date_format(d_from, "DATE_FORMAT"),
+ to_date=date_format(d_to, "DATE_FORMAT"),
+ )
)
- if show_end_date:
- p_str = (
- shorten(self.invoice.event.name) + '\n' +
- pgettext('invoice', '{from_date}\nuntil {to_date}').format(
- from_date=self.invoice.event.get_date_from_display(show_times=False),
- to_date=self.invoice.event.get_date_to_display(show_times=False)
- )
- )
- else:
- p_str = (
- shorten(self.invoice.event.name) + '\n' + self.invoice.event.get_date_from_display(show_times=False)
- )
+ elif d_from:
+ p_str = shorten(self.invoice.event.name) + '\n' + date_format(d_from, "DATE_FORMAT")
else:
p_str = shorten(self.invoice.event.name)
@@ -657,6 +664,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _get_story(self, doc):
has_taxes = any(il.tax_value for il in self.invoice.lines.all()) or self.invoice.reverse_charge
+ header_dates = self._date_range_in_header()
+ tz = self.invoice.event.timezone
story = [
NextPageTemplate('FirstPage'),
@@ -700,15 +709,68 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
)]
def _group_key(line):
- return (line.description, line.tax_rate, line.tax_name, line.net_value, line.gross_value, line.subevent_id,
- line.event_date_from, line.event_date_to)
+ return (line.description, line.tax_rate, line.tax_name, line.net_value, line.gross_value, line.subevent,
+ line.period_start, line.period_end)
+
+ def day(dt: datetime.datetime) -> datetime.date:
+ if dt is None:
+ return None
+ return dt.astimezone(tz).date()
total = Decimal('0.00')
- for (description, tax_rate, tax_name, net_value, gross_value, *ignored), lines in addon_aware_groupby(
+ for (description, tax_rate, tax_name, net_value, gross_value, subevent, period_start, period_end), lines in addon_aware_groupby(
self.invoice.lines.all(),
key=_group_key,
is_addon=lambda l: l.description.startswith(" +"),
):
+ # Try to be clever and figure out when organizers would want to show the period. This heuristic is
+ # not perfect and the only "fully correct" way would be to include the period on every line always,
+ # however this will cause confusion (a) due to useless repetition of the same date all over the invoice
+ # (b) due to not respecting the show_date_to setting of events in cases where we could have respected it.
+ # Still, we want to show the date explicitly if its different to the event or invoice date.
+ if period_start and period_end and day(period_end) != day(period_start):
+ # It's a multi-day period, such as the validity of the ticket or an event date period
+
+ if day(period_start) == header_dates[0] and day(period_end) == header_dates[1]:
+ # This is the exact event period we already printed in the header, no need to repeat it.
+ period_line = ""
+
+ elif (self.event.has_subevents and subevent and day(subevent.date_from) == day(period_start) and
+ day(subevent.date_to) == day(period_end)):
+ # For subevents, build_invoice already includes the date in the description in the event-default format.
+ period_line = ""
+
+ else:
+ period_line = f"\n{date_format(day(period_start), 'SHORT_DATE_FORMAT')} – {date_format(day(period_end), 'SHORT_END_FORMAT')}"
+
+ elif period_start or period_end:
+ # It's a single-day period
+
+ delivery_day = day(period_end or period_start)
+ if delivery_day in (header_dates[0], header_dates[1]):
+ # This is the event date we already printed in the header, no need to repeat it.
+ period_line = ""
+
+ elif self.event.has_subevents and subevent and delivery_day in (day(subevent.date_from), day(subevent.date_to)):
+ # For subevents, build_invoice already includes the date in the description in the event-default format.
+ period_line = ""
+
+ elif (delivery_day == self.invoice.date) and header_dates[0] is None:
+ # This is a shop that doesn't show the date of the event in the header, and the period is the invoice
+ # date. We assume that this is an 'everything is executed immediately' situation and do not want to
+ # confuse with showing additional dates on the invoice. This is the case that is not guaranteed to be
+ # correct in all cases and might need to change in the future. If customers have legal concerns, a
+ # quick fix is including a sentence like "Delivery date is the invoice date unless otherwise indicated:"
+ # in a custom text on the invoice.
+ period_line = ""
+
+ else:
+ period_line = f"\n{date_format(day(delivery_day), 'SHORT_DATE_FORMAT')}"
+ else:
+ # No period known
+ period_line = ""
+
+ description += period_line
lines = list(lines)
if has_taxes:
if len(lines) > 1:
@@ -717,6 +779,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
gross_price=money_filter(gross_value, self.invoice.event.currency),
)
description = description + "\n" + single_price_line
+
tdata.append((
FontFallbackParagraph(
self._clean_text(description, tags=['br']),
diff --git a/src/pretix/base/services/invoices.py b/src/pretix/base/services/invoices.py
index 307b6d1bce..d1a86a9c97 100644
--- a/src/pretix/base/services/invoices.py
+++ b/src/pretix/base/services/invoices.py
@@ -82,6 +82,9 @@ def build_invoice(invoice: Invoice) -> Invoice:
lp = invoice.order.payments.last()
+ min_period_start = None
+ max_period_end = None
+
with (language(invoice.locale, invoice.event.settings.region)):
invoice.invoice_from = invoice.event.settings.get('invoice_address_from')
invoice.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
@@ -208,7 +211,9 @@ def build_invoice(invoice: Invoice) -> Invoice:
positions = list(
invoice.order.positions.select_related('addon_to', 'item', 'tax_rule', 'subevent', 'variation').annotate(
addon_c=Count('addons')
- ).prefetch_related('answers', 'answers__options', 'answers__question').order_by('positionid', 'id')
+ ).prefetch_related(
+ 'answers', 'answers__options', 'answers__question', 'granted_memberships',
+ ).order_by('positionid', 'id')
)
reverse_charge = False
@@ -267,6 +272,10 @@ def build_invoice(invoice: Invoice) -> Invoice:
location=_location_oneliner(location)
)
+ period_start, period_end = _service_period_for_position(invoice, p)
+ min_period_start = min(min_period_start or period_start, period_start)
+ max_period_end = min(max_period_end or period_end, period_end)
+
InvoiceLine.objects.create(
position=i,
invoice=invoice,
@@ -277,8 +286,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,
- 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,
+ period_start=period_start,
+ period_end=period_end,
event_location=location if invoice.event.settings.invoice_event_location else None,
tax_rate=p.tax_rate,
tax_code=p.tax_code,
@@ -301,13 +310,29 @@ def build_invoice(invoice: Invoice) -> Invoice:
fee_title = _(fee.get_fee_type_display())
if fee.description:
fee_title += " - " + fee.description
+
+ if min_period_start and max_period_end:
+ # Consider fees to have the same service period as the products sold
+ period_start = min_period_start
+ period_end = max_period_end
+ else:
+ # Usually can only happen if everything except a cancellation fee is removed
+ if invoice.event.settings.invoice_period in ("auto", "auto_no_event", "event_date") and not invoice.event.has_subevents:
+ # Non-series event, let's be backwards-compatible and tag everything with the event period
+ period_start = invoice.event.date_from
+ period_end = invoice.event.date_to
+ else:
+ # We could try to work from the canceled positions, but it doesn't really make sense. A cancellation
+ # fee is not "delivered" at the event date, it is rather effective right now.
+ period_start = period_end = now()
+
InvoiceLine.objects.create(
position=i + offset,
invoice=invoice,
description=fee_title,
gross_value=fee.value,
- 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,
+ period_start=period_start,
+ period_end=period_end,
event_location=(
None if invoice.event.has_subevents
else (str(invoice.event.location)
@@ -351,6 +376,43 @@ def build_cancellation(invoice: Invoice):
return invoice
+def _service_period_for_position(invoice, position):
+ if invoice.event.settings.invoice_period in ("auto", "auto_no_event"):
+ if position.valid_from or position.valid_until:
+ period_start = position.valid_from or now()
+ period_end = position.valid_until
+ elif memberships := list(position.granted_memberships.all()):
+ period_start = min(m.date_start for m in memberships)
+ period_end = max(m.date_end for m in memberships)
+ elif invoice.event.has_subevents and position.subevent:
+ period_start = position.subevent.date_from
+ period_end = position.subevent.date_to
+ elif invoice.event.settings.invoice_period == "auto_no_event":
+ period_start = now()
+ period_end = now()
+ else:
+ period_start = invoice.event.date_from
+ period_end = invoice.event.date_to
+ elif invoice.event.settings.invoice_period == "order_date":
+ period_start = invoice.order.datetime
+ period_end = invoice.order.datetime
+ elif invoice.event.settings.invoice_period == "event_date":
+ if position.subevent:
+ period_start = position.subevent.date_from
+ period_end = position.subevent.date_to
+ else:
+ period_start = invoice.event.date_from
+ period_end = invoice.event.date_to
+ elif invoice.event.settings.invoice_period == "invoice_date":
+ period_start = period_end = now()
+ else:
+ raise ValueError(f"Invalid invoice period setting '{invoice.event.settings.invoice_period}'")
+
+ if not period_end:
+ period_end = period_start
+ return period_start, period_end
+
+
def generate_cancellation(invoice: Invoice, trigger_pdf=True):
if invoice.canceled:
raise ValueError("Invoice should not be canceled twice.")
@@ -456,6 +518,12 @@ def build_preview_invoice_pdf(event):
if not locale or locale == '__user__':
locale = event.settings.locale
+ if event.settings.invoice_period in ("auto", "auto_no_event", "event_date"):
+ period_start = event.date_from
+ period_end = event.date_to or event.date_from
+ else:
+ period_start = period_end = timezone.now()
+
with rolledback_transaction(), language(locale, event.settings.region):
order = event.orders.create(
status=Order.STATUS_PENDING, datetime=timezone.now(),
@@ -506,8 +574,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,
- period_start=event.date_from,
- period_end=event.date_to,
+ period_start=period_start,
+ period_end=period_end,
event_location=event.settings.invoice_event_location,
)
else:
@@ -515,8 +583,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,
- period_start=event.date_from,
- period_end=event.date_to,
+ period_start=period_start,
+ period_end=period_end,
event_location=event.settings.invoice_event_location,
)
diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py
index 15e37ebb53..2b87f00bb9 100644
--- a/src/pretix/base/settings.py
+++ b/src/pretix/base/settings.py
@@ -1105,8 +1105,9 @@ DEFAULTS = {
'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')),
+ ('auto', _('Automatic based on ticket-specific validity, membership validity, event series date, or event date)')),
+ ('auto_no_event', _('Automatic, but prefer invoice date over event date')),
+ ('event_date', _('Event date')),
('order_date', _('Order date')),
('invoice_date', _('Invoice date')),
),
@@ -1115,13 +1116,15 @@ DEFAULTS = {
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')),
+ ('auto', _('Automatic based on ticket-specific validity, membership validity, event series date, or event date)')),
+ ('auto_no_event', _('Automatic, but prefer invoice date over event date')),
+ ('event_date', _('Event 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."),
+ required=True,
)
},
'invoice_reissue_after_modify': {
diff --git a/src/tests/api/test_invoices.py b/src/tests/api/test_invoices.py
index 3eb33f1ff2..10cf0fb1a4 100644
--- a/src/tests/api/test_invoices.py
+++ b/src/tests/api/test_invoices.py
@@ -231,9 +231,9 @@ TEST_INVOICE_RES = {
"description": "Budget Ticket
Attendee: Peter",
'subevent': None,
'event_date_from': '2017-12-27T10:00:00Z',
- 'event_date_to': None,
+ 'event_date_to': '2017-12-27T10:00:00Z',
'period_start': '2017-12-27T10:00:00Z',
- 'period_end': None,
+ 'period_end': '2017-12-27T10:00:00Z',
'event_location': None,
'attendee_name': 'Peter',
'item': None,
@@ -251,9 +251,9 @@ TEST_INVOICE_RES = {
"description": "Payment fee",
'subevent': None,
'event_date_from': '2017-12-27T10:00:00Z',
- 'event_date_to': None,
+ 'event_date_to': '2017-12-27T10:00:00Z',
'period_start': '2017-12-27T10:00:00Z',
- 'period_end': None,
+ 'period_end': '2017-12-27T10:00:00Z',
'event_location': None,
'attendee_name': None,
'fee_type': "payment",
diff --git a/src/tests/api/test_order_change.py b/src/tests/api/test_order_change.py
index c8d724022c..e061aad40e 100644
--- a/src/tests/api/test_order_change.py
+++ b/src/tests/api/test_order_change.py
@@ -608,9 +608,9 @@ def test_order_create_invoice(token_client, organizer, event, order):
'description': 'Budget Ticket
Attendee: Peter',
'subevent': None,
'event_date_from': '2017-12-27T10:00:00Z',
- 'event_date_to': None,
+ 'event_date_to': '2017-12-27T10:00:00Z',
'period_start': '2017-12-27T10:00:00Z',
- 'period_end': None,
+ 'period_end': '2017-12-27T10:00:00Z',
'event_location': None,
'fee_type': None,
'fee_internal_type': None,
@@ -628,9 +628,9 @@ def test_order_create_invoice(token_client, organizer, event, order):
'description': 'Payment fee',
'subevent': None,
'event_date_from': '2017-12-27T10:00:00Z',
- 'event_date_to': None,
+ 'event_date_to': '2017-12-27T10:00:00Z',
'period_start': '2017-12-27T10:00:00Z',
- 'period_end': None,
+ 'period_end': '2017-12-27T10:00:00Z',
'event_location': None,
'fee_type': "payment",
'fee_internal_type': None,
diff --git a/src/tests/base/test_invoices.py b/src/tests/base/test_invoices.py
index 584a2bb017..2ce90e440c 100644
--- a/src/tests/base/test_invoices.py
+++ b/src/tests/base/test_invoices.py
@@ -19,7 +19,6 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# .
#
-
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at .
#
@@ -33,7 +32,7 @@
# License for the specific language governing permissions and limitations under the License.
import json
-from datetime import date, timedelta
+from datetime import date, datetime, timedelta, timezone
from decimal import Decimal
import pytest
@@ -42,6 +41,7 @@ from django.utils.itercompat import is_iterable
from django.utils.timezone import now
from django_countries.fields import Country
from django_scopes import scope, scopes_disabled
+from i18nfield.strings import LazyI18nString
from pretix.base.invoice import addon_aware_groupby
from pretix.base.models import (
@@ -62,7 +62,8 @@ def env():
with scope(organizer=o):
event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
- date_from=now(), plugins='pretix.plugins.banktransfer'
+ date_from=datetime(2024, 12, 1, 9, 0, 0, tzinfo=timezone.utc),
+ plugins='pretix.plugins.banktransfer'
)
o = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
@@ -660,3 +661,154 @@ def test_addon_aware_groupby():
[True, 102, 3.00],
]],
]
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize("period", ["auto", "event_date"])
+def test_period_from_event_start(env, period):
+ event, order = env
+ event.settings.invoice_period = period
+ inv = generate_invoice(order)
+ l1 = inv.lines.first()
+ assert l1.period_start == event.date_from
+ assert l1.period_end == event.date_from
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize("period", ["auto", "event_date"])
+def test_period_from_event_range(env, period):
+ event, order = env
+ event.date_to = event.date_from + timedelta(days=1)
+ event.settings.invoice_period = period
+ inv = generate_invoice(order)
+ l1 = inv.lines.first()
+ assert l1.period_start == event.date_from
+ assert l1.period_end == event.date_to
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize("period", ["auto", "auto_no_event"])
+def test_period_from_ticket_validity(env, period):
+ event, order = env
+ p1 = order.positions.first()
+ p1.valid_from = datetime(2025, 1, 1, 0, 0, 0, tzinfo=event.timezone)
+ p1.valid_until = datetime(2025, 12, 31, 23, 59, 59, tzinfo=event.timezone)
+ p1.save()
+ event.date_to = event.date_from + timedelta(days=1)
+ event.settings.invoice_period = period
+ inv = generate_invoice(order)
+ l1 = inv.lines.first()
+ assert l1.period_start == p1.valid_from
+ assert l1.period_end == p1.valid_until
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize("period", ["auto", "auto_no_event"])
+def test_period_from_subevent(env, period):
+ event, order = env
+ event.has_subevents = True
+ event.save()
+ se1 = event.subevents.create(
+ name=event.name,
+ active=True,
+ date_from=datetime((now().year + 1), 7, 31, 9, 0, 0, tzinfo=timezone.utc),
+ date_to=datetime((now().year + 1), 7, 31, 17, 0, 0, tzinfo=timezone.utc),
+ )
+ p1 = order.positions.first()
+ p1.subevent = se1
+ p1.save()
+ event.date_to = event.date_from + timedelta(days=1)
+ event.settings.invoice_period = period
+ inv = generate_invoice(order)
+ l1 = inv.lines.first()
+ assert l1.period_start == se1.date_from
+ assert l1.period_end == se1.date_to
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize("period", ["auto", "auto_no_event"])
+def test_period_from_memberships(env, period):
+ event, order = env
+ event.date_to = event.date_from + timedelta(days=1)
+ event.settings.invoice_period = period
+ p1 = order.positions.first()
+ membershiptype = event.organizer.membership_types.create(
+ name=LazyI18nString({"en": "Week pass"}),
+ transferable=True,
+ allow_parallel_usage=False,
+ max_usages=15,
+ )
+ customer = event.organizer.customers.create(
+ identifier="8WSAJCJ",
+ email="foo@example.org",
+ name_parts={"_legacy": "Foo"},
+ name_cached="Foo",
+ is_verified=False,
+ )
+ m = customer.memberships.create(
+ membership_type=membershiptype,
+ granted_in=p1,
+ date_start=datetime(2021, 4, 1, 0, 0, 0, 0, tzinfo=timezone.utc),
+ date_end=datetime(2021, 4, 8, 23, 59, 59, 999999, tzinfo=timezone.utc),
+ attendee_name_parts={
+ "_scheme": "given_family",
+ 'given_name': 'John',
+ 'family_name': 'Doe',
+ }
+ )
+ inv = generate_invoice(order)
+ l1 = inv.lines.first()
+ assert l1.period_start == m.date_start
+ assert l1.period_end == m.date_end
+
+
+@pytest.mark.django_db
+def test_period_auto_no_event_from_invoice(env):
+ event, order = env
+ event.settings.invoice_period = "auto_no_event"
+ inv = generate_invoice(order)
+ l1 = inv.lines.first()
+ assert abs(l1.period_start - now()) < timedelta(seconds=10)
+ assert abs(l1.period_end - now()) < timedelta(seconds=10)
+
+
+@pytest.mark.django_db
+def test_period_always_invoice_date(env):
+ event, order = env
+ p1 = order.positions.first()
+ p1.valid_from = datetime(2025, 1, 1, 0, 0, 0, tzinfo=event.timezone)
+ p1.valid_until = datetime(2025, 12, 31, 23, 59, 59, tzinfo=event.timezone)
+ p1.save()
+ event.settings.invoice_period = "invoice_date"
+ inv = generate_invoice(order)
+ l1 = inv.lines.first()
+ assert abs(l1.period_start - now()) < timedelta(seconds=10)
+ assert abs(l1.period_end - now()) < timedelta(seconds=10)
+
+
+@pytest.mark.django_db
+def test_period_always_event_date(env):
+ event, order = env
+ p1 = order.positions.first()
+ p1.valid_from = datetime(2025, 1, 1, 0, 0, 0, tzinfo=event.timezone)
+ p1.valid_until = datetime(2025, 12, 31, 23, 59, 59, tzinfo=event.timezone)
+ p1.save()
+ event.settings.invoice_period = "event_date"
+ inv = generate_invoice(order)
+ l1 = inv.lines.first()
+ assert l1.period_start == event.date_from
+ assert l1.period_end == event.date_from
+
+
+@pytest.mark.django_db
+def test_period_always_order_date(env):
+ event, order = env
+ p1 = order.positions.first()
+ p1.valid_from = datetime(2025, 1, 1, 0, 0, 0, tzinfo=event.timezone)
+ p1.valid_until = datetime(2025, 12, 31, 23, 59, 59, tzinfo=event.timezone)
+ p1.save()
+ event.settings.invoice_period = "order_date"
+ inv = generate_invoice(order)
+ l1 = inv.lines.first()
+ assert l1.period_start == order.datetime
+ assert l1.period_end == order.datetime