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_provider string **DEPRECATED AND INACCURATE** Payment provider used for 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
|
||||
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.
|
||||
@@ -142,6 +143,10 @@ plugin_data object Additional data
|
||||
|
||||
The ``plugin_data`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2025.8
|
||||
|
||||
The ``tax_rounding_mode`` attribute has been added.
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
Order position resource
|
||||
@@ -349,6 +354,7 @@ List of all orders
|
||||
"payment_provider": "banktransfer",
|
||||
"fees": [],
|
||||
"total": "23.00",
|
||||
"tax_rounding_mode": "line",
|
||||
"comment": "",
|
||||
"custom_followup_at": null,
|
||||
"checkin_attention": false,
|
||||
@@ -590,6 +596,7 @@ Fetching individual orders
|
||||
"payment_provider": "banktransfer",
|
||||
"fees": [],
|
||||
"total": "23.00",
|
||||
"tax_rounding_mode": "line",
|
||||
"comment": "",
|
||||
"api_meta": {},
|
||||
"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*
|
||||
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.
|
||||
* ``tax_rounding_mode`` (optional)
|
||||
* ``comment`` (optional)
|
||||
* ``custom_followup_at`` (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.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
|
||||
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
|
||||
ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule, Voucher,
|
||||
CachedFile, Checkin, Customer, Device, Invoice, InvoiceAddress,
|
||||
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
|
||||
QuestionAnswer, ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule,
|
||||
Voucher,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
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.locking import LOCK_TRUST_WINDOW, lock_objects
|
||||
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.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.helpers.countries import CachedCountries
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
@@ -781,14 +785,15 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
list_serializer_class = OrderListSerializer
|
||||
fields = (
|
||||
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
||||
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date', 'plugin_data',
|
||||
'payment_provider', 'fees', 'total', 'tax_rounding_mode', 'comment', 'custom_followup_at', 'invoice_address',
|
||||
'positions', 'downloads', 'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds',
|
||||
'require_approval', 'sales_channel', 'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date',
|
||||
'plugin_data',
|
||||
)
|
||||
read_only_fields = (
|
||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'positions', 'downloads', 'customer',
|
||||
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'cancellation_date'
|
||||
'payment_provider', 'fees', 'total', 'tax_rounding_mode', 'positions', 'downloads', 'customer',
|
||||
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'cancellation_date',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -1103,6 +1108,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
)
|
||||
tax_rounding_mode = serializers.ChoiceField(choices=ROUNDING_MODES, allow_null=True, required=False,)
|
||||
locale = serializers.ChoiceField(choices=[], required=False, allow_null=True)
|
||||
|
||||
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',
|
||||
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
||||
'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):
|
||||
if pp is None:
|
||||
@@ -1641,7 +1647,31 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
else:
|
||||
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:
|
||||
order.fees = fees
|
||||
order.positions = pos_map.values()
|
||||
|
||||
@@ -344,6 +344,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
ctx['auth'] = self.request.auth
|
||||
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -77,7 +77,6 @@ from pretix.control.forms import (
|
||||
)
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
|
||||
|
||||
ROUNDING_MODES = (
|
||||
('line', _('Rounding every line individually')),
|
||||
('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',
|
||||
'tax_rounding_mode': 'line',
|
||||
'comment': '',
|
||||
'api_meta': {},
|
||||
"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)
|
||||
assert m.linked_orderposition == o.positions.first()
|
||||
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/",
|
||||
"payment_provider": "banktransfer",
|
||||
"total": "23.00",
|
||||
"tax_rounding_mode": "line",
|
||||
"comment": "",
|
||||
"api_meta": {},
|
||||
"custom_followup_at": None,
|
||||
|
||||
Reference in New Issue
Block a user