mirror of
https://github.com/pretix/pretix.git
synced 2026-05-08 15:44:02 +00:00
@@ -11,9 +11,10 @@ from django.utils.translation import pgettext_lazy, ugettext as _
|
||||
|
||||
from pretix.base.i18n import LazyLocaleException, language
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, Item, ItemVariation, Voucher,
|
||||
CartPosition, Event, InvoiceAddress, Item, ItemVariation, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice
|
||||
from pretix.base.services.async import ProfiledTask
|
||||
from pretix.base.services.locking import LockTimeoutException
|
||||
from pretix.base.services.pricing import get_price
|
||||
@@ -68,7 +69,7 @@ error_messages = {
|
||||
|
||||
class CartManager:
|
||||
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'price', 'voucher', 'quotas',
|
||||
'addon_to', 'subevent'))
|
||||
'addon_to', 'subevent', 'includes_tax'))
|
||||
RemoveOperation = namedtuple('RemoveOperation', ('position',))
|
||||
ExtendOperation = namedtuple('ExtendOperation', ('position', 'count', 'item', 'variation', 'price', 'voucher',
|
||||
'quotas', 'subevent'))
|
||||
@@ -78,7 +79,7 @@ class CartManager:
|
||||
AddOperation: 30
|
||||
}
|
||||
|
||||
def __init__(self, event: Event, cart_id: str):
|
||||
def __init__(self, event: Event, cart_id: str, invoice_address: InvoiceAddress=None):
|
||||
self.event = event
|
||||
self.cart_id = cart_id
|
||||
self.now_dt = now()
|
||||
@@ -89,6 +90,7 @@ class CartManager:
|
||||
self._subevents_cache = {}
|
||||
self._variations_cache = {}
|
||||
self._expiry = None
|
||||
self.invoice_address = invoice_address
|
||||
|
||||
@property
|
||||
def positions(self):
|
||||
@@ -213,8 +215,12 @@ class CartManager:
|
||||
|
||||
def _get_price(self, item: Item, variation: Optional[ItemVariation],
|
||||
voucher: Optional[Voucher], custom_price: Optional[Decimal],
|
||||
subevent: Optional[SubEvent]):
|
||||
return get_price(item, variation, voucher, custom_price, subevent, self.event.settings.display_net_prices)
|
||||
subevent: Optional[SubEvent], cp_is_net: bool=None):
|
||||
return get_price(
|
||||
item, variation, voucher, custom_price, subevent,
|
||||
custom_price_is_net=cp_is_net if cp_is_net is not None else self.event.settings.display_net_prices,
|
||||
invoice_address=self.invoice_address
|
||||
)
|
||||
|
||||
def extend_expired_positions(self):
|
||||
expired = self.positions.filter(expires__lte=self.now_dt).select_related(
|
||||
@@ -222,7 +228,12 @@ class CartManager:
|
||||
).prefetch_related('item__quotas', 'variation__quotas')
|
||||
err = None
|
||||
for cp in expired:
|
||||
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent)
|
||||
if not cp.includes_tax:
|
||||
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent,
|
||||
cp_is_net=True)
|
||||
price = TaxedPrice(net=price.net, gross=price.net, rate=0, tax=0, name='')
|
||||
else:
|
||||
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent)
|
||||
|
||||
quotas = list(cp.quotas)
|
||||
if not quotas:
|
||||
@@ -296,7 +307,7 @@ class CartManager:
|
||||
price = self._get_price(item, variation, voucher, i.get('price'), subevent)
|
||||
op = self.AddOperation(
|
||||
count=i['count'], item=item, variation=variation, price=price, voucher=voucher, quotas=quotas,
|
||||
addon_to=False, subevent=subevent
|
||||
addon_to=False, subevent=subevent, includes_tax=bool(price.rate)
|
||||
)
|
||||
self._check_item_constraints(op)
|
||||
operations.append(op)
|
||||
@@ -395,13 +406,13 @@ class CartManager:
|
||||
quota_diff[quota] += 1
|
||||
|
||||
if price_included[cp.pk].get(item.category_id):
|
||||
price = Decimal('0.00')
|
||||
price = TAXED_ZERO
|
||||
else:
|
||||
price = self._get_price(item, variation, None, None, cp.subevent)
|
||||
|
||||
op = self.AddOperation(
|
||||
count=1, item=item, variation=variation, price=price, voucher=None, quotas=quotas,
|
||||
addon_to=cp, subevent=cp.subevent
|
||||
addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate)
|
||||
)
|
||||
self._check_item_constraints(op)
|
||||
operations.append(op)
|
||||
@@ -557,15 +568,14 @@ class CartManager:
|
||||
for k in range(available_count):
|
||||
new_cart_positions.append(CartPosition(
|
||||
event=self.event, item=op.item, variation=op.variation,
|
||||
price=op.price, expires=self._expiry,
|
||||
cart_id=self.cart_id, voucher=op.voucher,
|
||||
addon_to=op.addon_to if op.addon_to else None,
|
||||
subevent=op.subevent
|
||||
price=op.price.gross, expires=self._expiry, cart_id=self.cart_id,
|
||||
voucher=op.voucher, addon_to=op.addon_to if op.addon_to else None,
|
||||
subevent=op.subevent, includes_tax=op.includes_tax
|
||||
))
|
||||
elif isinstance(op, self.ExtendOperation):
|
||||
if available_count == 1:
|
||||
op.position.expires = self._expiry
|
||||
op.position.price = op.price
|
||||
op.position.price = op.price.gross
|
||||
op.position.save()
|
||||
elif available_count == 0:
|
||||
op.position.delete()
|
||||
@@ -591,8 +601,34 @@ class CartManager:
|
||||
raise CartError(err)
|
||||
|
||||
|
||||
def update_tax_rates(event: Event, cart_id: str, invoice_address: InvoiceAddress):
|
||||
positions = CartPosition.objects.filter(
|
||||
cart_id=cart_id, event=event
|
||||
).select_related('item', 'item__tax_rule')
|
||||
totaldiff = Decimal('0.00')
|
||||
for pos in positions:
|
||||
if not pos.item.tax_rule:
|
||||
continue
|
||||
charge_tax = pos.item.tax_rule.tax_applicable(invoice_address)
|
||||
if pos.includes_tax and not charge_tax:
|
||||
price = pos.item.tax(pos.price, base_price_is='gross').net
|
||||
totaldiff += price - pos.price
|
||||
pos.price = price
|
||||
pos.includes_tax = False
|
||||
pos.save(update_fields=['price', 'includes_tax'])
|
||||
elif charge_tax and not pos.includes_tax:
|
||||
price = pos.item.tax(pos.price, base_price_is='net').gross
|
||||
totaldiff += price - pos.price
|
||||
pos.price = price
|
||||
pos.includes_tax = True
|
||||
pos.save(update_fields=['price', 'includes_tax'])
|
||||
|
||||
return totaldiff
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en') -> None:
|
||||
def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en',
|
||||
invoice_address: int=None) -> None:
|
||||
"""
|
||||
Adds a list of items to a user's cart.
|
||||
:param event: The event ID in question
|
||||
@@ -602,9 +638,17 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
|
||||
"""
|
||||
with language(locale):
|
||||
event = Event.objects.get(id=event)
|
||||
|
||||
ia = False
|
||||
if invoice_address:
|
||||
try:
|
||||
ia = InvoiceAddress.objects.get(pk=invoice_address)
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
pass
|
||||
|
||||
try:
|
||||
try:
|
||||
cm = CartManager(event=event, cart_id=cart_id)
|
||||
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia)
|
||||
cm.add_new_items(items)
|
||||
cm.commit()
|
||||
except LockTimeoutException:
|
||||
@@ -655,7 +699,8 @@ def clear_cart(self, event: int, cart_id: str=None, locale='en') -> None:
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None, locale='en') -> None:
|
||||
def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None, locale='en',
|
||||
invoice_address: int=None) -> None:
|
||||
"""
|
||||
Removes a list of items from a user's cart.
|
||||
:param event: The event ID in question
|
||||
@@ -664,9 +709,16 @@ def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None, loc
|
||||
"""
|
||||
with language(locale):
|
||||
event = Event.objects.get(id=event)
|
||||
|
||||
ia = False
|
||||
if invoice_address:
|
||||
try:
|
||||
ia = InvoiceAddress.objects.get(pk=invoice_address)
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
pass
|
||||
try:
|
||||
try:
|
||||
cm = CartManager(event=event, cart_id=cart_id)
|
||||
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia)
|
||||
cm.set_addons(addons)
|
||||
cm.commit()
|
||||
except LockTimeoutException:
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
import copy
|
||||
from decimal import Decimal
|
||||
import json
|
||||
import logging
|
||||
import urllib.error
|
||||
from datetime import date, timedelta
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
import vat_moss.exchange_rates
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import transaction
|
||||
from django.db.models import Count
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext, ugettext as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Invoice, InvoiceAddress, InvoiceLine, Order
|
||||
from pretix.base.models.tax import EU_CURRENCIES
|
||||
from pretix.base.services.async import TransactionAwareTask
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.base.signals import periodic_task
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers.database import rolledback_transaction
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def build_invoice(invoice: Invoice) -> Invoice:
|
||||
@@ -33,6 +47,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
invoice.payment_provider_text = str(payment).replace('\n', '<br />')
|
||||
|
||||
try:
|
||||
ia = invoice.order.invoice_address
|
||||
addr_template = pgettext("invoice", """{i.company}
|
||||
{i.name}
|
||||
{i.street}
|
||||
@@ -44,7 +59,31 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
).strip()
|
||||
if invoice.order.invoice_address.vat_id:
|
||||
invoice.invoice_to += "\n" + pgettext("invoice", "VAT-ID: %s") % invoice.order.invoice_address.vat_id
|
||||
|
||||
cc = str(invoice.order.invoice_address.country)
|
||||
|
||||
if cc in EU_CURRENCIES and EU_CURRENCIES[cc] != invoice.event.currency:
|
||||
invoice.foreign_currency_display = EU_CURRENCIES[cc]
|
||||
|
||||
if settings.FETCH_ECB_RATES:
|
||||
gs = GlobalSettingsObject()
|
||||
rates_date = gs.settings.get('ecb_rates_date', as_type=date)
|
||||
rates_dict = gs.settings.get('ecb_rates_dict', as_type=dict)
|
||||
convert = (
|
||||
rates_date and rates_dict and
|
||||
rates_date > (now() - timedelta(days=7)).date() and
|
||||
invoice.event.currency in rates_dict and
|
||||
invoice.foreign_currency_display in rates_dict
|
||||
)
|
||||
if convert:
|
||||
invoice.foreign_currency_rate = (
|
||||
Decimal(rates_dict[invoice.foreign_currency_display])
|
||||
/ Decimal(rates_dict[invoice.event.currency])
|
||||
).quantize(Decimal('0.0001'), ROUND_HALF_UP)
|
||||
invoice.foreign_currency_rate_date = rates_date
|
||||
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
ia = None
|
||||
invoice.invoice_to = ""
|
||||
|
||||
invoice.file = None
|
||||
@@ -52,10 +91,13 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
invoice.lines.all().delete()
|
||||
|
||||
positions = list(
|
||||
invoice.order.positions.select_related('addon_to', 'item', 'variation').annotate(
|
||||
invoice.order.positions.select_related('addon_to', 'item', 'tax_rule', 'variation').annotate(
|
||||
addon_c=Count('addons')
|
||||
)
|
||||
)
|
||||
|
||||
reverse_charge = False
|
||||
|
||||
positions.sort(key=lambda p: p.sort_key)
|
||||
for p in positions:
|
||||
if not invoice.event.settings.invoice_include_free and p.price == Decimal('0.00') and not p.addon_c:
|
||||
@@ -69,15 +111,29 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
InvoiceLine.objects.create(
|
||||
invoice=invoice, description=desc,
|
||||
gross_value=p.price, tax_value=p.tax_value,
|
||||
tax_rate=p.tax_rate
|
||||
tax_rate=p.tax_rate, tax_name=p.tax_rule.name if p.tax_rule else ''
|
||||
)
|
||||
|
||||
if p.tax_rule and p.tax_rule.is_reverse_charge(ia) and p.price and not p.tax_value:
|
||||
reverse_charge = True
|
||||
|
||||
if reverse_charge:
|
||||
if invoice.additional_text:
|
||||
invoice.additional_text += "<br /><br />"
|
||||
invoice.additional_text += pgettext(
|
||||
"invoice",
|
||||
"Reverse Charge: According to Article 194, 196 of Council Directive 2006/112/EEC, VAT liability "
|
||||
"rests with the service recipient."
|
||||
)
|
||||
invoice.save()
|
||||
|
||||
if invoice.order.payment_fee:
|
||||
InvoiceLine.objects.create(
|
||||
invoice=invoice,
|
||||
description=_('Payment via {method}').format(method=str(payment_provider.verbose_name)),
|
||||
gross_value=invoice.order.payment_fee, tax_value=invoice.order.payment_fee_tax_value,
|
||||
tax_rate=invoice.order.payment_fee_tax_rate
|
||||
tax_rate=invoice.order.payment_fee_tax_rate,
|
||||
tax_name=invoice.order.payment_fee_tax_rule.name if invoice.order.payment_fee_tax_rule else ''
|
||||
)
|
||||
|
||||
return invoice
|
||||
@@ -200,3 +256,20 @@ def build_preview_invoice_pdf(event):
|
||||
tax_rate=19
|
||||
)
|
||||
return event.invoice_renderer.generate(invoice)
|
||||
|
||||
|
||||
@receiver(signal=periodic_task)
|
||||
def fetch_ecb_rates(sender, **kwargs):
|
||||
if not settings.FETCH_ECB_RATES:
|
||||
return
|
||||
|
||||
gs = GlobalSettingsObject()
|
||||
if gs.settings.ecb_rates_date == now().strftime("%Y-%m-%d"):
|
||||
return
|
||||
|
||||
try:
|
||||
date, rates = vat_moss.exchange_rates.fetch()
|
||||
gs.settings.ecb_rates_date = date
|
||||
gs.settings.ecb_rates_dict = json.dumps(rates, cls=DjangoJSONEncoder)
|
||||
except urllib.error.URLError:
|
||||
logger.exception('Could not retrieve rates from ECB')
|
||||
|
||||
@@ -24,6 +24,7 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.orders import CachedTicket, InvoiceAddress
|
||||
from pretix.base.models.tax import TaxedPrice
|
||||
from pretix.base.payment import BasePaymentProvider
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.services.async import ProfiledTask
|
||||
@@ -236,7 +237,7 @@ def _check_date(event: Event, now_dt: datetime):
|
||||
raise OrderError(error_messages['ended'])
|
||||
|
||||
|
||||
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition]):
|
||||
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition], address: InvoiceAddress=None):
|
||||
err = None
|
||||
errargs = None
|
||||
_check_date(event, now_dt)
|
||||
@@ -293,7 +294,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
continue
|
||||
|
||||
price = get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent, custom_price_is_net=False,
|
||||
addon_to=cp.addon_to)
|
||||
addon_to=cp.addon_to, invoice_address=address)
|
||||
|
||||
if price is False or len(quotas) == 0:
|
||||
err = err or error_messages['unavailable']
|
||||
@@ -306,9 +307,10 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
cp.delete()
|
||||
continue
|
||||
|
||||
if price != cp.price and not (cp.item.free_price and cp.price > price):
|
||||
if price.gross != cp.price and not (cp.item.free_price and cp.price > price.gross):
|
||||
positions[i] = cp
|
||||
cp.price = price
|
||||
cp.price = price.gross
|
||||
cp.includes_tax = bool(price.rate)
|
||||
cp.save()
|
||||
err = err or error_messages['price_changed']
|
||||
continue
|
||||
@@ -389,20 +391,17 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
payment_provider=payment_provider.identifier,
|
||||
meta_info=json.dumps(meta_info or {}),
|
||||
)
|
||||
|
||||
if address:
|
||||
if address.order is not None:
|
||||
address.pk = None
|
||||
address.order = order
|
||||
address.save()
|
||||
|
||||
order._calculate_tax() # Might have changed due to new invoice address
|
||||
order.save()
|
||||
|
||||
OrderPosition.transform_cart_positions(positions, order)
|
||||
|
||||
if address is not None:
|
||||
try:
|
||||
addr = InvoiceAddress.objects.get(
|
||||
pk=address
|
||||
)
|
||||
if addr.order is not None:
|
||||
addr.pk = None
|
||||
addr.order = order
|
||||
addr.save()
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
pass
|
||||
|
||||
order.log_action('pretix.event.order.placed')
|
||||
|
||||
order_placed.send(event, order=order)
|
||||
@@ -417,6 +416,13 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
if not pprov:
|
||||
raise OrderError(error_messages['internal'])
|
||||
|
||||
addr = None
|
||||
if address is not None:
|
||||
try:
|
||||
addr = InvoiceAddress.objects.get(pk=address)
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
pass
|
||||
|
||||
with event.lock() as now_dt:
|
||||
positions = list(CartPosition.objects.filter(
|
||||
id__in=position_ids).select_related('item', 'variation', 'subevent'))
|
||||
@@ -424,9 +430,9 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
raise OrderError(error_messages['empty'])
|
||||
if len(position_ids) != len(positions):
|
||||
raise OrderError(error_messages['internal'])
|
||||
_check_positions(event, now_dt, positions)
|
||||
_check_positions(event, now_dt, positions, address=addr)
|
||||
order = _create_order(event, email, positions, now_dt, pprov,
|
||||
locale=locale, address=address, meta_info=meta_info)
|
||||
locale=locale, address=addr, meta_info=meta_info)
|
||||
|
||||
if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
|
||||
if not order.invoices.exists():
|
||||
@@ -597,7 +603,8 @@ class OrderChangeManager:
|
||||
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
|
||||
raise OrderError(self.error_messages['product_without_variation'])
|
||||
|
||||
price = get_price(item, variation, voucher=position.voucher, subevent=position.subevent)
|
||||
price = get_price(item, variation, voucher=position.voucher, subevent=position.subevent,
|
||||
invoice_address=self._invoice_address)
|
||||
|
||||
if price is None: # NOQA
|
||||
raise OrderError(self.error_messages['product_invalid'])
|
||||
@@ -607,16 +614,17 @@ class OrderChangeManager:
|
||||
if not new_quotas:
|
||||
raise OrderError(self.error_messages['quota_missing'])
|
||||
|
||||
if self.order.event.settings.invoice_include_free or price != Decimal('0.00') or position.price != Decimal('0.00'):
|
||||
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00') or position.price != Decimal('0.00'):
|
||||
self._invoice_dirty = True
|
||||
|
||||
self._totaldiff = price - position.price
|
||||
self._totaldiff += price.gross - position.price
|
||||
self._quotadiff.update(new_quotas)
|
||||
self._quotadiff.subtract(position.quotas)
|
||||
self._operations.append(self.ItemOperation(position, item, variation, price))
|
||||
|
||||
def change_subevent(self, position: OrderPosition, subevent: SubEvent):
|
||||
price = get_price(position.item, position.variation, voucher=position.voucher, subevent=subevent)
|
||||
price = get_price(position.item, position.variation, voucher=position.voucher, subevent=subevent,
|
||||
invoice_address=self._invoice_address)
|
||||
|
||||
if price is None: # NOQA
|
||||
raise OrderError(self.error_messages['product_invalid'])
|
||||
@@ -626,24 +634,48 @@ class OrderChangeManager:
|
||||
if not new_quotas:
|
||||
raise OrderError(self.error_messages['quota_missing'])
|
||||
|
||||
if self.order.event.settings.invoice_include_free or price != Decimal('0.00') or position.price != Decimal('0.00'):
|
||||
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00') or position.price != Decimal('0.00'):
|
||||
self._invoice_dirty = True
|
||||
|
||||
self._totaldiff = price - position.price
|
||||
self._totaldiff += price.gross - position.price
|
||||
self._quotadiff.update(new_quotas)
|
||||
self._quotadiff.subtract(position.quotas)
|
||||
self._operations.append(self.SubeventOperation(position, subevent, price))
|
||||
|
||||
def change_price(self, position: OrderPosition, price: Decimal):
|
||||
self._totaldiff = price - position.price
|
||||
price = position.item.tax(price)
|
||||
|
||||
if self.order.event.settings.invoice_include_free or price != Decimal('0.00') or position.price != Decimal('0.00'):
|
||||
self._totaldiff += price.gross - position.price
|
||||
|
||||
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00') or position.price != Decimal('0.00'):
|
||||
self._invoice_dirty = True
|
||||
|
||||
self._operations.append(self.PriceOperation(position, price))
|
||||
|
||||
def recalculate_taxes(self):
|
||||
positions = self.order.positions.select_related('item', 'item__tax_rule')
|
||||
ia = self._invoice_address
|
||||
for pos in positions:
|
||||
if not pos.item.tax_rule:
|
||||
continue
|
||||
if not pos.price:
|
||||
continue
|
||||
|
||||
charge_tax = pos.item.tax_rule.tax_applicable(ia)
|
||||
if pos.tax_value and not charge_tax:
|
||||
net_price = pos.price - pos.tax_value
|
||||
price = TaxedPrice(gross=net_price, net=net_price, tax=Decimal('0.00'), rate=Decimal('0.00'), name='')
|
||||
if price.gross != pos.price:
|
||||
self._totaldiff += price.gross - pos.price
|
||||
self._operations.append(self.PriceOperation(pos, price))
|
||||
elif charge_tax and not pos.tax_value:
|
||||
price = pos.item.tax(pos.price, base_price_is='net')
|
||||
if price.gross != pos.price:
|
||||
self._totaldiff += price.gross - pos.price
|
||||
self._operations.append(self.PriceOperation(pos, price))
|
||||
|
||||
def cancel(self, position: OrderPosition):
|
||||
self._totaldiff = -position.price
|
||||
self._totaldiff += -position.price
|
||||
self._quotadiff.subtract(position.quotas)
|
||||
self._operations.append(self.CancelOperation(position))
|
||||
|
||||
@@ -653,7 +685,13 @@ class OrderChangeManager:
|
||||
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: Order = None,
|
||||
subevent: SubEvent = None):
|
||||
if price is None:
|
||||
price = get_price(item, variation, subevent=subevent)
|
||||
price = get_price(item, variation, subevent=subevent, invoice_address=self._invoice_address)
|
||||
else:
|
||||
if item.tax_rule.tax_applicable(self._invoice_address):
|
||||
price = item.tax(price, base_price_is='gross')
|
||||
else:
|
||||
price = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'), rate=Decimal('0.00'), name='')
|
||||
|
||||
if price is None:
|
||||
raise OrderError(self.error_messages['product_invalid'])
|
||||
if not addon_to and item.category and item.category.is_addon:
|
||||
@@ -669,10 +707,10 @@ class OrderChangeManager:
|
||||
if not new_quotas:
|
||||
raise OrderError(self.error_messages['quota_missing'])
|
||||
|
||||
if self.order.event.settings.invoice_include_free or price != Decimal('0.00'):
|
||||
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00'):
|
||||
self._invoice_dirty = True
|
||||
|
||||
self._totaldiff = price
|
||||
self._totaldiff += price.gross
|
||||
self._quotadiff.update(new_quotas)
|
||||
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent))
|
||||
|
||||
@@ -712,12 +750,14 @@ class OrderChangeManager:
|
||||
'new_variation': op.variation.pk if op.variation else None,
|
||||
'old_price': op.position.price,
|
||||
'addon_to': op.position.addon_to_id,
|
||||
'new_price': op.price
|
||||
'new_price': op.price.gross
|
||||
})
|
||||
op.position.item = op.item
|
||||
op.position.variation = op.variation
|
||||
op.position.price = op.price
|
||||
op.position._calculate_tax()
|
||||
op.position.price = op.price.gross
|
||||
op.position.tax_rate = op.price.rate
|
||||
op.position.tax_value = op.price.tax
|
||||
op.position.tax_rule = op.item.tax_rule
|
||||
op.position.save()
|
||||
elif isinstance(op, self.SubeventOperation):
|
||||
self.order.log_action('pretix.event.order.changed.subevent', user=self.user, data={
|
||||
@@ -726,11 +766,13 @@ class OrderChangeManager:
|
||||
'old_subevent': op.position.subevent.pk,
|
||||
'new_subevent': op.subevent.pk,
|
||||
'old_price': op.position.price,
|
||||
'new_price': op.price
|
||||
'new_price': op.price.gross
|
||||
})
|
||||
op.position.subevent = op.subevent
|
||||
op.position.price = op.price
|
||||
op.position._calculate_tax()
|
||||
op.position.price = op.price.gross
|
||||
op.position.tax_rate = op.price.rate
|
||||
op.position.tax_value = op.price.tax
|
||||
op.position.tax_rule = op.position.item.tax_rule
|
||||
op.position.save()
|
||||
elif isinstance(op, self.PriceOperation):
|
||||
self.order.log_action('pretix.event.order.changed.price', user=self.user, data={
|
||||
@@ -738,10 +780,12 @@ class OrderChangeManager:
|
||||
'positionid': op.position.positionid,
|
||||
'old_price': op.position.price,
|
||||
'addon_to': op.position.addon_to_id,
|
||||
'new_price': op.price
|
||||
'new_price': op.price.gross
|
||||
})
|
||||
op.position.price = op.price
|
||||
op.position._calculate_tax()
|
||||
op.position.price = op.price.gross
|
||||
op.position.tax_rate = op.price.rate
|
||||
op.position.tax_value = op.price.tax
|
||||
op.position.tax_rule = op.position.item.tax_rule
|
||||
op.position.save()
|
||||
elif isinstance(op, self.CancelOperation):
|
||||
for opa in op.position.addons.all():
|
||||
@@ -765,7 +809,8 @@ class OrderChangeManager:
|
||||
elif isinstance(op, self.AddOperation):
|
||||
pos = OrderPosition.objects.create(
|
||||
item=op.item, variation=op.variation, addon_to=op.addon_to,
|
||||
price=op.price, order=self.order,
|
||||
price=op.price.gross, order=self.order, tax_rate=op.price.rate,
|
||||
tax_value=op.price.tax, tax_rule=op.item.tax_rule,
|
||||
positionid=nextposid, subevent=op.subevent
|
||||
)
|
||||
nextposid += 1
|
||||
@@ -774,7 +819,7 @@ class OrderChangeManager:
|
||||
'item': op.item.pk,
|
||||
'variation': op.variation.pk if op.variation else None,
|
||||
'addon_to': op.addon_to.pk if op.addon_to else None,
|
||||
'price': op.price,
|
||||
'price': op.price.gross,
|
||||
'positionid': pos.positionid,
|
||||
'subevent': op.subevent.pk if op.subevent else None,
|
||||
})
|
||||
@@ -802,6 +847,13 @@ class OrderChangeManager:
|
||||
if cancels == self.order.positions.count():
|
||||
raise OrderError(self.error_messages['complete_cancel'])
|
||||
|
||||
@property
|
||||
def _invoice_address(self):
|
||||
try:
|
||||
return self.order.invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
return None
|
||||
|
||||
def _notify_user(self):
|
||||
with language(self.order.locale):
|
||||
try:
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import (
|
||||
AbstractPosition, Item, ItemAddOn, ItemVariation, Voucher,
|
||||
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||
|
||||
|
||||
def get_price(item: Item, variation: ItemVariation = None,
|
||||
voucher: Voucher = None, custom_price: Decimal = None,
|
||||
subevent: SubEvent = None, custom_price_is_net: bool = False,
|
||||
addon_to: AbstractPosition = None):
|
||||
addon_to: AbstractPosition = None, invoice_address: InvoiceAddress = None) -> TaxedPrice:
|
||||
if addon_to:
|
||||
try:
|
||||
iao = addon_to.item.addons.get(addon_category_id=item.category_id)
|
||||
if iao.price_included:
|
||||
return Decimal('0.00')
|
||||
return TAXED_ZERO
|
||||
except ItemAddOn.DoesNotExist:
|
||||
pass
|
||||
|
||||
@@ -32,13 +32,31 @@ def get_price(item: Item, variation: ItemVariation = None,
|
||||
if voucher:
|
||||
price = voucher.calculate_price(price)
|
||||
|
||||
if item.tax_rule:
|
||||
tax_rule = item.tax_rule
|
||||
else:
|
||||
tax_rule = TaxRule(
|
||||
name='',
|
||||
rate=Decimal('0.00'),
|
||||
price_includes_tax=True,
|
||||
eu_reverse_charge=False,
|
||||
)
|
||||
price = tax_rule.tax(price)
|
||||
|
||||
if item.free_price and custom_price is not None and custom_price != "":
|
||||
if not isinstance(custom_price, Decimal):
|
||||
custom_price = Decimal(str(custom_price).replace(",", "."))
|
||||
if custom_price > 100000000:
|
||||
raise ValueError('price_too_high')
|
||||
if custom_price_is_net:
|
||||
custom_price = round_decimal(custom_price * (100 + item.tax_rate) / 100)
|
||||
price = max(custom_price, price)
|
||||
price = tax_rule.tax(max(custom_price, price.net), base_price_is='net')
|
||||
else:
|
||||
price = tax_rule.tax(max(custom_price, price.gross), base_price_is='gross')
|
||||
|
||||
if invoice_address and not tax_rule.tax_applicable(invoice_address):
|
||||
price.tax = Decimal('0.00')
|
||||
price.rate = Decimal('0.00')
|
||||
price.gross = price.net
|
||||
price.name = ''
|
||||
|
||||
return price
|
||||
|
||||
Reference in New Issue
Block a user