diff --git a/shopinvader_delivery_carrier/controllers/main.py b/shopinvader_delivery_carrier/controllers/main.py index d7d3f401ca..35ef2cbb1a 100644 --- a/shopinvader_delivery_carrier/controllers/main.py +++ b/shopinvader_delivery_carrier/controllers/main.py @@ -2,15 +2,12 @@ # Cédric Pigeon # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -import logging - from odoo.addons.shopinvader.controllers import main from odoo.http import route -_logger = logging.getLogger(__name__) - class InvaderController(main.InvaderController): @route(["/shopinvader/cart/get_delivery_methods"], methods=["GET"]) def get_delivery_methods(self, **params): + # deprecated!!! return self._process_method("cart", "get_delivery_methods", params) diff --git a/shopinvader_delivery_carrier/models/__init__.py b/shopinvader_delivery_carrier/models/__init__.py index 1f6bccc058..4f04ce793b 100644 --- a/shopinvader_delivery_carrier/models/__init__.py +++ b/shopinvader_delivery_carrier/models/__init__.py @@ -1,4 +1,5 @@ from . import backend +from . import delivery_carrier from . import sale from . import shopinvader_notification from . import stock_picking diff --git a/shopinvader_delivery_carrier/models/delivery_carrier.py b/shopinvader_delivery_carrier/models/delivery_carrier.py new file mode 100644 index 0000000000..5f51d9c9b5 --- /dev/null +++ b/shopinvader_delivery_carrier/models/delivery_carrier.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 ACSONE SA/NV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from contextlib import contextmanager + +from odoo import api, models + + +class DeliveryCarrier(models.Model): + _inherit = "delivery.carrier" + + @contextmanager + def _simulate_delivery_cost(self, partner): + """ + Change the env mode (draft) to avoid real update on the partner. + Then, restore the partner with previous values. + :param partner: res.partner recordset + :return: + """ + partner.read(["country_id", "zip"]) + partner_values = partner._convert_to_write(partner._cache) + with partner.env.do_in_draft(): + yield + # Restore values + partner.update(partner_values) + + @api.model + def _get_country_from_context(self): + """ + Load the country from context + :return: res.country recordset + """ + country_id = self.env.context.get("delivery_force_country_id", 0) + return self.env["res.country"].browse(country_id) + + @api.model + def _get_zip_from_context(self): + """ + Load the zip code from context + :return: str + """ + return self.env.context.get("delivery_force_zip_code", "") + + @api.multi + def available_carriers(self, contact): + """ + Inherit the function to force some values on the given contact + (only in cache). + :param contact: res.partner recordset + :return: False or self + """ + country = self._get_country_from_context() + zip_code = self._get_zip_from_context() + if country or zip_code: + with self._simulate_delivery_cost(contact): + # Edit country and zip + # Even if some info are not provided, we have to fill them + # Ex: if the zip code is not provided, we have to do the + # simulation with an empty zip code on the partner. Because his + # current zip could be related to another country and simulate + # a wrong price. + contact.update({"country_id": country.id, "zip": zip_code}) + result = super(DeliveryCarrier, self).available_carriers( + contact + ) + else: + result = super(DeliveryCarrier, self).available_carriers(contact) + return result diff --git a/shopinvader_delivery_carrier/services/__init__.py b/shopinvader_delivery_carrier/services/__init__.py index 41fa417e79..befd5f040c 100644 --- a/shopinvader_delivery_carrier/services/__init__.py +++ b/shopinvader_delivery_carrier/services/__init__.py @@ -1,3 +1,4 @@ from . import abstract_sale from . import cart +from . import delivery_carrier from . import delivery diff --git a/shopinvader_delivery_carrier/services/cart.py b/shopinvader_delivery_carrier/services/cart.py index 29ba1d8e10..88540cbdcd 100644 --- a/shopinvader_delivery_carrier/services/cart.py +++ b/shopinvader_delivery_carrier/services/cart.py @@ -1,37 +1,62 @@ # Copyright 2017 Akretion (http://www.akretion.com). # @author Sébastien BEAU # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging +from odoo.addons.base_rest.components.service import to_int from odoo.addons.component.core import Component from odoo.exceptions import UserError from odoo.tools.translate import _ +_logger = logging.getLogger(__name__) + class CartService(Component): _inherit = "shopinvader.cart.service" + def set_carrier(self, **params): + """ + This service will set the given delivery method to the current + cart + :param params: The carrier_id to set + :return: + """ + cart = self._get() + if not cart: + raise UserError(_("There is not cart")) + else: + self._set_carrier(cart, params["carrier_id"]) + return self._to_json(cart) + + # DEPRECATED METHODS # def get_delivery_methods(self): """ + !!!!DEPRECATED!!!!! Uses delivery_carrier.search + This service will return all possible delivery methods for the current cart + :return: """ - cart = self._get() - return self._get_available_carrier(cart) + _logger.warning( + "DEPRECATED: You should use %s in service %s", + "search", + "delivery_carrier", + ) + return self.component("delivery_carrier").search( + target="current_cart" + )["rows"] def apply_delivery_method(self, **params): """ + !!!!DEPRECATED!!!!! Uses set_carrier + This service will apply the given delivery method to the current cart :param params: Dict containing delivery method to apply :return: """ - cart = self._get() - if not cart: - raise UserError(_("There is not cart")) - else: - self._set_carrier(cart, params["carrier"]["id"]) - return self._to_json(cart) + return self.set_carrier(carrier_id=params["carrier"]["id"]) # Validator def _validator_apply_delivery_method(self): @@ -50,9 +75,27 @@ def _validator_apply_delivery_method(self): } def _validator_get_delivery_methods(self): - return {} + return { + "country_id": { + "coerce": to_int, + "required": False, + "type": "integer", + }, + "zip_code": {"required": False, "type": "string"}, + } + + def _validator_set_carrier(self): + return { + "carrier_id": { + "coerce": int, + "nullable": True, + "required": True, + "type": "integer", + } + } # internal methods + def _add_item(self, cart, params): res = super()._add_item(cart, params) self._unset_carrier(cart) @@ -69,9 +112,7 @@ def _delete_item(self, cart, params): return res def _set_carrier(self, cart, carrier_id): - if carrier_id not in [ - x["id"] for x in self._get_available_carrier(cart) - ]: + if carrier_id not in [x["id"] for x in cart._get_available_carrier()]: raise UserError( _("This delivery method is not available for you order") ) diff --git a/shopinvader_delivery_carrier/services/delivery_carrier.py b/shopinvader_delivery_carrier/services/delivery_carrier.py new file mode 100644 index 0000000000..4e0953b8e5 --- /dev/null +++ b/shopinvader_delivery_carrier/services/delivery_carrier.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# pylint: disable=consider-merging-classes-inherited,method-required-super + +from odoo.addons.base_rest.components.service import to_int +from odoo.addons.component.core import Component + + +class DeliveryCarrierService(Component): + _inherit = "base.shopinvader.service" + _name = "shopinvader.delivery.carrier.service" + _usage = "delivery_carrier" + _description = """ + This service allows you to retrieve the informations of available + delivery carriers. + """ + + # Public services: + + def search(self, **params): + """ + Returns the list of available carriers + + If the target params == current_cart, the list will be limited to the + carriers applying to the current cart and a price will be filled + into the response otherwise the price is not relevant (always 0). + + The field type is a technical field only use inform if the carrier + provides some specialized functionalities + """ + cart = None + if params.get("target") == "current_cart": + cart = self.component(usage="cart")._get() + delivery_carriers = self._search(cart=cart, **params) + return { + "count": len(delivery_carriers), + "rows": [ + self._prepare_carrier(dc, cart) for dc in delivery_carriers + ], + } + + # Validators + + def _validator_search(self): + return { + "target": { + "type": "string", + "required": False, + "allowed": ["current_cart"], + }, + "country_id": { + "coerce": to_int, + "required": False, + "type": "integer", + }, + "zip_code": {"required": False, "type": "string"}, + } + + def _validator_return_search(self): + return { + "count": {"type": "integer", "required": True}, + "rows": { + "type": "list", + "required": True, + "schema": { + "type": "dict", + "schema": { + "id": {"type": "integer", "required": True}, + "name": { + "type": "string", + "required": False, + "nullable": True, + }, + "description": { + "type": "string", + "required": False, + "nullable": True, + }, + "price": {"type": "float", "required": False}, + "type": { + "type": "string", + "required": False, + "allowed": self.allowed_carrier_types, + "nullable": True, + }, + }, + }, + }, + } + + # Services implementation + + def _search(self, cart, **params): + """ + Search for delivery carriers + :param: cart: if provided, the list will be limited to the carrier + applying to the given cart + :param params: see _validator_search + :return: delivery.carriers recordset + """ + if cart: + country = self._load_country(params) + zip_code = self._load_zip_code(params) + if country or zip_code: + cart = cart.with_context( + delivery_force_country_id=country.id, + delivery_force_zip_code=zip_code, + ) + return cart._get_available_carrier() + return self.shopinvader_backend.carrier_ids + + def _prepare_carrier(self, carrier, cart=None): + res = carrier.jsonify(self._json_parser_carrier)[0] + res["type"] = None + price = 0.0 + if cart: + price = carrier.rate_shipment(cart).get("price", 0.0) + res["price"] = price + return res + + def _load_country(self, params): + """ + Load the country from given params + :param params: dict + :return: res.country recordset + """ + country_id = params.pop("country_id", 0) + return self.env["res.country"].browse(country_id) + + def _load_zip_code(self, params): + """ + Load the country from given params + :param params: dict + :return: str + """ + return params.pop("zip_code", "") + + @property + def allowed_carrier_types(self): + return [] + + @property + def _json_parser_carrier(self): + return ["id", "name", "name:description"] diff --git a/shopinvader_delivery_carrier/tests/__init__.py b/shopinvader_delivery_carrier/tests/__init__.py index a5efb99e18..6aedbfb769 100644 --- a/shopinvader_delivery_carrier/tests/__init__.py +++ b/shopinvader_delivery_carrier/tests/__init__.py @@ -1,3 +1,4 @@ from . import test_carrier from . import test_notification +from . import test_delivery_carrier from . import test_delivery_service diff --git a/shopinvader_delivery_carrier/tests/common.py b/shopinvader_delivery_carrier/tests/common.py index ee6344e522..ea7525844c 100644 --- a/shopinvader_delivery_carrier/tests/common.py +++ b/shopinvader_delivery_carrier/tests/common.py @@ -6,11 +6,13 @@ class CommonCarrierCase(CommonConnectedCartCase): - def setUp(self): - super(CommonCarrierCase, self).setUp() - self.free_carrier = self.env.ref("delivery.free_delivery_carrier") - self.poste_carrier = self.env.ref("delivery.delivery_carrier") - self.product_1 = self.env.ref("product.product_product_4b") + @classmethod + def setUpClass(cls): + super(CommonCarrierCase, cls).setUpClass() + cls.free_carrier = cls.env.ref("delivery.free_delivery_carrier") + cls.poste_carrier = cls.env.ref("delivery.delivery_carrier") + cls.product_1 = cls.env.ref("product.product_product_4b") + cls.precision = 2 def extract_cart(self, response): self.shopinvader_session["cart_id"] = response["set_session"][ @@ -39,6 +41,13 @@ def delete_item(self, item_id): ) def _set_carrier(self, carrier): + response = self.service.dispatch( + "set_carrier", params={"carrier_id": carrier.id} + ) + self.assertEqual(self.cart.carrier_id.id, carrier.id) + return response["data"] + + def _apply_delivery_method(self, carrier): response = self.service.dispatch( "apply_delivery_method", params={"carrier": {"id": carrier.id}} ) diff --git a/shopinvader_delivery_carrier/tests/test_carrier.py b/shopinvader_delivery_carrier/tests/test_carrier.py index 3c90c13b48..a43680ac87 100644 --- a/shopinvader_delivery_carrier/tests/test_carrier.py +++ b/shopinvader_delivery_carrier/tests/test_carrier.py @@ -6,10 +6,18 @@ class CarrierCase(CommonCarrierCase): + def setUp(self): + super(CarrierCase, self).setUp() + self.carrier_service = self.service.component("delivery_carrier") + def test_available_carriers(self): response = self.service.dispatch("get_delivery_methods") self.assertEqual(len(response), 2) + def test_deprecated_apply_delivery_method(self): + cart = self._apply_delivery_method(self.free_carrier) + self.assertEqual(cart["shipping"]["amount"]["total"], 0) + def test_setting_free_carrier(self): cart = self._set_carrier(self.free_carrier) self.assertEqual(cart["shipping"]["amount"]["total"], 0) @@ -70,3 +78,206 @@ def test_reset_carrier_on_delte_item(self): cart = self.delete_item(items[0]["id"]) self.assertEqual(cart["shipping"]["amount"]["total"], 0) self.assertFalse(cart["shipping"]["selected_carrier"]) + + def test_get_cart_price_by_country1(self): + """ + Check the service get_cart_price_by_country. + For this case, the cart doesn't have an existing delivery line. + :return: + """ + french_country = self.env.ref("base.fr") + belgium = self.env.ref("base.be") + self.backend.carrier_ids.write( + {"country_ids": [(6, False, [belgium.id, french_country.id])]} + ) + partner = self.service.partner + # Use the partner of the service + self.cart.write({"partner_id": partner.id}) + partner.write({"country_id": False}) + self.cart.write({"carrier_id": False}) + # Force load every fields + self.cart.read() + cart_values_before = self.cart._convert_to_write(self.cart._cache) + lines = {} + for line in self.cart.order_line: + line.read() + lines.update({line.id: line._convert_to_write(line._cache)}) + nb_lines_before = self.env["sale.order.line"].search_count([]) + self.service.shopinvader_session.update({"cart_id": self.cart.id}) + params = {"country_id": belgium.id, "target": "current_cart"} + result = self.carrier_service.dispatch("search", params=params) + self.cart.read() + cart_values_after = self.cart._convert_to_write(self.cart._cache) + nb_lines_after = self.env["sale.order.line"].search_count([]) + self.assertDictEqual(cart_values_before, cart_values_after) + self.assertEqual(nb_lines_after, nb_lines_before) + + partner.write({"country_id": french_country.id}) + self.cart.write({"carrier_id": self.poste_carrier.id}) + # Force load every fields + self.cart.read() + cart_values_before = self.cart._convert_to_write(self.cart._cache) + lines = {} + for line in self.cart.order_line: + line.read() + lines.update({line.id: line._convert_to_write(line._cache)}) + nb_lines_before = self.env["sale.order.line"].search_count([]) + self.service.shopinvader_session.update({"cart_id": self.cart.id}) + params = {"country_id": belgium.id, "target": "current_cart"} + result = self.carrier_service.dispatch("search", params=params) + self.assertEqual(self.cart.name, cart_values_before.get("name", "")) + self.cart.read() + cart_values_after = self.cart._convert_to_write(self.cart._cache) + self.assertDictEqual(cart_values_before, cart_values_after) + nb_lines_after = self.env["sale.order.line"].search_count([]) + self.assertEqual(nb_lines_after, nb_lines_before) + # Ensure lines still ok + self.assertEqual(len(lines), len(self.cart.order_line)) + for line_id, line_values in lines.items(): + order_line = self.cart.order_line.filtered( + lambda l, lid=line_id: l.id == lid + ) + order_line.read() + self.assertDictEqual( + order_line._convert_to_write(order_line._cache), line_values + ) + self.assertEqual(self.cart.partner_id, partner) + self.assertEqual(french_country, partner.country_id) + self._check_carriers(result, country=belgium) + + def test_get_cart_price_by_country_anonymous(self): + """ + Check the service get_cart_price_by_country. + For this case, the cart doesn't have an existing delivery line. + :return: + """ + with self.work_on_services( + partner=self.backend.anonymous_partner_id, shopinvader_session={} + ) as work: + self.service = work.component(usage="cart") + # Update with anonymous user + self.test_get_cart_price_by_country1() + + def _check_carriers(self, result, country): + """ + Check carrier for current cart based on given result list of dict. + :param result: list of dict + :return: bool + """ + available_carriers = self.backend.carrier_ids + available_carriers = available_carriers.filtered( + lambda c, country=country: country in c.country_ids + or not c.country_ids + ) + carrier_rows = result.get("rows") + self.assertEqual(len(available_carriers), len(carrier_rows)) + for carrier_result in carrier_rows: + carrier = available_carriers.filtered( + lambda c: c.id == carrier_result.get("id") + ) + self.assertEqual(len(carrier), 1) + self.assertEqual(carrier.name, carrier_result.get("name")) + self.assertAlmostEqual( + carrier.rate_shipment(self.cart).get("price", 0.0), + carrier_result.get("price"), + places=self.precision, + ) + return True + + def test_get_cart_price_by_country2(self): + """ + Check the service get_cart_price_by_country. + For this case, the cart have 1 delivery line set + :return: + """ + french_country = self.env.ref("base.fr") + belgium = self.env.ref("base.be") + partner = self.cart.partner_id + partner.write({"country_id": french_country.id}) + self.cart.write({"carrier_id": self.poste_carrier.id}) + self.cart.get_delivery_price() + self.cart.set_delivery_line() + # Force load every fields + self.cart.read() + cart_values_before = self.cart._convert_to_write(self.cart._cache) + cart_values_before.pop("order_line", None) + lines = {} + for line in self.cart.order_line: + line.read() + lines.update({line.id: line._convert_to_write(line._cache)}) + nb_lines_before = self.env["sale.order.line"].search_count([]) + self.service.shopinvader_session.update({"cart_id": self.cart.id}) + params = {"country_id": belgium.id, "target": "current_cart"} + result = self.carrier_service.dispatch("search", params=params) + self._check_carriers(result, country=belgium) + self.assertEqual(self.cart.name, cart_values_before.get("name", "")) + self.cart.read() + cart_values_after = self.cart._convert_to_write(self.cart._cache) + cart_values_after.pop("order_line", None) + self.assertDictEqual(cart_values_before, cart_values_after) + nb_lines_after = self.env["sale.order.line"].search_count([]) + self.assertEqual(nb_lines_after, nb_lines_before) + # Ensure lines still ok + self.assertEqual(len(lines), len(self.cart.order_line)) + for line_id, line_values in lines.items(): + order_line = self.cart.order_line.filtered( + lambda l, lid=line_id: l.id == lid + ) + # Because delivery line has changed and the ID doesn't match + # anymore. + # But should still similar! + if not order_line: + order_line = self.cart.order_line.filtered( + lambda l: l.is_delivery + ) + order_line.read() + self.assertDictEqual( + order_line._convert_to_write(order_line._cache), line_values + ) + self.assertEqual(self.cart.partner_id, partner) + self.assertEqual(french_country, partner.country_id) + + def test_get_cart_price_by_country3(self): + """ + Check the service get_cart_price_by_country. + For this case, the cart have 1 delivery line set + The cart doesn't have a carrier set. + :return: + """ + french_country = self.env.ref("base.fr") + belgium = self.env.ref("base.be") + partner = self.cart.partner_id + partner.write({"country_id": french_country.id}) + self.assertFalse(self.cart.carrier_id) + # Force load every fields + self.cart.read() + cart_values_before = self.cart._convert_to_write(self.cart._cache) + cart_values_before.pop("order_line", None) + lines = {} + for line in self.cart.order_line: + line.read() + lines.update({line.id: line._convert_to_write(line._cache)}) + nb_lines_before = self.env["sale.order.line"].search_count([]) + self.service.shopinvader_session.update({"cart_id": self.cart.id}) + params = {"country_id": belgium.id, "target": "current_cart"} + result = self.carrier_service.dispatch("search", params=params) + self._check_carriers(result, country=belgium) + self.assertEqual(self.cart.name, cart_values_before.get("name", "")) + self.cart.read() + cart_values_after = self.cart._convert_to_write(self.cart._cache) + cart_values_after.pop("order_line", None) + self.assertDictEqual(cart_values_before, cart_values_after) + nb_lines_after = self.env["sale.order.line"].search_count([]) + self.assertEqual(nb_lines_after, nb_lines_before) + # Ensure lines still ok + self.assertEqual(len(lines), len(self.cart.order_line)) + for line_id, line_values in lines.items(): + order_line = self.cart.order_line.filtered( + lambda l, lid=line_id: l.id == lid + ) + order_line.read() + self.assertDictEqual( + order_line._convert_to_write(order_line._cache), line_values + ) + self.assertEqual(self.cart.partner_id, partner) + self.assertEqual(french_country, partner.country_id) diff --git a/shopinvader_delivery_carrier/tests/test_delivery_carrier.py b/shopinvader_delivery_carrier/tests/test_delivery_carrier.py new file mode 100644 index 0000000000..dc7d40d389 --- /dev/null +++ b/shopinvader_delivery_carrier/tests/test_delivery_carrier.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from .common import CommonCarrierCase + + +class TestDeliveryCarrier(CommonCarrierCase): + def setUp(self): + super(CommonCarrierCase, self).setUp() + self.carrier_service = self.service.component("delivery_carrier") + + def test_search_all(self): + res = self.carrier_service.search() + expected = { + "count": 2, + "rows": [ + { + "price": 0.0, + "description": self.free_carrier.name or None, + "id": self.free_carrier.id, + "name": self.free_carrier.name, + "type": None, + }, + { + "price": 0.0, + "description": self.poste_carrier.name or None, + "id": self.poste_carrier.id, + "name": self.poste_carrier.name, + "type": None, + }, + ], + } + self.assertDictEqual(res, expected) + + def test_search_current_cart(self): + res = self.carrier_service.search(target="current_cart") + expected = { + "count": 2, + "rows": [ + { + "price": 0.0, + "description": self.free_carrier.name or None, + "id": self.free_carrier.id, + "name": self.free_carrier.name, + "type": None, + }, + { + "price": 20.0, + "description": self.poste_carrier.name or None, + "id": self.poste_carrier.id, + "name": self.poste_carrier.name, + "type": None, + }, + ], + } + self.assertDictEqual(res, expected)