mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Order creation API
This commit is contained in:
@@ -41,6 +41,7 @@ expires datetime The order will
|
|||||||
payment_date date **DEPRECATED AND INACCURATE** Date of payment receipt
|
payment_date date **DEPRECATED AND INACCURATE** Date of payment receipt
|
||||||
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
|
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
|
||||||
total money (string) Total value of this order
|
total money (string) Total value of this order
|
||||||
|
tax_rounding_mode string Tax rounding mode, see :ref:`algorithms-rounding`
|
||||||
comment string Internal comment on this order
|
comment string Internal comment on this order
|
||||||
api_meta object Meta data for that order. Only available through API, no guarantees
|
api_meta object Meta data for that order. Only available through API, no guarantees
|
||||||
on the content structure. You can use this to save references to your system.
|
on the content structure. You can use this to save references to your system.
|
||||||
@@ -142,6 +143,10 @@ plugin_data object Additional data
|
|||||||
|
|
||||||
The ``plugin_data`` attribute has been added.
|
The ``plugin_data`` attribute has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2025.8
|
||||||
|
|
||||||
|
The ``tax_rounding_mode`` attribute has been added.
|
||||||
|
|
||||||
.. _order-position-resource:
|
.. _order-position-resource:
|
||||||
|
|
||||||
Order position resource
|
Order position resource
|
||||||
@@ -349,6 +354,7 @@ List of all orders
|
|||||||
"payment_provider": "banktransfer",
|
"payment_provider": "banktransfer",
|
||||||
"fees": [],
|
"fees": [],
|
||||||
"total": "23.00",
|
"total": "23.00",
|
||||||
|
"tax_rounding_mode": "line",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"custom_followup_at": null,
|
"custom_followup_at": null,
|
||||||
"checkin_attention": false,
|
"checkin_attention": false,
|
||||||
@@ -590,6 +596,7 @@ Fetching individual orders
|
|||||||
"payment_provider": "banktransfer",
|
"payment_provider": "banktransfer",
|
||||||
"fees": [],
|
"fees": [],
|
||||||
"total": "23.00",
|
"total": "23.00",
|
||||||
|
"tax_rounding_mode": "line",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"api_meta": {},
|
"api_meta": {},
|
||||||
"custom_followup_at": null,
|
"custom_followup_at": null,
|
||||||
@@ -996,6 +1003,7 @@ Creating orders
|
|||||||
provider will not be called to do anything about this (i.e. if you pass a bank account to a debit provider, *no*
|
provider will not be called to do anything about this (i.e. if you pass a bank account to a debit provider, *no*
|
||||||
charge will be created), this is just informative in case you *handled the payment already*.
|
charge will be created), this is just informative in case you *handled the payment already*.
|
||||||
* ``payment_date`` (optional) – Date and time of the completion of the payment.
|
* ``payment_date`` (optional) – Date and time of the completion of the payment.
|
||||||
|
* ``tax_rounding_mode`` (optional)
|
||||||
* ``comment`` (optional)
|
* ``comment`` (optional)
|
||||||
* ``custom_followup_at`` (optional)
|
* ``custom_followup_at`` (optional)
|
||||||
* ``checkin_attention`` (optional)
|
* ``checkin_attention`` (optional)
|
||||||
|
|||||||
@@ -50,9 +50,10 @@ from pretix.api.signals import order_api_details, orderposition_api_details
|
|||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
|
CachedFile, Checkin, Customer, Device, Invoice, InvoiceAddress,
|
||||||
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
|
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
|
||||||
ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule, Voucher,
|
QuestionAnswer, ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule,
|
||||||
|
Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||||
@@ -62,10 +63,13 @@ from pretix.base.pdf import get_images, get_variables
|
|||||||
from pretix.base.services.cart import error_messages
|
from pretix.base.services.cart import error_messages
|
||||||
from pretix.base.services.locking import LOCK_TRUST_WINDOW, lock_objects
|
from pretix.base.services.locking import LOCK_TRUST_WINDOW, lock_objects
|
||||||
from pretix.base.services.pricing import (
|
from pretix.base.services.pricing import (
|
||||||
apply_discounts, get_line_price, get_listed_price, is_included_for_free,
|
apply_discounts, apply_rounding, get_line_price, get_listed_price,
|
||||||
|
is_included_for_free,
|
||||||
)
|
)
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
from pretix.base.settings import (
|
||||||
|
COUNTRIES_WITH_STATE_IN_ADDRESS, ROUNDING_MODES,
|
||||||
|
)
|
||||||
from pretix.base.signals import register_ticket_outputs
|
from pretix.base.signals import register_ticket_outputs
|
||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import CachedCountries
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
@@ -781,14 +785,15 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
list_serializer_class = OrderListSerializer
|
list_serializer_class = OrderListSerializer
|
||||||
fields = (
|
fields = (
|
||||||
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
'payment_provider', 'fees', 'total', 'tax_rounding_mode', 'comment', 'custom_followup_at', 'invoice_address',
|
||||||
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
'positions', 'downloads', 'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds',
|
||||||
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date', 'plugin_data',
|
'require_approval', 'sales_channel', 'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date',
|
||||||
|
'plugin_data',
|
||||||
)
|
)
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'positions', 'downloads', 'customer',
|
'payment_provider', 'fees', 'total', 'tax_rounding_mode', 'positions', 'downloads', 'customer',
|
||||||
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'cancellation_date'
|
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'cancellation_date',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -1103,6 +1108,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
queryset=SalesChannel.objects.none(),
|
queryset=SalesChannel.objects.none(),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
tax_rounding_mode = serializers.ChoiceField(choices=ROUNDING_MODES, allow_null=True, required=False,)
|
||||||
locale = serializers.ChoiceField(choices=[], required=False, allow_null=True)
|
locale = serializers.ChoiceField(choices=[], required=False, allow_null=True)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -1118,7 +1124,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||||
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
||||||
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
||||||
'require_approval', 'valid_if_pending', 'expires', 'api_meta')
|
'require_approval', 'valid_if_pending', 'expires', 'api_meta', 'tax_rounding_mode')
|
||||||
|
|
||||||
def validate_payment_provider(self, pp):
|
def validate_payment_provider(self, pp):
|
||||||
if pp is None:
|
if pp is None:
|
||||||
@@ -1641,7 +1647,31 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
else:
|
else:
|
||||||
f.save()
|
f.save()
|
||||||
|
|
||||||
order.total += sum([f.value for f in fees])
|
rounding_mode = validated_data.get("tax_rounding_mode")
|
||||||
|
if not rounding_mode:
|
||||||
|
if isinstance(self.context.get("auth"), Device):
|
||||||
|
# Safety fallback to avoid differences in tax reporting
|
||||||
|
brand = self.context.get("auth").software_brand or ""
|
||||||
|
if "pretixPOS" in brand or "pretixKIOSK" in brand:
|
||||||
|
rounding_mode = "line"
|
||||||
|
if not rounding_mode:
|
||||||
|
rounding_mode = self.context["event"].settings.tax_rounding
|
||||||
|
changed = apply_rounding(
|
||||||
|
rounding_mode,
|
||||||
|
self.context["event"].currency,
|
||||||
|
[*pos_map.values(), *fees]
|
||||||
|
)
|
||||||
|
for line in changed:
|
||||||
|
if isinstance(line, OrderPosition):
|
||||||
|
line.save(update_fields=[
|
||||||
|
"price", "price_includes_rounding_correction", "tax_value", "tax_value_includes_rounding_correction"
|
||||||
|
])
|
||||||
|
elif isinstance(line, OrderFee):
|
||||||
|
line.save(update_fields=[
|
||||||
|
"value", "value_includes_rounding_correction", "tax_value", "tax_value_includes_rounding_correction"
|
||||||
|
])
|
||||||
|
|
||||||
|
order.total = sum([c.price for c in pos_map.values()]) + sum([f.value for f in fees])
|
||||||
if simulate:
|
if simulate:
|
||||||
order.fees = fees
|
order.fees = fees
|
||||||
order.positions = pos_map.values()
|
order.positions = pos_map.values()
|
||||||
|
|||||||
@@ -344,6 +344,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
|
ctx['auth'] = self.request.auth
|
||||||
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
|
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ from pretix.control.forms import (
|
|||||||
)
|
)
|
||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import CachedCountries
|
||||||
|
|
||||||
|
|
||||||
ROUNDING_MODES = (
|
ROUNDING_MODES = (
|
||||||
('line', _('Rounding every line individually')),
|
('line', _('Rounding every line individually')),
|
||||||
('sum_by_net', _('Rounding by order total, keeping net prices stable')),
|
('sum_by_net', _('Rounding by order total, keeping net prices stable')),
|
||||||
|
|||||||
@@ -420,6 +420,7 @@ def test_order_create_simulate(token_client, organizer, event, item, quota, ques
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
'total': '21.75',
|
'total': '21.75',
|
||||||
|
'tax_rounding_mode': 'line',
|
||||||
'comment': '',
|
'comment': '',
|
||||||
'api_meta': {},
|
'api_meta': {},
|
||||||
"custom_followup_at": None,
|
"custom_followup_at": None,
|
||||||
@@ -3099,3 +3100,79 @@ def test_order_create_create_medium(token_client, organizer, event, item, quota,
|
|||||||
m = organizer.reusable_media.get(identifier=i)
|
m = organizer.reusable_media.get(identifier=i)
|
||||||
assert m.linked_orderposition == o.positions.first()
|
assert m.linked_orderposition == o.positions.first()
|
||||||
assert m.type == "barcode"
|
assert m.type == "barcode"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_order_create_rounding_mode(token_client, organizer, event, item, quota, question, taxrule):
|
||||||
|
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||||
|
res["tax_rounding_mode"] = "sum_by_net"
|
||||||
|
res['fees'][0]['_split_taxes_like_products'] = True
|
||||||
|
res['fees'][0]['value'] = Decimal("100.00")
|
||||||
|
res['positions'] = [
|
||||||
|
{
|
||||||
|
"item": 1,
|
||||||
|
"price": "100.00",
|
||||||
|
}
|
||||||
|
] * 4
|
||||||
|
|
||||||
|
for simulate in (True, False):
|
||||||
|
res["simulate"] = simulate
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||||
|
organizer.slug, event.slug
|
||||||
|
), format='json', data=res
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
assert resp.data["total"] == "499.98"
|
||||||
|
assert resp.data["positions"][0]["price"] == "99.99"
|
||||||
|
assert resp.data["positions"][-1]["price"] == "100.00"
|
||||||
|
|
||||||
|
res["tax_rounding_mode"] = "sum_by_gross"
|
||||||
|
for simulate in (True, False):
|
||||||
|
res["simulate"] = simulate
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||||
|
organizer.slug, event.slug
|
||||||
|
), format='json', data=res
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
assert resp.data["total"] == "500.00"
|
||||||
|
assert resp.data["positions"][0]["tax_value"] == "15.96"
|
||||||
|
assert resp.data["positions"][-1]["tax_value"] == "15.97"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_order_create_rounding_default_pretixpos_fallback(device, device_client, organizer, event, item, quota, question, taxrule):
|
||||||
|
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||||
|
res['fees'][0]['_split_taxes_like_products'] = True
|
||||||
|
res['fees'][0]['value'] = Decimal("100.00")
|
||||||
|
res['positions'] = [
|
||||||
|
{
|
||||||
|
"item": 1,
|
||||||
|
"price": "100.00",
|
||||||
|
}
|
||||||
|
] * 4
|
||||||
|
|
||||||
|
event.settings.tax_rounding = "sum_by_net"
|
||||||
|
|
||||||
|
resp = device_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||||
|
organizer.slug, event.slug
|
||||||
|
), format='json', data=res
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
assert resp.data["total"] == "499.98"
|
||||||
|
assert resp.data["positions"][0]["price"] == "99.99"
|
||||||
|
assert resp.data["positions"][-1]["price"] == "100.00"
|
||||||
|
|
||||||
|
device.software_brand = "pretixPOS Android"
|
||||||
|
device.save()
|
||||||
|
resp = device_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||||
|
organizer.slug, event.slug
|
||||||
|
), format='json', data=res
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
assert resp.data["total"] == "500.00"
|
||||||
|
assert resp.data["positions"][0]["price"] == "100.00"
|
||||||
|
assert resp.data["positions"][-1]["price"] == "100.00"
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ TEST_ORDER_RES = {
|
|||||||
"url": "http://example.com/dummy/dummy/order/FOO/k24fiuwvu8kxz3y1/",
|
"url": "http://example.com/dummy/dummy/order/FOO/k24fiuwvu8kxz3y1/",
|
||||||
"payment_provider": "banktransfer",
|
"payment_provider": "banktransfer",
|
||||||
"total": "23.00",
|
"total": "23.00",
|
||||||
|
"tax_rounding_mode": "line",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"api_meta": {},
|
"api_meta": {},
|
||||||
"custom_followup_at": None,
|
"custom_followup_at": None,
|
||||||
|
|||||||
Reference in New Issue
Block a user