From 2fead05512663f63b880dfeb45122d98eeeb6ad8 Mon Sep 17 00:00:00 2001 From: "matthieu.saison" Date: Mon, 27 Nov 2023 17:42:42 +0100 Subject: [PATCH] migrate shopinvader quotation to fastapi --- .../odoo/addons/shopinvader_api_quotation | 1 + setup/shopinvader_api_quotation/setup.py | 6 + shopinvader_api_quotation/README.rst | 82 ++++ shopinvader_api_quotation/__init__.py | 1 + shopinvader_api_quotation/__manifest__.py | 28 ++ .../readme/CONTRIBUTORS.rst | 5 + shopinvader_api_quotation/readme/CREDITS.rst | 4 + .../readme/DESCRIPTION.rst | 11 + shopinvader_api_quotation/routers/__init__.py | 2 + shopinvader_api_quotation/routers/cart.py | 34 ++ .../routers/quotation.py | 102 ++++ shopinvader_api_quotation/schemas/__init__.py | 1 + shopinvader_api_quotation/schemas/sale.py | 27 ++ .../static/description/index.html | 436 ++++++++++++++++++ shopinvader_api_quotation/tests/__init__.py | 1 + .../tests/test_shopinvader_api_quotation.py | 132 ++++++ 16 files changed, 873 insertions(+) create mode 120000 setup/shopinvader_api_quotation/odoo/addons/shopinvader_api_quotation create mode 100644 setup/shopinvader_api_quotation/setup.py create mode 100644 shopinvader_api_quotation/README.rst create mode 100644 shopinvader_api_quotation/__init__.py create mode 100644 shopinvader_api_quotation/__manifest__.py create mode 100644 shopinvader_api_quotation/readme/CONTRIBUTORS.rst create mode 100644 shopinvader_api_quotation/readme/CREDITS.rst create mode 100644 shopinvader_api_quotation/readme/DESCRIPTION.rst create mode 100644 shopinvader_api_quotation/routers/__init__.py create mode 100644 shopinvader_api_quotation/routers/cart.py create mode 100644 shopinvader_api_quotation/routers/quotation.py create mode 100644 shopinvader_api_quotation/schemas/__init__.py create mode 100644 shopinvader_api_quotation/schemas/sale.py create mode 100644 shopinvader_api_quotation/static/description/index.html create mode 100644 shopinvader_api_quotation/tests/__init__.py create mode 100644 shopinvader_api_quotation/tests/test_shopinvader_api_quotation.py diff --git a/setup/shopinvader_api_quotation/odoo/addons/shopinvader_api_quotation b/setup/shopinvader_api_quotation/odoo/addons/shopinvader_api_quotation new file mode 120000 index 0000000000..1c2757bcc2 --- /dev/null +++ b/setup/shopinvader_api_quotation/odoo/addons/shopinvader_api_quotation @@ -0,0 +1 @@ +../../../../shopinvader_api_quotation \ No newline at end of file diff --git a/setup/shopinvader_api_quotation/setup.py b/setup/shopinvader_api_quotation/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/shopinvader_api_quotation/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/shopinvader_api_quotation/README.rst b/shopinvader_api_quotation/README.rst new file mode 100644 index 0000000000..27e92755fe --- /dev/null +++ b/shopinvader_api_quotation/README.rst @@ -0,0 +1,82 @@ +========================= +Shopinvader Api Quotation +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6927e0db040c31a025248a0b3b7bfae37432b4ab69f097ef9f857071489feb5b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_quotation + :alt: shopinvader/odoo-shopinvader + +|badge1| |badge2| |badge3| + +This module adds a REST API for shopinvader to manage quotations. + +The Customer can convert a cart into a quotation (the typology of the sale +order is set to quotation). + +Initially, the quotation has the `shopinvader_state` "estimating". +After updating the price manually when the button "sent" on Odoo backend +is submitted, the quotation will be sent by email (native behaviour) and the +shopinvader_state will switch to "estimated". + +On Shopinvader site, the customer can see the state, the amount ... of quotation. + +**Table of contents** + +.. contents:: + :local: + +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 +~~~~~~~~~~~~ + +* Sebastien BEAU +* Benoît GUILLOT +* Iván Todorovich +* Simone Orsi +* Matthieu Saison + +Other credits +~~~~~~~~~~~~~ + +The development of this module has been financially supported by: + +* Akretion R&D +* LaboAndCo + +Maintainers +~~~~~~~~~~~ + +This module is part of the `shopinvader/odoo-shopinvader `_ project on GitHub. + +You are welcome to contribute. diff --git a/shopinvader_api_quotation/__init__.py b/shopinvader_api_quotation/__init__.py new file mode 100644 index 0000000000..62a5d54f85 --- /dev/null +++ b/shopinvader_api_quotation/__init__.py @@ -0,0 +1 @@ +from . import routers diff --git a/shopinvader_api_quotation/__manifest__.py b/shopinvader_api_quotation/__manifest__.py new file mode 100644 index 0000000000..c76fb89108 --- /dev/null +++ b/shopinvader_api_quotation/__manifest__.py @@ -0,0 +1,28 @@ +{ + "name": "Shopinvader Api Quotation", + "summary": "Shopinvader Quotation", + "version": "16.0.1.0.1", + "category": "e-commerce", + "website": "https://github.com/shopinvader/odoo-shopinvader", + "author": "Akretion", + "license": "AGPL-3", + "depends": [ + "crm", + "fastapi", + "shopinvader_schema_sale", + "shopinvader_api_security_sale", + "shopinvader_api_cart", + "shopinvader_api_sale", + "sale_cart", + "sale_quotation", + ], + "data": [], + "external_dependencies": { + "python": [ + "fastapi", + "pydantic>=2.0.0", + "extendable_pydantic>=1.0.0", + ] + }, + "installable": True, +} diff --git a/shopinvader_api_quotation/readme/CONTRIBUTORS.rst b/shopinvader_api_quotation/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..f12b3c8321 --- /dev/null +++ b/shopinvader_api_quotation/readme/CONTRIBUTORS.rst @@ -0,0 +1,5 @@ +* Sebastien BEAU +* Benoît GUILLOT +* Iván Todorovich +* Simone Orsi +* Matthieu Saison diff --git a/shopinvader_api_quotation/readme/CREDITS.rst b/shopinvader_api_quotation/readme/CREDITS.rst new file mode 100644 index 0000000000..2ec2b3dd73 --- /dev/null +++ b/shopinvader_api_quotation/readme/CREDITS.rst @@ -0,0 +1,4 @@ +The development of this module has been financially supported by: + +* Akretion R&D +* LaboAndCo diff --git a/shopinvader_api_quotation/readme/DESCRIPTION.rst b/shopinvader_api_quotation/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..020f938663 --- /dev/null +++ b/shopinvader_api_quotation/readme/DESCRIPTION.rst @@ -0,0 +1,11 @@ +This module adds a REST API for shopinvader to manage quotations. + +The Customer can convert a cart into a quotation (the typology of the sale +order is set to quotation). + +Initially, the quotation has the `shopinvader_state` "estimating". +After updating the price manually when the button "sent" on Odoo backend +is submitted, the quotation will be sent by email (native behaviour) and the +shopinvader_state will switch to "estimated". + +On Shopinvader site, the customer can see the state, the amount ... of quotation. diff --git a/shopinvader_api_quotation/routers/__init__.py b/shopinvader_api_quotation/routers/__init__.py new file mode 100644 index 0000000000..731dc1aa9b --- /dev/null +++ b/shopinvader_api_quotation/routers/__init__.py @@ -0,0 +1,2 @@ +from . import quotation +from . import cart diff --git a/shopinvader_api_quotation/routers/cart.py b/shopinvader_api_quotation/routers/cart.py new file mode 100644 index 0000000000..6f71211ded --- /dev/null +++ b/shopinvader_api_quotation/routers/cart.py @@ -0,0 +1,34 @@ +from typing import Annotated + +from fastapi import Depends + +from odoo import api, models + +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.shopinvader_api_cart.routers import cart_router +from odoo.addons.shopinvader_schema_sale.schemas.sale import Sale + + +@cart_router.post("/{uuid}/request_quotation") +def request_quotation( + env: Annotated[api.Environment, Depends(authenticated_partner_env)], + partner: Annotated["ResPartner", Depends(authenticated_partner)], + uuid: str | None = None, +) -> Sale: + sale = env["shopinvader_api_cart.cart_router.helper"]._request_quotation( + partner, uuid + ) + return Sale.from_sale_order(sale) + + +class ShopinvaderApiCartRouterHelper(models.AbstractModel): + _inherit = "shopinvader_api_cart.cart_router.helper" + + def _request_quotation(self, partner, uuid): + sale = self.env["sale.order"]._find_open_cart(partner.id, uuid) + sale.action_request_quotation() + return sale diff --git a/shopinvader_api_quotation/routers/quotation.py b/shopinvader_api_quotation/routers/quotation.py new file mode 100644 index 0000000000..7e77fc9709 --- /dev/null +++ b/shopinvader_api_quotation/routers/quotation.py @@ -0,0 +1,102 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends + +from odoo import models +from odoo.api import Environment + +from odoo.addons.base.models.res_partner import Partner as ResPartner +from odoo.addons.extendable_fastapi.schemas import PagedCollection +from odoo.addons.fastapi.dependencies import ( + authenticated_partner, + authenticated_partner_env, + paging, +) +from odoo.addons.fastapi.schemas import Paging +from odoo.addons.shopinvader_schema_sale.schemas.sale import Sale, SaleSearch + +from ..schemas.sale import QuotationUpdateInput + +# create a router +quotation_router = APIRouter(tags=["quotations"]) + + +@quotation_router.get("/quotations/{quotation_id}") +def get( + env: Annotated[Environment, Depends(authenticated_partner_env)], + partner: Annotated[ResPartner, Depends(authenticated_partner)], + quotation_id: int, +) -> Sale | None: + return Sale.from_sale_order( + env["shopinvader_api_quotation.quotations_router.helper"] + .new({"partner": partner}) + ._get(quotation_id) + ) + + +@quotation_router.post("/quotations/{quotation_id}/confirm", status_code=200) +def confirm_quotation( + env: Annotated[Environment, Depends(authenticated_partner_env)], + partner: Annotated[ResPartner, Depends(authenticated_partner)], + quotation_id: int, +) -> None: + order = ( + env["shopinvader_api_quotation.quotations_router.helper"] + .new({"partner": partner}) + ._confirm(quotation_id) + ) + return Sale.from_sale_order(order) + + +@quotation_router.get("/quotations", status_code=200) +def search_quotation( + params: Annotated[SaleSearch, Depends()], + paging: Annotated[Paging, Depends(paging)], + env: Annotated[Environment, Depends(authenticated_partner_env)], + partner: Annotated[ResPartner, Depends(authenticated_partner)], +) -> PagedCollection[Sale]: + count, orders = ( + env["shopinvader_api_quotation.quotations_router.helper"] + .new({"partner": partner}) + ._search(paging, params) + ) + return PagedCollection[Sale]( + count=count, + items=[Sale.from_sale_order(order) for order in orders], + ) + + +@quotation_router.post("/quotations/{quotation_id}") +def update_quotation( + data: QuotationUpdateInput, + env: Annotated[Environment, Depends(authenticated_partner_env)], + partner: Annotated["ResPartner", Depends(authenticated_partner)], + quotation_id: int, +) -> Sale: + order = ( + env["shopinvader_api_quotation.quotations_router.helper"] + .new({"partner": partner}) + ._update(quotation_id, data) + ) + return Sale.from_sale_order(order) + + +class ShopinvaderApiSaleSalesRouterHelper(models.AbstractModel): + _name = "shopinvader_api_quotation.quotations_router.helper" + _inherit = "shopinvader_api_sale.sales_router.helper" + + def _get_domain_adapter(self): + return [ + ("partner_id", "=", self.partner.id), + ("typology", "=", "quotation"), + ] + + def _confirm(self, quotation_id): + order = self._get(quotation_id) + order.action_confirm_quotation() + return order + + def _update(self, quotation_id, data): + order = self._get(quotation_id) + order.write(data.to_sale_order_vals()) + return order diff --git a/shopinvader_api_quotation/schemas/__init__.py b/shopinvader_api_quotation/schemas/__init__.py new file mode 100644 index 0000000000..8a0dc04e1f --- /dev/null +++ b/shopinvader_api_quotation/schemas/__init__.py @@ -0,0 +1 @@ +from . import sale diff --git a/shopinvader_api_quotation/schemas/sale.py b/shopinvader_api_quotation/schemas/sale.py new file mode 100644 index 0000000000..f35b524d94 --- /dev/null +++ b/shopinvader_api_quotation/schemas/sale.py @@ -0,0 +1,27 @@ +from extendable_pydantic import StrictExtendableBaseModel + +from odoo.addons.shopinvader_schema_sale.schemas.sale import Sale + + +class Sale(Sale, extends=True): + available_for_quotation: bool | None = None + shop_only_quotation: bool | None = None + customer_ref: str | None = None + + @classmethod + def from_sale_order(cls, odoo_rec): + res = super().from_sale_order(odoo_rec) + res.available_for_quotation = True + res.shop_only_quotation = odoo_rec.shop_only_quotation + res.customer_ref = odoo_rec.client_order_ref or None + # res.shop_only_quotation = any( + # odoo_rec.order_line.product_id.mapped("shop_only_quotation") + # ) mettre un champs calculé coté odoo sur model sale_order + return res + + +class QuotationUpdateInput(StrictExtendableBaseModel): + customer_ref: str | None = None + + def to_sale_order_vals(self) -> dict: + return {"client_order_ref": self.customer_ref} diff --git a/shopinvader_api_quotation/static/description/index.html b/shopinvader_api_quotation/static/description/index.html new file mode 100644 index 0000000000..16a4b425fe --- /dev/null +++ b/shopinvader_api_quotation/static/description/index.html @@ -0,0 +1,436 @@ + + + + + + +Shopinvader Api Quotation + + + +
+

Shopinvader Api Quotation

+ + +

Beta License: AGPL-3 shopinvader/odoo-shopinvader

+

This module adds a REST API for shopinvader to manage quotations.

+

The Customer can convert a cart into a quotation (the typology of the sale +order is set to quotation).

+

Initially, the quotation has the shopinvader_state “estimating”. +After updating the price manually when the button “sent” on Odoo backend +is submitted, the quotation will be sent by email (native behaviour) and the +shopinvader_state will switch to “estimated”.

+

On Shopinvader site, the customer can see the state, the amount … of quotation.

+

Table of contents

+ +
+

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

+ +
+
+

Other credits

+

The development of this module has been financially supported by:

+
    +
  • Akretion R&D
  • +
  • LaboAndCo
  • +
+
+
+

Maintainers

+

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

+

You are welcome to contribute.

+
+
+
+ + diff --git a/shopinvader_api_quotation/tests/__init__.py b/shopinvader_api_quotation/tests/__init__.py new file mode 100644 index 0000000000..eb758d8b7f --- /dev/null +++ b/shopinvader_api_quotation/tests/__init__.py @@ -0,0 +1 @@ +from . import test_shopinvader_api_quotation diff --git a/shopinvader_api_quotation/tests/test_shopinvader_api_quotation.py b/shopinvader_api_quotation/tests/test_shopinvader_api_quotation.py new file mode 100644 index 0000000000..71de6fcb39 --- /dev/null +++ b/shopinvader_api_quotation/tests/test_shopinvader_api_quotation.py @@ -0,0 +1,132 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json + +from fastapi import status +from requests import Response + +from odoo.tests.common import tagged + +from odoo.addons.extendable_fastapi.tests.common import FastAPITransactionCase + +from ..routers.cart import cart_router +from ..routers.quotation import quotation_router + + +@tagged("post_install", "-at_install") +class TestQuotation(FastAPITransactionCase): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + partner = cls.env["res.partner"].create({"name": "FastAPI Cart Demo"}) + + cls.user_no_rights = cls.env["res.users"].create( + { + "name": "Test User Without Rights", + "login": "user_no_rights", + "groups_id": [(6, 0, [])], + } + ) + user_with_rights = cls.env["res.users"].create( + { + "name": "Test User With Rights", + "login": "user_with_rights", + "groups_id": [ + ( + 6, + 0, + [ + cls.env.ref( + "shopinvader_api_security_sale.shopinvader_sale_user_group" + ).id, + ], + ) + ], + } + ) + cls.default_fastapi_running_user = user_with_rights + cls.default_fastapi_authenticated_partner = partner.with_user(user_with_rights) + cls.default_fastapi_router = quotation_router + + cls.partner_in_user_no_rights = cls.env(user=cls.user_no_rights)[ + "res.partner" + ].browse(cls.default_fastapi_authenticated_partner.id) + + cls.product_1 = cls.env["product.product"].create( + { + "name": "product_1", + "uom_id": cls.env.ref("uom.product_uom_unit").id, + } + ) + cls.product_2 = cls.env["product.product"].create( + { + "name": "product_2", + "uom_id": cls.env.ref("uom.product_uom_unit").id, + } + ) + + def test_search_quotations(self): + self.env["sale.order"].create( + { + "partner_id": self.default_fastapi_authenticated_partner.id, + "typology": "quotation", + } + ) + with self._create_test_client(router=quotation_router) as test_client: + response: Response = test_client.get("/quotations") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["count"], 1) + + def test_get_quotation(self): + sale = self.env["sale.order"].create( + { + "partner_id": self.default_fastapi_authenticated_partner.id, + "typology": "quotation", + } + ) + with self._create_test_client(router=quotation_router) as test_client: + response: Response = test_client.get(f"/quotations/{sale.id}") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["name"], sale.name) + + def test_confirm_quotation(self): + quotation = self.env["sale.order"].create( + { + "partner_id": self.default_fastapi_authenticated_partner.id, + "typology": "quotation", + } + ) + with self._create_test_client(router=quotation_router) as test_client: + response: Response = test_client.post(f"/quotations/{quotation.id}/confirm") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["id"], quotation.id) + + def test_request_quotation(self): + cart = self.env["sale.order"]._create_empty_cart( + self.default_fastapi_authenticated_partner.id + ) + with self._create_test_client(router=cart_router) as test_client: + response: Response = test_client.post(f"/{cart.uuid}/request_quotation") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["uuid"], cart.uuid) + self.assertEqual(response.json()["typology"], "quotation") + + def test_update_quotation(self): + data = {"customer_ref": "PO_123123"} + quotation = self.env["sale.order"].create( + { + "partner_id": self.default_fastapi_authenticated_partner.id, + "typology": "quotation", + } + ) + with self._create_test_client(router=quotation_router) as test_client: + response: Response = test_client.post( + f"/quotations/{quotation.id}", content=json.dumps(data) + ) + self.assertEqual( + response.status_code, + status.HTTP_200_OK, + msg=f"error message: {response.text}", + ) + self.assertEqual(response.json()["id"], quotation.id)