diff --git a/doc/api/resources/invoices.rst b/doc/api/resources/invoices.rst
index b54c0ecf6c..30fb4005c5 100644
--- a/doc/api/resources/invoices.rst
+++ b/doc/api/resources/invoices.rst
@@ -15,8 +15,24 @@ number string Invoice number
order string Order code of the order this invoice belongs to
is_cancellation boolean ``true``, if this invoice is the cancellation of a
different invoice.
-invoice_from string Sender address
-invoice_to string Receiver address
+invoice_from_name string Sender address: Name
+invoice_from string Sender address: Address lines
+invoice_from_zipcode string Sender address: ZIP code
+invoice_from_city string Sender address: City
+invoice_from_country string Sender address: Country code
+invoice_from_tax_id string Sender address: Local Tax ID
+invoice_from_vat_id string Sender address: EU VAT ID
+invoice_to string Full recipient address
+invoice_to_company string Recipient address: Company name
+invoice_to_name string Recipient address: Person name
+invoice_to_street string Recipient address: Address lines
+invoice_to_zipcode string Recipient address: ZIP code
+invoice_to_city string Recipient address: City
+invoice_to_state string Recipient address: State (only used in some countries)
+invoice_to_country string Recipient address: Country code
+invoice_to_vat_id string Recipient address: EU VAT ID
+invoice_to_beneficiary string Invoice beneficiary
+custom_field string Custom invoice address field
date date Invoice date
refers string Invoice number of an invoice this invoice refers to
(for example a cancellation refers to the invoice it
@@ -30,6 +46,31 @@ footer_text string Text to be prin
lines list of objects The actual invoice contents
├ position integer Number of the line within an invoice.
├ description string Text representing the invoice line (e.g. product name)
+├ item integer Product used to create this line. Note that everything
+ about the product might have changed since the creation
+ of the invoice. Can be ``null`` for all invoice lines
+ created before this field was introduced as well as for
+ all lines not created by a product (e.g. a shipping or
+ cancellation fee).
+├ variation integer Product variation used to create this line. Note that everything
+ about the product might have changed since the creation
+ of the invoice. Can be ``null`` for all invoice lines
+ created before this field was introduced as well as for
+ all lines not created by a product (e.g. a shipping or
+ cancellation fee).
+├ 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.
+├ attendee_name string Attendee name at time of invoice creation. Can be ``null`` if no
+ name was set or if names are configured to not be added to invoices.
├ gross_value money (string) Price including taxes
├ tax_value money (string) Tax amount included
├ tax_name string Name of used tax rate (e.g. "VAT")
@@ -50,6 +91,12 @@ internal_reference string Customer's refe
The attribute ``lines.number`` has been added.
+.. versionchanged:: 3.17
+
+ The attribute ``invoice_to_*``, ``invoice_from_*``, ``custom_field``, ``lines.item``, ``lines.variation``, ``lines.event_date_from``,
+ ``lines.event_date_to``, and ``lines.attendee_name`` have been added.
+ ``refers`` now returns an invoice number including the prefix.
+
Endpoints
---------
@@ -83,8 +130,24 @@ Endpoints
"number": "SAMPLECONF-00001",
"order": "ABC12",
"is_cancellation": false,
- "invoice_from": "Big Events LLC\nDemo street 12\nDemo town",
- "invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT ID: EU123456789",
+ "invoice_from_name": "Big Events LLC",
+ "invoice_from": "Demo street 12",
+ "invoice_from_zipcode":"",
+ "invoice_from_city":"Demo town",
+ "invoice_from_country":"US",
+ "invoice_from_tax_id":"",
+ "invoice_from_vat_id":"",
+ "invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
+ "invoice_to_company": "Sample company",
+ "invoice_to_name": "John Doe",
+ "invoice_to_street": "Test street 12",
+ "invoice_to_zipcode": "12345",
+ "invoice_to_city": "Testington",
+ "invoice_to_state": null,
+ "invoice_to_country": "TE",
+ "invoice_to_vat_id": "EU123456789",
+ "invoice_to_beneficiary": "",
+ "custom_field": null,
"date": "2017-12-01",
"refers": null,
"locale": "en",
@@ -97,6 +160,11 @@ Endpoints
{
"position": 1,
"description": "Budget Ticket",
+ "item": 1234,
+ "variation": 245,
+ "event_date_from": "2017-12-27T10:00:00Z",
+ "event_date_to": null,
+ "attendee_name": null,
"gross_value": "23.00",
"tax_value": "0.00",
"tax_name": "VAT",
@@ -148,8 +216,24 @@ Endpoints
"number": "SAMPLECONF-00001",
"order": "ABC12",
"is_cancellation": false,
- "invoice_from": "Big Events LLC\nDemo street 12\nDemo town",
- "invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT ID: EU123456789",
+ "invoice_from_name": "Big Events LLC",
+ "invoice_from": "Demo street 12",
+ "invoice_from_zipcode":"",
+ "invoice_from_city":"Demo town",
+ "invoice_from_country":"US",
+ "invoice_from_tax_id":"",
+ "invoice_from_vat_id":"",
+ "invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
+ "invoice_to_company": "Sample company",
+ "invoice_to_name": "John Doe",
+ "invoice_to_street": "Test street 12",
+ "invoice_to_zipcode": "12345",
+ "invoice_to_city": "Testington",
+ "invoice_to_state": null,
+ "invoice_to_country": "TE",
+ "invoice_to_vat_id": "EU123456789",
+ "invoice_to_beneficiary": "",
+ "custom_field": null,
"date": "2017-12-01",
"refers": null,
"locale": "en",
@@ -162,6 +246,11 @@ Endpoints
{
"position": 1,
"description": "Budget Ticket",
+ "item": 1234,
+ "variation": 245,
+ "event_date_from": "2017-12-27T10:00:00Z",
+ "event_date_to": null,
+ "attendee_name": null,
"gross_value": "23.00",
"tax_value": "0.00",
"tax_name": "VAT",
diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py
index c3a5016988..7ba2a607f8 100644
--- a/src/pretix/api/serializers/order.py
+++ b/src/pretix/api/serializers/order.py
@@ -45,6 +45,14 @@ class CompatibleCountryField(serializers.Field):
return instance.country_old
+class CountryField(serializers.Field):
+ def to_internal_value(self, data):
+ return {self.field_name: Country(data)}
+
+ def to_representation(self, src):
+ return str(src) if src else None
+
+
class InvoiceAddressSerializer(I18nAwareModelSerializer):
country = CompatibleCountryField(source='*')
name = serializers.CharField(required=False)
@@ -1322,17 +1330,24 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
class Meta:
model = InvoiceLine
- fields = ('position', 'description', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
+ fields = ('position', 'description', 'item', 'variation', 'attendee_name', 'event_date_from',
+ 'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
class InvoiceSerializer(I18nAwareModelSerializer):
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
- refers = serializers.SlugRelatedField(slug_field='invoice_no', read_only=True)
+ refers = serializers.SlugRelatedField(slug_field='full_invoice_no', read_only=True)
lines = InlineInvoiceLineSerializer(many=True)
+ invoice_to_country = CountryField()
+ invoice_from_country = CountryField()
class Meta:
model = Invoice
- fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_to', 'date', 'refers', 'locale',
+ fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
+ 'invoice_from_city', 'invoice_from_country', 'invoice_from_tax_id', 'invoice_from_vat_id',
+ 'invoice_to', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street', 'invoice_to_zipcode',
+ 'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id', 'invoice_to_beneficiary',
+ 'custom_field', 'date', 'refers', 'locale',
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines',
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date',
'internal_reference')
diff --git a/src/pretix/base/migrations/0178_auto_20210308_1326.py b/src/pretix/base/migrations/0178_auto_20210308_1326.py
new file mode 100644
index 0000000000..ddea1371e3
--- /dev/null
+++ b/src/pretix/base/migrations/0178_auto_20210308_1326.py
@@ -0,0 +1,34 @@
+# Generated by Django 3.0.12 on 2021-03-08 13:26
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('pretixbase', '0177_auto_20210301_1510'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='invoiceline',
+ name='attendee_name',
+ field=models.TextField(null=True),
+ ),
+ migrations.AddField(
+ model_name='invoiceline',
+ name='event_date_to',
+ field=models.DateTimeField(null=True),
+ ),
+ migrations.AddField(
+ model_name='invoiceline',
+ name='item',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Item'),
+ ),
+ migrations.AddField(
+ model_name='invoiceline',
+ name='variation',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.ItemVariation'),
+ ),
+ ]
diff --git a/src/pretix/base/models/invoices.py b/src/pretix/base/models/invoices.py
index 1692ac2895..b63b59d3be 100644
--- a/src/pretix/base/models/invoices.py
+++ b/src/pretix/base/models/invoices.py
@@ -273,6 +273,14 @@ class InvoiceLine(models.Model):
: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 item: The item this line refers to
+ :type item: Item
+ :param variation: The variation this line refers to
+ :type variation: ItemVariation
+ :param attendee_name: The attendee name at the time the invoice was created
+ :type attendee_name: str
"""
invoice = models.ForeignKey('Invoice', related_name='lines', on_delete=models.CASCADE)
position = models.PositiveIntegerField(default=0)
@@ -283,6 +291,10 @@ class InvoiceLine(models.Model):
tax_name = models.CharField(max_length=190)
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)
+ item = models.ForeignKey('Item', null=True, blank=True, on_delete=models.PROTECT)
+ variation = models.ForeignKey('ItemVariation', null=True, blank=True, on_delete=models.PROTECT)
+ attendee_name = models.TextField(null=True, blank=True)
@property
def net_value(self):
diff --git a/src/pretix/base/services/invoices.py b/src/pretix/base/services/invoices.py
index 5dba47cfa1..aaec7bab5c 100644
--- a/src/pretix/base/services/invoices.py
+++ b/src/pretix/base/services/invoices.py
@@ -171,9 +171,17 @@ def build_invoice(invoice: Invoice) -> Invoice:
if invoice.event.has_subevents:
desc += "
" + pgettext("subevent", "Date: {}").format(p.subevent)
InvoiceLine.objects.create(
- position=i, invoice=invoice, description=desc,
- gross_value=p.price, tax_value=p.tax_value,
- subevent=p.subevent, event_date_from=(p.subevent.date_from if p.subevent else invoice.event.date_from),
+ position=i,
+ invoice=invoice,
+ description=desc,
+ gross_value=p.price,
+ tax_value=p.tax_value,
+ subevent=p.subevent,
+ 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,
tax_rate=p.tax_rate, tax_name=p.tax_rule.name if p.tax_rule else ''
)
@@ -198,6 +206,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,
tax_value=fee.tax_value,
tax_rate=fee.tax_rate,
tax_name=fee.tax_rule.name if fee.tax_rule else ''
diff --git a/src/pretix/control/forms/filter.py b/src/pretix/control/forms/filter.py
index a2f01da8f4..a2a0b41f2b 100644
--- a/src/pretix/control/forms/filter.py
+++ b/src/pretix/control/forms/filter.py
@@ -5,7 +5,9 @@ from urllib.parse import urlencode
from django import forms
from django.apps import apps
from django.conf import settings
-from django.db.models import Exists, F, Max, Model, OuterRef, Q, QuerySet, Count
+from django.db.models import (
+ Count, Exists, F, Max, Model, OuterRef, Q, QuerySet,
+)
from django.db.models.functions import Coalesce, ExtractWeekDay
from django.urls import reverse, reverse_lazy
from django.utils.formats import date_format, localize
diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py
index 67cedd2e40..305ff3f064 100644
--- a/src/tests/api/test_orders.py
+++ b/src/tests/api/test_orders.py
@@ -960,8 +960,24 @@ TEST_INVOICE_RES = {
"order": "FOO",
"number": "DUMMY-00001",
"is_cancellation": False,
+ "invoice_from_name": "",
"invoice_from": "",
+ "invoice_from_zipcode": "",
+ "invoice_from_city": "",
+ "invoice_from_country": None,
+ "invoice_from_tax_id": "",
+ "invoice_from_vat_id": "",
"invoice_to": "Sample company\nNew Zealand\nVAT-ID: DE123",
+ "invoice_to_company": "Sample company",
+ "invoice_to_name": "",
+ "invoice_to_street": "",
+ "invoice_to_zipcode": "",
+ "invoice_to_city": "",
+ "invoice_to_state": "",
+ "invoice_to_country": "NZ",
+ "invoice_to_vat_id": "DE123",
+ "invoice_to_beneficiary": "",
+ "custom_field": None,
"date": "2017-12-10",
"refers": None,
"locale": "en",
@@ -977,6 +993,11 @@ TEST_INVOICE_RES = {
{
"position": 1,
"description": "Budget Ticket
Attendee: Peter",
+ 'event_date_from': '2017-12-27T10:00:00Z',
+ 'event_date_to': None,
+ 'attendee_name': 'Peter',
+ 'item': None,
+ 'variation': None,
"gross_value": "23.00",
"tax_value": "0.00",
"tax_name": "",
@@ -985,6 +1006,11 @@ TEST_INVOICE_RES = {
{
"position": 2,
"description": "Payment fee",
+ 'event_date_from': '2017-12-27T10:00:00Z',
+ 'event_date_to': None,
+ 'attendee_name': None,
+ 'item': None,
+ 'variation': None,
"gross_value": "0.25",
"tax_value": "0.05",
"tax_name": "",
@@ -995,8 +1021,9 @@ TEST_INVOICE_RES = {
@pytest.mark.django_db
-def test_invoice_list(token_client, organizer, event, order, invoice):
+def test_invoice_list(token_client, organizer, event, order, item, invoice):
res = dict(TEST_INVOICE_RES)
+ res['lines'][0]['item'] = item.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
@@ -1043,8 +1070,9 @@ def test_invoice_list(token_client, organizer, event, order, invoice):
@pytest.mark.django_db
-def test_invoice_detail(token_client, organizer, event, invoice):
+def test_invoice_detail(token_client, organizer, event, item, invoice):
res = dict(TEST_INVOICE_RES)
+ res['lines'][0]['item'] = item.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/{}/'.format(organizer.slug, event.slug,
invoice.number))
@@ -4300,12 +4328,30 @@ def test_order_create_invoice(token_client, organizer, event, order):
), format='json', data={}
)
assert resp.status_code == 201
- assert resp.data == {
+ with scopes_disabled():
+ pos = order.positions.first()
+ assert json.loads(json.dumps(resp.data)) == {
'order': 'FOO',
'number': 'DUMMY-00001',
'is_cancellation': False,
- 'invoice_from': '',
- 'invoice_to': 'Sample company\nNew Zealand\nVAT-ID: DE123',
+ "invoice_from_name": "",
+ "invoice_from": "",
+ "invoice_from_zipcode": "",
+ "invoice_from_city": "",
+ "invoice_from_country": None,
+ "invoice_from_tax_id": "",
+ "invoice_from_vat_id": "",
+ "invoice_to": "Sample company\nNew Zealand\nVAT-ID: DE123",
+ "invoice_to_company": "Sample company",
+ "invoice_to_name": "",
+ "invoice_to_street": "",
+ "invoice_to_zipcode": "",
+ "invoice_to_city": "",
+ "invoice_to_state": "",
+ "invoice_to_country": "NZ",
+ "invoice_to_vat_id": "DE123",
+ "invoice_to_beneficiary": "",
+ "custom_field": None,
'date': now().date().isoformat(),
'refers': None,
'locale': 'en',
@@ -4317,6 +4363,11 @@ def test_order_create_invoice(token_client, organizer, event, order):
{
'position': 1,
'description': 'Budget Ticket
Attendee: Peter',
+ 'event_date_from': '2017-12-27T10:00:00Z',
+ 'event_date_to': None,
+ 'attendee_name': 'Peter',
+ 'item': pos.item_id,
+ 'variation': None,
'gross_value': '23.00',
'tax_value': '0.00',
'tax_rate': '0.00',
@@ -4325,6 +4376,11 @@ def test_order_create_invoice(token_client, organizer, event, order):
{
'position': 2,
'description': 'Payment fee',
+ 'event_date_from': '2017-12-27T10:00:00Z',
+ 'event_date_to': None,
+ 'attendee_name': None,
+ 'item': None,
+ 'variation': None,
'gross_value': '0.25',
'tax_value': '0.05',
'tax_rate': '19.00',