mirror of
https://github.com/pretix/pretix.git
synced 2025-12-20 16:32:26 +00:00
Compare commits
7 Commits
widget-dia
...
revert-545
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1480a5cdc3 | ||
|
|
ccd922b7f0 | ||
|
|
2b5d659f64 | ||
|
|
1662c65b9b | ||
|
|
dabdac6ff2 | ||
|
|
50d724f9e3 | ||
|
|
ae5c0a5537 |
@@ -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):
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# 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/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import unicodedata
|
import unicodedata
|
||||||
@@ -522,6 +523,20 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Event'))))
|
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Event'))))
|
||||||
canvas.drawText(textobject)
|
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 _draw_event(self, canvas):
|
||||||
def shorten(txt):
|
def shorten(txt):
|
||||||
txt = str(txt)
|
txt = str(txt)
|
||||||
@@ -535,25 +550,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
p_size = p.wrap(self.event_width, self.event_height)
|
p_size = p.wrap(self.event_width, self.event_height)
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
|
d_from, d_to = self._date_range_in_header()
|
||||||
tz = self.invoice.event.timezone
|
if d_from and d_to:
|
||||||
show_end_date = (
|
p_str = (
|
||||||
self.invoice.event.settings.show_date_to and
|
shorten(self.invoice.event.name) + '\n' +
|
||||||
self.invoice.event.date_to and
|
pgettext('invoice', '{from_date}\nuntil {to_date}').format(
|
||||||
self.invoice.event.date_to.astimezone(tz).date() != self.invoice.event.date_from.astimezone(tz).date()
|
from_date=date_format(d_from, "DATE_FORMAT"),
|
||||||
|
to_date=date_format(d_to, "DATE_FORMAT"),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if show_end_date:
|
elif d_from:
|
||||||
p_str = (
|
p_str = shorten(self.invoice.event.name) + '\n' + date_format(d_from, "DATE_FORMAT")
|
||||||
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)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
p_str = shorten(self.invoice.event.name)
|
p_str = shorten(self.invoice.event.name)
|
||||||
|
|
||||||
@@ -657,6 +664,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
def _get_story(self, doc):
|
def _get_story(self, doc):
|
||||||
has_taxes = any(il.tax_value for il in self.invoice.lines.all()) or self.invoice.reverse_charge
|
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
|
||||||
|
has_multiple_service_dates = len(set(
|
||||||
|
(il.period_start, il.period_end) for il in self.invoice.lines.all()
|
||||||
|
)) > 1
|
||||||
|
request_show_service_date = False
|
||||||
|
|
||||||
story = [
|
story = [
|
||||||
NextPageTemplate('FirstPage'),
|
NextPageTemplate('FirstPage'),
|
||||||
@@ -700,15 +713,73 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
)]
|
)]
|
||||||
|
|
||||||
def _group_key(line):
|
def _group_key(line):
|
||||||
return (line.description, line.tax_rate, line.tax_name, line.net_value, line.gross_value, line.subevent_id,
|
return (line.description, line.tax_rate, line.tax_name, line.net_value, line.gross_value, line.subevent,
|
||||||
line.event_date_from, line.event_date_to)
|
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')
|
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(),
|
self.invoice.lines.all(),
|
||||||
key=_group_key,
|
key=_group_key,
|
||||||
is_addon=lambda l: l.description.startswith(" +"),
|
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"{date_format(day(period_start), 'SHORT_DATE_FORMAT')} – {date_format(day(period_end), 'SHORT_DATE_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 = date_format(delivery_day, 'SHORT_DATE_FORMAT')
|
||||||
|
else:
|
||||||
|
# No period known
|
||||||
|
period_line = ""
|
||||||
|
|
||||||
|
if not has_multiple_service_dates and period_line:
|
||||||
|
# Group together at the end of the invoice
|
||||||
|
request_show_service_date = period_line
|
||||||
|
else:
|
||||||
|
description += "\n" + period_line
|
||||||
|
|
||||||
lines = list(lines)
|
lines = list(lines)
|
||||||
if has_taxes:
|
if has_taxes:
|
||||||
if len(lines) > 1:
|
if len(lines) > 1:
|
||||||
@@ -717,6 +788,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
gross_price=money_filter(gross_value, self.invoice.event.currency),
|
gross_price=money_filter(gross_value, self.invoice.event.currency),
|
||||||
)
|
)
|
||||||
description = description + "\n" + single_price_line
|
description = description + "\n" + single_price_line
|
||||||
|
|
||||||
tdata.append((
|
tdata.append((
|
||||||
FontFallbackParagraph(
|
FontFallbackParagraph(
|
||||||
self._clean_text(description, tags=['br']),
|
self._clean_text(description, tags=['br']),
|
||||||
@@ -821,6 +893,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
story.append(Spacer(1, 10 * mm))
|
story.append(Spacer(1, 10 * mm))
|
||||||
|
|
||||||
|
if request_show_service_date:
|
||||||
|
story.append(FontFallbackParagraph(
|
||||||
|
self._normalize(pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date)),
|
||||||
|
self.stylesheet['Normal']
|
||||||
|
))
|
||||||
|
|
||||||
if self.invoice.payment_provider_text:
|
if self.invoice.payment_provider_text:
|
||||||
story.append(FontFallbackParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
self._normalize(self.invoice.payment_provider_text),
|
self._normalize(self.invoice.payment_provider_text),
|
||||||
|
|||||||
80
src/pretix/base/migrations/0289_invoiceline_period.py
Normal file
80
src/pretix/base/migrations/0289_invoiceline_period.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Generated by Django 4.2.17 on 2025-09-08 08:14
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def set_invoice_period(apps, schema_editor):
|
||||||
|
EventSettingsStore = apps.get_model("pretixbase", "Event_SettingsStore")
|
||||||
|
ev_seen = set()
|
||||||
|
insert_queue = []
|
||||||
|
flush_queue = []
|
||||||
|
|
||||||
|
def store():
|
||||||
|
EventSettingsStore.objects.bulk_create(
|
||||||
|
insert_queue,
|
||||||
|
update_conflicts=True,
|
||||||
|
update_fields=["value"],
|
||||||
|
unique_fields=["object", "key"],
|
||||||
|
)
|
||||||
|
for f in flush_queue:
|
||||||
|
cache.delete(f)
|
||||||
|
flush_queue.clear()
|
||||||
|
insert_queue.clear()
|
||||||
|
|
||||||
|
# Existing events that use pretix-zugferd and have explicitly disabled delivery dates
|
||||||
|
for setting in EventSettingsStore.objects.filter(key="zugferd_include_delivery_date", value="False"):
|
||||||
|
flush_queue.append("hierarkey_{}_{}".format("event", setting.object_id))
|
||||||
|
insert_queue.append(
|
||||||
|
EventSettingsStore(
|
||||||
|
object_id=setting.object_id,
|
||||||
|
key="invoice_period",
|
||||||
|
value="invoice_date",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ev_seen.add(setting.object_id)
|
||||||
|
|
||||||
|
if len(insert_queue) > 1000:
|
||||||
|
store()
|
||||||
|
|
||||||
|
# Existing events that previously hid their date on invoices
|
||||||
|
for setting in EventSettingsStore.objects.filter(key="show_dates_on_frontpage", value="False"):
|
||||||
|
if setting.object_id in ev_seen:
|
||||||
|
continue
|
||||||
|
|
||||||
|
flush_queue.append("hierarkey_{}_{}".format("event", setting.object_id))
|
||||||
|
insert_queue.append(
|
||||||
|
EventSettingsStore(
|
||||||
|
object_id=setting.object_id,
|
||||||
|
key="invoice_period",
|
||||||
|
value="auto_no_event",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ev_seen.add(setting.object_id)
|
||||||
|
|
||||||
|
if len(insert_queue) > 1000:
|
||||||
|
store()
|
||||||
|
|
||||||
|
store()
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
set_invoice_period,
|
||||||
|
migrations.RunPython.noop,
|
||||||
|
)
|
||||||
|
]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
|||||||
|
|
||||||
lp = invoice.order.payments.last()
|
lp = invoice.order.payments.last()
|
||||||
|
|
||||||
|
min_period_start = None
|
||||||
|
max_period_end = None
|
||||||
|
now_dt = now()
|
||||||
|
|
||||||
with (language(invoice.locale, invoice.event.settings.region)):
|
with (language(invoice.locale, invoice.event.settings.region)):
|
||||||
invoice.invoice_from = invoice.event.settings.get('invoice_address_from')
|
invoice.invoice_from = invoice.event.settings.get('invoice_address_from')
|
||||||
invoice.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
|
invoice.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
|
||||||
@@ -208,7 +212,9 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
|||||||
positions = list(
|
positions = list(
|
||||||
invoice.order.positions.select_related('addon_to', 'item', 'tax_rule', 'subevent', 'variation').annotate(
|
invoice.order.positions.select_related('addon_to', 'item', 'tax_rule', 'subevent', 'variation').annotate(
|
||||||
addon_c=Count('addons')
|
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
|
reverse_charge = False
|
||||||
@@ -267,6 +273,10 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
|||||||
location=_location_oneliner(location)
|
location=_location_oneliner(location)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
period_start, period_end = _service_period_for_position(invoice, p, now_dt)
|
||||||
|
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(
|
InvoiceLine.objects.create(
|
||||||
position=i,
|
position=i,
|
||||||
invoice=invoice,
|
invoice=invoice,
|
||||||
@@ -277,8 +287,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=period_start,
|
||||||
event_date_to=p.subevent.date_to if invoice.event.has_subevents else invoice.event.date_to,
|
period_end=period_end,
|
||||||
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,
|
||||||
@@ -301,13 +311,29 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
|||||||
fee_title = _(fee.get_fee_type_display())
|
fee_title = _(fee.get_fee_type_display())
|
||||||
if fee.description:
|
if fee.description:
|
||||||
fee_title += " - " + 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(
|
InvoiceLine.objects.create(
|
||||||
position=i + offset,
|
position=i + offset,
|
||||||
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=period_start,
|
||||||
event_date_to=None if invoice.event.has_subevents else invoice.event.date_to,
|
period_end=period_end,
|
||||||
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)
|
||||||
@@ -351,6 +377,49 @@ def build_cancellation(invoice: Invoice):
|
|||||||
return invoice
|
return invoice
|
||||||
|
|
||||||
|
|
||||||
|
def _service_period_for_position(invoice, position, invoice_dt):
|
||||||
|
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:
|
||||||
|
if position.subevent:
|
||||||
|
period_start = position.subevent.date_from
|
||||||
|
period_end = position.subevent.date_to
|
||||||
|
else:
|
||||||
|
# Currently impossible case, but might not be in the future and never makes
|
||||||
|
# sense to use the event date here
|
||||||
|
period_start = invoice_dt
|
||||||
|
period_end = invoice_dt
|
||||||
|
elif invoice.event.settings.invoice_period == "auto_no_event":
|
||||||
|
period_start = invoice_dt
|
||||||
|
period_end = invoice_dt
|
||||||
|
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 = invoice_dt
|
||||||
|
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):
|
def generate_cancellation(invoice: Invoice, trigger_pdf=True):
|
||||||
if invoice.canceled:
|
if invoice.canceled:
|
||||||
raise ValueError("Invoice should not be canceled twice.")
|
raise ValueError("Invoice should not be canceled twice.")
|
||||||
@@ -456,6 +525,12 @@ def build_preview_invoice_pdf(event):
|
|||||||
if not locale or locale == '__user__':
|
if not locale or locale == '__user__':
|
||||||
locale = event.settings.locale
|
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):
|
with rolledback_transaction(), language(locale, event.settings.region):
|
||||||
order = event.orders.create(
|
order = event.orders.create(
|
||||||
status=Order.STATUS_PENDING, datetime=timezone.now(),
|
status=Order.STATUS_PENDING, datetime=timezone.now(),
|
||||||
@@ -506,8 +581,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=period_start,
|
||||||
event_date_to=event.date_to,
|
period_end=period_end,
|
||||||
event_location=event.settings.invoice_event_location,
|
event_location=event.settings.invoice_event_location,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -515,8 +590,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=period_start,
|
||||||
event_date_to=event.date_to,
|
period_end=period_end,
|
||||||
event_location=event.settings.invoice_event_location,
|
event_location=event.settings.invoice_event_location,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1098,6 +1098,35 @@ 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', _('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')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Date of service"),
|
||||||
|
widget=forms.RadioSelect,
|
||||||
|
choices=(
|
||||||
|
('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': {
|
'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,19 @@
|
|||||||
{% 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" %}
|
||||||
|
|
||||||
|
{% if not request.event.settings.show_dates_on_frontpage %}
|
||||||
|
<div data-display-dependency="input[name=invoice_period][value=auto],input[name=invoice_period][value=event_date]">
|
||||||
|
<div class="alert alert-warning dynamic">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
You configured that your shop is not an event and the event date should not be shown.
|
||||||
|
Therefore, we recommend that you set the date of service to a different option.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% 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" %}
|
||||||
|
|||||||
@@ -231,7 +231,9 @@ TEST_INVOICE_RES = {
|
|||||||
"description": "Budget Ticket<br />Attendee: Peter",
|
"description": "Budget Ticket<br />Attendee: Peter",
|
||||||
'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': '2017-12-27T10:00:00Z',
|
||||||
|
'period_start': '2017-12-27T10:00:00Z',
|
||||||
|
'period_end': '2017-12-27T10:00:00Z',
|
||||||
'event_location': None,
|
'event_location': None,
|
||||||
'attendee_name': 'Peter',
|
'attendee_name': 'Peter',
|
||||||
'item': None,
|
'item': None,
|
||||||
@@ -249,7 +251,9 @@ TEST_INVOICE_RES = {
|
|||||||
"description": "Payment fee",
|
"description": "Payment fee",
|
||||||
'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': '2017-12-27T10:00:00Z',
|
||||||
|
'period_start': '2017-12-27T10:00:00Z',
|
||||||
|
'period_end': '2017-12-27T10:00:00Z',
|
||||||
'event_location': None,
|
'event_location': None,
|
||||||
'attendee_name': None,
|
'attendee_name': None,
|
||||||
'fee_type': "payment",
|
'fee_type': "payment",
|
||||||
|
|||||||
@@ -608,7 +608,9 @@ def test_order_create_invoice(token_client, organizer, event, order):
|
|||||||
'description': 'Budget Ticket<br />Attendee: Peter',
|
'description': 'Budget Ticket<br />Attendee: Peter',
|
||||||
'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': '2017-12-27T10:00:00Z',
|
||||||
|
'period_start': '2017-12-27T10:00:00Z',
|
||||||
|
'period_end': '2017-12-27T10:00:00Z',
|
||||||
'event_location': None,
|
'event_location': None,
|
||||||
'fee_type': None,
|
'fee_type': None,
|
||||||
'fee_internal_type': None,
|
'fee_internal_type': None,
|
||||||
@@ -626,7 +628,9 @@ def test_order_create_invoice(token_client, organizer, event, order):
|
|||||||
'description': 'Payment fee',
|
'description': 'Payment fee',
|
||||||
'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': '2017-12-27T10:00:00Z',
|
||||||
|
'period_start': '2017-12-27T10:00:00Z',
|
||||||
|
'period_end': '2017-12-27T10:00:00Z',
|
||||||
'event_location': None,
|
'event_location': None,
|
||||||
'fee_type': "payment",
|
'fee_type': "payment",
|
||||||
'fee_internal_type': None,
|
'fee_internal_type': None,
|
||||||
|
|||||||
@@ -33,7 +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 json
|
import json
|
||||||
from datetime import date, timedelta
|
from datetime import date, datetime, timedelta, timezone
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -42,6 +42,7 @@ from django.utils.itercompat import is_iterable
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
from pretix.base.invoice import addon_aware_groupby
|
from pretix.base.invoice import addon_aware_groupby
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -62,7 +63,8 @@ def env():
|
|||||||
with scope(organizer=o):
|
with scope(organizer=o):
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=o, name='Dummy', slug='dummy',
|
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(
|
o = Order.objects.create(
|
||||||
code='FOO', event=event, email='dummy@dummy.test',
|
code='FOO', event=event, email='dummy@dummy.test',
|
||||||
@@ -660,3 +662,154 @@ def test_addon_aware_groupby():
|
|||||||
[True, 102, 3.00],
|
[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
|
||||||
|
|||||||
Reference in New Issue
Block a user