From a2b8a4fa7f9cf0ae6090a9b5706f533fcad3eae2 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Thu, 4 May 2023 12:01:32 +0200 Subject: [PATCH 01/13] [IMP] product_expiry: Don't trigger move lines expiration date recomputation If one change the use_create_lots field on stock.picking.type, we should not trigger all move lines recomputation of that picking type. --- addons/product_expiry/models/stock_move_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/product_expiry/models/stock_move_line.py b/addons/product_expiry/models/stock_move_line.py index 8ce2606ffb657..fb13fd3ef08f7 100644 --- a/addons/product_expiry/models/stock_move_line.py +++ b/addons/product_expiry/models/stock_move_line.py @@ -28,7 +28,7 @@ def _auto_init(self): create_column(self._cr, "stock_move_line", "expiration_date", "timestamp") return super()._auto_init() - @api.depends('product_id', 'picking_type_use_create_lots', 'lot_id.expiration_date') + @api.depends('product_id', 'lot_id.expiration_date') def _compute_expiration_date(self): for move_line in self: if move_line.lot_id.expiration_date: From d98c7066d6bea11c068ad17fa8b28d54fce3e54a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 25 Sep 2022 21:56:20 +0200 Subject: [PATCH 02/13] Add some type annotations --- odoo/addons/base/models/res_partner.py | 3 ++- odoo/api.py | 9 +++++++-- odoo/models.py | 20 +++++++++++--------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py index 9388aaa7a44c9..7342277da28de 100644 --- a/odoo/addons/base/models/res_partner.py +++ b/odoo/addons/base/models/res_partner.py @@ -8,6 +8,7 @@ import pytz import threading import re +from typing_extensions import Self import requests from collections import defaultdict @@ -694,7 +695,7 @@ def write(self, vals): return result @api.model_create_multi - def create(self, vals_list): + def create(self, vals_list) -> Self: if self.env.context.get('import_file'): self._check_import_consistency(vals_list) for vals in vals_list: diff --git a/odoo/api.py b/odoo/api.py index 4c93eb1820a21..382c20a940be8 100644 --- a/odoo/api.py +++ b/odoo/api.py @@ -22,6 +22,7 @@ from inspect import signature from pprint import pformat from weakref import WeakSet +from typing import ParamSpec, TypeVar, Any, Callable from decorator import decorate @@ -31,6 +32,10 @@ _logger = logging.getLogger(__name__) +P = ParamSpec("P") +R = TypeVar("R", bound=Any) + + # The following attributes are used, and reflected on wrapping methods: # - method._constrains: set by @constrains, specifies constraint dependencies # - method._depends: set by @depends, specifies compute dependencies @@ -94,7 +99,7 @@ def propagate(method1, method2): return method2 -def constrains(*args): +def constrains(*args: str) -> Callable[[Callable[P, R]], Callable[P, R]]: """Decorate a constraint checker. Each argument must be a field name used in the check:: @@ -240,7 +245,7 @@ def _onchange_partner(self): return attrsetter('_onchange', args) -def depends(*args): +def depends(*args: str) -> Callable[[Callable[P, R]], Callable[P, R]]: """ Return a decorator that specifies the field dependencies of a "compute" method (for new-style function fields). Each argument must be a string that consists in a dot-separated sequence of field names:: diff --git a/odoo/models.py b/odoo/models.py index 74ae697c3da57..4e530ea9823f6 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -42,6 +42,8 @@ from contextlib import closing from inspect import getmembers, currentframe from operator import attrgetter, itemgetter +from typing import Iterator, Union +from typing_extensions import Self import babel.dates import dateutil.relativedelta @@ -461,12 +463,12 @@ class BaseModel(metaclass=MetaModel): .. seealso:: :class:`TransientModel` """ - _name = None #: the model name (in dot-notation, module namespace) - _description = None #: the model's informal name + _name: Union[str, None] = None #: the model name (in dot-notation, module namespace) + _description: Union[str, None] = None #: the model's informal name _module = None #: the model's module (in the Odoo sense) _custom = False #: should be True for custom models only - _inherit = () + _inherit: Union[str, list[str]] = () """Python-inherited models: :type: str or list(str) @@ -1490,7 +1492,7 @@ def search_count(self, domain, limit=None): @api.returns('self', upgrade=lambda self, value, domain, offset=0, limit=None, order=None, count=False: value if count else self.browse(value), downgrade=lambda self, value, domain, offset=0, limit=None, order=None, count=False: value if count else value.ids) - def search(self, domain, offset=0, limit=None, order=None, count=False): + def search(self, domain, offset=0, limit=None, order=None, count=False) -> Self: """ search(domain[, offset=0][, limit=None][, order=None][, count=False]) Searches for records based on the ``domain`` @@ -3839,7 +3841,7 @@ def _write(self, vals): @api.model_create_multi @api.returns('self', lambda value: value.id) - def create(self, vals_list): + def create(self, vals_list) -> Self: """ create(vals_list) -> records Creates new records for the model. @@ -5056,7 +5058,7 @@ def _revert_method(cls, name): # - the global cache is only an index to "resolve" a record 'id'. # - def __init__(self, env, ids, prefetch_ids): + def __init__(self, env, ids=(), prefetch_ids=()): """ Create a recordset instance. :param env: an environment @@ -5067,7 +5069,7 @@ def __init__(self, env, ids, prefetch_ids): self._ids = ids self._prefetch_ids = prefetch_ids - def browse(self, ids=None): + def browse(self, ids=None) -> Self: """ browse([ids]) -> records Returns a recordset for the ids provided as parameter in the current @@ -5686,7 +5688,7 @@ def __len__(self): """ Return the size of ``self``. """ return len(self._ids) - def __iter__(self): + def __iter__(self) -> Iterator[Self]: """ Return an iterator over ``self``. """ if len(self._ids) > PREFETCH_MAX and self._prefetch_ids is self._ids: for ids in self.env.cr.split_for_in_conditions(self._ids): @@ -5696,7 +5698,7 @@ def __iter__(self): for id_ in self._ids: yield self.__class__(self.env, (id_,), self._prefetch_ids) - def __reversed__(self): + def __reversed__(self) -> Iterator[Self]: """ Return an reversed iterator over ``self``. """ if len(self._ids) > PREFETCH_MAX and self._prefetch_ids is self._ids: for ids in self.env.cr.split_for_in_conditions(reversed(self._ids)): From 410c6391a46219b5088fcab3327d913ae8c1e0bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 8 Oct 2022 18:04:03 +0200 Subject: [PATCH 03/13] Annotate Field descriptors --- odoo/fields.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/odoo/fields.py b/odoo/fields.py index e08cece47a1ab..0388abde58212 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -18,6 +18,9 @@ import logging import uuid import warnings +from typing import Any, TypeVar, Generic + +_T = TypeVar("_T", bound=Any) from markupsafe import Markup import psycopg2 @@ -127,7 +130,7 @@ def __init__(cls, name, bases, attrs): _global_seq = iter(itertools.count()) -class Field(MetaField('DummyField', (object,), {})): +class Field(MetaField('DummyField', (object,), {}), Generic[_T]): """The field descriptor contains the field definition, and manages accesses and assignments of the corresponding field on records. The following attributes may be provided when instantiating a field: @@ -1137,7 +1140,7 @@ def write(self, records, value): # Descriptor methods # - def __get__(self, record, owner): + def __get__(self, record, owner) -> _T: """ return the value of field ``self`` on ``record`` """ if record is None: return self # the field is accessed through the owner class @@ -1281,7 +1284,7 @@ def mapped(self, records): return self.convert_to_record_multi(vals, records) - def __set__(self, records, value): + def __set__(self, records, value: _T) -> None: """ set the value of field ``self`` on ``records`` """ protected_ids = [] new_ids = [] @@ -1396,7 +1399,7 @@ def convert_to_export(self, value, record): return value -class Integer(Field): +class Integer(Field[int]): """ Encapsulates an :class:`int`. """ type = 'integer' column_type = ('int4', 'int4') @@ -1434,7 +1437,7 @@ def convert_to_export(self, value, record): return '' -class Float(Field): +class Float(Field[float]): """ Encapsulates a :class:`float`. The precision digits are given by the (optional) ``digits`` attribute. @@ -1625,7 +1628,7 @@ def convert_to_write(self, value, record): return value -class _String(Field): +class _String(Field[str]): """ Abstract class for string fields. """ translate = False # whether the field is translated unaccent = True @@ -2759,14 +2762,14 @@ def convert_to_display_name(self, value, record): return value.display_name if value else False -class _Relational(Field): +class _Relational(Generic[_T], Field[_T]): """ Abstract class for relational fields. """ relational = True domain = [] # domain for searching values context = {} # context for searching values check_company = False - def __get__(self, records, owner): + def __get__(self, records, owner) -> _T: # base case: do the regular access if records is None or len(records._ids) <= 1: return super().__get__(records, owner) @@ -2823,7 +2826,7 @@ def null(self, record): return record.env[self.comodel_name] -class Many2one(_Relational): +class Many2one(Generic[_T], _Relational[_T]): """ The value of such a field is a recordset of size 0 (no record) or 1 (a single record). @@ -4027,7 +4030,7 @@ def set(cls, ids: list): return (cls.SET, 0, ids) -class _RelationalMulti(_Relational): +class _RelationalMulti(Generic[_T], _Relational[_T]): r"Abstract class for relational fields \*2many." write_sequence = 20 @@ -4239,7 +4242,7 @@ def write_batch(self, records_commands_list, create=False): return self.write_new(records_commands_list) -class One2many(_RelationalMulti): +class One2many(Generic[_T], _RelationalMulti[_T]): """One2many field; the value of such a field is the recordset of all the records in ``comodel_name`` such that the field ``inverse_name`` is equal to the current record. @@ -4303,7 +4306,7 @@ def get_domain_list(self, records): domain = domain + [(inverse_field.model_field, '=', records._name)] return domain - def __get__(self, records, owner): + def __get__(self, records, owner) -> _T: if records is not None and self.inverse_name is not None: # force the computation of the inverse field to ensure that the # cache value of self is consistent From 59a521fe699b6cdb73505278c63aef3d08cd925a Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Thu, 20 Oct 2022 19:54:06 +0200 Subject: [PATCH 04/13] Init comodel_name on relational fields from generic type's arg This change allows to take advantage of field descriptors on relational fields to initialize the comodel_name from optional type's arg that can be specified on field declaration. With this change, this kind of syntax becomes valid and will avoid errors in the comodel_name args. partner = fields.Many2one[Partner]() This code is not part of the _Relational class constructor since we need to get access to the __orig_class__ attribute which is set on the instance by the Generic metaclass after the call to the _Relational class constructor. --- odoo/fields.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/odoo/fields.py b/odoo/fields.py index 0388abde58212..f48e3eefa92a1 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -16,6 +16,7 @@ import itertools import json import logging +import typing import uuid import warnings from typing import Any, TypeVar, Generic @@ -2776,6 +2777,18 @@ def __get__(self, records, owner) -> _T: # multirecord case: use mapped return self.mapped(records) + def setup(self, model): + if not self.comodel_name: + # Auto initialize the comodel name from the generic type's var if used + # This allows to get the comodel from the type when the field is declared + # as `partner = fields.Many2one[Partner]()` + type_args = typing.get_args(getattr(self, "__orig_class__", None)) + if type_args and issubclass(type_args[0], BaseModel): + self.comodel_name = type_args[0]._name + # TODO We could error out if the comodel attribute is set and is + # incompatible with the type. + return super().setup(model) + def setup_nonrelated(self, model): super().setup_nonrelated(model) if self.comodel_name not in model.pool: From 4f1c9b22b8d67f5cf40f3e5acdd5a3c1e1957eb9 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Fri, 21 Oct 2022 11:28:04 +0200 Subject: [PATCH 05/13] Take into account FrowardRef when computing the comodel_name To avoid circular import it's common to reference a type as a string for generics: ```python from odoo import models, fields from typing import TYPE_CHECKING if TYPE_CHECKING: from .res_partner import Partner as ResPartner class Thing(models.Model): _name = "thing" _description = "Thing" partner_id = fields.Many2one["ResPartner"]() ``` In such a case, the type's arg at runtime is not a class but an instance of typing.ForwadRef. In such a case, the comodel_name is computed by converting the classname provided by te ForwardRef from CamelCase to dot.case --- odoo/fields.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/odoo/fields.py b/odoo/fields.py index f48e3eefa92a1..d97f84ea2cadb 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -16,10 +16,11 @@ import itertools import json import logging +import re import typing import uuid import warnings -from typing import Any, TypeVar, Generic +from typing import Any, TypeVar, Generic, ForwardRef _T = TypeVar("_T", bound=Any) @@ -62,6 +63,10 @@ Default = object() # default value for __init__() methods +def camel_case_to_dot_case(name): + return re.sub(r"(? 1 else records @@ -2783,8 +2788,11 @@ def setup(self, model): # This allows to get the comodel from the type when the field is declared # as `partner = fields.Many2one[Partner]()` type_args = typing.get_args(getattr(self, "__orig_class__", None)) - if type_args and issubclass(type_args[0], BaseModel): - self.comodel_name = type_args[0]._name + if type_args: + if type_args[0].__class__ == ForwardRef: + self.comodel_name = camel_case_to_dot_case(type_args[0].__forward_arg__) + elif issubclass(type_args[0], BaseModel): + self.comodel_name = type_args[0]._name # TODO We could error out if the comodel attribute is set and is # incompatible with the type. return super().setup(model) From 8b4b7dfb62716b1c510aabf112ffb135589d6a17 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Fri, 21 Oct 2022 16:42:58 +0200 Subject: [PATCH 06/13] Add typing_extension to the requirements list the typing_extension is required by the type annotations --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 2b4ddbb37e401..328ff9c89770e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -53,6 +53,7 @@ qrcode==6.1 reportlab==3.5.59 ; python_version <= '3.10' # version < 3.5.54 are not compatible with Pillow 8.1.2 and 3.5.59 is bullseye reportlab==3.6.12 ; python_version > '3.10' requests==2.25.1 # versions < 2.25 aren't compatible w/ urllib3 1.26. Bullseye = 2.25.1. min version = 2.22.0 (Focal) +typing_extensions==4.4.0 urllib3==1.26.5 # indirect / min version = 1.25.8 (Focal with security backports) vobject==0.9.6.1 Werkzeug==0.16.1 ; python_version <= '3.9' From d23a4ceda6249dff35885e09c6444bd2fdb94f26 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Thu, 12 Jan 2023 16:40:09 +0100 Subject: [PATCH 07/13] Annotate Field decriptoor for Many2many --- odoo/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odoo/fields.py b/odoo/fields.py index d97f84ea2cadb..38332d45d10f8 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -4555,7 +4555,7 @@ def unlink(lines): return records -class Many2many(_RelationalMulti): +class Many2many(Generic[_T], _RelationalMulti[_T]): """ Many2many field; the value of such a field is the recordset. :param comodel_name: name of the target model (string) From ffae38c96c359d90892d6a1bacb2949f6a7f0835 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Mon, 15 May 2023 13:42:53 +0200 Subject: [PATCH 08/13] Fix code after rebase on odoo at 2f22565 Calls a dedicated method to clone an existing field to ensure that the type args is preserved when instantiating the cloned instance --- odoo/fields.py | 8 ++++++++ odoo/models.py | 8 +++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/odoo/fields.py b/odoo/fields.py index 38332d45d10f8..5cc2e3ab4e5cd 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -346,6 +346,14 @@ def __init__(self, string=Default, **kwargs): self._sequence = next(_global_seq) self.args = {key: val for key, val in kwargs.items() if val is not Default} + def new(self, **kwargs): + """ Return a field of the same type as ``self``, with its own parameters. """ + type_args = typing.get_args(getattr(self, "__orig_class__", None)) + t = type(self) + if type_args: + t = t[type_args[0]] + return t(**kwargs) + def __str__(self): if self.name is None: return "<%s.%s>" % (__name__, type(self).__name__) diff --git a/odoo/models.py b/odoo/models.py index 4e530ea9823f6..7b7ff600a893e 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2658,8 +2658,7 @@ def _add_inherited_fields(self): # following specific properties: # - reading inherited fields should not bypass access rights # - copy inherited fields iff their original field is copied - Field = type(field) - self._add_field(name, Field( + self._add_field(name, field.new( inherited=True, inherited_field=field, related=f"{parent_fname}.{name}", @@ -2748,12 +2747,11 @@ def _setup_base(self): if not translate: # patch the field definition by adding an override _logger.debug("Patching %s.%s with translate=True", cls._name, name) - fields_.append(type(fields_[0])(translate=True)) + fields_.append(fields_[0].new(translate=True)) if len(fields_) == 1 and fields_[0]._direct and fields_[0].model_name == cls._name: cls._fields[name] = fields_[0] else: - Field = type(fields_[-1]) - self._add_field(name, Field(_base_fields=fields_)) + self._add_field(name, fields_[-1].new(_base_fields=fields_)) # 2. add manual fields if self.pool._init_modules: From 2efccd427f46c536273b34cd390bc3f0a2543ae1 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Fri, 25 Nov 2022 10:07:11 +0100 Subject: [PATCH 09/13] [FIX] core: importlib find_module is deprecated As find_module has been deprecated since python 3.4: https://github.com/python/cpython/blob/05c28b08f6e2fc8782472b026c98a3fdd61a2ba9/Lib/importlib/_bootstrap.py#L1347 and warnings added in python 3.10: https://github.com/python/cpython/blob/f91dfdf5ff9f68a4b012e1b70ab9997c6dc1542d/Lib/importlib/_bootstrap.py#L764 to respect the PEP-451 specification : https://peps.python.org/pep-0451/ So, override the find_spec() method to display depreaction warnings if applicable. --- odoo/modules/module.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/odoo/modules/module.py b/odoo/modules/module.py index 5d5c7244bf18d..fbc9f92d9b3c1 100644 --- a/odoo/modules/module.py +++ b/odoo/modules/module.py @@ -82,6 +82,13 @@ def find_module(self, name, path=None): DeprecationWarning, stacklevel=2) return self + def find_spec(self, fullname, path=None, target=None): + if fullname.startswith('openerp.addons.') and fullname.count('.') == 2: + warnings.warn( + '"openerp.addons" is a deprecated alias to "odoo.addons".', + DeprecationWarning, stacklevel=2) + return importlib.util.spec_from_loader(fullname, self) + def load_module(self, name): assert name not in sys.modules @@ -107,6 +114,15 @@ def find_module(self, name, path=None): DeprecationWarning, stacklevel=2) return self + def find_spec(self, fullname, path=None, target=None): + # openerp.addons. should already be matched by AddonsHook, + # only framework and subdirectories of modules should match + if re.match(r'^openerp\b', fullname): + warnings.warn( + 'openerp is a deprecated alias to odoo.', + DeprecationWarning, stacklevel=2) + return importlib.util.spec_from_loader(fullname, self) + def load_module(self, name): assert name not in sys.modules @@ -137,6 +153,14 @@ def find_module(self, name, path=None): # legacy name. return self + def find_spec(self, fullname, path=None, target=None): + if re.match(r"^odoo.addons.base.maintenance.migrations\b", fullname): + # We can't trigger a DeprecationWarning in this case. + # In order to be cross-versions, the multi-versions upgrade scripts (0.0.0 scripts), + # the tests, and the common files (utility functions) still needs to import from the + # legacy name. + return importlib.util.spec_from_loader(fullname, self) + def load_module(self, name): assert name not in sys.modules From d29587918c3c3d924e708a7fe1c0afca6a2a9b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Fri, 16 Aug 2019 23:10:14 +0200 Subject: [PATCH 10/13] setup: support PEP 660 and setuptools >= 64 Allow building a wheel or an sdist through the pep517 interface, taking care of the required pre-processing to copy addons in odoo/addons. Force the setuptools compat mode for editable installs. This mode is the simplest, since it just installs a .pth files which extends PYTHONPATH with the Odoo root directory. --- MANIFEST.in | 2 ++ pyproject.toml | 4 +++ setup/pep517_odoo.py | 65 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 pyproject.toml create mode 100644 setup/pep517_odoo.py diff --git a/MANIFEST.in b/MANIFEST.in index d1d9628c55b4f..f9ff8c6a9d35a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,8 @@ include requirements.txt include LICENSE include README.md +include pyproject.toml +include setup/pep517_odoo.py graft odoo recursive-exclude * *.py[co] recursive-exclude .git * diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000..228f3019ecc9c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,4 @@ +[build-system] +requires = ["setuptools>=41"] +build-backend = "pep517_odoo" +backend-path = ["setup"] diff --git a/setup/pep517_odoo.py b/setup/pep517_odoo.py new file mode 100644 index 0000000000000..bd93c9e238d3b --- /dev/null +++ b/setup/pep517_odoo.py @@ -0,0 +1,65 @@ +"""Specialized PEP 517 build backend for Odoo. + +It is based on setuptools, and extends it to + +- symlink all addons into odoo/addons before building, so setuptools discovers them + automatically, and +- enforce the 'compat' editable mode, because the default mode for flat layout + is not compatible with the Odoo addon import system. +""" +import contextlib +from pathlib import Path + +from setuptools import build_meta +from setuptools.build_meta import * # noqa: F403 + + +@contextlib.contextmanager +def _symlink_addons(): + symlinks = [] + try: + target_addons_path = Path("addons") + addons_path = Path("odoo", "addons") + link_target = Path("..", "..", "addons") + if target_addons_path.is_dir(): + for target_addon_path in target_addons_path.iterdir(): + if not target_addon_path.is_dir(): + continue + addon_path = addons_path / target_addon_path.name + if not addon_path.is_symlink(): + addon_path.symlink_to( + link_target / target_addon_path.name, target_is_directory=True + ) + symlinks.append(addon_path) + yield + finally: + for symlink in symlinks: + symlink.unlink() + + +def build_sdist(*args, **kwargs): + with _symlink_addons(): + return build_meta.build_sdist(*args, **kwargs) + + +def build_wheel(*args, **kwargs): + with _symlink_addons(): + return build_meta.build_wheel(*args, **kwargs) + + +if hasattr(build_meta, "build_editable"): + + def build_editable( + wheel_directory, config_settings=None, metadata_directory=None, **kwargs + ): + if config_settings is None: + config_settings = {} + # Use setuptools's compat editable mode, because the default mode for + # flat layout projects is not compatible with pkgutil.extend_path, + # and the strict mode is too strict for the Odoo development workflow + # where new files are added frequently. This is currently being discussed + # by the setuptools maintainers. + config_settings["editable-mode"] = "compat" + return build_meta.build_editable( + wheel_directory, config_settings, metadata_directory, **kwargs + ) From 283f5012b50d5899b4c088eef303799f0d163f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 23 Dec 2021 14:54:28 +0100 Subject: [PATCH 11/13] core: add odoo.__main__ This allows launching Odoo with "python -m odoo". This manner of launching python applications is now widespread. In particular it makes it easier to configure an IDE debugger to run with the correct python version. --- odoo/__main__.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 odoo/__main__.py diff --git a/odoo/__main__.py b/odoo/__main__.py new file mode 100644 index 0000000000000..fff693f2ed4d0 --- /dev/null +++ b/odoo/__main__.py @@ -0,0 +1,3 @@ +from .cli.command import main + +main() From 6768b10d2c3be8b9cf4515dcc394dbd4432b57aa Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Thu, 16 Nov 2023 16:13:59 +0100 Subject: [PATCH 12/13] Remove unused auto install addon test_mail_sms --- addons/test_mail_sms/__init__.py | 4 - addons/test_mail_sms/__manifest__.py | 22 - addons/test_mail_sms/models/__init__.py | 4 - .../models/test_mail_sms_models.py | 172 ----- .../security/ir.model.access.csv | 13 - addons/test_mail_sms/tests/__init__.py | 12 - addons/test_mail_sms/tests/common.py | 22 - .../tests/test_mail_thread_phone.py | 110 ---- .../tests/test_phone_blacklist.py | 82 --- .../test_mail_sms/tests/test_sms_composer.py | 621 ------------------ .../tests/test_sms_management.py | 199 ------ .../tests/test_sms_performance.py | 137 ---- addons/test_mail_sms/tests/test_sms_post.py | 437 ------------ .../tests/test_sms_server_actions.py | 94 --- addons/test_mail_sms/tests/test_sms_sms.py | 105 --- .../test_mail_sms/tests/test_sms_template.py | 89 --- 16 files changed, 2123 deletions(-) delete mode 100644 addons/test_mail_sms/__init__.py delete mode 100644 addons/test_mail_sms/__manifest__.py delete mode 100644 addons/test_mail_sms/models/__init__.py delete mode 100644 addons/test_mail_sms/models/test_mail_sms_models.py delete mode 100644 addons/test_mail_sms/security/ir.model.access.csv delete mode 100644 addons/test_mail_sms/tests/__init__.py delete mode 100644 addons/test_mail_sms/tests/common.py delete mode 100644 addons/test_mail_sms/tests/test_mail_thread_phone.py delete mode 100644 addons/test_mail_sms/tests/test_phone_blacklist.py delete mode 100644 addons/test_mail_sms/tests/test_sms_composer.py delete mode 100644 addons/test_mail_sms/tests/test_sms_management.py delete mode 100644 addons/test_mail_sms/tests/test_sms_performance.py delete mode 100644 addons/test_mail_sms/tests/test_sms_post.py delete mode 100644 addons/test_mail_sms/tests/test_sms_server_actions.py delete mode 100644 addons/test_mail_sms/tests/test_sms_sms.py delete mode 100644 addons/test_mail_sms/tests/test_sms_template.py diff --git a/addons/test_mail_sms/__init__.py b/addons/test_mail_sms/__init__.py deleted file mode 100644 index dc5e6b693d19d..0000000000000 --- a/addons/test_mail_sms/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import models diff --git a/addons/test_mail_sms/__manifest__.py b/addons/test_mail_sms/__manifest__.py deleted file mode 100644 index 418855f8c441b..0000000000000 --- a/addons/test_mail_sms/__manifest__.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- - -{ - 'name': 'SMS Tests', - 'version': '1.0', - 'category': 'Hidden', - 'sequence': 9876, - 'summary': 'SMS Tests: performances and tests specific to SMS', - 'description': """This module contains tests related to SMS. Those are -present in a separate module as it contains models used only to perform -tests independently to functional aspects of other models. """, - 'depends': [ - 'mail', - 'sms', - 'test_performance', - ], - 'data': [ - 'security/ir.model.access.csv', - ], - 'installable': True, - 'license': 'LGPL-3', -} diff --git a/addons/test_mail_sms/models/__init__.py b/addons/test_mail_sms/models/__init__.py deleted file mode 100644 index f4acac8556c71..0000000000000 --- a/addons/test_mail_sms/models/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import test_mail_sms_models diff --git a/addons/test_mail_sms/models/test_mail_sms_models.py b/addons/test_mail_sms/models/test_mail_sms_models.py deleted file mode 100644 index fe47cfcb8a271..0000000000000 --- a/addons/test_mail_sms/models/test_mail_sms_models.py +++ /dev/null @@ -1,172 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, fields, models - - -class MailTestSMS(models.Model): - """ A model inheriting from mail.thread with some fields used for SMS - gateway, like a partner, a specific mobile phone, ... """ - _description = 'Chatter Model for SMS Gateway' - _name = 'mail.test.sms' - _inherit = ['mail.thread'] - _mailing_enabled = True - _order = 'name asc, id asc' - - name = fields.Char() - subject = fields.Char() - email_from = fields.Char() - phone_nbr = fields.Char() - mobile_nbr = fields.Char() - customer_id = fields.Many2one('res.partner', 'Customer') - - def _mail_get_partner_fields(self): - return ['customer_id'] - - def _sms_get_partner_fields(self): - return ['customer_id'] - - def _sms_get_number_fields(self): - return ['phone_nbr', 'mobile_nbr'] - - -class MailTestSMSBL(models.Model): - """ A model inheriting from mail.thread.phone allowing to test auto formatting - of phone numbers, blacklist, ... """ - _description = 'SMS Mailing Blacklist Enabled' - _name = 'mail.test.sms.bl' - _inherit = ['mail.thread.phone'] - _mailing_enabled = True - _order = 'name asc, id asc' - - name = fields.Char() - subject = fields.Char() - email_from = fields.Char() - phone_nbr = fields.Char(compute='_compute_phone_nbr', readonly=False, store=True) - mobile_nbr = fields.Char(compute='_compute_mobile_nbr', readonly=False, store=True) - customer_id = fields.Many2one('res.partner', 'Customer') - - @api.depends('customer_id') - def _compute_mobile_nbr(self): - for phone_record in self.filtered(lambda rec: not rec.mobile_nbr and rec.customer_id): - phone_record.mobile_nbr = phone_record.customer_id.mobile - - @api.depends('customer_id') - def _compute_phone_nbr(self): - for phone_record in self.filtered(lambda rec: not rec.phone_nbr and rec.customer_id): - phone_record.phone_nbr = phone_record.customer_id.phone - - def _mail_get_partner_fields(self): - return ['customer_id'] - - def _sms_get_partner_fields(self): - return ['customer_id'] - - def _phone_get_number_fields(self): - return ['phone_nbr', 'mobile_nbr'] - - -class MailTestSMSBLActivity(models.Model): - """ A model inheriting from mail.thread.phone allowing to test auto formatting - of phone numbers, blacklist, ... as well as activities management. """ - _description = 'SMS Mailing Blacklist Enabled with activities' - _name = 'mail.test.sms.bl.activity' - _inherit = [ - 'mail.test.sms.bl', - 'mail.activity.mixin', - ] - _mailing_enabled = True - _order = 'name asc, id asc' - - -class MailTestSMSOptout(models.Model): - """ Model using blacklist mechanism and a hijacked opt-out mechanism for - mass mailing features. """ - _description = 'SMS Mailing Blacklist / Optout Enabled' - _name = 'mail.test.sms.bl.optout' - _inherit = ['mail.thread.phone'] - _mailing_enabled = True - _order = 'name asc, id asc' - - name = fields.Char() - subject = fields.Char() - email_from = fields.Char() - phone_nbr = fields.Char() - mobile_nbr = fields.Char() - customer_id = fields.Many2one('res.partner', 'Customer') - opt_out = fields.Boolean() - - def _mail_get_partner_fields(self): - return ['customer_id'] - - def _mailing_get_opt_out_list_sms(self, mailing): - res_ids = mailing._get_recipients() - return self.search([ - ('id', 'in', res_ids), - ('opt_out', '=', True) - ]).ids - - def _sms_get_partner_fields(self): - return ['customer_id'] - - def _sms_get_number_fields(self): - # TDE note: should override _phone_get_number_fields but ok as sms in dependencies - return ['phone_nbr', 'mobile_nbr'] - - -class MailTestSMSPartner(models.Model): - """ A model like sale order having only a customer, not specific phone - or mobile fields. """ - _description = 'Chatter Model for SMS Gateway (Partner only)' - _name = 'mail.test.sms.partner' - _inherit = ['mail.thread'] - _mailing_enabled = True - - name = fields.Char() - customer_id = fields.Many2one('res.partner', 'Customer') - opt_out = fields.Boolean() - - def _mail_get_partner_fields(self): - return ['customer_id'] - - def _mailing_get_opt_out_list_sms(self, mailing): - res_ids = mailing._get_recipients() - return self.search([ - ('id', 'in', res_ids), - ('opt_out', '=', True) - ]).ids - - def _sms_get_partner_fields(self): - return ['customer_id'] - - def _sms_get_number_fields(self): - return [] - - -class MailTestSMSPartner2Many(models.Model): - """ A model like sale order having only a customer, not specific phone - or mobile fields. """ - _description = 'Chatter Model for SMS Gateway (M2M Partners only)' - _name = 'mail.test.sms.partner.2many' - _inherit = ['mail.thread'] - _mailing_enabled = True - - name = fields.Char() - customer_ids = fields.Many2many('res.partner', string='Customers') - opt_out = fields.Boolean() - - def _mail_get_partner_fields(self): - return ['customer_ids'] - - def _mailing_get_opt_out_list_sms(self, mailing): - res_ids = mailing._get_recipients() - return self.search([ - ('id', 'in', res_ids), - ('opt_out', '=', True) - ]).ids - - def _sms_get_partner_fields(self): - return ['customer_ids'] - - def _sms_get_number_fields(self): - return [] diff --git a/addons/test_mail_sms/security/ir.model.access.csv b/addons/test_mail_sms/security/ir.model.access.csv deleted file mode 100644 index 657c8e387824c..0000000000000 --- a/addons/test_mail_sms/security/ir.model.access.csv +++ /dev/null @@ -1,13 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_mail_test_sms_all,mail.test.sms.all,model_mail_test_sms,,0,0,0,0 -access_mail_test_sms_user,mail.test.sms.user,model_mail_test_sms,base.group_user,1,1,1,1 -access_mail_test_sms_bl_all,mail.test.sms.bl.all,model_mail_test_sms_bl,,0,0,0,0 -access_mail_test_sms_bl_user,mail.test.sms.bl.user,model_mail_test_sms_bl,base.group_user,1,1,1,1 -access_mail_test_sms_bl_activity_all,mail.test.sms.bl.activity.all,model_mail_test_sms_bl_activity,,0,0,0,0 -access_mail_test_sms_bl_activity_user,mail.test.sms.bl.activity.user,model_mail_test_sms_bl_activity,base.group_user,1,1,1,1 -access_mail_test_sms_bl_optout_all,mail.test.sms.bl.optout.all,model_mail_test_sms_bl_optout,,0,0,0,0 -access_mail_test_sms_bl_optout_user,mail.test.sms.bl.optout.user,model_mail_test_sms_bl_optout,base.group_user,1,1,1,1 -access_mail_test_sms_partner_all,mail.test.sms.partner.all,model_mail_test_sms_partner,,0,0,0,0 -access_mail_test_sms_partner_user,mail.test.sms.partner.user,model_mail_test_sms_partner,base.group_user,1,1,1,1 -access_mail_test_sms_partner_2many_all,mail.test.sms.partner.2many.all,model_mail_test_sms_partner_2many,,0,0,0,0 -access_mail_test_sms_partner_2many_user,mail.test.sms.partner.2many.user,model_mail_test_sms_partner_2many,base.group_user,1,1,1,1 diff --git a/addons/test_mail_sms/tests/__init__.py b/addons/test_mail_sms/tests/__init__.py deleted file mode 100644 index a7a67dc3ef44e..0000000000000 --- a/addons/test_mail_sms/tests/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import test_mail_thread_phone -from . import test_phone_blacklist -from . import test_sms_composer -from . import test_sms_management -from . import test_sms_performance -from . import test_sms_post -from . import test_sms_server_actions -from . import test_sms_sms -from . import test_sms_template diff --git a/addons/test_mail_sms/tests/common.py b/addons/test_mail_sms/tests/common.py deleted file mode 100644 index d3a3590afa4e9..0000000000000 --- a/addons/test_mail_sms/tests/common.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.addons.phone_validation.tools import phone_validation -from odoo.addons.sms.tests.common import SMSCommon -from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients - - -class TestSMSCommon(SMSCommon, TestMailCommon): - """ Main entry point for functional tests. Kept to ease backward - compatibility and updating common. """ - - -class TestSMSRecipients(TestRecipients): - - @classmethod - def setUpClass(cls): - super(TestSMSRecipients, cls).setUpClass() - cls.partner_numbers = [ - phone_validation.phone_format(partner.mobile, partner.country_id.code, partner.country_id.phone_code, force_format='E164') - for partner in (cls.partner_1 | cls.partner_2) - ] diff --git a/addons/test_mail_sms/tests/test_mail_thread_phone.py b/addons/test_mail_sms/tests/test_mail_thread_phone.py deleted file mode 100644 index 0ad61f0e27960..0000000000000 --- a/addons/test_mail_sms/tests/test_mail_thread_phone.py +++ /dev/null @@ -1,110 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.addons.test_mail_sms.tests.common import TestSMSCommon, TestSMSRecipients -from odoo.tests import tagged, users - - -@tagged('mail_thread') -class TestSMSActionsCommon(TestSMSCommon, TestSMSRecipients): - """ Test mail.thread.phone mixin, its tools and API """ - - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.test_phone_records, cls.test_phone_partners = cls._create_records_for_batch( - 'mail.test.sms.bl', - 5, - ) - cls.test_phone_records += cls.env['mail.test.sms.bl'].create([ - { - 'phone_nbr': '+32475110505', - 'mobile_nbr': '+32475000505', - }, { - 'phone_nbr': '0032475110606', - 'mobile_nbr': '0032475000606', - }, { - 'phone_nbr': '0032475110707', - 'mobile_nbr': False, - }, { - 'phone_nbr': False, - 'mobile_nbr': False, - }, - ]) - - def test_initial_data(self): - """ Test initial data for this class, allowing to be sure of I/O of tests. """ - self.assertEqual( - self.test_phone_records.mapped('mobile_nbr'), - ['0475000000', '0475000101', '0475000202', '0475000303', '0475000404', - '+32475000505', '0032475000606', - False, False, - ] - ) - self.assertEqual( - self.test_phone_records.mapped('phone_nbr'), - [False] * 5 + ['+32475110505', '0032475110606', '0032475110707', False] - ) - - @users('employee') - def test_search_phone_mobile_search_boolean(self): - test_phone_records = self.test_phone_records.with_env(self.env) - - # test Falsy -> is set / is not set - for test_values in [False, '', ' ']: - # test is not set -> both fields should be not set - results = self.env['mail.test.sms.bl'].search([('phone_mobile_search', '=', test_values)]) - self.assertEqual(results, test_phone_records[-1], - 'Search on phone_mobile_search: = False: record with two void values') - # test is set -> at least one field should be set - results = self.env['mail.test.sms.bl'].search([('phone_mobile_search', '!=', test_values)]) - self.assertEqual(results, test_phone_records[:-1], - 'Search on phone_mobile_search: != False: record at least one value set') - - # test Truthy -> is set / is not set - results = self.env['mail.test.sms.bl'].search([('phone_mobile_search', '=', True)]) - self.assertEqual(results, test_phone_records[:-1], - 'Search on phone_mobile_search: = True: record at least one value set') - results = self.env['mail.test.sms.bl'].search([('phone_mobile_search', '!=', True)]) - self.assertEqual(results, test_phone_records[-1], - 'Search on phone_mobile_search: != True: record with two void values') - - @users('employee') - def test_search_phone_mobile_search_equal(self): - """ Test searching by phone/mobile with direct search """ - test_phone_records = self.test_phone_records.with_env(self.env) - - # test "=" search - results = self.env['mail.test.sms.bl'].search([('phone_mobile_search', '=', '0475')]) - self.assertFalse(results, 'Search on phone_mobile_search: = should return only matching results') - results = self.env['mail.test.sms.bl'].search([('phone_mobile_search', '=', '0475000000')]) - self.assertEqual(results, test_phone_records[0]) - results = self.env['mail.test.sms.bl'].search([('phone_mobile_search', '=', '0032475110606')]) - self.assertEqual(results, test_phone_records[6]) - results = self.env['mail.test.sms.bl'].search([('phone_mobile_search', '=', '+32475110606')]) - self.assertEqual(results, test_phone_records[6]) - - @users('employee') - def test_search_phone_mobile_search_ilike(self): - """ Test searching by phone/mobile on various ilike combinations """ - test_phone_records = self.test_phone_records.with_env(self.env) - - # test ilike search - results = self.env['mail.test.sms.bl'].search([('phone_mobile_search', 'ilike', '0475')]) - self.assertEqual(results, test_phone_records[:5]) - results = self.env['mail.test.sms.bl'].search([('phone_mobile_search', 'ilike', '101')]) - self.assertEqual(results, test_phone_records[1]) - - # test search using +32/0032 - results = self.env['mail.test.sms.bl'].search([('phone_mobile_search', 'ilike', '+32475')]) - self.assertEqual(results, test_phone_records[5:8], - 'Search on phone_mobile_search: +32/0032 likeliness') - results = self.env['mail.test.sms.bl'].search([('phone_mobile_search', 'ilike', '0032475')]) - self.assertEqual(results, test_phone_records[5:8], - 'Search on phone_mobile_search: +32/0032 likeliness') - - # test inverse ilike search - results = self.env['mail.test.sms.bl'].search([('phone_mobile_search', 'not ilike', '0475')]) - self.assertEqual(results, test_phone_records - test_phone_records[:5]) - results = self.env['mail.test.sms.bl'].search([('phone_mobile_search', 'not ilike', '101')]) - self.assertEqual(results, test_phone_records - test_phone_records[1]) diff --git a/addons/test_mail_sms/tests/test_phone_blacklist.py b/addons/test_mail_sms/tests/test_phone_blacklist.py deleted file mode 100644 index 2d3ae14469e3f..0000000000000 --- a/addons/test_mail_sms/tests/test_phone_blacklist.py +++ /dev/null @@ -1,82 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.addons.test_mail_sms.tests.common import TestSMSCommon, TestSMSRecipients - - -class TestPhoneBlacklist(TestSMSCommon, TestSMSRecipients): - """ Test phone blacklist management """ - - @classmethod - def setUpClass(cls): - super(TestPhoneBlacklist, cls).setUpClass() - cls._test_body = 'VOID CONTENT' - - cls.test_record = cls.env['mail.test.sms.bl'].with_context(**cls._test_context).create({ - 'name': 'Test', - 'customer_id': cls.partner_1.id, - 'mobile_nbr': cls.test_numbers[0], - 'phone_nbr': cls.test_numbers[1], - }) - cls.test_record = cls._reset_mail_context(cls.test_record) - - def test_phone_blacklist_internals(self): - with self.with_user('employee'): - test_record = self.env['mail.test.sms.bl'].browse(self.test_record.id) - self.assertEqual(test_record.phone_sanitized, self.test_numbers_san[1]) - self.assertFalse(test_record.phone_sanitized_blacklisted) - - bl_record = self.env['phone.blacklist'].sudo().create([{'number': self.test_numbers_san[1]}]) - test_record.invalidate_recordset() - self.assertTrue(test_record.phone_sanitized_blacklisted) - - self.env['phone.blacklist'].sudo().remove(self.test_numbers_san[1]) - self.assertFalse(bl_record.active) - test_record.invalidate_recordset() - self.assertFalse(test_record.phone_sanitized_blacklisted) - - self.env['phone.blacklist'].sudo().add(self.test_numbers_san[1]) - self.assertTrue(bl_record.active) - test_record.invalidate_recordset() - self.assertTrue(test_record.phone_sanitized_blacklisted) - - bl_record_2 = self.env['phone.blacklist'].sudo().create([{'number': self.test_numbers_san[1]}]) - self.assertEqual(bl_record, bl_record_2) - - rec = self.env['mail.test.sms.bl'].search([('phone_sanitized_blacklisted', '=', True)]) - self.assertEqual(rec, test_record) - - bl_record.unlink() - rec = self.env['mail.test.sms.bl'].search([('phone_sanitized_blacklisted', '=', True)]) - self.assertEqual(rec, self.env['mail.test.sms.bl']) - - def test_phone_sanitize_api(self): - with self.with_user('employee'): - test_record = self.env['mail.test.sms.bl'].browse(self.test_record.id) - self.assertFalse(test_record.phone_sanitized_blacklisted) - - test_record._phone_set_blacklisted() - test_record.invalidate_recordset() - self.assertTrue(test_record.phone_sanitized_blacklisted) - - test_record._phone_reset_blacklisted() - test_record.invalidate_recordset() - self.assertFalse(test_record.phone_sanitized_blacklisted) - - def test_phone_sanitize_internals(self): - with self.with_user('employee'): - test_record = self.env['mail.test.sms.bl'].browse(self.test_record.id) - self.assertEqual(test_record.phone_nbr, self.test_numbers[1]) - self.assertEqual(test_record.phone_sanitized, self.test_numbers_san[1]) - - test_record.write({'phone_nbr': 'incorrect'}) - self.assertEqual(test_record.phone_nbr, 'incorrect') - self.assertEqual(test_record.phone_sanitized, self.test_numbers_san[0]) - - test_record.write({'mobile_nbr': 'incorrect'}) - self.assertEqual(test_record.mobile_nbr, 'incorrect') - self.assertEqual(test_record.phone_sanitized, False) - - test_record.write({'phone_nbr': self.test_numbers[1]}) - self.assertEqual(test_record.phone_nbr, self.test_numbers[1]) - self.assertEqual(test_record.phone_sanitized, self.test_numbers_san[1]) diff --git a/addons/test_mail_sms/tests/test_sms_composer.py b/addons/test_mail_sms/tests/test_sms_composer.py deleted file mode 100644 index 6a185002606b4..0000000000000 --- a/addons/test_mail_sms/tests/test_sms_composer.py +++ /dev/null @@ -1,621 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.addons.test_mail_sms.tests.common import TestSMSCommon, TestSMSRecipients - - -class TestSMSComposerComment(TestSMSCommon, TestSMSRecipients): - """ TODO LIST - - * add test for default_res_model / default_res_id and stuff like that; - * add test for comment put in queue; - * add test for language support (set template lang context); - * add test for sanitized / wrong numbers; - """ - - @classmethod - def setUpClass(cls): - super(TestSMSComposerComment, cls).setUpClass() - cls._test_body = 'VOID CONTENT' - - cls.test_record = cls.env['mail.test.sms'].with_context(**cls._test_context).create({ - 'name': 'Test', - 'customer_id': cls.partner_1.id, - 'mobile_nbr': cls.test_numbers[0], - 'phone_nbr': cls.test_numbers[1], - }) - cls.test_record = cls._reset_mail_context(cls.test_record) - - cls.sms_template = cls.env['sms.template'].create({ - 'name': 'Test Template', - 'model_id': cls.env['ir.model']._get('mail.test.sms').id, - 'body': 'Dear {{ object.display_name }} this is an SMS.', - }) - - def test_composer_comment_not_mail_thread(self): - with self.with_user('employee'): - record = self.env['test_performance.base'].create({'name': 'TestBase'}) - composer = self.env['sms.composer'].with_context( - active_model='test_performance.base', active_id=record.id - ).create({ - 'body': self._test_body, - 'numbers': ','.join(self.random_numbers), - }) - - with self.mockSMSGateway(): - composer._action_send_sms() - - # use sms.api directly, does not create sms.sms - self.assertNoSMS() - self.assertSMSIapSent(self.random_numbers_san, self._test_body) - - def test_composer_comment_default(self): - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - active_model='mail.test.sms', active_id=self.test_record.id - ).create({ - 'body': self._test_body, - }) - - with self.mockSMSGateway(): - messages = composer._action_send_sms() - - self.assertSMSNotification([{'partner': self.test_record.customer_id, 'number': self.test_numbers_san[1]}], self._test_body, messages) - - def test_composer_comment_field_1(self): - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - active_model='mail.test.sms', active_id=self.test_record.id, - ).create({ - 'body': self._test_body, - 'number_field_name': 'mobile_nbr', - }) - - with self.mockSMSGateway(): - messages = composer._action_send_sms() - - self.assertSMSNotification([{'partner': self.test_record.customer_id, 'number': self.test_numbers_san[0]}], self._test_body, messages) - - def test_composer_comment_field_2(self): - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - active_model='mail.test.sms', active_id=self.test_record.id, - ).create({ - 'body': self._test_body, - 'number_field_name': 'phone_nbr', - }) - - with self.mockSMSGateway(): - messages = composer._action_send_sms() - - self.assertSMSNotification([{'partner': self.test_record.customer_id, 'number': self.test_numbers_san[1]}], self._test_body, messages) - - def test_composer_comment_field_w_numbers(self): - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - active_model='mail.test.sms', active_id=self.test_record.id, - default_number_field_name='mobile_nbr', - ).create({ - 'body': self._test_body, - 'numbers': ','.join(self.random_numbers), - }) - - with self.mockSMSGateway(): - messages = composer._action_send_sms() - - self.assertSMSNotification([ - {'partner': self.test_record.customer_id, 'number': self.test_record.mobile_nbr}, - {'number': self.random_numbers_san[0]}, {'number': self.random_numbers_san[1]}], self._test_body, messages) - - def test_composer_comment_field_w_template(self): - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - active_model='mail.test.sms', active_id=self.test_record.id, - default_template_id=self.sms_template.id, - default_number_field_name='mobile_nbr', - ).create({}) - - with self.mockSMSGateway(): - messages = composer._action_send_sms() - - self.assertSMSNotification([{'partner': self.test_record.customer_id, 'number': self.test_record.mobile_nbr}], 'Dear %s this is an SMS.' % self.test_record.display_name, messages) - - def test_composer_comment_invalid_field(self): - """ Test the Send Message in SMS Composer when a Model does not contain a number field name """ - test_record = self.env['mail.test.sms.partner'].create({ - 'name': 'Test', - 'customer_id': self.partner_1.id, - }) - sms_composer = self.env['sms.composer'].create({ - 'body': self._test_body, - 'number_field_name': 'phone_nbr', - 'recipient_single_number_itf': self.random_numbers_san[0], - 'res_id': test_record.id, - 'res_model': 'mail.test.sms.partner' - }) - - self.assertNotIn(','.join(test_record._fields), 'phone_nbr') - with self.mockSMSGateway(): - sms_composer._action_send_sms() - self.assertSMSNotification([{'number': self.random_numbers_san[0]}], self._test_body) - - def test_composer_comment_nofield(self): - """ Test the Send Message in SMS Composer when a Model does not contain any phone number related field """ - test_record = self.env['mail.test.sms.partner'].create({'name': 'Test'}) - sms_composer = self.env['sms.composer'].create({ - 'body': self._test_body, - 'recipient_single_number_itf': self.random_numbers_san[0], - 'res_id': test_record.id, - 'res_model': 'mail.test.sms.partner' - }) - with self.mockSMSGateway(): - sms_composer._action_send_sms() - self.assertSMSNotification([{'number': self.random_numbers_san[0]}], self._test_body) - - def test_composer_default_recipient(self): - """ Test default description of SMS composer must be partner name""" - self.test_record.write({ - 'phone_nbr': '0123456789', - }) - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - default_res_model='mail.test.sms', default_res_id=self.test_record.id, - ).create({ - 'body': self._test_body, - 'number_field_name': 'phone_nbr', - }) - - self.assertEqual(composer.recipient_single_description, self.test_record.customer_id.display_name) - - def test_composer_nofield_w_customer(self): - """ Test SMS composer without number field, the number on partner must be used instead""" - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - default_res_model='mail.test.sms', default_res_id=self.test_record.id, - ).create({ - 'body': self._test_body, - }) - - self.assertTrue(composer.recipient_single_valid) - self.assertEqual(composer.recipient_single_number, self.test_numbers[1]) - self.assertEqual(composer.recipient_single_number_itf, self.test_numbers[1]) - - def test_composer_internals(self): - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - default_res_model='mail.test.sms', default_res_id=self.test_record.id, - ).create({ - 'body': self._test_body, - 'number_field_name': 'phone_nbr', - }) - - self.assertEqual(composer.res_model, self.test_record._name) - self.assertEqual(composer.res_id, self.test_record.id) - self.assertEqual(composer.number_field_name, 'phone_nbr') - self.assertTrue(composer.comment_single_recipient) - self.assertEqual(composer.recipient_single_description, self.test_record.customer_id.display_name) - self.assertEqual(composer.recipient_single_number, self.test_numbers[1]) - self.assertEqual(composer.recipient_single_number_itf, self.test_numbers[1]) - self.assertTrue(composer.recipient_single_valid) - self.assertEqual(composer.recipient_valid_count, 1) - self.assertEqual(composer.recipient_invalid_count, 0) - - with self.with_user('employee'): - composer.update({'recipient_single_number_itf': '0123456789'}) - - self.assertFalse(composer.recipient_single_valid) - - with self.with_user('employee'): - composer.update({'recipient_single_number_itf': self.random_numbers[0]}) - - self.assertTrue(composer.recipient_single_valid) - - with self.with_user('employee'): - with self.mockSMSGateway(): - composer.action_send_sms() - - self.test_record.flush_recordset() - self.assertEqual(self.test_record.phone_nbr, self.random_numbers[0]) - - def test_composer_comment_wo_partner_wo_value_update(self): - """ Test record without partner and without phone values: should allow updating first found phone field """ - self.test_record.write({ - 'customer_id': False, - 'phone_nbr': False, - 'mobile_nbr': False, - }) - default_field_name = self.env['mail.test.sms']._sms_get_number_fields()[0] - - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - active_model='mail.test.sms', active_id=self.test_record.id, - default_composition_mode='comment', - ).create({ - 'body': self._test_body, - }) - self.assertFalse(composer.recipient_single_number_itf) - self.assertFalse(composer.recipient_single_number) - self.assertEqual(composer.number_field_name, default_field_name) - - composer.write({ - 'recipient_single_number_itf': self.random_numbers_san[0], - }) - self.assertEqual(composer.recipient_single_number_itf, self.random_numbers_san[0]) - self.assertFalse(composer.recipient_single_number) - - with self.mockSMSGateway(): - messages = composer._action_send_sms() - - self.assertEqual(self.test_record[default_field_name], self.random_numbers_san[0]) - self.assertSMSNotification([{'partner': self.env['res.partner'], 'number': self.random_numbers_san[0]}], self._test_body, messages) - - def test_composer_numbers_no_model(self): - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - default_composition_mode='numbers' - ).create({ - 'body': self._test_body, - 'numbers': ','.join(self.random_numbers), - }) - - with self.mockSMSGateway(): - composer._action_send_sms() - - # use sms.api directly, does not create sms.sms - self.assertNoSMS() - self.assertSMSIapSent(self.random_numbers_san, self._test_body) - - def test_composer_sending_with_no_number_field(self): - test_record = self.env['mail.test.sms.partner'].create({'name': 'Test'}) - sms_composer = self.env['sms.composer'].create({ - 'body': self._test_body, - 'composition_mode': 'comment', - 'mass_force_send': False, - 'mass_keep_log': True, - 'number_field_name': False, - 'numbers': False, - 'recipient_single_number_itf': self.random_numbers_san[0], - 'res_id': test_record.id, - 'res_model': 'mail.test.sms.partner' - }) - with self.mockSMSGateway(): - sms_composer._action_send_sms() - self.assertSMSNotification([{'number': self.random_numbers_san[0]}], self._test_body) - - -class TestSMSComposerBatch(TestSMSCommon): - @classmethod - def setUpClass(cls): - super(TestSMSComposerBatch, cls).setUpClass() - cls._test_body = 'Hello {{ object.name }} zizisse an SMS.' - - cls._create_records_for_batch('mail.test.sms', 3) - cls.sms_template = cls._create_sms_template('mail.test.sms') - - def test_composer_batch_active_ids(self): - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - default_composition_mode='comment', - default_res_model='mail.test.sms', - active_ids=self.records.ids - ).create({ - 'body': self._test_body, - }) - - with self.mockSMSGateway(): - messages = composer._action_send_sms() - - for record, message in zip(self.records, messages): - self.assertSMSNotification( - [{'partner': record.customer_id}], - 'Hello %s zizisse an SMS.' % record.name, - message - ) - - def test_composer_batch_res_ids(self): - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - default_composition_mode='comment', - default_res_model='mail.test.sms', - default_res_ids=repr(self.records.ids), - ).create({ - 'body': self._test_body, - }) - - with self.mockSMSGateway(): - messages = composer._action_send_sms() - - for record, message in zip(self.records, messages): - self.assertSMSNotification( - [{'partner': record.customer_id}], - 'Hello %s zizisse an SMS.' % record.name, - message - ) - - -class TestSMSComposerMass(TestSMSCommon): - - @classmethod - def setUpClass(cls): - super(TestSMSComposerMass, cls).setUpClass() - cls._test_body = 'Hello {{ object.name }} zizisse an SMS.' - - cls._create_records_for_batch('mail.test.sms', 10) - cls.sms_template = cls._create_sms_template('mail.test.sms') - - def test_composer_mass_active_ids(self): - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - default_composition_mode='mass', - default_res_model='mail.test.sms', - active_ids=self.records.ids, - ).create({ - 'body': self._test_body, - 'mass_keep_log': False, - }) - - with self.mockSMSGateway(): - composer.action_send_sms() - - for partner, record in zip(self.partners, self.records): - self.assertSMSOutgoing( - partner, None, - content='Hello %s zizisse an SMS.' % record.name - ) - - def test_composer_mass_active_ids_w_blacklist(self): - self.env['phone.blacklist'].create([{ - 'number': p.phone_sanitized, - 'active': True, - } for p in self.partners[:5]]) - - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - default_composition_mode='mass', - default_res_model='mail.test.sms', - active_ids=self.records.ids, - ).create({ - 'body': self._test_body, - 'mass_keep_log': False, - 'mass_use_blacklist': True, - }) - - with self.mockSMSGateway(): - composer.action_send_sms() - - for partner, record in zip(self.partners[5:], self.records[5:]): - self.assertSMSOutgoing( - partner, partner.phone_sanitized, - content='Hello %s zizisse an SMS.' % record.name - ) - for partner, record in zip(self.partners[:5], self.records[:5]): - self.assertSMSCanceled( - partner, partner.phone_sanitized, - failure_type='sms_blacklist', - content='Hello %s zizisse an SMS.' % record.name - ) - - def test_composer_mass_active_ids_wo_blacklist(self): - self.env['phone.blacklist'].create([{ - 'number': p.phone_sanitized, - 'active': True, - } for p in self.partners[:5]]) - - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - default_composition_mode='mass', - default_res_model='mail.test.sms', - active_ids=self.records.ids, - ).create({ - 'body': self._test_body, - 'mass_keep_log': False, - 'mass_use_blacklist': False, - }) - - with self.mockSMSGateway(): - composer.action_send_sms() - - for partner, record in zip(self.partners, self.records): - self.assertSMSOutgoing( - partner, partner.phone_sanitized, - content='Hello %s zizisse an SMS.' % record.name - ) - - def test_composer_mass_active_ids_w_blacklist_and_done(self): - """ Create some duplicates + blacklist. record[5] will have duplicated - number on 6 and 7. """ - self.env['phone.blacklist'].create([{ - 'number': p.phone_sanitized, - 'active': True, - } for p in self.partners[:5]]) - for p in self.partners[5:8]: - p.mobile = self.partners[5].mobile - self.assertEqual(p.phone_sanitized, self.partners[5].phone_sanitized) - - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - default_composition_mode='mass', - default_res_model='mail.test.sms', - active_ids=self.records.ids, - ).create({ - 'body': self._test_body, - 'mass_keep_log': False, - 'mass_use_blacklist': True, - }) - - with self.mockSMSGateway(): - composer.action_send_sms() - - self.assertSMSOutgoing( - self.partners[5], self.partners[5].phone_sanitized, - content='Hello %s zizisse an SMS.' % self.records[5].name - ) - for partner, record in zip(self.partners[8:], self.records[8:]): - self.assertSMSOutgoing( - partner, partner.phone_sanitized, - content='Hello %s zizisse an SMS.' % record.name - ) - # duplicates - for partner, record in zip(self.partners[6:8], self.records[6:8]): - self.assertSMSCanceled( - partner, partner.phone_sanitized, - failure_type='sms_duplicate', - content='Hello %s zizisse an SMS.' % record.name - ) - # blacklist - for partner, record in zip(self.partners[:5], self.records[:5]): - self.assertSMSCanceled( - partner, partner.phone_sanitized, - failure_type='sms_blacklist', - content='Hello %s zizisse an SMS.' % record.name - ) - - def test_composer_mass_active_ids_w_template(self): - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - default_composition_mode='mass', - default_res_model='mail.test.sms', - active_ids=self.records.ids, - default_template_id=self.sms_template.id, - ).create({ - 'mass_keep_log': False, - }) - - with self.mockSMSGateway(): - composer.action_send_sms() - - for record in self.records: - self.assertSMSOutgoing( - record.customer_id, None, - content='Dear %s this is an SMS.' % record.display_name - ) - - def test_composer_mass_active_ids_w_template_and_lang(self): - self.env['res.lang']._activate_lang('fr_FR') - self.sms_template.with_context(lang='fr_FR').body = 'Cher·e· {{ object.display_name }} ceci est un SMS.' - # set template to try to use customer lang - self.sms_template.write({ - 'lang': '{{ object.customer_id.lang }}', - }) - # set one customer as french speaking - self.partners[2].write({'lang': 'fr_FR'}) - - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - default_composition_mode='mass', - default_res_model='mail.test.sms', - active_ids=self.records.ids, - default_template_id=self.sms_template.id, - ).create({ - 'mass_keep_log': False, - }) - - with self.mockSMSGateway(): - composer.action_send_sms() - - for record in self.records: - if record.customer_id == self.partners[2]: - self.assertSMSOutgoing( - record.customer_id, None, - content='Cher·e· %s ceci est un SMS.' % record.display_name - ) - else: - self.assertSMSOutgoing( - record.customer_id, None, - content='Dear %s this is an SMS.' % record.display_name - ) - - def test_composer_mass_active_ids_w_template_and_log(self): - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - default_composition_mode='mass', - default_res_model='mail.test.sms', - active_ids=self.records.ids, - default_template_id=self.sms_template.id, - ).create({ - 'mass_keep_log': True, - }) - - with self.mockSMSGateway(): - composer.action_send_sms() - - for record in self.records: - self.assertSMSOutgoing( - record.customer_id, None, - content='Dear %s this is an SMS.' % record.display_name - ) - self.assertSMSLogged(record, 'Dear %s this is an SMS.' % record.display_name) - - def test_composer_template_context_action(self): - """ Test the context action from a SMS template (Add context action button) - and the usage with the sms composer """ - # Create the lang info - self.env['res.lang']._activate_lang('fr_FR') - self.sms_template.with_context(lang='fr_FR').body = 'Hello {{ object.display_name }} ceci est en français.' - # set template to try to use customer lang - self.sms_template.write({ - 'lang': '{{ object.customer_id.lang }}', - }) - # create a second record linked to a customer in another language - self.partners[2].write({'lang': 'fr_FR'}) - test_record_2 = self.env['mail.test.sms'].create({ - 'name': 'Test', - 'customer_id': self.partners[2].id, - }) - test_record_1 = self.env['mail.test.sms'].create({ - 'name': 'Test', - 'customer_id': self.partners[1].id, - }) - # Composer creation with context from a template context action (simulate) - comment (single recipient) - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - sms_composition_mode='guess', - default_res_ids=[test_record_2.id], - default_res_id=test_record_2.id, - active_ids=[test_record_2.id], - active_id=test_record_2.id, - active_model='mail.test.sms', - default_template_id=self.sms_template.id, - ).create({ - 'mass_keep_log': False, - }) - self.assertEqual(composer.composition_mode, "comment") - self.assertEqual(composer.body, "Hello %s ceci est en français." % test_record_2.display_name) - - with self.mockSMSGateway(): - messages = composer._action_send_sms() - - number = self.partners[2].phone_get_sanitized_number() - self.assertSMSNotification( - [{'partner': test_record_2.customer_id, 'number': number}], - "Hello %s ceci est en français." % test_record_2.display_name, messages - ) - - # Composer creation with context from a template context action (simulate) - mass (multiple recipient) - with self.with_user('employee'): - composer = self.env['sms.composer'].with_context( - sms_composition_mode='guess', - default_res_ids=[test_record_1.id, test_record_2.id], - default_res_id=test_record_1.id, - active_ids=[test_record_1.id, test_record_2.id], - active_id=test_record_1.id, - active_model='mail.test.sms', - default_template_id=self.sms_template.id, - ).create({ - 'mass_keep_log': True, - }) - self.assertEqual(composer.composition_mode, "mass") - # In english because by default but when sinding depending of record - self.assertEqual(composer.body, "Dear {{ object.display_name }} this is an SMS.") - - with self.mockSMSGateway(): - composer.action_send_sms() - - self.assertSMSOutgoing( - test_record_1.customer_id, None, - content='Dear %s this is an SMS.' % test_record_1.display_name - ) - self.assertSMSOutgoing( - test_record_2.customer_id, None, - content="Hello %s ceci est en français." % test_record_2.display_name - ) diff --git a/addons/test_mail_sms/tests/test_sms_management.py b/addons/test_mail_sms/tests/test_sms_management.py deleted file mode 100644 index 1bdf940ad74e5..0000000000000 --- a/addons/test_mail_sms/tests/test_sms_management.py +++ /dev/null @@ -1,199 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.addons.test_mail_sms.tests.common import TestSMSCommon, TestSMSRecipients -from odoo.tests import tagged -from odoo.tools import mute_logger - - -class TestSMSActionsCommon(TestSMSCommon, TestSMSRecipients): - - @classmethod - def setUpClass(cls): - super(TestSMSActionsCommon, cls).setUpClass() - cls.test_record = cls.env['mail.test.sms'].with_context(**cls._test_context).create({ - 'name': 'Test', - 'customer_id': cls.partner_1.id, - }) - cls.test_record = cls._reset_mail_context(cls.test_record) - cls.msg = cls.test_record.message_post(body='TEST BODY', author_id=cls.partner_employee.id) - cls.sms_p1 = cls.env['sms.sms'].create({ - 'body': 'TEST BODY', - 'failure_type': 'sms_number_format', - 'mail_message_id': cls.msg.id, - 'number': cls.partner_1.mobile, - 'partner_id': cls.partner_1.id, - 'state': 'error', - }) - cls.notif_p1 = cls.env['mail.notification'].create({ - 'author_id': cls.msg.author_id.id, - 'mail_message_id': cls.msg.id, - 'res_partner_id': cls.partner_1.id, - 'sms_id': cls.sms_p1.id, - 'sms_number': cls.partner_1.mobile, - 'notification_type': 'sms', - 'notification_status': 'exception', - 'failure_type': 'sms_number_format', - }) - cls.sms_p2 = cls.env['sms.sms'].create({ - 'body': 'TEST BODY', - 'failure_type': 'sms_credit', - 'mail_message_id': cls.msg.id, - 'number': cls.partner_2.mobile, - 'partner_id': cls.partner_2.id, - 'state': 'error', - }) - cls.notif_p2 = cls.env['mail.notification'].create({ - 'author_id': cls.msg.author_id.id, - 'mail_message_id': cls.msg.id, - 'res_partner_id': cls.partner_2.id, - 'sms_id': cls.sms_p2.id, - 'sms_number': cls.partner_2.mobile, - 'notification_type': 'sms', - 'notification_status': 'exception', - 'failure_type': 'sms_credit', - }) - - -@tagged('sms_management') -class TestSMSActions(TestSMSActionsCommon): - - def test_sms_notify_cancel(self): - self._reset_bus() - - with self.with_user('employee'): - self.test_record.with_user(self.env.user).notify_cancel_by_type('sms') - self.assertEqual((self.notif_p1 | self.notif_p2).mapped('notification_status'), ['canceled', 'canceled']) - - self.assertMessageBusNotifications(self.msg) - - def test_sms_set_cancel(self): - self._reset_bus() - self.sms_p1.action_set_canceled() - self.assertEqual(self.sms_p1.state, 'canceled') - - self.assertMessageBusNotifications(self.msg) - self.assertSMSNotification([ - {'partner': self.partner_1, 'number': self.notif_p1.sms_number, 'state': 'canceled', 'failure_type': 'sms_number_format'}, - {'partner': self.partner_2, 'number': self.notif_p2.sms_number, 'state': 'exception', 'failure_type': 'sms_credit'} - ], 'TEST BODY', self.msg, check_sms=False) # do not check new sms as they already exist - - self._reset_bus() - self.sms_p2.with_context(sms_skip_msg_notification=True).action_set_canceled() - self.assertEqual(self.sms_p2.state, 'canceled') - - self.assertEqual(self.env['bus.bus'].search([]), self.env['bus.bus'], 'SMS: no bus notifications unless asked') - self.assertSMSNotification([ - {'partner': self.partner_1, 'number': self.notif_p1.sms_number, 'state': 'canceled', 'failure_type': 'sms_number_format'}, - {'partner': self.partner_2, 'number': self.notif_p2.sms_number, 'state': 'canceled', 'failure_type': 'sms_credit'} - ], 'TEST BODY', self.msg, check_sms=False) # do not check new sms as they already exist - - - def test_sms_set_error(self): - self._reset_bus() - (self.sms_p1 + self.sms_p2).with_context(sms_skip_msg_notification=True).action_set_canceled() - self.assertEqual(self.sms_p1.state, 'canceled') - self.assertEqual(self.sms_p2.state, 'canceled') - self.assertEqual(self.env['bus.bus'].search([]), self.env['bus.bus'], 'SMS: no bus notifications unless asked') - - (self.sms_p1 + self.sms_p2).action_set_error('sms_server') - self.assertEqual(self.sms_p1.state, 'error') - self.assertEqual(self.sms_p2.state, 'error') - - self.assertMessageBusNotifications(self.msg) - self.assertSMSNotification([ - {'partner': self.partner_1, 'number': self.notif_p1.sms_number, 'state': 'exception', 'failure_type': 'sms_server'}, - {'partner': self.partner_2, 'number': self.notif_p2.sms_number, 'state': 'exception', 'failure_type': 'sms_server'} - ], 'TEST BODY', self.msg, check_sms=False) # do not check new sms as they already exist - - def test_sms_set_outgoing(self): - self._reset_bus() - (self.sms_p1 + self.sms_p2).action_set_outgoing() - self.assertEqual(self.sms_p1.state, 'outgoing') - self.assertEqual(self.sms_p2.state, 'outgoing') - - self.assertMessageBusNotifications(self.msg) - self.assertSMSNotification([ - {'partner': self.partner_1, 'number': self.notif_p1.sms_number, 'state': 'ready'}, - {'partner': self.partner_2, 'number': self.notif_p2.sms_number, 'state': 'ready'} - ], 'TEST BODY', self.msg, check_sms=False) # do not check new sms as they already exist - - -@tagged('sms_management') -class TestSMSWizards(TestSMSActionsCommon): - - @mute_logger('odoo.addons.sms.models.sms_sms') - def test_sms_resend(self): - self._reset_bus() - - with self.with_user('employee'): - wizard = self.env['sms.resend'].with_context(default_mail_message_id=self.msg.id).create({}) - wizard.write({'recipient_ids': [(1, r.id, {'resend': True}) for r in wizard.recipient_ids]}) - with self.mockSMSGateway(): - wizard.action_resend() - - self.assertSMSNotification([ - {'partner': self.partner_1, 'state': 'sent'}, - {'partner': self.partner_2, 'state': 'sent'} - ], 'TEST BODY', self.msg, check_sms=True) - self.assertMessageBusNotifications(self.msg) - - @mute_logger('odoo.addons.sms.models.sms_sms') - def test_sms_resend_update_number(self): - self._reset_bus() - - with self.with_user('employee'): - wizard = self.env['sms.resend'].with_context(default_mail_message_id=self.msg.id).create({}) - wizard.write({'recipient_ids': [(1, r.id, {'resend': True, 'sms_number': self.random_numbers[idx]}) for idx, r in enumerate(wizard.recipient_ids.sorted())]}) - with self.mockSMSGateway(): - wizard.action_resend() - - self.assertSMSNotification([ - {'partner': self.partner_1, 'state': 'sent', 'number': self.random_numbers_san[0]}, - {'partner': self.partner_2, 'state': 'sent', 'number': self.random_numbers_san[1]} - ], 'TEST BODY', self.msg, check_sms=True) - self.assertMessageBusNotifications(self.msg) - - def test_sms_resend_cancel(self): - self._reset_bus() - - with self.with_user('employee'): - wizard = self.env['sms.resend'].with_context(default_mail_message_id=self.msg.id).create({}) - with self.mockSMSGateway(): - wizard.action_cancel() - - self.assertSMSNotification([ - {'partner': self.partner_1, 'state': 'canceled', 'number': self.notif_p1.sms_number, 'failure_type': 'sms_number_format'}, - {'partner': self.partner_2, 'state': 'canceled', 'number': self.notif_p2.sms_number, 'failure_type': 'sms_credit'} - ], 'TEST BODY', self.msg, check_sms=False) - self.assertMessageBusNotifications(self.msg) - - @mute_logger('odoo.addons.sms.models.sms_sms') - def test_sms_resend_internals(self): - self._reset_bus() - self.assertSMSNotification([ - {'partner': self.partner_1, 'state': 'exception', 'number': self.notif_p1.sms_number, 'failure_type': 'sms_number_format'}, - {'partner': self.partner_2, 'state': 'exception', 'number': self.notif_p2.sms_number, 'failure_type': 'sms_credit'} - ], 'TEST BODY', self.msg, check_sms=False) - - with self.with_user('employee'): - wizard = self.env['sms.resend'].with_context(default_mail_message_id=self.msg.id).create({}) - self.assertTrue(wizard.has_insufficient_credit) - self.assertEqual(set(wizard.mapped('recipient_ids.partner_name')), set((self.partner_1 | self.partner_2).mapped('display_name'))) - wizard.write({'recipient_ids': [(1, r.id, {'resend': True}) for r in wizard.recipient_ids]}) - with self.mockSMSGateway(): - wizard.action_resend() - - @mute_logger('odoo.addons.sms.models.sms_sms') - def test_sms_resend_w_cancel(self): - self._reset_bus() - - with self.with_user('employee'): - wizard = self.env['sms.resend'].with_context(default_mail_message_id=self.msg.id).create({}) - wizard.write({'recipient_ids': [(1, r.id, {'resend': True if r.partner_id == self.partner_1 else False}) for r in wizard.recipient_ids]}) - with self.mockSMSGateway(): - wizard.action_resend() - - self.assertSMSNotification([{'partner': self.partner_1, 'state': 'sent'}], 'TEST BODY', self.msg, check_sms=True) - self.assertSMSNotification([{'partner': self.partner_2, 'state': 'canceled', 'number': self.notif_p2.sms_number, 'failure_type': 'sms_credit'}], 'TEST BODY', self.msg, check_sms=False) - self.assertMessageBusNotifications(self.msg) diff --git a/addons/test_mail_sms/tests/test_sms_performance.py b/addons/test_mail_sms/tests/test_sms_performance.py deleted file mode 100644 index f1e7f9e21a68c..0000000000000 --- a/addons/test_mail_sms/tests/test_sms_performance.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.addons.sms.tests import common as sms_common -from odoo.addons.test_mail.tests.test_performance import BaseMailPerformance -from odoo.tests.common import users, warmup -from odoo.tests import tagged -from odoo.tools import mute_logger - - -@tagged('mail_performance', 'post_install', '-at_install') -class TestSMSPerformance(BaseMailPerformance, sms_common.SMSCase): - - def setUp(self): - super(TestSMSPerformance, self).setUp() - - self.test_record = self.env['mail.test.sms'].with_context(self._test_context).create({ - 'name': 'Test', - 'customer_id': self.customer.id, - 'phone_nbr': '0456999999', - }) - - # prepare recipients to test for more realistic workload - self.partners = self.env['res.partner'].with_context(self._test_context).create([ - {'name': 'Test %s' % x, - 'email': 'test%s@example.com' % x, - 'mobile': '0456%s%s0000' % (x, x), - 'country_id': self.env.ref('base.be').id, - } for x in range(0, 10) - ]) - - @mute_logger('odoo.addons.sms.models.sms_sms') - @users('employee') - @warmup - def test_message_sms_record_1_partner(self): - record = self.test_record.with_user(self.env.user) - pids = self.customer.ids - with self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=26): - messages = record._message_sms( - body='Performance Test', - partner_ids=pids, - ) - - self.assertEqual(record.message_ids[0].body, '

Performance Test

') - self.assertSMSNotification([{'partner': self.customer}], 'Performance Test', messages, sent_unlink=True) - - @mute_logger('odoo.addons.sms.models.sms_sms') - @users('employee') - @warmup - def test_message_sms_record_10_partners(self): - record = self.test_record.with_user(self.env.user) - pids = self.partners.ids - with self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=26): - messages = record._message_sms( - body='Performance Test', - partner_ids=pids, - ) - - self.assertEqual(record.message_ids[0].body, '

Performance Test

') - self.assertSMSNotification([{'partner': partner} for partner in self.partners], 'Performance Test', messages, sent_unlink=True) - - @mute_logger('odoo.addons.sms.models.sms_sms') - @users('employee') - @warmup - def test_message_sms_record_default(self): - record = self.test_record.with_user(self.env.user) - with self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=28): - messages = record._message_sms( - body='Performance Test', - ) - - self.assertEqual(record.message_ids[0].body, '

Performance Test

') - self.assertSMSNotification([{'partner': self.customer}], 'Performance Test', messages, sent_unlink=True) - - -@tagged('mail_performance', 'post_install', '-at_install') -class TestSMSMassPerformance(BaseMailPerformance, sms_common.MockSMS): - - def setUp(self): - super(TestSMSMassPerformance, self).setUp() - be_country_id = self.env.ref('base.be').id - - self._test_body = 'MASS SMS' - - records = self.env['mail.test.sms'] - partners = self.env['res.partner'] - for x in range(50): - partners += self.env['res.partner'].with_context(**self._test_context).create({ - 'name': 'Partner_%s' % (x), - 'email': '_test_partner_%s@example.com' % (x), - 'country_id': be_country_id, - 'mobile': '047500%02d%02d' % (x, x) - }) - records += self.env['mail.test.sms'].with_context(**self._test_context).create({ - 'name': 'Test_%s' % (x), - 'customer_id': partners[x].id, - }) - self.partners = partners - self.records = records - - self.sms_template = self.env['sms.template'].create({ - 'name': 'Test Template', - 'model_id': self.env['ir.model']._get('mail.test.sms').id, - 'body': 'Dear {{ object.display_name }} this is an SMS.', - }) - - @mute_logger('odoo.addons.sms.models.sms_sms') - @users('employee') - @warmup - def test_sms_composer_mass(self): - composer = self.env['sms.composer'].with_context( - default_composition_mode='mass', - default_res_model='mail.test.sms', - active_ids=self.records.ids, - ).create({ - 'body': self._test_body, - 'mass_keep_log': False, - }) - - with self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=56): - composer.action_send_sms() - - @mute_logger('odoo.addons.sms.models.sms_sms') - @users('employee') - @warmup - def test_sms_composer_mass_w_log(self): - composer = self.env['sms.composer'].with_context( - default_composition_mode='mass', - default_res_model='mail.test.sms', - active_ids=self.records.ids, - ).create({ - 'body': self._test_body, - 'mass_keep_log': True, - }) - - with self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=58): - composer.action_send_sms() diff --git a/addons/test_mail_sms/tests/test_sms_post.py b/addons/test_mail_sms/tests/test_sms_post.py deleted file mode 100644 index deb66ad85744e..0000000000000 --- a/addons/test_mail_sms/tests/test_sms_post.py +++ /dev/null @@ -1,437 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.addons.test_mail_sms.tests.common import TestSMSCommon, TestSMSRecipients - - -class TestSMSPost(TestSMSCommon, TestSMSRecipients): - """ TODO - - * add tests for new mail.message and mail.thread fields; - """ - - @classmethod - def setUpClass(cls): - super(TestSMSPost, cls).setUpClass() - cls._test_body = 'VOID CONTENT' - - cls.test_record = cls.env['mail.test.sms'].with_context(**cls._test_context).create({ - 'name': 'Test', - 'customer_id': cls.partner_1.id, - 'mobile_nbr': cls.test_numbers[0], - 'phone_nbr': cls.test_numbers[1], - }) - cls.test_record = cls._reset_mail_context(cls.test_record) - - def test_message_sms_internals_body(self): - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms('

Mega SMS
Top moumoutte

', partner_ids=self.partner_1.ids) - - self.assertEqual(messages.body, '

Mega SMS
Top moumoutte

') - self.assertEqual(messages.subtype_id, self.env.ref('mail.mt_note')) - self.assertSMSNotification([{'partner': self.partner_1}], 'Mega SMS\nTop moumoutte', messages) - - def test_message_sms_internals_resend_existingd(self): - with self.with_user('employee'), self.mockSMSGateway(sim_error='wrong_number_format'): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, partner_ids=self.partner_1.ids) - - self.assertSMSNotification([{'partner': self.partner_1, 'state': 'exception', 'failure_type': 'sms_number_format'}], self._test_body, messages) - - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - test_record._notify_thread_by_sms(messages, [{'id': self.partner_1.id, 'notif': 'sms'}], resend_existing=True) - self.assertSMSNotification([{'partner': self.partner_1}], self._test_body, messages) - - def test_message_sms_internals_sms_numbers(self): - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, partner_ids=self.partner_1.ids, sms_numbers=self.random_numbers) - - self.assertSMSNotification([{'partner': self.partner_1}, {'number': self.random_numbers_san[0]}, {'number': self.random_numbers_san[1]}], self._test_body, messages) - - def test_message_sms_internals_subtype(self): - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms('

Mega SMS
Top moumoutte

', subtype_id=self.env.ref('mail.mt_comment').id, partner_ids=self.partner_1.ids) - - self.assertEqual(messages.body, '

Mega SMS
Top moumoutte

') - self.assertEqual(messages.subtype_id, self.env.ref('mail.mt_comment')) - self.assertSMSNotification([{'partner': self.partner_1}], 'Mega SMS\nTop moumoutte', messages) - - def test_message_sms_internals_pid_to_number(self): - pid_to_number = { - self.partner_1.id: self.random_numbers[0], - self.partner_2.id: self.random_numbers[1], - } - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2).ids, sms_pid_to_number=pid_to_number) - - self.assertSMSNotification([ - {'partner': self.partner_1, 'number': self.random_numbers_san[0]}, - {'partner': self.partner_2, 'number': self.random_numbers_san[1]}], - self._test_body, messages) - - def test_message_sms_model_partner(self): - with self.with_user('employee'), self.mockSMSGateway(): - messages = self.partner_1._message_sms(self._test_body) - messages |= self.partner_2._message_sms(self._test_body) - self.assertSMSNotification([{'partner': self.partner_1}, {'partner': self.partner_2}], self._test_body, messages) - - def test_message_sms_model_partner_fallback(self): - self.partner_1.write({'mobile': False, 'phone': self.random_numbers[0]}) - - with self.mockSMSGateway(): - messages = self.partner_1._message_sms(self._test_body) - messages |= self.partner_2._message_sms(self._test_body) - - self.assertSMSNotification([{'partner': self.partner_1, 'number': self.random_numbers_san[0]}, {'partner': self.partner_2}], self._test_body, messages) - - def test_message_sms_model_w_partner_only(self): - with self.with_user('employee'): - record = self.env['mail.test.sms.partner'].create({'customer_id': self.partner_1.id}) - - with self.mockSMSGateway(): - messages = record._message_sms(self._test_body) - - self.assertSMSNotification([{'partner': self.partner_1}], self._test_body, messages) - - def test_message_sms_model_w_partner_only_void(self): - with self.with_user('employee'): - record = self.env['mail.test.sms.partner'].create({'customer_id': False}) - - with self.mockSMSGateway(): - messages = record._message_sms(self._test_body) - - # should not crash but have a failed notification - self.assertSMSNotification([{'partner': self.env['res.partner'], 'number': False, 'state': 'exception', 'failure_type': 'sms_number_missing'}], self._test_body, messages) - - def test_message_sms_model_w_partner_m2m_only(self): - with self.with_user('employee'): - record = self.env['mail.test.sms.partner.2many'].create({'customer_ids': [(4, self.partner_1.id)]}) - - with self.mockSMSGateway(): - messages = record._message_sms(self._test_body) - - self.assertSMSNotification([{'partner': self.partner_1}], self._test_body, messages) - - # TDE: should take first found one according to partner ordering - with self.with_user('employee'): - record = self.env['mail.test.sms.partner.2many'].create({'customer_ids': [(4, self.partner_1.id), (4, self.partner_2.id)]}) - - with self.mockSMSGateway(): - messages = record._message_sms(self._test_body) - - self.assertSMSNotification([{'partner': self.partner_2}], self._test_body, messages) - - def test_message_sms_on_field_w_partner(self): - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, number_field='mobile_nbr') - - self.assertSMSNotification([{'partner': self.partner_1, 'number': self.test_record.mobile_nbr}], self._test_body, messages) - - def test_message_sms_on_field_wo_partner(self): - self.test_record.write({'customer_id': False}) - - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, number_field='mobile_nbr') - - self.assertSMSNotification([{'number': self.test_record.mobile_nbr}], self._test_body, messages) - - def test_message_sms_on_field_wo_partner_wo_value(self): - """ Test record without a partner and without phone values. """ - self.test_record.write({ - 'customer_id': False, - 'phone_nbr': False, - 'mobile_nbr': False, - }) - - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body) - - # should not crash but have a failed notification - self.assertSMSNotification([{'partner': self.env['res.partner'], 'number': False, 'state': 'exception', 'failure_type': 'sms_number_missing'}], self._test_body, messages) - - def test_message_sms_on_field_wo_partner_default_field(self): - self.test_record.write({'customer_id': False}) - - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body) - - self.assertSMSNotification([{'number': self.test_numbers_san[1]}], self._test_body, messages) - - def test_message_sms_on_field_wo_partner_default_field_2(self): - self.test_record.write({'customer_id': False, 'phone_nbr': False}) - - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body) - - self.assertSMSNotification([{'number': self.test_numbers_san[0]}], self._test_body, messages) - - def test_message_sms_on_numbers(self): - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, sms_numbers=self.random_numbers_san) - self.assertSMSNotification([{'number': self.random_numbers_san[0]}, {'number': self.random_numbers_san[1]}], self._test_body, messages) - - def test_message_sms_on_numbers_sanitization(self): - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, sms_numbers=self.random_numbers) - self.assertSMSNotification([{'number': self.random_numbers_san[0]}, {'number': self.random_numbers_san[1]}], self._test_body, messages) - - def test_message_sms_on_partner_ids(self): - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2).ids) - - self.assertSMSNotification([{'partner': self.partner_1}, {'partner': self.partner_2}], self._test_body, messages) - - def test_message_sms_on_partner_ids_default(self): - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body) - - self.assertSMSNotification([{'partner': self.test_record.customer_id, 'number': self.test_numbers_san[1]}], self._test_body, messages) - - def test_message_sms_on_partner_ids_w_numbers(self): - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, partner_ids=self.partner_1.ids, sms_numbers=self.random_numbers[:1]) - - self.assertSMSNotification([{'partner': self.partner_1}, {'number': self.random_numbers_san[0]}], self._test_body, messages) - - def test_message_sms_with_template(self): - sms_template = self.env['sms.template'].create({ - 'name': 'Test Template', - 'model_id': self.env['ir.model']._get('mail.test.sms').id, - 'body': 'Dear {{ object.display_name }} this is an SMS.', - }) - - with self.with_user('employee'): - with self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms_with_template(template=sms_template) - - self.assertSMSNotification([{'partner': self.partner_1, 'number': self.test_numbers_san[1]}], 'Dear %s this is an SMS.' % self.test_record.display_name, messages) - - def test_message_sms_with_template_fallback(self): - with self.with_user('employee'): - with self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms_with_template(template_xmlid='test_mail_full.this_should_not_exists', template_fallback='Fallback for {{ object.id }}') - - self.assertSMSNotification([{'partner': self.partner_1, 'number': self.test_numbers_san[1]}], 'Fallback for %s' % self.test_record.id, messages) - - def test_message_sms_with_template_xmlid(self): - sms_template = self.env['sms.template'].create({ - 'name': 'Test Template', - 'model_id': self.env['ir.model']._get('mail.test.sms').id, - 'body': 'Dear {{ object.display_name }} this is an SMS.', - }) - self.env['ir.model.data'].create({ - 'name': 'this_should_exists', - 'module': 'test_mail_full', - 'model': sms_template._name, - 'res_id': sms_template.id, - }) - - with self.with_user('employee'): - with self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms_with_template(template_xmlid='test_mail_full.this_should_exists') - - self.assertSMSNotification([{'partner': self.partner_1, 'number': self.test_numbers_san[1]}], 'Dear %s this is an SMS.' % self.test_record.display_name, messages) - - -class TestSMSPostException(TestSMSCommon, TestSMSRecipients): - - @classmethod - def setUpClass(cls): - super(TestSMSPostException, cls).setUpClass() - cls._test_body = 'VOID CONTENT' - - cls.test_record = cls.env['mail.test.sms'].with_context(**cls._test_context).create({ - 'name': 'Test', - 'customer_id': cls.partner_1.id, - }) - cls.test_record = cls._reset_mail_context(cls.test_record) - cls.partner_3 = cls.env['res.partner'].with_context({ - 'mail_create_nolog': True, - 'mail_create_nosubscribe': True, - 'mail_notrack': True, - 'no_reset_password': True, - }).create({ - 'name': 'Ernestine Loubine', - 'email': 'ernestine.loubine@agrolait.com', - 'country_id': cls.env.ref('base.be').id, - 'mobile': '0475556644', - }) - - def test_message_sms_w_numbers_invalid(self): - random_numbers = self.random_numbers + ['6988754'] - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, sms_numbers=random_numbers) - - # invalid numbers are still given to IAP currently as they are - self.assertSMSNotification([{'number': self.random_numbers_san[0]}, {'number': self.random_numbers_san[1]}, {'number': random_numbers[2]}], self._test_body, messages) - - def test_message_sms_w_partners_nocountry(self): - self.test_record.customer_id.write({ - 'mobile': self.random_numbers[0], - 'phone': self.random_numbers[1], - 'country_id': False, - }) - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, partner_ids=self.test_record.customer_id.ids) - - self.assertSMSNotification([{'partner': self.test_record.customer_id}], self._test_body, messages) - - def test_message_sms_w_partners_falsy(self): - # TDE FIXME: currently sent to IAP - self.test_record.customer_id.write({ - 'mobile': 'youpie', - 'phone': 'youpla', - }) - with self.with_user('employee'), self.mockSMSGateway(): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, partner_ids=self.test_record.customer_id.ids) - - # self.assertSMSNotification({self.test_record.customer_id: {}}, {}, self._test_body, messages) - - def test_message_sms_w_numbers_sanitization_duplicate(self): - pass - # TDE FIXME: not sure - # random_numbers = self.random_numbers + [self.random_numbers[1]] - # random_numbers_san = self.random_numbers_san + [self.random_numbers_san[1]] - # with self.with_user('employee'), self.mockSMSGateway(): - # messages = self.test_record._message_sms(self._test_body, sms_numbers=random_numbers) - # self.assertSMSNotification({}, {random_numbers_san[0]: {}, random_numbers_san[1]: {}, random_numbers_san[2]: {}}, self._test_body, messages) - - def test_message_sms_crash_credit(self): - with self.with_user('employee'), self.mockSMSGateway(sim_error='credit'): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2).ids) - - self.assertSMSNotification([ - {'partner': self.partner_1, 'state': 'exception', 'failure_type': 'sms_credit'}, - {'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_credit'}, - ], self._test_body, messages) - - def test_message_sms_crash_credit_single(self): - with self.with_user('employee'), self.mockSMSGateway(nbr_t_error={self.partner_2.phone_get_sanitized_number(): 'credit'}): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2 | self.partner_3).ids) - - self.assertSMSNotification([ - {'partner': self.partner_1, 'state': 'sent'}, - {'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_credit'}, - {'partner': self.partner_3, 'state': 'sent'}, - ], self._test_body, messages) - - def test_message_sms_crash_server_crash(self): - with self.with_user('employee'), self.mockSMSGateway(sim_error='jsonrpc_exception'): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2 | self.partner_3).ids) - - self.assertSMSNotification([ - {'partner': self.partner_1, 'state': 'exception', 'failure_type': 'sms_server'}, - {'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_server'}, - {'partner': self.partner_3, 'state': 'exception', 'failure_type': 'sms_server'}, - ], self._test_body, messages) - - def test_message_sms_crash_unregistered(self): - with self.with_user('employee'), self.mockSMSGateway(sim_error='unregistered'): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2).ids) - - self.assertSMSNotification([ - {'partner': self.partner_1, 'state': 'exception', 'failure_type': 'sms_acc'}, - {'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_acc'}, - ], self._test_body, messages) - - def test_message_sms_crash_unregistered_single(self): - with self.with_user('employee'), self.mockSMSGateway(nbr_t_error={self.partner_2.phone_get_sanitized_number(): 'unregistered'}): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2 | self.partner_3).ids) - - self.assertSMSNotification([ - {'partner': self.partner_1, 'state': 'sent'}, - {'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_acc'}, - {'partner': self.partner_3, 'state': 'sent'}, - ], self._test_body, messages) - - def test_message_sms_crash_wrong_number(self): - with self.with_user('employee'), self.mockSMSGateway(sim_error='wrong_number_format'): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2).ids) - - self.assertSMSNotification([ - {'partner': self.partner_1, 'state': 'exception', 'failure_type': 'sms_number_format'}, - {'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_number_format'}, - ], self._test_body, messages) - - def test_message_sms_crash_wrong_number_single(self): - with self.with_user('employee'), self.mockSMSGateway(nbr_t_error={self.partner_2.phone_get_sanitized_number(): 'wrong_number_format'}): - test_record = self.env['mail.test.sms'].browse(self.test_record.id) - messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2 | self.partner_3).ids) - - self.assertSMSNotification([ - {'partner': self.partner_1, 'state': 'sent'}, - {'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_number_format'}, - {'partner': self.partner_3, 'state': 'sent'}, - ], self._test_body, messages) - - -class TestSMSApi(TestSMSCommon): - - @classmethod - def setUpClass(cls): - super(TestSMSApi, cls).setUpClass() - cls._test_body = 'Zizisse an SMS.' - - cls._create_records_for_batch('mail.test.sms', 3) - cls.sms_template = cls._create_sms_template('mail.test.sms') - - def test_message_schedule_sms(self): - with self.with_user('employee'): - with self.mockSMSGateway(): - self.env['mail.test.sms'].browse(self.records.ids)._message_sms_schedule_mass(body=self._test_body, mass_keep_log=False) - - for record in self.records: - self.assertSMSOutgoing(record.customer_id, None, content=self._test_body) - - def test_message_schedule_sms_w_log(self): - with self.with_user('employee'): - with self.mockSMSGateway(): - self.env['mail.test.sms'].browse(self.records.ids)._message_sms_schedule_mass(body=self._test_body, mass_keep_log=True) - - for record in self.records: - self.assertSMSOutgoing(record.customer_id, None, content=self._test_body) - self.assertSMSLogged(record, self._test_body) - - def test_message_schedule_sms_w_template(self): - with self.with_user('employee'): - with self.mockSMSGateway(): - self.env['mail.test.sms'].browse(self.records.ids)._message_sms_schedule_mass(template=self.sms_template, mass_keep_log=False) - - for record in self.records: - self.assertSMSOutgoing(record.customer_id, None, content='Dear %s this is an SMS.' % record.display_name) - - def test_message_schedule_sms_w_template_and_log(self): - with self.with_user('employee'): - with self.mockSMSGateway(): - self.env['mail.test.sms'].browse(self.records.ids)._message_sms_schedule_mass(template=self.sms_template, mass_keep_log=True) - - for record in self.records: - self.assertSMSOutgoing(record.customer_id, None, content='Dear %s this is an SMS.' % record.display_name) - self.assertSMSLogged(record, 'Dear %s this is an SMS.' % record.display_name) diff --git a/addons/test_mail_sms/tests/test_sms_server_actions.py b/addons/test_mail_sms/tests/test_sms_server_actions.py deleted file mode 100644 index dfe9d6ba1633a..0000000000000 --- a/addons/test_mail_sms/tests/test_sms_server_actions.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.addons.test_mail_sms.tests.common import TestSMSCommon, TestSMSRecipients -from odoo.tests import tagged -from odoo.tools import mute_logger - - -@tagged('ir_actions') -class TestServerAction(TestSMSCommon, TestSMSRecipients): - - @classmethod - def setUpClass(cls): - super(TestServerAction, cls).setUpClass() - cls.test_record = cls.env['mail.test.sms'].with_context(**cls._test_context).create({ - 'name': 'Test', - 'customer_id': cls.partner_1.id, - }) - cls.test_record_2 = cls.env['mail.test.sms'].with_context(**cls._test_context).create({ - 'name': 'Test Record 2', - 'customer_id': False, - 'phone_nbr': cls.test_numbers[0], - }) - - cls.sms_template = cls._create_sms_template('mail.test.sms') - cls.action = cls.env['ir.actions.server'].create({ - 'name': 'Test SMS Action', - 'model_id': cls.env['ir.model']._get('mail.test.sms').id, - 'state': 'sms', - 'sms_method': 'sms', - 'sms_template_id': cls.sms_template.id, - 'groups_id': cls.env.ref('base.group_user'), - }) - - def test_action_sms(self): - context = { - 'active_model': 'mail.test.sms', - 'active_ids': (self.test_record | self.test_record_2).ids, - } - - with self.with_user('employee'), self.mockSMSGateway(): - self.action.with_user(self.env.user).with_context(**context).run() - - self.assertSMSOutgoing(self.test_record.customer_id, None, content='Dear %s this is an SMS.' % self.test_record.display_name) - self.assertSMSOutgoing(self.env['res.partner'], self.test_numbers_san[0], content='Dear %s this is an SMS.' % self.test_record_2.display_name) - - def test_action_sms_single(self): - context = { - 'active_model': 'mail.test.sms', - 'active_id': self.test_record.id, - } - - with self.with_user('employee'), self.mockSMSGateway(): - self.action.with_user(self.env.user).with_context(**context).run() - self.assertSMSOutgoing(self.test_record.customer_id, None, content='Dear %s this is an SMS.' % self.test_record.display_name) - - def test_action_sms_w_log(self): - self.action.sms_method = 'note' - context = { - 'active_model': 'mail.test.sms', - 'active_ids': (self.test_record | self.test_record_2).ids, - } - - with self.with_user('employee'), self.mockSMSGateway(): - self.action.with_user(self.env.user).with_context(**context).run() - - self.assertSMSOutgoing(self.test_record.customer_id, None, content='Dear %s this is an SMS.' % self.test_record.display_name) - self.assertSMSLogged(self.test_record, 'Dear %s this is an SMS.' % self.test_record.display_name) - - self.assertSMSOutgoing(self.env['res.partner'], self.test_numbers_san[0], content='Dear %s this is an SMS.' % self.test_record_2.display_name) - self.assertSMSLogged(self.test_record_2, 'Dear %s this is an SMS.' % self.test_record_2.display_name) - - @mute_logger('odoo.addons.sms.models.sms_sms') - def test_action_sms_w_post(self): - self.action.sms_method = 'comment' - context = { - 'active_model': 'mail.test.sms', - 'active_ids': (self.test_record | self.test_record_2).ids, - } - - with self.with_user('employee'), self.mockSMSGateway(): - self.action.with_user(self.env.user).with_context(**context).run() - - self.assertSMSNotification( - [{'partner': self.test_record.customer_id}], - 'Dear %s this is an SMS.' % self.test_record.display_name, - messages=self.test_record.message_ids[-1] - ) - self.assertSMSNotification( - [{'partner': self.env['res.partner'], - 'number': self.test_numbers_san[0]}], - 'Dear %s this is an SMS.' % self.test_record_2.display_name, - messages=self.test_record_2.message_ids[-1] - ) diff --git a/addons/test_mail_sms/tests/test_sms_sms.py b/addons/test_mail_sms/tests/test_sms_sms.py deleted file mode 100644 index 87a77a9fb6174..0000000000000 --- a/addons/test_mail_sms/tests/test_sms_sms.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from unittest.mock import patch -from unittest.mock import DEFAULT - -from odoo import exceptions -from odoo.addons.link_tracker.tests.common import MockLinkTracker -from odoo.addons.sms.models.sms_sms import SmsSms as SmsModel -from odoo.addons.test_mail_sms.tests.common import TestSMSCommon -from odoo.tests import tagged - - -@tagged('link_tracker') -class TestSMSPost(TestSMSCommon, MockLinkTracker): - - @classmethod - def setUpClass(cls): - super(TestSMSPost, cls).setUpClass() - cls._test_body = 'VOID CONTENT' - - cls.sms_all = cls.env['sms.sms'] - for x in range(10): - cls.sms_all |= cls.env['sms.sms'].create({ - 'number': '+324560000%s%s' % (x, x), - 'body': cls._test_body, - }) - - def test_sms_send_batch_size(self): - self.count = 0 - - def _send(sms_self, unlink_failed=False, unlink_sent=True, raise_exception=False): - self.count += 1 - return DEFAULT - - self.env['ir.config_parameter'].set_param('sms.session.batch.size', '3') - with patch.object(SmsModel, '_send', autospec=True, side_effect=_send) as _send_mock: - self.env['sms.sms'].browse(self.sms_all.ids).send() - - self.assertEqual(self.count, 4) - - def test_sms_send_crash_employee(self): - with self.assertRaises(exceptions.AccessError): - self.env['sms.sms'].with_user(self.user_employee).browse(self.sms_all.ids).send() - - def test_sms_send_delete_all(self): - with self.mockSMSGateway(sms_allow_unlink=True, sim_error='jsonrpc_exception'): - self.env['sms.sms'].browse(self.sms_all.ids).send(unlink_failed=True, unlink_sent=True, raise_exception=False) - self.assertFalse(len(self.sms_all.exists())) - - def test_sms_send_delete_default(self): - """ Test default send behavior: keep failed SMS, remove sent. """ - with self.mockSMSGateway(sms_allow_unlink=True, nbr_t_error={ - '+32456000011': 'wrong_number_format', - '+32456000022': 'credit', - '+32456000033': 'server_error', - '+32456000044': 'unregistered', - }): - self.env['sms.sms'].browse(self.sms_all.ids).send(raise_exception=False) - remaining = self.sms_all.exists() - self.assertEqual(len(remaining), 4) - self.assertTrue(all(sms.state == 'error') for sms in remaining) - - def test_sms_send_delete_failed(self): - with self.mockSMSGateway(sms_allow_unlink=True, nbr_t_error={ - '+32456000011': 'wrong_number_format', - '+32456000022': 'wrong_number_format', - }): - self.env['sms.sms'].browse(self.sms_all.ids).send(unlink_failed=True, unlink_sent=False, raise_exception=False) - remaining = self.sms_all.exists() - self.assertEqual(len(remaining), 8) - self.assertTrue(all(sms.state == 'sent') for sms in remaining) - - def test_sms_send_delete_none(self): - with self.mockSMSGateway(sms_allow_unlink=True, nbr_t_error={ - '+32456000011': 'wrong_number_format', - '+32456000022': 'wrong_number_format', - }): - self.env['sms.sms'].browse(self.sms_all.ids).send(unlink_failed=False, unlink_sent=False, raise_exception=False) - self.assertEqual(len(self.sms_all.exists()), 10) - success_sms = self.sms_all[:1] + self.sms_all[3:] - error_sms = self.sms_all[1:3] - self.assertTrue(all(sms.state == 'sent') for sms in success_sms) - self.assertTrue(all(sms.state == 'error') for sms in error_sms) - - def test_sms_send_delete_sent(self): - with self.mockSMSGateway(sms_allow_unlink=True, nbr_t_error={ - '+32456000011': 'wrong_number_format', - '+32456000022': 'wrong_number_format', - }): - self.env['sms.sms'].browse(self.sms_all.ids).send(unlink_failed=False, unlink_sent=True, raise_exception=False) - remaining = self.sms_all.exists() - self.assertEqual(len(remaining), 2) - self.assertTrue(all(sms.state == 'error') for sms in remaining) - - def test_sms_send_raise(self): - with self.assertRaises(exceptions.AccessError): - with self.mockSMSGateway(sim_error='jsonrpc_exception'): - self.env['sms.sms'].browse(self.sms_all.ids).send(raise_exception=True) - self.assertEqual(set(self.sms_all.mapped('state')), set(['outgoing'])) - - def test_sms_send_raise_catch(self): - with self.mockSMSGateway(sim_error='jsonrpc_exception'): - self.env['sms.sms'].browse(self.sms_all.ids).send(raise_exception=False) - self.assertEqual(set(self.sms_all.mapped('state')), set(['error'])) diff --git a/addons/test_mail_sms/tests/test_sms_template.py b/addons/test_mail_sms/tests/test_sms_template.py deleted file mode 100644 index 225a95fdda3a5..0000000000000 --- a/addons/test_mail_sms/tests/test_sms_template.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.addons.test_mail_sms.tests.common import TestSMSCommon, TestSMSRecipients - - -class TestSmsTemplate(TestSMSCommon, TestSMSRecipients): - - @classmethod - def setUpClass(cls): - super(TestSmsTemplate, cls).setUpClass() - cls.test_record = cls.env['mail.test.sms'].with_context(**cls._test_context).create({ - 'name': 'Test', - 'customer_id': cls.partner_1.id, - }) - cls.test_record = cls._reset_mail_context(cls.test_record) - - cls.body_en = 'Dear {{ object.display_name }} this is an SMS.' - cls.body_fr = u"Hello {{ object.display_name }} ceci est en français." - cls.sms_template = cls._create_sms_template('mail.test.sms', body=cls.body_en) - - def test_sms_template_render(self): - rendered_body = self.sms_template._render_template(self.sms_template.body, self.sms_template.model, self.test_record.ids) - self.assertEqual(rendered_body[self.test_record.id], 'Dear %s this is an SMS.' % self.test_record.display_name) - - rendered_body = self.sms_template._render_field('body', self.test_record.ids) - self.assertEqual(rendered_body[self.test_record.id], 'Dear %s this is an SMS.' % self.test_record.display_name) - - def test_sms_template_lang(self): - self.env['res.lang']._activate_lang('fr_FR') - self.user_admin.write({'lang': 'en_US'}) - self.sms_template.update_field_translations('body', { - 'fr_FR': self.body_fr - }) - # set template to try to use customer lang - self.sms_template.write({ - 'lang': '{{ object.customer_id.lang }}', - }) - # create a second record linked to a customer in another language - self.partner_2.write({ - 'lang': 'fr_FR', - }) - test_record_2 = self.env['mail.test.sms'].create({ - 'name': 'Test', - 'customer_id': self.partner_2.id, - }) - - self.assertEqual(self.sms_template.body, self.body_en) - self.assertEqual(self.sms_template.with_context(lang='fr_FR').body, self.body_fr) - - rid_to_lang = self.sms_template._render_lang((self.test_record | test_record_2).ids) - self.assertEqual(set(rid_to_lang.keys()), set((self.test_record | test_record_2).ids)) - for rid, lang in rid_to_lang.items(): - # TDE FIXME: False or en_US ? - if rid == self.test_record.id: - self.assertEqual(lang, 'en_US') - elif rid == test_record_2.id: - self.assertEqual(lang, 'fr_FR') - else: - self.assertTrue(False) - - tpl_to_rids = self.sms_template._classify_per_lang((self.test_record | test_record_2).ids) - for lang, (tpl, rids) in tpl_to_rids.items(): - # TDE FIXME: False or en_US ? - if lang == 'en_US': - self.assertEqual(rids, self.test_record.ids) - elif lang == 'fr_FR': - self.assertEqual(rids, test_record_2.ids) - else: - self.assertTrue(False, 'Should not return lang %s' % lang) - - def test_sms_template_create_and_unlink_sidebar_action(self): - ActWindow = self.env['ir.actions.act_window'] - self.sms_template.action_create_sidebar_action() - action_id = self.sms_template.sidebar_action_id.id - - self.assertNotEqual(action_id, False) - self.assertEqual(ActWindow.search_count([('id', '=', action_id)]), 1) - - self.sms_template.action_unlink_sidebar_action() - self.assertEqual(ActWindow.search_count([('id', '=', action_id)]), 0) - - def test_sms_template_unlink_with_action(self): - ActWindow = self.env['ir.actions.act_window'] - self.sms_template.action_create_sidebar_action() - action_id = self.sms_template.sidebar_action_id.id - - self.sms_template.unlink() - self.assertEqual(ActWindow.search_count([('id', '=', action_id)]), 0) From fa6ad36df0e5e2d8eba15032ad63fc8b329272a0 Mon Sep 17 00:00:00 2001 From: Jacques-Etienne Baudoux Date: Thu, 30 Nov 2023 21:16:00 +0100 Subject: [PATCH 13/13] [FIX] purchase_stock: AVCO computation - support line discount --- addons/purchase_stock/models/stock_move.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/addons/purchase_stock/models/stock_move.py b/addons/purchase_stock/models/stock_move.py index 44c995e6eca6f..4059f85c696bd 100644 --- a/addons/purchase_stock/models/stock_move.py +++ b/addons/purchase_stock/models/stock_move.py @@ -55,11 +55,14 @@ def _get_price_unit(self): invoiced_value = 0 invoiced_qty = 0 for invoice_line in line.sudo().invoice_lines: - if invoice_line.tax_ids: - invoiced_value += invoice_line.tax_ids.with_context(round=False).compute_all( - invoice_line.price_unit, currency=invoice_line.currency_id, quantity=invoice_line.quantity)['total_void'] - else: - invoiced_value += invoice_line.price_unit * invoice_line.quantity + # FIX je@bcim.be - Use discounted untaxed sub-total in price unit used for avco computation + # Current odoo computation is missing the line discount + invoiced_value += invoice_line.price_subtotal + # if invoice_line.tax_ids: + # invoiced_value += invoice_line.tax_ids.with_context(round=False).compute_all( + # invoice_line.price_unit, currency=invoice_line.currency_id, quantity=invoice_line.quantity)['total_void'] + # else: + # invoiced_value += invoice_line.price_unit * invoice_line.quantity invoiced_qty += invoice_line.product_uom_id._compute_quantity(invoice_line.quantity, line.product_id.uom_id) # TODO currency check remaining_value = invoiced_value - receipt_value