Skip to content

Commit

Permalink
Merge PR #1430 into 16.0
Browse files Browse the repository at this point in the history
Signed-off-by lmignon
  • Loading branch information
shopinvader-git-bot committed Feb 1, 2024
2 parents 5e49856 + 39c4f27 commit 0529db2
Show file tree
Hide file tree
Showing 31 changed files with 2,269 additions and 0 deletions.
6 changes: 6 additions & 0 deletions setup/shopinvader_api_sale_loyalty/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
88 changes: 88 additions & 0 deletions shopinvader_api_sale_loyalty/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
============================
Shopinvader API Sale Loyalty
============================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:181f22ca86c6b563f98c38b69eb73b031e2d5464d9cba7c3ebd919f889c80c73
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-shopinvader%2Fodoo--shopinvader-lightgray.png?logo=github
:target: https://github.com/shopinvader/odoo-shopinvader/tree/16.0/shopinvader_api_sale_loyalty
:alt: shopinvader/odoo-shopinvader

|badge1| |badge2| |badge3|

This module extends the functionality of Shopinvader to support Coupons
and Promotions from `sale_loyalty` core module.
It is the new version providing FastAPI services.

Not to be confused with `shopinvader_promotion_rule`, that implements
promotion programs from OCA module `sale_promotion_rule`, an alternative
to the core `sale_loyalty` module.

Available services:

* ``/loyalty/{code}`` under the ``loyalty`` router, to get all rewards claimable with a given coupon code
* ``/coupon`` under the ``cart`` router, to apply a given coupon to the cart. Allows to specify which reward and/or which free product to offer.
* ``/reward`` under the ``cart`` router, to apply a given reward (automatic promotion). Note that automatic promotions are applied automatically at cart update, when possible (if no choice must be done). This service allows to apply an automatic promotion for which the reward/free product choice is mandatory.

**Table of contents**

.. contents::
:local:

Usage
=====

Some routes are added to the `cart_router`.
Please note that for this router, the `/cart` prefix is not automatically
added, to allow to mount it as a nested app.
But you must ensure that this prefix is added when using this router.

See the README of `shopinvader_api_cart` for more details on how to do it.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/shopinvader/odoo-shopinvader/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/shopinvader/odoo-shopinvader/issues/new?body=module:%20shopinvader_api_sale_loyalty%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
~~~~~~~

* ACSONE SA/NV

Contributors
~~~~~~~~~~~~

* `Camptocamp <https://www.camptocamp.com>`_

* Iván Todorovich <[email protected]>

* `Acsone <https://www.acsone.eu>`_

* Marie Lejeune <[email protected]>

Maintainers
~~~~~~~~~~~

This module is part of the `shopinvader/odoo-shopinvader <https://github.com/shopinvader/odoo-shopinvader/tree/16.0/shopinvader_api_sale_loyalty>`_ project on GitHub.

You are welcome to contribute.
1 change: 1 addition & 0 deletions shopinvader_api_sale_loyalty/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import routers
41 changes: 41 additions & 0 deletions shopinvader_api_sale_loyalty/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright 2022 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

{
"name": "Shopinvader API Sale Loyalty",
"summary": """
FastAPI services to add coupons and loyalties to carts.""",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "ACSONE SA/NV",
"website": "https://github.com/shopinvader/odoo-shopinvader",
"depends": [
"sale_loyalty",
"sale_loyalty_order_info",
"shopinvader_api_cart",
"shopinvader_api_security_sale",
"shopinvader_schema_sale",
"pydantic",
"extendable",
"fastapi",
"extendable_fastapi",
],
"data": [
"security/groups.xml",
"security/acl_loyalty_card.xml",
"security/acl_loyalty_program.xml",
"security/acl_loyalty_reward.xml",
"security/acl_loyalty_rule.xml",
"security/acl_product_product.xml",
"security/acl_product_tag.xml",
"security/acl_sale_order_coupon_points.xml",
"security/acl_sale_loyalty_reward_wizard.xml",
],
"external_dependencies": {
"python": [
"fastapi",
"pydantic>=2.0.0",
"extendable-pydantic>=1.2.0",
]
},
}
7 changes: 7 additions & 0 deletions shopinvader_api_sale_loyalty/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
* `Camptocamp <https://www.camptocamp.com>`_

* Iván Todorovich <[email protected]>

* `Acsone <https://www.acsone.eu>`_

* Marie Lejeune <[email protected]>
13 changes: 13 additions & 0 deletions shopinvader_api_sale_loyalty/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
This module extends the functionality of Shopinvader to support Coupons
and Promotions from `sale_loyalty` core module.
It is the new version providing FastAPI services.

Not to be confused with `shopinvader_promotion_rule`, that implements
promotion programs from OCA module `sale_promotion_rule`, an alternative
to the core `sale_loyalty` module.

Available services:

* ``/loyalty/{code}`` under the ``loyalty`` router, to get all rewards claimable with a given coupon code
* ``/coupon`` under the ``cart`` router, to apply a given coupon to the cart. Allows to specify which reward and/or which free product to offer.
* ``/reward`` under the ``cart`` router, to apply a given reward (automatic promotion). Note that automatic promotions are applied automatically at cart update, when possible (if no choice must be done). This service allows to apply an automatic promotion for which the reward/free product choice is mandatory.
6 changes: 6 additions & 0 deletions shopinvader_api_sale_loyalty/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Some routes are added to the `cart_router`.
Please note that for this router, the `/cart` prefix is not automatically
added, to allow to mount it as a nested app.
But you must ensure that this prefix is added when using this router.

See the README of `shopinvader_api_cart` for more details on how to do it.
3 changes: 3 additions & 0 deletions shopinvader_api_sale_loyalty/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .loyalty import loyalty_router
from . import loyalty
from . import cart
205 changes: 205 additions & 0 deletions shopinvader_api_sale_loyalty/routers/cart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from typing import Annotated
from uuid import UUID

from fastapi import Depends

from odoo import _, api, models
from odoo.exceptions import UserError

from odoo.addons.base.models.res_partner import Partner as ResPartner
from odoo.addons.fastapi.dependencies import (
authenticated_partner,
authenticated_partner_env,
)
from odoo.addons.sale.models.sale_order import SaleOrder
from odoo.addons.shopinvader_api_cart.routers import cart_router
from odoo.addons.shopinvader_api_cart.schemas import CartTransaction

from ..schemas import LoyaltyCardInput, LoyaltyRewardInput, Sale


@cart_router.post("/apply_coupon/{uuid}", deprecated=True)
@cart_router.post("/apply_coupon", deprecated=True)
@cart_router.post("/{uuid}/coupon")
@cart_router.post("/current/coupon")
@cart_router.post("/coupon")
def apply_coupon(
data: LoyaltyCardInput,
env: Annotated[api.Environment, Depends(authenticated_partner_env)],
partner: Annotated[ResPartner, Depends(authenticated_partner)],
uuid: UUID | None = None,
) -> Sale | None:
"""
Apply a coupon on a specific cart.
One can specify in LoyaltyCartInput which reward to choose, and
which free product to choose.
If some info is missing to uniquely determine which reward to apply,
raise an error.
"""
cart = env["sale.order"]._find_open_cart(partner.id, str(uuid) if uuid else None)
if cart:
env["shopinvader_api_cart.cart_router.helper"]._apply_coupon(cart, data)
return Sale.from_sale_order(cart) if cart else None


@cart_router.post("/apply_reward/{uuid}", deprecated=True)
@cart_router.post("/apply_reward", deprecated=True)
@cart_router.post("/{uuid}/reward")
@cart_router.post("/current/reward")
@cart_router.post("/reward")
def apply_reward(
data: LoyaltyRewardInput,
env: Annotated[api.Environment, Depends(authenticated_partner_env)],
partner: Annotated[ResPartner, Depends(authenticated_partner)],
uuid: UUID | None = None,
) -> Sale | None:
"""
Apply claimable rewards on a specific cart.
One can specify in LoyaltyReardInput which free product to choose.
If this piece of info is needed and missing, raise an error.
"""
cart = env["sale.order"]._find_open_cart(partner.id, str(uuid) if uuid else None)
if cart:
env["shopinvader_api_cart.cart_router.helper"]._apply_reward(cart, data)
return Sale.from_sale_order(cart) if cart else None


class ShopinvaderApiCartRouterHelper(models.AbstractModel):
_inherit = "shopinvader_api_cart.cart_router.helper"

# Apply coupon
@api.model
def _check_code_and_reward(self, cart, code, reward_id=None):
"""
* Check the code is valid.
* Check the provided reward is allowed.
If no reward is given, return the unique allowed reward,
or raise an error.
:return: the browsed reward
"""
status = cart._try_apply_code(code)
if "error" in status:
raise UserError(status["error"])
all_rewards = self.env["loyalty.reward"]
for rewards in status.values():
all_rewards |= rewards

if not all_rewards:
raise UserError(_("No reward available for this code."))
if reward_id and reward_id not in all_rewards.ids:
raise UserError(_("Reward not allowed for this code."))
if not reward_id and len(all_rewards) > 1:
raise UserError(_("Several rewards available. Please specify one."))

if reward_id:
return self.env["loyalty.reward"].browse(reward_id)
return all_rewards[0] # In this case only 1 reward available

@api.model
def _check_free_reward_product(self, reward, product_id=None):
"""
Check the free product is allowed (if given) or check
that it is unique (if not given).
:return: the free product id
"""
if not reward.reward_type == "product":
return

reward_products = reward.reward_product_ids

if not reward_products:
raise UserError(_("No free products available."))
if product_id and product_id not in reward_products.ids:
raise UserError(_("Free product not allowed for this reward."))
if not product_id and len(reward_products) > 1:
raise UserError(_("Several free products available. Please specify one."))

if product_id:
return product_id
return reward_products[0].id

@api.model
def _apply_coupon(self, cart: "SaleOrder", data: LoyaltyCardInput):
"""Apply a coupon or promotion code.
It can raise UserError if coupon is not applicable, or
if the coupon let the choice between several rewards, and the
selected reward is not specified.
"""
reward = self._check_code_and_reward(cart, data.code, data.reward_id)
product_id = None
if reward.reward_type == "product":
product_id = self._check_free_reward_product(reward, data.free_product_id)

self.env["sale.loyalty.reward.wizard"].sudo().create(
{
"order_id": cart.id,
"selected_reward_id": reward.id if reward else None,
"selected_product_id": product_id if product_id else None,
}
).action_apply()
return cart

# Apply claimable reward
def _apply_reward(self, cart: "SaleOrder", data: LoyaltyRewardInput):
"""
Try to apply the given reward.
Raise an error if there is a choice between free products
and the chosen product is not specified.
"""
cart._update_programs_and_rewards()
reward = self.env["loyalty.reward"].browse(data.reward_id)
product_id = self._check_free_reward_product(reward, data.free_product_id)
return (
self.env["sale.loyalty.reward.wizard"]
.sudo()
.create(
{
"order_id": cart.id,
"selected_reward_id": data.reward_id,
"selected_product_id": product_id if product_id else None,
}
)
.action_apply()
)

# Apply automatic rewards at cart updates
def _apply_automatic_rewards(self, cart: SaleOrder):
claimable_rewards = cart._get_claimable_rewards()

for coupon, rewards in claimable_rewards.items():
if (
len(rewards) > 1
or len(coupon.program_id.reward_ids) != 1
or (rewards.reward_type == "product" and rewards.multi_product)
or rewards.id in cart.order_line.mapped("reward_id").ids
):
continue
try:
cart._apply_program_reward(rewards, coupon)
except UserError: # pylint: disable=except-pass
pass

@api.model
def _sync_cart(
self,
partner: ResPartner,
cart: SaleOrder,
uuid: str,
transactions: list[CartTransaction],
):
cart = super()._sync_cart(partner, cart, uuid, transactions)
# Try to auto apply rewards.
# Only rewards that are the only reward of the program and not
# with a multi product reward
if cart:
cart._update_programs_and_rewards()
self._apply_automatic_rewards(cart)
return cart
Loading

0 comments on commit 0529db2

Please sign in to comment.