Skip to content

Commit

Permalink
add request and update quotation, and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
matthieu.saison committed Nov 14, 2023
1 parent 3a2c41d commit 0eef8e8
Show file tree
Hide file tree
Showing 13 changed files with 221 additions and 30 deletions.
1 change: 1 addition & 0 deletions shopinvader_api_quotation/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file is going to be generated by oca-gen-addon-readme.
10 changes: 9 additions & 1 deletion shopinvader_api_quotation/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@
"website": "https://github.com/shopinvader/odoo-shopinvader",
"author": "Akretion",
"license": "AGPL-3",
"depends": ["crm", "fastapi", "shopinvader_schema_sale", "shopinvader_api_cart"],
"depends": [
"crm",
"fastapi",
"shopinvader_schema_sale",
"shopinvader_api_security_sale",
"shopinvader_api_cart",
"shopinvader_api_sale",
"sale_cart",
],
"external_dependencies": {
"python": [
"fastapi",
Expand Down
1 change: 1 addition & 0 deletions shopinvader_api_quotation/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import sale_order
from . import product_template
42 changes: 42 additions & 0 deletions shopinvader_api_quotation/models/product_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright 2017-2018 Akretion (http://www.akretion.com).
# Copyright 2021 Camptocamp (https://www.camptocamp.com).
# @author Benoît GUILLOT <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import api, fields, models


class ProductTemplate(models.Model):
_inherit = "product.template"

shop_only_quotation = fields.Boolean(
string="Shopinvader: Only for Quotation",
compute="_compute_shop_only_quotation",
inverse="_inverse_shop_only_quotation",
store=True,
)

@api.depends("product_variant_ids.shop_only_quotation")
def _compute_shop_only_quotation(self):
# True only if true for all its variants
for rec in self:
rec.shop_only_quotation = (
all(rec.product_variant_ids.mapped("shop_only_quotation"))
if rec.product_variant_ids
else False
)

def _inverse_shop_only_quotation(self):
# Sets the value on all its variants
for rec in self:
rec.product_variant_ids.shop_only_quotation = rec.shop_only_quotation

def _create_variant_ids(self):
# Make sure new variants have the same value than the template.
templates = self.filtered("shop_only_quotation")
res = super()._create_variant_ids()
products = templates.product_variant_ids.filtered(
lambda rec: not rec.shop_only_quotation
)
products.shop_only_quotation = True
return res
29 changes: 26 additions & 3 deletions shopinvader_api_quotation/models/sale_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).


from odoo import fields, models

# from odoo.exceptions import UserError
from odoo import _, fields, models
from odoo.exceptions import UserError


class SaleOrder(models.Model):
Expand All @@ -16,3 +15,27 @@ class SaleOrder(models.Model):
selection_add=[("quotation", "Quotation")],
ondelete={"quotation": "cascade"},
)
available_for_quotation = fields.Boolean(compute="_compute_available_for_quotation")
shop_only_quotation = fields.Boolean(compute="_compute_shop_only_quotation")

def action_request_quotation(self):
if any(rec.state != "draft" or rec.typology != "cart" for rec in self):
raise UserError(
_(
"Only orders of cart typology in draft state "
"can be converted to quotation"
)
)
for rec in self:
rec.typology = "quotation"
return self

def _compute_available_for_quotation(self):
for record in self:
record.available_for_quotation = True

def _compute_shop_only_quotation(self):
for record in self:
record.shop_only_quotation = any(
record.order_line.product_id.mapped("shop_only_quotation")
)
5 changes: 5 additions & 0 deletions shopinvader_api_quotation/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
* Sebastien BEAU <[email protected]>
* Benoît GUILLOT <[email protected]>
* Iván Todorovich <[email protected]>
* Simone Orsi <[email protected]>
* Matthieu Saison <[email protected]>
4 changes: 4 additions & 0 deletions shopinvader_api_quotation/readme/CREDITS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The development of this module has been financially supported by:

* Akretion R&D
* LaboAndCo
11 changes: 11 additions & 0 deletions shopinvader_api_quotation/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions shopinvader_api_quotation/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import quotation
from . import cart
26 changes: 26 additions & 0 deletions shopinvader_api_quotation/routers/cart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Annotated

from fastapi import APIRouter, Depends

from odoo import api

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_schema_sale.schemas.sale import Sale

cart_router = APIRouter(tags=["carts"])


@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["sale.order"]._find_open_cart(partner.id, uuid)
sale.action_request_quotation()

return Sale.from_sale_order(sale)
62 changes: 37 additions & 25 deletions shopinvader_api_quotation/routers/quotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
paging,
)
from odoo.addons.fastapi.schemas import Paging
from odoo.addons.fastapi.utils import FilteredDomainAdapter
from odoo.addons.shopinvader_schema_sale.schemas.sale_order import Sale, SaleSearch
from odoo.addons.shopinvader_schema_sale.schemas.sale import Sale, SaleSearch

from ..schemas.sale import QuotationUpdateInput

# create a router
quotation_router = APIRouter(tags=["quotations"])
Expand All @@ -27,7 +28,9 @@ def get(
quotation_id: int | None = None,
) -> Sale | None:
return Sale.from_sale_order(
env["shopinvader_api_quotation.quotations_router.helper"]._get(quotation_id)
env["shopinvader_api_sale.sales_router.helper"]
.new({"partner": partner})
._get(quotation_id)
)


Expand All @@ -37,11 +40,11 @@ def confirm_quotation(
partner: Annotated[ResPartner, Depends(authenticated_partner)],
quotation_id: int | None = None,
) -> None:
order = env["shopinvader_api_quotation.quotations_router.helper"]._confirm(
quotation_id
order = (
env["shopinvader_api_sale.sales_router.helper"]
.new({"partner": partner})
._confirm(quotation_id)
)
# env["sale.order"]._confirm(order)
# order.action_confirm()
return Sale.from_sale_order(order)


Expand All @@ -52,34 +55,43 @@ def search_quotation(
env: Annotated[Environment, Depends(authenticated_partner_env)],
partner: Annotated[ResPartner, Depends(authenticated_partner)],
) -> PagedCollection[Sale]:
count, orders = env["shopinvader_api_quotation.quotations_router.helper"]._search(
paging, params
count, orders = (
env["shopinvader_api_sale.sales_router.helper"]
.new({"partner": partner})
._search(paging, params)
)
return PagedCollection[Sale](
count=count,
items=[Sale.from_sale_order(order) for order in orders],
)


class ShopinvaderApiQuotationquotationsRouterHelper(models.AbstractModel):
_name = "shopinvader_api_quotation.quotations_router.helper"
_description = "Shopinvader Api Quotation Service Helper"
@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_sale.sales_router.helper"]
.new({"partner": partner})
._get(quotation_id)
)

vals = data.to_sale_order_vals()
order.write(vals)
return Sale.from_sale_order(order)

@property
def adapter(self):
return FilteredDomainAdapter(
self.env["sale.order"], [("typology", "=", "quotation")]
)

def _get(self, record_id):
return self.adapter.get(record_id)
class ShopinvaderApiSaleSalesRouterHelper(models.AbstractModel):
_inherit = "shopinvader_api_sale.sales_router.helper"

def _search(self, paging, params):
return self.adapter.search_with_count(
params.to_odoo_domain(),
limit=paging.limit,
offset=paging.offset,
)
def _get_domain_adapter(self):
return [
("partner_id", "=", self.partner.id),
("typology", "=", "quotation"),
]

def _confirm(self, quotation):
order = self._get(quotation)
Expand Down
25 changes: 25 additions & 0 deletions shopinvader_api_quotation/schemas/sale.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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

@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.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}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# 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


Expand Down Expand Up @@ -35,7 +38,7 @@ def setUpClass(cls) -> None:
0,
[
cls.env.ref(
"shopinvader_api_cart.shopinvader_cart_user_group"
"shopinvader_api_security_sale.shopinvader_sale_user_group"
).id,
],
)
Expand Down Expand Up @@ -98,3 +101,32 @@ def test_confirm_quotation(self):
response: Response = test_client.get(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)

0 comments on commit 0eef8e8

Please sign in to comment.