# This file is part of Shuup.
#
# Copyright (c) 2012-2021, Shuup Commerce Inc. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.
"""
Utilities for displaying prices correctly.
Contents:
* Class `PriceDisplayOptions` for storing the display options.
* Helper function `render_price_property` for rendering prices
correctly from Python code.
* Various filter classes for implementing Jinja2 filters.
"""
import django_jinja.library
import jinja2
from shuup.core.pricing import PriceDisplayOptions, Priceful
from shuup.core.templatetags.shuup_common import money, percent
from shuup.core.utils.price_cache import (
cache_many_price_info,
cache_price_info,
get_cached_price_info,
get_many_cached_price_info,
)
from shuup.core.utils.prices import convert_taxness
PRICED_CHILDREN_CACHE_KEY = "%s-%s_priced_children"
[docs]def render_price_property(request, item, priceful, property_name="price"):
"""
Render price property of a Priceful object.
:type request: django.http.HttpRequest
:type item: shuup.core.taxing.TaxableItem
:type priceful: shuup.core.pricing.Priceful
:type propert_name: str
:rtype: str
"""
options = PriceDisplayOptions.from_context({"request": request})
if options.hide_prices:
return ""
new_priceful = convert_taxness(request, item, priceful, options.include_taxes)
price_value = getattr(new_priceful, property_name)
return money(price_value)
class _ContextObject(object):
def __init__(self, name, property_name=None):
self.name = name
self.property_name = property_name or name
self._register()
class _ContextFilter(_ContextObject):
def _register(self):
django_jinja.library.filter(name=self.name, fn=jinja2.contextfilter(self))
@property
def cache_identifier(self):
return "price_filter_%s" % self.name
class _ContextFunction(_ContextObject):
def _register(self):
django_jinja.library.global_function(name=self.name, fn=jinja2.contextfunction(self))
[docs]class PriceDisplayFilter(_ContextFilter):
def __call__(self, context, item, quantity=1, include_taxes=None, allow_cache=True, supplier=None):
options = PriceDisplayOptions.from_context(context)
if options.hide_prices:
return ""
if include_taxes is None:
include_taxes = options.include_taxes
request = context.get("request")
price_info = (
get_cached_price_info(request, item, quantity, include_taxes=include_taxes, supplier=supplier)
if allow_cache
else None
)
if not price_info:
price_info = _get_priceful(request, item, quantity, supplier)
if not price_info:
return ""
price_info = convert_taxness(request, item, price_info, include_taxes)
if allow_cache:
cache_price_info(request, item, quantity, price_info, include_taxes=include_taxes, supplier=supplier)
return money(getattr(price_info, self.property_name))
[docs]class PricePropertyFilter(_ContextFilter):
def __call__(self, context, item, quantity=1, allow_cache=True, supplier=None):
request = context.get("request")
price_info = get_cached_price_info(request, item, quantity, supplier=supplier) if allow_cache else None
if not price_info:
price_info = _get_priceful(request, item, quantity, supplier)
if not price_info:
return ""
if allow_cache:
cache_price_info(request, item, quantity, price_info, supplier=supplier)
return getattr(price_info, self.property_name)
[docs]class PricePercentPropertyFilter(_ContextFilter):
def __call__(self, context, item, quantity=1, allow_cache=True, supplier=None):
request = context.get("request")
price_info = get_cached_price_info(request, item, quantity, supplier=supplier) if allow_cache else None
if not price_info:
price_info = _get_priceful(request, item, quantity, supplier)
if not price_info:
return ""
if allow_cache:
cache_price_info(request, item, quantity, price_info, supplier=supplier)
return percent(getattr(price_info, self.property_name))
[docs]class TotalPriceDisplayFilter(_ContextFilter):
def __call__(self, context, source, include_taxes=None):
"""
:type source: shuup.core.order_creator.OrderSource|
shuup.core.models.Order
"""
options = PriceDisplayOptions.from_context(context)
if options.hide_prices:
return ""
if include_taxes is None:
include_taxes = options.include_taxes
try:
if include_taxes is None:
total = source.total_price
elif include_taxes:
total = source.taxful_total_price
else:
total = source.taxless_total_price
except TypeError:
total = source.total_price
return money(total)
[docs]def get_priced_children_for_price_range(request, product, quantity, supplier):
product_queryset = product.variation_children.visible(shop=request.shop, customer=request.customer).order_by(
"shop_products__default_price_value", "pk"
)
if supplier:
product_queryset = product_queryset.filter(shop_products__suppliers=supplier)
low = product_queryset.first()
high = product_queryset.last()
if not (low or high):
return
return [
(low, _get_priceful(request, low, quantity, supplier)),
(high, _get_priceful(request, high, quantity, supplier)),
]
[docs]class PriceRangeDisplayFilter(_ContextFilter):
def __call__(self, context, product, quantity=1, allow_cache=True, supplier=None):
"""
:type product: shuup.core.models.Product
"""
options = PriceDisplayOptions.from_context(context)
if options.hide_prices:
return ("", "")
request = context.get("request")
priced_products = (
get_many_cached_price_info(
request, product, quantity, include_taxes=options.include_taxes, supplier=supplier
)
if allow_cache
else None
)
if priced_products is None:
priced_children_key = PRICED_CHILDREN_CACHE_KEY % (product.id, quantity)
priced_products = []
if hasattr(request, priced_children_key):
priced_children = getattr(request, priced_children_key)
else:
priced_children = get_priced_children_for_price_range(request, product, quantity, supplier) or [
(product, _get_priceful(request, product, quantity, supplier))
]
setattr(request, priced_children_key, priced_children)
for child_product, price_info in priced_children:
if not price_info:
continue
priceful = convert_taxness(request, child_product, price_info, options.include_taxes)
priced_products.append(priceful)
if priced_products and allow_cache:
cache_many_price_info(
request, product, quantity, priced_products, include_taxes=options.include_taxes, supplier=supplier
)
if not priced_products:
return ("", "")
return (money(priced_products[0].price), money(priced_products[-1].price))
def _get_priceful(request, item, quantity, supplier):
"""
Get priceful from given item.
If item has `get_price_info` method, it will be called with given
`request` and `quantity` as arguments, otherwise the item itself
should implement the `Priceful` interface.
:type request: django.http.HttpRequest
:param request: used as pricing context
:type item: shuup.core.taxing.TaxableItem
:type quantity: numbers.Number
:type supplier: shuup.core.models.Supplier
:param supplier: used to pass for pricing context
:rtype: shuup.core.pricing.Priceful|None
"""
if supplier:
# Passed from template and sometimes chosen by end user,
# but most of the time just decided by supplier strategy.
setattr(request, "supplier", supplier)
if hasattr(item, "supplier"):
# When item already has supplier fe. order and basket lines.
# This is always forced and supplier passed from template
# can't override this. Though developer should never pass
# supplier to template filter while getting price for source line.
setattr(request, "supplier", getattr(item, "supplier"))
if hasattr(item, "get_price_info"):
key_prefix = "%s-%s-" % (item.id, quantity)
if supplier:
key_prefix += "-%s" % (supplier.id)
price_key = "%s_get_priceful" % key_prefix
if hasattr(request, price_key):
return getattr(request, price_key)
price = item.get_price_info(request, quantity=quantity)
setattr(request, price_key, price)
return price
if hasattr(item, "get_total_cost"):
return item.get_total_cost(request.basket)
assert isinstance(item, Priceful)
return item