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',