diff --git a/shopinvader_api_sale/routers/__init__.py b/shopinvader_api_sale/routers/__init__.py index 7b95576c52..20c121efbd 100644 --- a/shopinvader_api_sale/routers/__init__.py +++ b/shopinvader_api_sale/routers/__init__.py @@ -1 +1,2 @@ from .sales import sale_router +from .sale_lines import sale_line_router diff --git a/shopinvader_api_sale/routers/sale_lines.py b/shopinvader_api_sale/routers/sale_lines.py new file mode 100644 index 0000000000..5e6f91b806 --- /dev/null +++ b/shopinvader_api_sale/routers/sale_lines.py @@ -0,0 +1,86 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from typing import Annotated + +from fastapi import APIRouter, Depends + +from odoo import api, fields, models + +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.sale.models.sale_order_line import SaleOrderLine +from odoo.addons.shopinvader_filtered_model.utils import FilteredModelAdapter + +from ..schemas import SaleLineSearch, SaleLineWithSale + +sale_line_router = APIRouter(tags=["sales"]) + + +@sale_line_router.get("/sale_lines") +def search( + params: Annotated[SaleLineSearch, Depends()], + paging: Annotated[Paging, Depends(paging)], + env: Annotated[api.Environment, Depends(authenticated_partner_env)], + partner: Annotated[ResPartner, Depends(authenticated_partner)], +) -> PagedCollection[SaleLineWithSale]: + """Get / search sale order lines. The list contains only sale order lines from the + authenticated user""" + count, sols = ( + env["shopinvader_api_sale.sale_line_router.helper"] + .new({"partner": partner}) + ._search(paging, params) + ) + return PagedCollection[SaleLineWithSale]( + count=count, + items=[SaleLineWithSale.from_sale_order_line(sol) for sol in sols], + ) + + +@sale_line_router.get("/sale_lines/{sale_line_id}") +def get( + sale_line_id: int, + env: Annotated[api.Environment, Depends(authenticated_partner_env)], + partner: Annotated[ResPartner, Depends(authenticated_partner)], +) -> SaleLineWithSale: + """ + Get sale order of authenticated user with specific sale_id + """ + return SaleLineWithSale.from_sale_order_line( + env["shopinvader_api_sale.sale_line_router.helper"] + .new({"partner": partner}) + ._get(sale_line_id) + ) + + +class ShopinvaderApiSaleSaleLineRouterHelper(models.AbstractModel): + _name = "shopinvader_api_sale.sale_line_router.helper" + _description = "Shopinvader Api Sale Line Service Helper" + + partner = fields.Many2one("res.partner") + + def _get_domain_adapter(self): + return [ + ("order_id.partner_id", "=", self.partner.id), + ("order_id.typology", "=", "sale"), + ] + + @property + def model_adapter(self) -> FilteredModelAdapter[SaleOrderLine]: + return FilteredModelAdapter[SaleOrderLine](self.env, self._get_domain_adapter()) + + def _get(self, record_id) -> SaleOrderLine: + return self.model_adapter.get(record_id) + + def _search(self, paging, params) -> tuple[int, SaleOrderLine]: + return self.model_adapter.search_with_count( + params.to_odoo_domain(self.env), + limit=paging.limit, + offset=paging.offset, + ) diff --git a/shopinvader_api_sale/schemas.py b/shopinvader_api_sale/schemas.py new file mode 100644 index 0000000000..023aae8de3 --- /dev/null +++ b/shopinvader_api_sale/schemas.py @@ -0,0 +1,65 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from datetime import datetime +from typing import Annotated + +from extendable_pydantic import StrictExtendableBaseModel +from pydantic import Field + +from odoo import api + +from odoo.addons.shopinvader_schema_sale.schemas.sale_line import SaleLine + + +class SaleOrderFromLine(StrictExtendableBaseModel): + id: int + name: str + state: str + date_order: datetime + + @classmethod + def from_sale_order(cls, odoo_rec): + return cls.model_construct( + id=odoo_rec.id, + name=odoo_rec.name, + state=odoo_rec.state, + date_order=odoo_rec.date_order, + ) + + +class SaleLineWithSale(SaleLine): + order: SaleOrderFromLine + + @classmethod + def from_sale_order_line(cls, odoo_rec): + res = super().from_sale_order_line(odoo_rec) + res.order = SaleOrderFromLine.from_sale_order(odoo_rec.order_id) + return res + + +class SaleLineSearch(StrictExtendableBaseModel, extra="ignore"): + order_name: Annotated[ + str | None, + Field( + description="When used, the search look for any sale order lines " + "where the order name contains the given value case insensitively." + ), + ] = None + product_name: Annotated[ + str | None, + Field( + description="When used, the search look for any sale order lines " + "where the product name contains the given value case insensitively." + ), + ] = None + + def to_odoo_domain(self, env: api.Environment): + domain = [] + if self.order_name: + domain.append(("order_id.name", "ilike", self.order_name)) + + if self.product_name: + domain.append(("product_id.name", "ilike", self.product_name)) + + return domain diff --git a/shopinvader_api_sale/tests/test_shopinvader_sale_api.py b/shopinvader_api_sale/tests/test_shopinvader_sale_api.py index f3727ac559..a22945f64b 100644 --- a/shopinvader_api_sale/tests/test_shopinvader_sale_api.py +++ b/shopinvader_api_sale/tests/test_shopinvader_sale_api.py @@ -8,7 +8,7 @@ from odoo.addons.extendable_fastapi.tests.common import FastAPITransactionCase -from ..routers import sale_router +from ..routers import sale_line_router, sale_router @tagged("post_install", "-at_install") @@ -94,3 +94,65 @@ def test_download(self): response: Response = test_client.get(f"/sales/{sale.id}/download") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.headers["Content-Type"], "application/pdf") + + def test_search_sale_lines(self): + so1 = self.env["sale.order"].create( + {"partner_id": self.default_fastapi_authenticated_partner.id} + ) + so1.write( + { + "order_line": [ + (0, 0, {"product_id": self.product_1.id, "product_uom_qty": 2}), + (0, 0, {"product_id": self.product_2.id, "product_uom_qty": 6}), + ] + } + ) + so2 = self.env["sale.order"].create( + {"partner_id": self.default_fastapi_authenticated_partner.id} + ) + so2.write( + { + "order_line": [ + (0, 0, {"product_id": self.product_1.id, "product_uom_qty": 1}), + (0, 0, {"product_id": self.product_2.id, "product_uom_qty": 3}), + (0, 0, {"product_id": self.product_1.id, "product_uom_qty": 4}), + ] + } + ) + + with self._create_test_client(router=sale_line_router) as test_client: + response: Response = test_client.get("/sale_lines") + self.assertEqual(response.status_code, status.HTTP_200_OK) + lines = response.json() + self.assertEqual(lines["count"], 5) + sols = lines["items"] + self.assertEqual(sols[0]["order"]["id"], so2.id) + self.assertEqual(sols[1]["order"]["id"], so2.id) + self.assertEqual(sols[2]["order"]["id"], so2.id) + self.assertEqual(sols[3]["order"]["id"], so1.id) + self.assertEqual(sols[4]["order"]["id"], so1.id) + self.assertEqual(sols[0]["order"]["name"], so2.name) + self.assertEqual(sols[1]["order"]["name"], so2.name) + self.assertEqual(sols[2]["order"]["name"], so2.name) + self.assertEqual(sols[3]["order"]["name"], so1.name) + self.assertEqual(sols[4]["order"]["name"], so1.name) + self.assertEqual(sols[0]["order"]["state"], so2.state) + self.assertEqual(sols[1]["order"]["state"], so2.state) + self.assertEqual(sols[2]["order"]["state"], so2.state) + self.assertEqual(sols[3]["order"]["state"], so1.state) + self.assertEqual(sols[4]["order"]["state"], so1.state) + self.assertEqual( + sols[0]["order"]["date_order"], so2.date_order.isoformat(timespec="seconds") + ) + self.assertEqual( + sols[1]["order"]["date_order"], so2.date_order.isoformat(timespec="seconds") + ) + self.assertEqual( + sols[2]["order"]["date_order"], so2.date_order.isoformat(timespec="seconds") + ) + self.assertEqual( + sols[3]["order"]["date_order"], so1.date_order.isoformat(timespec="seconds") + ) + self.assertEqual( + sols[4]["order"]["date_order"], so1.date_order.isoformat(timespec="seconds") + )