From c63cedf769186b59a208433ee2b66086e5087397 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 16 Dec 2024 18:16:54 +0100 Subject: [PATCH] [ADD] shopinvader_api_cart_options --- .../odoo/addons/shopinvader_api_cart_options | 1 + setup/shopinvader_api_cart_options/setup.py | 6 + shopinvader_api_cart_options/README.rst | 139 +++++ shopinvader_api_cart_options/__init__.py | 2 + shopinvader_api_cart_options/__manifest__.py | 16 + shopinvader_api_cart_options/helper.py | 28 + .../readme/CONTRIBUTORS.md | 1 + .../readme/DESCRIPTION.md | 4 + shopinvader_api_cart_options/readme/USAGE.md | 70 +++ shopinvader_api_cart_options/schemas.py | 27 + .../static/description/index.html | 489 ++++++++++++++++++ shopinvader_api_cart_options/test.py | 40 ++ .../tests/__init__.py | 1 + .../test_shopinvader_api_cart_options.py | 466 +++++++++++++++++ 14 files changed, 1290 insertions(+) create mode 120000 setup/shopinvader_api_cart_options/odoo/addons/shopinvader_api_cart_options create mode 100644 setup/shopinvader_api_cart_options/setup.py create mode 100644 shopinvader_api_cart_options/README.rst create mode 100644 shopinvader_api_cart_options/__init__.py create mode 100644 shopinvader_api_cart_options/__manifest__.py create mode 100644 shopinvader_api_cart_options/helper.py create mode 100644 shopinvader_api_cart_options/readme/CONTRIBUTORS.md create mode 100644 shopinvader_api_cart_options/readme/DESCRIPTION.md create mode 100644 shopinvader_api_cart_options/readme/USAGE.md create mode 100644 shopinvader_api_cart_options/schemas.py create mode 100644 shopinvader_api_cart_options/static/description/index.html create mode 100644 shopinvader_api_cart_options/test.py create mode 100644 shopinvader_api_cart_options/tests/__init__.py create mode 100644 shopinvader_api_cart_options/tests/test_shopinvader_api_cart_options.py diff --git a/setup/shopinvader_api_cart_options/odoo/addons/shopinvader_api_cart_options b/setup/shopinvader_api_cart_options/odoo/addons/shopinvader_api_cart_options new file mode 120000 index 0000000000..8420274a10 --- /dev/null +++ b/setup/shopinvader_api_cart_options/odoo/addons/shopinvader_api_cart_options @@ -0,0 +1 @@ +../../../../shopinvader_api_cart_options \ No newline at end of file diff --git a/setup/shopinvader_api_cart_options/setup.py b/setup/shopinvader_api_cart_options/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/shopinvader_api_cart_options/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/shopinvader_api_cart_options/README.rst b/shopinvader_api_cart_options/README.rst new file mode 100644 index 0000000000..ffc890834c --- /dev/null +++ b/shopinvader_api_cart_options/README.rst @@ -0,0 +1,139 @@ +============================ +Shopinvader API Cart Options +============================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:946cd22cb562f3d64d31537d9fdfb741356c31fcfa820646b6827b07492a263a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_cart_options + :alt: shopinvader/odoo-shopinvader + +|badge1| |badge2| |badge3| + +This module allows to add options to the cart lines, grouping cart +transaction on the combination of the product and the options. + +It also handles the cart transfer merge by using the options to merge +the cart lines. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +This module is designed to be extended, so you can add your own options +to the cart lines. + +In order to do so, you need to extend the ``SaleLineOptions`` schema and +add your own options: + +.. code:: python + + class SaleLineOptions(BaseSaleLineOptions, extends=True): + note: str | None = None + special: bool = False + + @classmethod + def from_sale_order_line(cls, odoo_rec): + rv = super().from_sale_order_line(odoo_rec) + rv.note = odoo_rec.note or None + rv.special = odoo_rec.special + return rv + +Then you will need to extend the ``SaleOrderLine`` model to add support +for you options in the cart line matching and in the cart line transfer: + +.. code:: python + + class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + note = fields.Char() + special = fields.Boolean() + + def _match_cart_line( + self, + product_id, + note=None, + special=None, + **kwargs, + ): + rv = super()._match_cart_line( + product_id, + note=note, + special=special, + **kwargs, + ) + return rv and self.note == note and self.special == special + + def _prepare_cart_line_transfer_values(self): + vals = super()._prepare_cart_line_transfer_values() + vals["note"] = self.note + vals["special"] = self.special + return vals + +And finally, you will need to extend the +``ShopinvaderApiCartRouterHelper`` to add support for your options in +the cart line creation from the transaction API: + +.. code:: python + + class ShopinvaderApiCartRouterHelper(models.AbstractModel): + _inherit = "shopinvader_api_cart.cart_router.helper" + + @api.model + def _apply_transactions_creating_new_cart_line_prepare_vals( + self, cart: SaleOrder, transactions: list[CartTransaction], values: dict + ): + options = transactions[0].options + if options: + if options.note is not None: + values["note"] = options.note + + values["special"] = options.special + + return values + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Akretion + +Contributors +------------ + +- Florian Mounier florian.mounier@akretion.com + +Maintainers +----------- + +This module is part of the `shopinvader/odoo-shopinvader `_ project on GitHub. + +You are welcome to contribute. diff --git a/shopinvader_api_cart_options/__init__.py b/shopinvader_api_cart_options/__init__.py new file mode 100644 index 0000000000..ded7c26888 --- /dev/null +++ b/shopinvader_api_cart_options/__init__.py @@ -0,0 +1,2 @@ +from . import helper +from . import schemas diff --git a/shopinvader_api_cart_options/__manifest__.py b/shopinvader_api_cart_options/__manifest__.py new file mode 100644 index 0000000000..2d32d5c02b --- /dev/null +++ b/shopinvader_api_cart_options/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Shopinvader API Cart Options", + "version": "16.0.1.0.0", + "author": "Akretion, Odoo Community Association (OCA)", + "summary": "Add product options to the cart API", + "depends": ["shopinvader_api_cart"], + "website": "https://github.com/shopinvader/odoo-shopinvader", + "data": [], + "demo": [], + "installable": True, + "license": "AGPL-3", +} diff --git a/shopinvader_api_cart_options/helper.py b/shopinvader_api_cart_options/helper.py new file mode 100644 index 0000000000..5ee49fb23d --- /dev/null +++ b/shopinvader_api_cart_options/helper.py @@ -0,0 +1,28 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from collections import namedtuple + +from odoo import models + +from odoo.addons.shopinvader_api_cart.schemas import CartTransaction + +from .schemas import SaleLineOptions + + +class ShopinvaderApiCartRouterHelper(models.AbstractModel): + _inherit = "shopinvader_api_cart.cart_router.helper" + + def _get_transaction_key(self, transaction: CartTransaction): + """ + Override the method to add the options defined in the SaleLineOptions + """ + key = super()._get_transaction_key(transaction) + options = tuple(SaleLineOptions._get_assembled_cls().model_fields.keys()) + return namedtuple(key.__class__.__name__, key._fields + options)( + *key, + *tuple( + getattr(transaction.options, key) if transaction.options else None + for key in options + ), + ) diff --git a/shopinvader_api_cart_options/readme/CONTRIBUTORS.md b/shopinvader_api_cart_options/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..328a37da87 --- /dev/null +++ b/shopinvader_api_cart_options/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Florian Mounier diff --git a/shopinvader_api_cart_options/readme/DESCRIPTION.md b/shopinvader_api_cart_options/readme/DESCRIPTION.md new file mode 100644 index 0000000000..28b5d48486 --- /dev/null +++ b/shopinvader_api_cart_options/readme/DESCRIPTION.md @@ -0,0 +1,4 @@ +This module allows to add options to the cart lines, grouping cart transaction on the +combination of the product and the options. + +It also handles the cart transfer merge by using the options to merge the cart lines. diff --git a/shopinvader_api_cart_options/readme/USAGE.md b/shopinvader_api_cart_options/readme/USAGE.md new file mode 100644 index 0000000000..d0ed7c2a68 --- /dev/null +++ b/shopinvader_api_cart_options/readme/USAGE.md @@ -0,0 +1,70 @@ +This module is designed to be extended, so you can add your own options to the cart +lines. + +In order to do so, you need to extend the `SaleLineOptions` schema and add your own +options: + +```python +class SaleLineOptions(BaseSaleLineOptions, extends=True): + note: str | None = None + special: bool = False + + @classmethod + def from_sale_order_line(cls, odoo_rec): + rv = super().from_sale_order_line(odoo_rec) + rv.note = odoo_rec.note or None + rv.special = odoo_rec.special + return rv +``` + +Then you will need to extend the `SaleOrderLine` model to add support for you options in +the cart line matching and in the cart line transfer: + +```python +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + note = fields.Char() + special = fields.Boolean() + + def _match_cart_line( + self, + product_id, + note=None, + special=None, + **kwargs, + ): + rv = super()._match_cart_line( + product_id, + note=note, + special=special, + **kwargs, + ) + return rv and self.note == note and self.special == special + + def _prepare_cart_line_transfer_values(self): + vals = super()._prepare_cart_line_transfer_values() + vals["note"] = self.note + vals["special"] = self.special + return vals +``` + +And finally, you will need to extend the `ShopinvaderApiCartRouterHelper` to add support +for your options in the cart line creation from the transaction API: + +```python +class ShopinvaderApiCartRouterHelper(models.AbstractModel): + _inherit = "shopinvader_api_cart.cart_router.helper" + + @api.model + def _apply_transactions_creating_new_cart_line_prepare_vals( + self, cart: SaleOrder, transactions: list[CartTransaction], values: dict + ): + options = transactions[0].options + if options: + if options.note is not None: + values["note"] = options.note + + values["special"] = options.special + + return values +``` diff --git a/shopinvader_api_cart_options/schemas.py b/shopinvader_api_cart_options/schemas.py new file mode 100644 index 0000000000..7044942f6a --- /dev/null +++ b/shopinvader_api_cart_options/schemas.py @@ -0,0 +1,27 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.extendable_fastapi import StrictExtendableBaseModel +from odoo.addons.shopinvader_api_cart.schemas import cart +from odoo.addons.shopinvader_schema_sale.schemas import sale_line + + +class SaleLineOptions(StrictExtendableBaseModel): + @classmethod + def from_sale_order_line(cls, odoo_rec): + return cls.model_construct() + + +class SaleLine(sale_line.SaleLine, extends=True): + options: SaleLineOptions + + @classmethod + def from_sale_order_line(cls, odoo_rec): + res = super().from_sale_order_line(odoo_rec) + res.options = SaleLineOptions.from_sale_order_line(odoo_rec) + return res + + +class CartTransaction(cart.CartTransaction, extends=True): + options: SaleLineOptions | None = None diff --git a/shopinvader_api_cart_options/static/description/index.html b/shopinvader_api_cart_options/static/description/index.html new file mode 100644 index 0000000000..62c13e05b4 --- /dev/null +++ b/shopinvader_api_cart_options/static/description/index.html @@ -0,0 +1,489 @@ + + + + + + +Shopinvader API Cart Options + + + +
+

Shopinvader API Cart Options

+ + +

Beta License: AGPL-3 shopinvader/odoo-shopinvader

+

This module allows to add options to the cart lines, grouping cart +transaction on the combination of the product and the options.

+

It also handles the cart transfer merge by using the options to merge +the cart lines.

+

Table of contents

+ +
+

Usage

+

This module is designed to be extended, so you can add your own options +to the cart lines.

+

In order to do so, you need to extend the SaleLineOptions schema and +add your own options:

+
+class SaleLineOptions(BaseSaleLineOptions, extends=True):
+    note: str | None = None
+    special: bool = False
+
+    @classmethod
+    def from_sale_order_line(cls, odoo_rec):
+        rv = super().from_sale_order_line(odoo_rec)
+        rv.note = odoo_rec.note or None
+        rv.special = odoo_rec.special
+        return rv
+
+

Then you will need to extend the SaleOrderLine model to add support +for you options in the cart line matching and in the cart line transfer:

+
+class SaleOrderLine(models.Model):
+    _inherit = "sale.order.line"
+    note = fields.Char()
+    special = fields.Boolean()
+
+    def _match_cart_line(
+        self,
+        product_id,
+        note=None,
+        special=None,
+        **kwargs,
+    ):
+        rv = super()._match_cart_line(
+            product_id,
+            note=note,
+            special=special,
+            **kwargs,
+        )
+        return rv and self.note == note and self.special == special
+
+    def _prepare_cart_line_transfer_values(self):
+        vals = super()._prepare_cart_line_transfer_values()
+        vals["note"] = self.note
+        vals["special"] = self.special
+        return vals
+
+

And finally, you will need to extend the +ShopinvaderApiCartRouterHelper to add support for your options in +the cart line creation from the transaction API:

+
+class ShopinvaderApiCartRouterHelper(models.AbstractModel):
+    _inherit = "shopinvader_api_cart.cart_router.helper"
+
+    @api.model
+    def _apply_transactions_creating_new_cart_line_prepare_vals(
+        self, cart: SaleOrder, transactions: list[CartTransaction], values: dict
+    ):
+        options = transactions[0].options
+        if options:
+            if options.note is not None:
+                values["note"] = options.note
+
+            values["special"] = options.special
+
+        return values
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+ +
+

Maintainers

+

This module is part of the shopinvader/odoo-shopinvader project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/shopinvader_api_cart_options/test.py b/shopinvader_api_cart_options/test.py new file mode 100644 index 0000000000..f87f3b4fdf --- /dev/null +++ b/shopinvader_api_cart_options/test.py @@ -0,0 +1,40 @@ +from extendable_pydantic import StrictExtendableBaseModel + + +class Sale(StrictExtendableBaseModel): + id: int + state: str + + @classmethod + def from_sale_order(cls, odoo_rec): + return cls.model_construct( + id=odoo_rec.id, + state=odoo_rec.state, + ) + + +class Sale1(Sale, extends=True): + uuid: str | None = None + + @classmethod + def from_sale_order(cls, odoo_rec): + res = super().from_sale_order(odoo_rec) + res.uuid = odoo_rec.uuid or None + return res + + +class Sale2(Sale1, extends=True): + @classmethod + def from_sale_order(cls, odoo_rec): + return super().from_sale_order(odoo_rec) + + +class A: + pass + + +a = A() +a.id = 1 +a.state = "draft" + +sale = Sale.from_sale_order(a) diff --git a/shopinvader_api_cart_options/tests/__init__.py b/shopinvader_api_cart_options/tests/__init__.py new file mode 100644 index 0000000000..0e9de8a2e6 --- /dev/null +++ b/shopinvader_api_cart_options/tests/__init__.py @@ -0,0 +1 @@ +from . import test_shopinvader_api_cart_options diff --git a/shopinvader_api_cart_options/tests/test_shopinvader_api_cart_options.py b/shopinvader_api_cart_options/tests/test_shopinvader_api_cart_options.py new file mode 100644 index 0000000000..50fadc5e0a --- /dev/null +++ b/shopinvader_api_cart_options/tests/test_shopinvader_api_cart_options.py @@ -0,0 +1,466 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import json + +from odoo_test_helper import FakeModelLoader + +from odoo import api, fields, models + +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 odoo.addons.shopinvader_api_cart.tests.common import CommonSaleCart + +from ..schemas import SaleLineOptions as BaseSaleLineOptions + + +class TestSaleCartOption(CommonSaleCart): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.loader = FakeModelLoader(cls.env, cls.__module__) + cls.loader.backup_registry() + + class SaleLineOptions(BaseSaleLineOptions, extends=True): + note: str | None = None + special: bool = False + + @classmethod + def from_sale_order_line(cls, odoo_rec): + rv = super().from_sale_order_line(odoo_rec) + rv.note = odoo_rec.note or None + rv.special = odoo_rec.special + return rv + + class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + note = fields.Char() + special = fields.Boolean() + + def _match_cart_line( + self, + product_id, + note=None, + special=None, + **kwargs, + ): + rv = super()._match_cart_line( + product_id, + note=note, + special=special, + **kwargs, + ) + return rv and self.note == note and self.special == special + + def _prepare_cart_line_transfer_values(self): + vals = super()._prepare_cart_line_transfer_values() + vals["note"] = self.note + vals["special"] = self.special + return vals + + class ShopinvaderApiCartRouterHelper(models.AbstractModel): # pylint: disable=consider-merging-classes-inherited + _inherit = "shopinvader_api_cart.cart_router.helper" + + @api.model + def _apply_transactions_creating_new_cart_line_prepare_vals( + self, cart: SaleOrder, transactions: list[CartTransaction], values: dict + ): + options = transactions[0].options + if options: + if options.note is not None: + values["note"] = options.note + + values["special"] = options.special + + return values + + cls.loader.update_registry((SaleOrderLine, ShopinvaderApiCartRouterHelper)) + + # /!\ THIS IS IMPORTANT when using FakeModelLoader, otherwise we get some + # TypeError: super(type, obj): obj must be an instance or subtype of type + # when calling schemas super(): + cls.reset_extendable_registry() + cls.init_extendable_registry() + + @classmethod + def tearDownClass(cls): + cls.loader.restore_registry() + super().tearDownClass() + + def test_cart_no_options(self) -> None: + so = self.env["sale.order"]._create_empty_cart( + self.default_fastapi_authenticated_partner.id + ) + data = { + "transactions": [ + { + "uuid": self.trans_uuid_1, + "product_id": self.product_1.id, + "qty": 1, + }, + { + "uuid": self.trans_uuid_2, + "product_id": self.product_2.id, + "qty": 4, + }, + { + "uuid": self.trans_uuid_3, + "product_id": self.product_1.id, + "qty": 2, + }, + ] + } + with self._create_test_client(router=cart_router) as test_client: + response = test_client.post(f"/{so.uuid}/sync", content=json.dumps(data)) + self.assertEqual(response.status_code, 201, response.text) + + data = response.json() + self.assertEqual(data["uuid"], so.uuid) + self.assertEqual(len(data["lines"]), 2) + + lines = data["lines"] + self.assertEqual(lines[0]["qty"], 3) + self.assertEqual(lines[0]["product_id"], self.product_1.id) + self.assertEqual(lines[0]["options"]["note"], None) + + self.assertEqual(lines[1]["qty"], 4) + self.assertEqual(lines[1]["product_id"], self.product_2.id) + self.assertEqual(lines[1]["options"]["note"], None) + + def test_cart_different_options(self) -> None: + so = self.env["sale.order"]._create_empty_cart( + self.default_fastapi_authenticated_partner.id + ) + data = { + "transactions": [ + { + "uuid": self.trans_uuid_1, + "product_id": self.product_1.id, + "qty": 1, + "options": {"note": "test1"}, + }, + { + "uuid": self.trans_uuid_2, + "product_id": self.product_2.id, + "qty": 4, + "options": {"note": "test2"}, + }, + { + "uuid": self.trans_uuid_3, + "product_id": self.product_1.id, + "qty": 2, + "options": {"note": "test3"}, + }, + ] + } + with self._create_test_client(router=cart_router) as test_client: + response = test_client.post(f"/{so.uuid}/sync", content=json.dumps(data)) + self.assertEqual(response.status_code, 201, response.text) + + data = response.json() + self.assertEqual(data["uuid"], so.uuid) + self.assertEqual(len(data["lines"]), 3) + + lines = data["lines"] + self.assertEqual(lines[0]["qty"], 1) + self.assertEqual(lines[0]["product_id"], self.product_1.id) + self.assertEqual(lines[0]["options"]["note"], "test1") + + self.assertEqual(lines[1]["qty"], 4) + self.assertEqual(lines[1]["product_id"], self.product_2.id) + self.assertEqual(lines[1]["options"]["note"], "test2") + + self.assertEqual(lines[2]["qty"], 2) + self.assertEqual(lines[2]["product_id"], self.product_1.id) + self.assertEqual(lines[2]["options"]["note"], "test3") + + def test_cart_same_options(self) -> None: + so = self.env["sale.order"]._create_empty_cart( + self.default_fastapi_authenticated_partner.id + ) + data = { + "transactions": [ + { + "uuid": self.trans_uuid_1, + "product_id": self.product_1.id, + "qty": 1, + "options": {"note": "test"}, + }, + { + "uuid": self.trans_uuid_2, + "product_id": self.product_2.id, + "qty": 4, + "options": {"note": "test"}, + }, + { + "uuid": self.trans_uuid_3, + "product_id": self.product_1.id, + "qty": 2, + "options": {"note": "test"}, + }, + ] + } + with self._create_test_client(router=cart_router) as test_client: + response = test_client.post(f"/{so.uuid}/sync", content=json.dumps(data)) + self.assertEqual(response.status_code, 201, response.text) + + data = response.json() + self.assertEqual(data["uuid"], so.uuid) + self.assertEqual(len(data["lines"]), 2) + + lines = data["lines"] + self.assertEqual(lines[0]["qty"], 3) + self.assertEqual(lines[0]["product_id"], self.product_1.id) + self.assertEqual(lines[0]["options"]["note"], "test") + + self.assertEqual(lines[1]["qty"], 4) + self.assertEqual(lines[1]["product_id"], self.product_2.id) + self.assertEqual(lines[1]["options"]["note"], "test") + + def test_cart_two_options(self) -> None: + so = self.env["sale.order"]._create_empty_cart( + self.default_fastapi_authenticated_partner.id + ) + data = { + "transactions": [ + { + "uuid": self.trans_uuid_1, + "product_id": self.product_1.id, + "qty": 1, + "options": {"note": "test"}, + }, + { + "uuid": self.trans_uuid_2, + "product_id": self.product_2.id, + "qty": 4, + "options": {"note": "test"}, + }, + { + "uuid": self.trans_uuid_3, + "product_id": self.product_1.id, + "qty": 2, + "options": {"note": "test"}, + }, + { + "uuid": self.trans_uuid_4, + "product_id": self.product_1.id, + "qty": 5, + "options": {"note": "test", "special": True}, + }, + ] + } + with self._create_test_client(router=cart_router) as test_client: + response = test_client.post(f"/{so.uuid}/sync", content=json.dumps(data)) + self.assertEqual(response.status_code, 201, response.text) + + data = response.json() + self.assertEqual(data["uuid"], so.uuid) + self.assertEqual(len(data["lines"]), 3) + + lines = data["lines"] + self.assertEqual(lines[0]["qty"], 3) + self.assertEqual(lines[0]["product_id"], self.product_1.id) + self.assertEqual(lines[0]["options"]["note"], "test") + self.assertFalse(lines[0]["options"]["special"]) + + self.assertEqual(lines[1]["qty"], 4) + self.assertEqual(lines[1]["product_id"], self.product_2.id) + self.assertEqual(lines[1]["options"]["note"], "test") + self.assertFalse(lines[1]["options"]["special"]) + + self.assertEqual(lines[2]["qty"], 5) + self.assertEqual(lines[2]["product_id"], self.product_1.id) + self.assertEqual(lines[2]["options"]["note"], "test") + self.assertTrue(lines[2]["options"]["special"]) + + def test_cart_successive_transactions(self) -> None: + so = self.env["sale.order"]._create_empty_cart( + self.default_fastapi_authenticated_partner.id + ) + for tx in [ + { + "uuid": self.trans_uuid_1, + "product_id": self.product_1.id, + "qty": 1, + "options": {"note": "test"}, + }, + { + "uuid": self.trans_uuid_2, + "product_id": self.product_2.id, + "qty": 4, + "options": {"note": "test"}, + }, + { + "uuid": self.trans_uuid_3, + "product_id": self.product_1.id, + "qty": 2, + "options": {"note": "test"}, + }, + { + "uuid": self.trans_uuid_4, + "product_id": self.product_1.id, + "qty": 5, + "options": {"note": "test", "special": True}, + }, + ]: + data = {"transactions": [tx]} + with self._create_test_client(router=cart_router) as test_client: + response = test_client.post( + f"/{so.uuid}/sync", content=json.dumps(data) + ) + self.assertEqual(response.status_code, 201, response.text) + + data = response.json() + self.assertEqual(data["uuid"], so.uuid) + self.assertEqual(len(data["lines"]), 3) + + lines = data["lines"] + self.assertEqual(lines[0]["qty"], 3) + self.assertEqual(lines[0]["product_id"], self.product_1.id) + self.assertEqual(lines[0]["options"]["note"], "test") + self.assertFalse(lines[0]["options"]["special"]) + + self.assertEqual(lines[1]["qty"], 4) + self.assertEqual(lines[1]["product_id"], self.product_2.id) + self.assertEqual(lines[1]["options"]["note"], "test") + self.assertFalse(lines[1]["options"]["special"]) + + self.assertEqual(lines[2]["qty"], 5) + self.assertEqual(lines[2]["product_id"], self.product_1.id) + self.assertEqual(lines[2]["options"]["note"], "test") + self.assertTrue(lines[2]["options"]["special"]) + + def test_cart_transfer_options(self): + so1 = self.env["sale.order"]._create_empty_cart( + self.default_fastapi_authenticated_partner.id + ) + data = { + "transactions": [ + { + "uuid": self.trans_uuid_1, + "product_id": self.product_1.id, + "qty": 1, + "options": {"note": "test"}, + }, + { + "uuid": self.trans_uuid_2, + "product_id": self.product_2.id, + "qty": 4, + "options": {"note": "test"}, + }, + ] + } + with self._create_test_client(router=cart_router) as test_client: + response = test_client.post(f"/{so1.uuid}/sync", content=json.dumps(data)) + self.assertEqual(response.status_code, 201, response.text) + + so2 = self.env["sale.order"]._create_empty_cart( + self.default_fastapi_authenticated_partner.id + ) + data = { + "transactions": [ + { + "uuid": self.trans_uuid_3, + "product_id": self.product_1.id, + "qty": 2, + "options": {"note": "test"}, + }, + { + "uuid": self.trans_uuid_4, + "product_id": self.product_1.id, + "qty": 5, + "options": {"note": "test", "special": True}, + }, + ] + } + with self._create_test_client(router=cart_router) as test_client: + response = test_client.post(f"/{so2.uuid}/sync", content=json.dumps(data)) + self.assertEqual(response.status_code, 201, response.text) + + so1._transfer_cart(so2.partner_id.id) + + with self._create_test_client(router=cart_router) as test_client: + response = test_client.get("/") + self.assertEqual(response.status_code, 200, response.text) + + data = response.json() + self.assertEqual(data["uuid"], so2.uuid) + self.assertEqual(len(data["lines"]), 3) + + lines = data["lines"] + self.assertEqual(lines[0]["qty"], 3) + self.assertEqual(lines[0]["product_id"], self.product_1.id) + self.assertEqual(lines[0]["options"]["note"], "test") + self.assertFalse(lines[0]["options"]["special"]) + + self.assertEqual(lines[1]["qty"], 5) + self.assertEqual(lines[1]["product_id"], self.product_1.id) + self.assertEqual(lines[1]["options"]["note"], "test") + self.assertTrue(lines[1]["options"]["special"]) + + self.assertEqual(lines[2]["qty"], 4) + self.assertEqual(lines[2]["product_id"], self.product_2.id) + self.assertEqual(lines[2]["options"]["note"], "test") + self.assertFalse(lines[2]["options"]["special"]) + + def test_cart_transfer_no_options(self): + so1 = self.env["sale.order"]._create_empty_cart( + self.default_fastapi_authenticated_partner.id + ) + data = { + "transactions": [ + { + "uuid": self.trans_uuid_1, + "product_id": self.product_1.id, + "qty": 1, + }, + { + "uuid": self.trans_uuid_2, + "product_id": self.product_2.id, + "qty": 4, + }, + ] + } + with self._create_test_client(router=cart_router) as test_client: + response = test_client.post(f"/{so1.uuid}/sync", content=json.dumps(data)) + self.assertEqual(response.status_code, 201, response.text) + + so2 = self.env["sale.order"]._create_empty_cart( + self.default_fastapi_authenticated_partner.id + ) + data = { + "transactions": [ + { + "uuid": self.trans_uuid_3, + "product_id": self.product_1.id, + "qty": 2, + }, + { + "uuid": self.trans_uuid_4, + "product_id": self.product_1.id, + "qty": 5, + }, + ] + } + with self._create_test_client(router=cart_router) as test_client: + response = test_client.post(f"/{so2.uuid}/sync", content=json.dumps(data)) + self.assertEqual(response.status_code, 201, response.text) + + so1._transfer_cart(so2.partner_id.id) + + with self._create_test_client(router=cart_router) as test_client: + response = test_client.get("/") + self.assertEqual(response.status_code, 200, response.text) + + data = response.json() + self.assertEqual(data["uuid"], so2.uuid) + self.assertEqual(len(data["lines"]), 2) + + lines = data["lines"] + self.assertEqual(lines[0]["qty"], 8) + self.assertEqual(lines[0]["product_id"], self.product_1.id) + + self.assertEqual(lines[1]["qty"], 4) + self.assertEqual(lines[1]["product_id"], self.product_2.id)