shop.modules ============ .. py:module:: shop.modules Submodules ---------- .. toctree:: :maxdepth: 1 /viur/shop/modules/abstract/index /viur/shop/modules/address/index /viur/shop/modules/api/index /viur/shop/modules/cart/index /viur/shop/modules/discount/index /viur/shop/modules/discount_condition/index /viur/shop/modules/order/index /viur/shop/modules/shipping/index /viur/shop/modules/shipping_config/index /viur/shop/modules/vat_rate/index Classes ------- .. autoapisummary:: shop.modules.Address shop.modules.Api shop.modules.Cart shop.modules.Discount shop.modules.DiscountCondition shop.modules.Order shop.modules.Shipping shop.modules.ShippingConfig shop.modules.VatRate Package Contents ---------------- .. py:class:: Address Bases: :py:obj:`shop.modules.abstract.ShopModuleAbstract`, :py:obj:`viur.core.prototypes.List` .. py:attribute:: moduleName :value: 'address' .. py:attribute:: kindName :value: '{{viur_shop_modulename}}_address' .. py:attribute:: reference_user_created_skeletons_in_session :value: True .. py:attribute:: default_order :type: viur.core.prototypes.skelmodule.DEFAULT_ORDER_TYPE .. py:method:: adminInfo() .. py:method:: canAdd() .. py:method:: canEdit(skel) .. py:method:: listFilter(query) .. py:method:: onAdded(skel) .. py:method:: onEdited(skel) .. py:method:: _disable_old_default(skel) Disable old is_default .. py:class:: Api(moduleName = None, modulePath = None, shop = None, *args, **kwargs) Bases: :py:obj:`viur.shop.modules.abstract.ShopModuleAbstract` Abstract Class for all viur-shop sub/nested modules. The implementations should set `moduleName` as class variable, so the final module name for routing it not affected by the name of custom classes. .. py:property:: json_renderer :type: viur.core.render.json.default.DefaultRender .. py:method:: article_view(article_key, parent_cart_key) View an article in the cart .. py:method:: article_add(*, article_key, quantity = 1, quantity_mode = QuantityMode.REPLACE, parent_cart_key = SENTINEL, **kwargs) Add an article to the cart :param article_key: Key of the article to add. :param quantity: Quantity of the article to add. :param quantity_mode: Behavior of the quantity: absolute or relative valuation :param parent_cart_key: Key of the (sub) cart (node) to which this leaf will be added as a child. Use "BASKET" as key to use the basket of the current session. .. py:method:: article_update(*, article_key, quantity, quantity_mode = QuantityMode.REPLACE, parent_cart_key = SENTINEL, **kwargs) Update an existing article in the cart :param article_key: Key of the article to update. Note: This is not the key of the leaf skel! :param quantity: Quantity of the article to update. :param quantity_mode: Behavior of the quantity: absolute or relative valuation :param parent_cart_key: Optional. Key of the (sub) cart (node) to which this leaf will be moved to as a child. Use "BASKET" as key to use the basket of the current session. .. py:method:: article_remove(*, article_key, parent_cart_key, **kwargs) Remove an article from the cart .. py:method:: article_move(*, article_key, parent_cart_key, new_parent_cart_key) Move an article inside a cart .. py:method:: cart_add(*, parent_cart_key = None, cart_type = SENTINEL, name = SENTINEL, customer_comment = SENTINEL, shipping_address_key = SENTINEL, shipping_key = SENTINEL, discount_key = SENTINEL, **kwargs) Add a new cart node :param parent_cart_key: Key of the parent cart :param cart_type: Type of the cart node, see :class:`CartType` :param name: Optional. Name of the cart node :param customer_comment: Optional. Comment to this node, by customer. :param shipping_address_key: Optional. Key of the address :param shipping_key: Optional. Key of the shipping :param discount_key: Optional. Key of the discount Returns: The created cart node skel .. py:method:: cart_update(*, cart_key, cart_type = SENTINEL, name = SENTINEL, customer_comment = SENTINEL, shipping_address_key = SENTINEL, shipping_key = SENTINEL, discount_key = SENTINEL, **kwargs) Update an existing cart node :param cart_key: Key of the cart node to be updated :param cart_type: Type of the cart node, see :class:`CartType` :param name: Optional. Name of the cart node :param customer_comment: Optional. Comment to this node, by customer. :param shipping_address_key: Optional. Key of the address :param shipping_key: Optional. Key of the shipping :param discount_key: Optional. Key of the discount Returns: The updated cart node skel .. py:method:: cart_remove(*, cart_key) Remove a cart node. Removes itself and all children :param cart_key: Key of the cart node to be removed .. py:method:: cart_clear(*, cart_key = SENTINEL, keep_sub_carts = False) Remove direct or all children :param cart_key: Key of the (sub) cart (node) from which the children should be removed. Use "BASKET" as key to use the basket of the current session. :param keep_sub_carts: Keep child nodes, remove only leafs .. py:method:: basket_list() List the children of the basket (the cart stored in the session) :raises errors.PreconditionFailed: If no basket created yet for this session .. py:method:: basket_view(*, create_if_missing = False) View the basket (the cart stored in the session) itself :param create_if_missing: Create the basket if not already created for this session :raises errors.PreconditionFailed: If no basket created yet for this session (and it should not be created) See also :meth:`basket_view` to view any cart. .. py:method:: cart_list(cart_key = None) List root nodes or children of a cart If a cart key is provided, the direct children (nodes and leafs) will be returned. Otherwise (without a key), the root nodes will be returned. :param cart_key: list direct children (nodes and leafs) of this parent node .. py:method:: cart_view(cart_key) View a cart itself See also :meth:`basket_view` to view the current basket. .. py:method:: order_add(*, cart_key, payment_provider = SENTINEL, billing_address_key = SENTINEL, customer_key = SENTINEL, state_ordered = SENTINEL, state_paid = SENTINEL, state_rts = SENTINEL, **kwargs) .. py:method:: order_update(*, order_key, payment_provider = SENTINEL, billing_address_key = SENTINEL, customer_key = SENTINEL, state_ordered = SENTINEL, state_paid = SENTINEL, state_rts = SENTINEL, **kwargs) .. py:method:: order_remove(*, order_key) .. py:method:: order_list(**kwargs) List the orders of the current user .. py:method:: order_view(order_key = 'SESSION') View an order :param order_key: Key of the order to view. Use "SESSION" as key to view the order of the current session .. py:method:: _get_order_view_result(order_skel) .. py:method:: discount_add(*, code = None, discount_key = None) parameter code xor discount_key: str | db.Key Sucht nach Rabatt mit dem code xor key, je nach Typ (Artikel/Warenkorb) suche ...ende parent_node oder erzeuge eine und setze dort die discount Relation. .. py:method:: discount_remove(*, discount_key) .. py:method:: shipping_list(cart_key) Lists available shipping options for a (sub)cart :param cart_key: Key of the parent cart :returns: list of :class:`ShippingSkel` `SkeletonInstance`s .. py:method:: _normalize_external_key(external_key, parameter_name, can_be_None = False) Convert urlsafe key to db.Key and raise an error on invalid in key. .. py:class:: Cart(moduleName = None, modulePath = None, shop = None, *args, **kwargs) Bases: :py:obj:`viur.shop.modules.abstract.ShopModuleAbstract`, :py:obj:`viur.core.prototypes.Tree` Abstract Class for all viur-shop sub/nested modules. The implementations should set `moduleName` as class variable, so the final module name for routing it not affected by the name of custom classes. .. py:attribute:: moduleName :value: 'cart' .. py:attribute:: nodeSkelCls .. py:attribute:: leafSkelCls .. py:method:: adminInfo() .. py:method:: baseSkel(skelType, sub_skel = None, *args, **kwargs) Extend default baseSkel() by sub_skel parameter .. py:method:: canView(skelType, skel) .. py:property:: current_session_cart_key :type: viur.core.db.Key | None .. py:method:: get_current_session_cart_key(*, create_if_missing = False) .. py:property:: current_session_cart :type: viur.shop.types.SkeletonInstance_T[shop.skeletons.cart.CartNodeSkel] .. py:method:: _ensure_current_session_cart() .. py:method:: detach_session_cart() Unlink the current cart from the session (and the user's basket). The cart entity itself is kept. Tolerates a session that has no ``session_cart_key`` (anymore) instead of raising a ``KeyError``. :return: The key of the detached cart, or ``None`` if none was set. .. py:method:: _set_basket_txn(user_key, basket_key) :staticmethod: .. py:method:: get_available_root_nodes(*args, **kwargs) .. py:attribute:: getAvailableRootNodes .. py:method:: listRootNodes(*args, **kwargs) Renders a list of all available repositories for the current user using the modules default renderer. :returns: The rendered representation of the available root-nodes. .. py:method:: is_valid_node(node_key, root_node = False) is this a valid node key for the user? :param node_key: Key of node to check :param root_node: Must this be a root node, or is any node okay? .. py:method:: get_children(parent_cart_key, **filters) .. py:method:: get_children_from_cache(parent_cart_key) .. py:method:: clear_children_cache() .. py:method:: cart_get(cart_key, skel_type) .. py:method:: get_article(article_key, parent_cart_key, *, must_be_listed = True) .. py:method:: add_or_update_article(article_key, parent_cart_key, *, quantity, quantity_mode, **kwargs) .. py:method:: copy_article_values(article_skel, skel) Copy values from the article to the cart leaf .. py:method:: move_article(article_key, parent_cart_key, new_parent_cart_key) .. py:method:: cart_add(*, parent_cart_key = None, cart_type = None, name = SENTINEL, customer_comment = SENTINEL, shipping_address_key = SENTINEL, shipping_key = SENTINEL, discount_key = SENTINEL, **kwargs) .. py:method:: cart_update(cart_key, *, parent_cart_key = SENTINEL, cart_type = None, name = SENTINEL, customer_comment = SENTINEL, shipping_address_key = SENTINEL, shipping_key = SENTINEL, discount_key = SENTINEL, **kwargs) .. py:method:: _cart_set_values(skel, *, parent_cart_key = SENTINEL, cart_type = None, name = SENTINEL, customer_comment = SENTINEL, shipping_address_key = SENTINEL, shipping_key = SENTINEL, discount_key = SENTINEL) .. py:method:: cart_remove(cart_key) Remove a cart node with its entire subtree. The subtree is deleted bottom-up and **before** the node itself: leafs first, then sub-nodes, the given node last. This order keeps the tree consistent at any point in time -- if the process crashes in between, no children can be left behind whose ``parententry`` points to an already deleted node (orphaned entries). A repeated call simply continues the work (idempotent). Frozen carts belong to an order and cannot be removed. This also checks upfront whether the node is locked by a ``RelationalConsistency.PreventDeletion`` relation (e.g. an order referencing an in-progress checkout cart that has not been frozen yet) and fails before touching any child -- deleting the children first and finding out only at ``skel.delete()`` that the node itself is locked would leave the order pointing at an emptied-out cart. :param cart_key: Key of the cart node to remove. :raises errors.NotFound: If the cart node does not exist. :raises errors.Forbidden: If the cart node is frozen. :raises errors.Locked: If the cart node is referenced by a PreventDeletion relation (e.g. an order). .. py:method:: _delete_children(parent_cart_key) Delete all children of a cart node bottom-up and synchronously. Leafs of *parent_cart_key* are deleted first, then each sub-node is processed recursively (its own children before the sub-node itself). Deliberately does not call the inherited :meth:`deleteRecursive` of the Tree prototype: that method is ``@CallDeferred`` and only deletes the children, while the node itself is deleted synchronously by the caller right away (see :meth:`Tree.delete` / :meth:`cart_remove`). If that deferred task gets lost (queue purge, crash, or the task being pinned to an App Engine version that no longer exists), the node is gone but its children survive it forever -- orphaned entries whose broken ``parententry`` chain crashes price computations and ``update_relations`` tasks later. Cart trees are small (a handful of nodes/leafs), so there is no need to defer this work at all; running it synchronously, bottom-up and before the node itself is deleted removes the race entirely instead of just narrowing it. :param parent_cart_key: Key of the cart node whose subtree gets deleted. .. py:method:: cart_clear(cart_key, *, keep_sub_carts = False) .. py:method:: additional_add_or_update_article(skel, /, **kwargs) Hook method called by :meth:`add_or_update_article` before the skeleton is saved. This method can be overridden in a subclass to implement additional API fields or make further modifications to the cart skeleton (`skel`). By default, it raises an exception if unexpected arguments (``kwargs``) are provided and returns the unchanged `skel` object. :param skel: The current instance of the cart item skeleton. :param kwargs: Additional optional arguments for extended implementations. :raises TooManyArgumentsException: If unexpected arguments are passed in ``kwargs``. :return: The (potentially modified) cart item skeleton. .. py:method:: additional_cart_add(skel, /, **kwargs) Hook method called by :meth:`cart_add` before the skeleton is saved. This method can be overridden in a subclass to implement additional API fields or make further modifications to the cart skeleton (`skel`). By default, it raises an exception if unexpected arguments (``kwargs``) are provided and returns the unchanged `skel` object. :param skel: The current instance of the cart skeleton. :param kwargs: Additional optional arguments for extended implementations. :raises TooManyArgumentsException: If unexpected arguments are passed in ``kwargs``. :return: The (potentially modified) cart skeleton. .. py:method:: additional_cart_update(skel, /, **kwargs) Hook method called by :meth:`cart_update` before the skeleton is saved. This method can be overridden in a subclass to implement additional API fields or make further modifications to the cart skeleton (`skel`). By default, it raises an exception if unexpected arguments (``kwargs``) are provided and returns the unchanged `skel` object. :param skel: The current instance of the cart skeleton. :param kwargs: Additional optional arguments for extended implementations. :raises TooManyArgumentsException: If unexpected arguments are passed in ``kwargs``. :return: The (potentially modified) cart skeleton. .. py:method:: freeze_cart(cart_key) Freeze (lock) cart values and children items. :param cart_key: Key of the (sub-)cart skeleton. :return: The frozen CartNode skeleton. .. py:method:: freeze_leaf(leaf_skel) .. py:method:: get_discount_for_leaf(leaf_key_or_skel) Collect the discounts of all cart nodes above the given leaf. Walks the ``parententry`` chain from the leaf up to the root node and collects the ``discount`` relation of every node on the way. The walk is tolerant against broken tree data: if a parent node does not exist anymore (orphaned entry) or the chain contains a cycle, the walk stops at the broken link with a warning and the discounts collected so far are returned. This keeps computed bones (e.g. the price) usable on orphaned entries instead of raising and thereby crashing deferred tasks (like ``update_relations``) forever. :param leaf_key_or_skel: Key or skeleton of the cart leaf to start from. :return: The discount ref-skels found on the ancestor nodes. .. py:method:: add_new_parent(leaf_skel, **kwargs) .. py:method:: get_cached_cart_skel(key) Read a cart node skeleton with request-local caching. :param key: Key of the cart node to read. :return: The cached or freshly read node skeleton or ``None`` if the node does not exist (anymore). Missing nodes are not cached, so a later call re-checks the datastore. .. py:method:: get_closest_node(start, condition = lambda skel: True) Walk the ``parententry`` chain upwards and return the first ancestor node that satisfies *condition*. The walk stops and returns ``None`` when * the root node is reached without a match, * the current entry has no ``parententry`` (detached entry), * a parent node does not exist anymore (orphaned entry) or * the ``parententry`` chain contains a cycle. The latter cases are broken tree states which must not crash callers like the price computation running inside deferred tasks. :param start: Leaf or node skeleton to start from (itself excluded). :param condition: Predicate evaluated for each ancestor node. :return: The closest matching ancestor node or ``None``. .. py:class:: Discount Bases: :py:obj:`shop.modules.abstract.ShopModuleAbstract`, :py:obj:`viur.core.prototypes.List` .. py:attribute:: moduleName :value: 'discount' .. py:attribute:: kindName :value: '{{viur_shop_modulename}}_discount' .. py:method:: adminInfo() .. py:method:: search(code = None, discount_key = None) .. py:method:: apply(code = None, discount_key = None) .. py:method:: can_apply(skel, *, cart_key = None, article_skel = None, code = None, context = DiscountValidationContext.NORMAL) .. py:property:: current_automatically_discounts :type: list[viur.shop.types.SkeletonInstance_T[shop.skeletons.DiscountSkel]] .. py:method:: remove(discount_key) .. py:class:: DiscountCondition Bases: :py:obj:`shop.modules.abstract.ShopModuleAbstract`, :py:obj:`viur.core.prototypes.List` .. py:attribute:: moduleName :value: 'discount_condition' .. py:attribute:: kindName :value: '{{viur_shop_modulename}}_discount_condition' .. py:method:: adminInfo() .. py:method:: canEdit(skel) .. py:method:: editSkel(*args, **kwargs) .. py:method:: onAdd(skel) .. py:method:: onEdit(skel) .. py:method:: onClone(skel, src_skel) .. py:method:: onAdded(skel) .. py:method:: onEdited(skel) .. py:method:: onCloned(skel, src_skel) .. py:method:: on_change(skel, event) .. py:method:: on_changed(skel, event) .. py:method:: generate_subcodes(parent_key, prefix, amount) Generate subcodes for a parent individual code. .. py:method:: get_skel(key) :classmethod: .. py:method:: get_by_code(code = None) .. py:method:: get_discounts_from_cart(cart_key) .. py:method:: mark_discount_used(order_skel, payment, *args, **kwargs) :staticmethod: Increase quantity_used on discount of an ordered cart .. py:class:: Order Bases: :py:obj:`shop.modules.abstract.ShopModuleAbstract`, :py:obj:`viur.core.prototypes.List` .. py:attribute:: moduleName :value: 'order' .. py:attribute:: kindName :value: '{{viur_shop_modulename}}_order' .. py:attribute:: reference_user_created_skeletons_in_session :value: True .. py:method:: adminInfo() .. py:property:: current_session_order_key :type: viur.core.db.Key | None .. py:property:: current_order_skel :type: viur.shop.types.SkeletonInstance_T[shop.skeletons.order.OrderSkel] | None .. py:method:: canView(skel) .. py:method:: payment_providers_list(only_available = True) Get a list of payment providers. This method returns a JSON response containing a dictionary of payment providers. The keys represent provider identifiers, and the values are instances of `PaymentProviderResult` (dict) containing the details of each provider. :param only_available: If ``True`` (default), only payment providers that are currently available will be included in the response. If ``False``, all providers will be listed regardless of availability. :return: A JSON response with a dictionary of payment providers. .. py:method:: get_payment_providers(only_available = True) .. py:method:: order_get(order_key) .. py:method:: order_add(cart_key, payment_provider = SENTINEL, billing_address_key = SENTINEL, customer_key = SENTINEL, state_ordered = SENTINEL, state_paid = SENTINEL, state_rts = SENTINEL, **kwargs) .. py:method:: order_update(order_key, payment_provider = SENTINEL, billing_address_key = SENTINEL, customer_key = SENTINEL, state_ordered = SENTINEL, state_paid = SENTINEL, state_rts = SENTINEL, **kwargs) .. py:method:: _order_set_values(skel, *, payment_provider = SENTINEL, billing_address_key = SENTINEL, customer_key = SENTINEL, state_ordered = SENTINEL, state_paid = SENTINEL, state_rts = SENTINEL) .. py:method:: customer_is_valid(order_skel, customer_key) Checks if the given customer is a valid customer for this skel. The customer must be the same user or an root user. .. py:method:: checkout_start(order_key) Start the checkout process. Requires no errors in :meth:`self.can_checkout`. .. py:method:: can_checkout(order_skel) .. py:method:: freeze_order(order_skel) .. py:method:: _default_assign_uid(order_skel) Default order assign id method. Called as default/fallback for :attr:`Hook.ORDER_ASSIGN_UID`. .. py:method:: checkout_order(order_key) The final order now step. Requires no errors in :meth:`self.can_order`. .. py:method:: can_order(order_skel) .. py:method:: set_checkout_in_progress(order_skel) Set an order to the state *is_checkout_in_progress* .. py:method:: set_ordered(order_skel, payment) Set an order to the state *ordered* .. py:method:: set_paid(order_skel) Set an order to the state *paid* .. py:method:: set_rts(order_skel) Set an order to the state *Ready to ship* .. py:method:: additional_order_add(skel, /, **kwargs) Hook method called by :meth:`order_add` before the skeleton is saved. This method can be overridden in a subclass to implement additional API fields or make further modifications to the order skeleton (`skel`). By default, it raises an exception if unexpected arguments (``kwargs``) are provided and returns the unchanged `skel` object. :param skel: The current instance of the order skeleton. :param kwargs: Additional optional arguments for extended implementations. :raises TooManyArgumentsException: If unexpected arguments are passed in ``kwargs``. :return: The (potentially modified) order skeleton. .. py:method:: additional_order_update(skel, /, **kwargs) Hook method called by :meth:`order_update` before the skeleton is saved. This method can be overridden in a subclass to implement additional API fields or make further modifications to the order skeleton (`skel`). By default, it raises an exception if unexpected arguments (``kwargs``) are provided and returns the unchanged `skel` object. :param skel: The current instance of the order skeleton. :param kwargs: Additional optional arguments for extended implementations. :raises TooManyArgumentsException: If unexpected arguments are passed in ``kwargs``. :return: The (potentially modified) order skeleton. .. py:method:: get_payment_provider_by_name(payment_provider_name) .. py:class:: Shipping Bases: :py:obj:`shop.modules.abstract.ShopModuleAbstract`, :py:obj:`viur.core.prototypes.List` .. py:attribute:: moduleName :value: 'shipping' .. py:attribute:: kindName :value: '{{viur_shop_modulename}}_shipping' .. py:method:: adminInfo() .. py:method:: choose_shipping_skel_for_article(article_skel, *, country = None) Chooses always the cheapest, applicable shipping for an article Ignores the supplier :param country: Ignore the context and get shipping for this country. # TODO(discuss): List all options? .. py:method:: get_shipping_skels_for_cart(*, cart_key = SENTINEL, cart_skel = SENTINEL, country = None, use_cache = False) Get all configured and applicable shippings of all items in the cart # TODO: how do we handle free shipping discounts? :param cart_key: Key of the parent cart node, can be a sub-cart too :param country: Ignore the context and get shipping for this country. :return: A list of :class:`SkeletonInstance`s for the :class:`ShippingSkel`. .. py:class:: ShippingConfig Bases: :py:obj:`shop.modules.abstract.ShopModuleAbstract`, :py:obj:`viur.core.prototypes.List` .. py:attribute:: moduleName :value: 'shipping_config' .. py:attribute:: kindName :value: '{{viur_shop_modulename}}_shipping_config' .. py:method:: adminInfo() .. py:method:: is_applicable(dest, rel, *, article_skel = None, cart_skel = None, country = None) Check if a shipping configuration is applicable in the current context. Provide eiter `article_skel` for single article context xor `cart_skel` for cart context. :param country: If provided, check if the shipping configuration is applicable for this country. .. py:class:: VatRate Bases: :py:obj:`shop.modules.abstract.ShopModuleAbstract`, :py:obj:`viur.core.prototypes.List` .. py:attribute:: moduleName :value: 'vat_rate' .. py:attribute:: kindName :value: '{{viur_shop_modulename}}_vat_rate' .. py:method:: adminInfo() .. py:method:: vat_rates() Cache the vat rates Since this configuration will not change much, we cache the whole thing persistently within the instance. So after changing the VAT rates, the instance would have to be restarted -- or someone would have to create a pull request with an alternative solution. .. py:method:: _vat_skel() .. py:method:: get_vat_rate_for_country(*, country = None, category) Get the configured vat rate percentage for a country.