-
Notifications
You must be signed in to change notification settings - Fork 104
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by lmignon
- Loading branch information
Showing
31 changed files
with
2,269 additions
and
0 deletions.
There are no files selected for viewing
1 change: 1 addition & 0 deletions
1
setup/shopinvader_api_sale_loyalty/odoo/addons/shopinvader_api_sale_loyalty
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../../../shopinvader_api_sale_loyalty |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import routers |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
] | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.