From 3ff3b6ed146f1902378b382bd15ecbdb534aa120 Mon Sep 17 00:00:00 2001
From: Quentin Groulard
Date: Tue, 19 Mar 2024 12:27:48 +0100
Subject: [PATCH 1/3] [ADD] shopinvader_sale_cart: transfer cart
---
shopinvader_sale_cart/models/sale_order.py | 36 ++++++++++++++++++
shopinvader_sale_cart/tests/__init__.py | 1 +
shopinvader_sale_cart/tests/test_sale_cart.py | 37 +++++++++++++++++++
3 files changed, 74 insertions(+)
create mode 100644 shopinvader_sale_cart/tests/__init__.py
create mode 100644 shopinvader_sale_cart/tests/test_sale_cart.py
diff --git a/shopinvader_sale_cart/models/sale_order.py b/shopinvader_sale_cart/models/sale_order.py
index 327d96596d..69208b4067 100644
--- a/shopinvader_sale_cart/models/sale_order.py
+++ b/shopinvader_sale_cart/models/sale_order.py
@@ -83,3 +83,39 @@ def _get_cart_line(self, product_id):
return self.order_line.filtered(
lambda l, product_id=product_id: l.product_id.id == product_id
)[:1]
+
+ def _update_cart_lines_from_cart(self, cart):
+ self.ensure_one()
+ update_cmds = []
+ for cart_line in cart.order_line:
+ line = self._get_cart_line(cart_line.product_id.id)
+ if line:
+ new_qty = line.product_uom_qty + cart_line.product_uom_qty
+ vals = {"product_uom_qty": new_qty}
+ vals.update(line._play_onchanges_cart_line(vals))
+ cmd = (1, line.id, vals)
+ else:
+ vals = {
+ "order_id": self.id,
+ "product_id": cart_line.product_id.id,
+ "product_uom_qty": cart_line.product_uom_qty,
+ }
+ vals.update(self.env["sale.order.line"]._play_onchanges_cart_line(vals))
+ cmd = (0, None, vals)
+ update_cmds.append(cmd)
+ self.write({"order_line": update_cmds})
+
+ def _transfer_cart(self, partner_id):
+ """
+ Copy the lines of this cart to the current cart of a given partner.
+ Merge lines of identical products.
+ If the partner does not have a current cart yet, create one.
+ This cart is not deleted.
+ It is the responsibility of the caller to remove it if desired.
+ """
+ self.ensure_one()
+ cart = self._find_open_cart(partner_id)
+ if not cart:
+ cart = self._create_empty_cart(partner_id)
+ cart._update_cart_lines_from_cart(self)
+ return cart
diff --git a/shopinvader_sale_cart/tests/__init__.py b/shopinvader_sale_cart/tests/__init__.py
new file mode 100644
index 0000000000..7388d2baaa
--- /dev/null
+++ b/shopinvader_sale_cart/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_sale_cart
diff --git a/shopinvader_sale_cart/tests/test_sale_cart.py b/shopinvader_sale_cart/tests/test_sale_cart.py
new file mode 100644
index 0000000000..a7b2aec1f8
--- /dev/null
+++ b/shopinvader_sale_cart/tests/test_sale_cart.py
@@ -0,0 +1,37 @@
+# Copyright 2024 ACSONE SA/NV
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo.addons.sale_cart.tests.test_sale_cart import TestSaleCart as TestSaleCartBase
+
+
+class TestSaleCart(TestSaleCartBase):
+ def test_transfer_cart(self):
+ product_2 = self.env["product.product"].create(
+ {"name": "product", "uom_id": self.env.ref("uom.product_uom_unit").id}
+ )
+ partner_2 = self.env["res.partner"].create({"name": "partner 2"})
+ so_cart_2 = self.env["sale.order"].create(
+ {
+ "partner_id": partner_2.id,
+ "order_line": [
+ (0, 0, {"product_id": self.product.id, "product_uom_qty": 1}),
+ (0, 0, {"product_id": product_2.id, "product_uom_qty": 1}),
+ ],
+ "typology": "cart",
+ }
+ )
+ merged_cart = so_cart_2._transfer_cart(self.partner.id)
+ self.assertEqual(merged_cart.id, self.so_cart.id)
+ self.assertEqual(len(merged_cart.order_line), 2)
+ self.assertEqual(
+ merged_cart.order_line.filtered(
+ lambda l, product=self.product: l.product_id.id == product.id
+ ).product_uom_qty,
+ 2,
+ )
+ self.assertEqual(
+ merged_cart.order_line.filtered(
+ lambda l, product=product_2: l.product_id.id == product.id
+ ).product_uom_qty,
+ 1,
+ )
From 1f388f032ca96853644f46aad1f85606125daac0 Mon Sep 17 00:00:00 2001
From: Quentin Groulard
Date: Wed, 20 Mar 2024 17:09:34 +0100
Subject: [PATCH 2/3] [ADD] shopinvader_anonymous_patner: delete anonymous
partner cookie
---
.../models/res_partner.py | 14 +++++++++++
.../tests/test_anonymous_partner.py | 24 ++++++++++++++++++-
2 files changed, 37 insertions(+), 1 deletion(-)
diff --git a/shopinvader_anonymous_partner/models/res_partner.py b/shopinvader_anonymous_partner/models/res_partner.py
index 4400851144..4bebb32e25 100644
--- a/shopinvader_anonymous_partner/models/res_partner.py
+++ b/shopinvader_anonymous_partner/models/res_partner.py
@@ -3,6 +3,7 @@
import secrets
import typing
+from datetime import datetime
from odoo import _, api, fields, models
@@ -16,6 +17,7 @@ def set_cookie(
key: str,
value: str,
max_age: int,
+ expires: datetime | str | int,
secure: bool,
httponly: bool,
samesite: typing.Literal["lax", "strict", "none"],
@@ -72,6 +74,18 @@ def _create_anonymous_partner__cookie(self, response: Response):
)
return partner
+ @api.model
+ def _delete_anonymous_partner__cookie(self, cookies: Cookies, response: Response):
+ """
+ Delete anonymous partner and cookie
+ """
+ self._get_anonymous_partner__cookie(cookies).unlink()
+ response.set_cookie(
+ key=COOKIE_NAME,
+ max_age=0,
+ expires=0,
+ )
+
@api.model
def _get_anonymous_partner__token(self, token: str):
return (
diff --git a/shopinvader_anonymous_partner/tests/test_anonymous_partner.py b/shopinvader_anonymous_partner/tests/test_anonymous_partner.py
index b59b99f669..ddec37f24a 100644
--- a/shopinvader_anonymous_partner/tests/test_anonymous_partner.py
+++ b/shopinvader_anonymous_partner/tests/test_anonymous_partner.py
@@ -34,6 +34,14 @@ def anonymous_partner_get(self):
return str(partner.id)
return ""
+ @route("/test/anonymous_partner_delete", type="http", auth="none")
+ def anonymous_partner_delete(self):
+ request.env["res.partner"].with_user(
+ SUPERUSER_ID
+ )._delete_anonymous_partner__cookie(
+ request.httprequest.cookies, request.future_response
+ )
+
class TestShopinvaderAnonymousPartner(TransactionCase):
def test_create(self):
@@ -69,9 +77,19 @@ def test_get(self):
)
self.assertEqual(len(partner2), 0)
+ def test_delete(self):
+ partner = self.env["res.partner"]._create_anonymous_partner__cookie(
+ mock.MagicMock()
+ )
+ self.assertTrue(partner.exists())
+ self.env["res.partner"]._delete_anonymous_partner__cookie(
+ cookies={COOKIE_NAME: partner.anonymous_token}, response=mock.MagicMock()
+ )
+ self.assertFalse(partner.exists())
+
class TestShopinvaderAnonymousPartnerEndToEnd(HttpCase):
- def test_create_and_get(self):
+ def test_create_and_get_and_delete(self):
resp = self.url_open("/test/anonymous_partner_create")
resp.raise_for_status()
token = resp.cookies.get(COOKIE_NAME)
@@ -89,3 +107,7 @@ def test_create_and_get(self):
)
resp.raise_for_status()
self.assertEqual(int(resp.text), partner_id)
+ # delete cookie
+ resp = self.url_open("/test/anonymous_partner_delete")
+ resp.raise_for_status()
+ self.assertFalse(resp.cookies.get(COOKIE_NAME))
From ba4e512966648f2548c45e91b9f11c8c0df23511 Mon Sep 17 00:00:00 2001
From: Quentin Groulard
Date: Mon, 25 Mar 2024 17:39:25 +0100
Subject: [PATCH 3/3] [IMP] shopinvader_api_signin_jwt: anonymous cart
---
shopinvader_api_signin_jwt/README.rst | 4 +--
shopinvader_api_signin_jwt/__manifest__.py | 6 +++-
.../readme/DESCRIPTION.rst | 1 +
shopinvader_api_signin_jwt/readme/ROADMAP.rst | 1 -
shopinvader_api_signin_jwt/routers/signin.py | 15 +++++++--
.../security/acl_res_partner.xml | 2 +-
.../static/description/index.html | 6 ++--
.../tests/test_signin.py | 33 +++++++++++++++++++
8 files changed, 58 insertions(+), 10 deletions(-)
diff --git a/shopinvader_api_signin_jwt/README.rst b/shopinvader_api_signin_jwt/README.rst
index 3bf6f61b41..0127f41706 100644
--- a/shopinvader_api_signin_jwt/README.rst
+++ b/shopinvader_api_signin_jwt/README.rst
@@ -7,7 +7,7 @@ Shopinvader Api Signin JWT
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:75ace7db3bf1d327ad236aa12212ec10ad5363f81a8d3fd9896afe3f4d195fc4
+ !! source digest: sha256:335295feb8193e9691d56e3537300ec06022d9a6abe43a72a0639e1ce6e3fc68
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
@@ -24,6 +24,7 @@ Shopinvader Api Signin JWT
This addon adds a web API to signin into the application and create a partner
if the email in the jwt payload is unknown.
+If we had an anonymous partner, transfer his cart to the loggedin partner and delete it.
**Table of contents**
@@ -33,7 +34,6 @@ if the email in the jwt payload is unknown.
Known issues / Roadmap
======================
-* Manage anonymous cart (see https://github.com/shopinvader/odoo-shopinvader/issues/1428)
* Use ``fastapi_auth_jwt.auth_jwt_authenticated_odoo_env`` dependency for the env (see https://github.com/OCA/rest-framework/issues/406)
Bug Tracker
diff --git a/shopinvader_api_signin_jwt/__manifest__.py b/shopinvader_api_signin_jwt/__manifest__.py
index c84506a719..a51247e489 100644
--- a/shopinvader_api_signin_jwt/__manifest__.py
+++ b/shopinvader_api_signin_jwt/__manifest__.py
@@ -8,7 +8,11 @@
"license": "AGPL-3",
"author": "ACSONE SA/NV",
"website": "https://github.com/shopinvader/odoo-shopinvader",
- "depends": ["fastapi_auth_jwt"],
+ "depends": [
+ "fastapi_auth_jwt",
+ "shopinvader_anonymous_partner",
+ "shopinvader_sale_cart",
+ ],
"data": [
"security/res_groups.xml",
"security/acl_res_partner.xml",
diff --git a/shopinvader_api_signin_jwt/readme/DESCRIPTION.rst b/shopinvader_api_signin_jwt/readme/DESCRIPTION.rst
index 9d807c0111..3c60d139e1 100644
--- a/shopinvader_api_signin_jwt/readme/DESCRIPTION.rst
+++ b/shopinvader_api_signin_jwt/readme/DESCRIPTION.rst
@@ -1,2 +1,3 @@
This addon adds a web API to signin into the application and create a partner
if the email in the jwt payload is unknown.
+If we had an anonymous partner, transfer his cart to the loggedin partner and delete it.
diff --git a/shopinvader_api_signin_jwt/readme/ROADMAP.rst b/shopinvader_api_signin_jwt/readme/ROADMAP.rst
index 945ce1163b..0bb1134142 100644
--- a/shopinvader_api_signin_jwt/readme/ROADMAP.rst
+++ b/shopinvader_api_signin_jwt/readme/ROADMAP.rst
@@ -1,2 +1 @@
-* Manage anonymous cart (see https://github.com/shopinvader/odoo-shopinvader/issues/1428)
* Use ``fastapi_auth_jwt.auth_jwt_authenticated_odoo_env`` dependency for the env (see https://github.com/OCA/rest-framework/issues/406)
diff --git a/shopinvader_api_signin_jwt/routers/signin.py b/shopinvader_api_signin_jwt/routers/signin.py
index 0b9b688b40..723c8040a6 100644
--- a/shopinvader_api_signin_jwt/routers/signin.py
+++ b/shopinvader_api_signin_jwt/routers/signin.py
@@ -4,7 +4,7 @@
import logging
from typing import Annotated, Union
-from fastapi import APIRouter, Depends, Response, status
+from fastapi import APIRouter, Depends, Request, Response, status
from odoo import api, models
@@ -28,17 +28,28 @@ def signin(
partner: Annotated[Partner, Depends(auth_jwt_optionally_authenticated_partner)],
payload: Annotated[Payload, Depends(auth_jwt_authenticated_payload)],
response: Response,
+ request: Request,
) -> None:
"""
Authenticate the partner based on a JWT token or a session cookie.
Set the session cookie if allowed.
Return HTTP code 201 if res.partner created (case of the first signin).
+ Transfer anonymous cart and delete anonymous partner if any.
"""
if not partner:
- env[
+ partner = env[
"shopinvader_api_signin_jwt.signin_router.helper"
]._create_partner_from_payload(payload)
response.status_code = status.HTTP_201_CREATED
+ anonymous_partner = env["res.partner"]._get_anonymous_partner__cookie(
+ request.cookies
+ )
+ if anonymous_partner:
+ anonymous_cart = env["sale.order"].sudo()._find_open_cart(anonymous_partner.id)
+ if anonymous_cart:
+ anonymous_cart._transfer_cart(partner.id)
+ anonymous_cart.unlink()
+ env["res.partner"]._delete_anonymous_partner__cookie(request.cookies, response)
@signin_router.post("/signout")
diff --git a/shopinvader_api_signin_jwt/security/acl_res_partner.xml b/shopinvader_api_signin_jwt/security/acl_res_partner.xml
index 64f0f24fab..a41b91a5f8 100644
--- a/shopinvader_api_signin_jwt/security/acl_res_partner.xml
+++ b/shopinvader_api_signin_jwt/security/acl_res_partner.xml
@@ -9,7 +9,7 @@
-
+
diff --git a/shopinvader_api_signin_jwt/static/description/index.html b/shopinvader_api_signin_jwt/static/description/index.html
index a5eb6e9e13..a478ab6d6d 100644
--- a/shopinvader_api_signin_jwt/static/description/index.html
+++ b/shopinvader_api_signin_jwt/static/description/index.html
@@ -367,11 +367,12 @@ Shopinvader Api Signin JWT
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:75ace7db3bf1d327ad236aa12212ec10ad5363f81a8d3fd9896afe3f4d195fc4
+!! source digest: sha256:335295feb8193e9691d56e3537300ec06022d9a6abe43a72a0639e1ce6e3fc68
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
This addon adds a web API to signin into the application and create a partner
-if the email in the jwt payload is unknown.
+if the email in the jwt payload is unknown.
+If we had an anonymous partner, transfer his cart to the loggedin partner and delete it.
Table of contents
@@ -388,7 +389,6 @@ Shopinvader Api Signin JWT
diff --git a/shopinvader_api_signin_jwt/tests/test_signin.py b/shopinvader_api_signin_jwt/tests/test_signin.py
index 3243e0283c..39ffa4ebbe 100644
--- a/shopinvader_api_signin_jwt/tests/test_signin.py
+++ b/shopinvader_api_signin_jwt/tests/test_signin.py
@@ -7,6 +7,7 @@
from odoo.addons.fastapi.tests.common import FastAPITransactionCase
from odoo.addons.fastapi_auth_jwt.dependencies import auth_jwt_default_validator_name
+from odoo.addons.shopinvader_anonymous_partner.models.res_partner import COOKIE_NAME
from ..routers import signin_router
@@ -79,6 +80,38 @@ def test_signin(self):
res = client.post("/signin", headers={"Authorization": token})
self.assertEqual(res.status_code, 200)
+ def test_signin_anonymous_cart(self):
+ anonymous_partner = self.env["res.partner"].create(
+ {"name": "Test anonymous", "anonymous_token": "1234", "active": False}
+ )
+ product = self.env["product.product"].create(
+ {"name": "product", "uom_id": self.env.ref("uom.product_uom_unit").id}
+ )
+ anonymous_cart = self.env["sale.order"].create(
+ {
+ "partner_id": anonymous_partner.id,
+ "order_line": [
+ (0, 0, {"product_id": product.id, "product_uom_qty": 1}),
+ ],
+ "typology": "cart",
+ }
+ )
+
+ token = self._get_token()
+ with self._create_test_client() as client:
+ res = client.post(
+ "/signin",
+ headers={"Authorization": token},
+ cookies={COOKIE_NAME: "1234"},
+ )
+ self.assertFalse(res.cookies.get(COOKIE_NAME))
+ self.assertFalse(anonymous_partner.exists())
+ self.assertFalse(anonymous_cart.exists())
+ partner = self.env["res.partner"].search([("email", "=", "test@mail.com")])
+ cart = self.env["sale.order"].search([("partner_id", "=", partner.id)])
+ self.assertEqual(len(cart.order_line), 1)
+ self.assertEqual(cart.order_line[0].product_id, product)
+
def test_signout(self):
self.validator.write({"cookie_enabled": True, "cookie_name": "test_cookie"})
token = self._get_token()