From 66b64d6907711e1b514f5373f2f5581f22e16a87 Mon Sep 17 00:00:00 2001 From: Julien DRECQ Date: Thu, 25 Feb 2021 17:08:56 +0100 Subject: [PATCH] [MIG] Migration smile_base to v14 --- smile_base/README.rst | 91 ++++ smile_base/__init__.py | 94 ++++ smile_base/__manifest__.py | 35 ++ smile_base/controllers/__init__.py | 5 + smile_base/controllers/main.py | 76 ++++ smile_base/data/warning_data.xml | 16 + smile_base/i18n/fr.po | 161 +++++++ smile_base/models/__init__.py | 20 + smile_base/models/api.py | 21 + smile_base/models/base.py | 202 +++++++++ smile_base/models/fetchmail.py | 16 + smile_base/models/ir_actions.py | 132 ++++++ smile_base/models/ir_actions_server.py | 12 + smile_base/models/ir_mail_server.py | 36 ++ smile_base/models/ir_model.py | 33 ++ smile_base/models/ir_module_module.py | 19 + smile_base/models/language.py | 38 ++ smile_base/models/mail_mail.py | 17 + smile_base/models/mail_template.py | 100 +++++ smile_base/models/module.py | 58 +++ smile_base/models/registry.py | 27 ++ smile_base/models/res_partner.py | 17 + smile_base/models/sql_db.py | 23 + smile_base/models/update.py | 16 + smile_base/security/base_security.xml | 11 + smile_base/security/res_users.xml | 8 + smile_base/static/description/icon.png | Bin 0 -> 9433 bytes smile_base/static/description/index.html | 458 ++++++++++++++++++++ smile_base/static/src/xml/base.xml | 10 + smile_base/tests/__init__.py | 6 + smile_base/tests/test_base.py | 72 +++ smile_base/tools/__init__.py | 6 + smile_base/tools/misc.py | 51 +++ smile_base/tools/sql.py | 21 + smile_base/views/ir_actions_server_view.xml | 17 + smile_base/views/ir_actions_view.xml | 34 ++ smile_base/views/template.xml | 12 + smile_base/wizard/__init__.py | 5 + smile_base/wizard/mail_compose_message.py | 57 +++ 39 files changed, 2033 insertions(+) create mode 100644 smile_base/README.rst create mode 100644 smile_base/__init__.py create mode 100644 smile_base/__manifest__.py create mode 100644 smile_base/controllers/__init__.py create mode 100644 smile_base/controllers/main.py create mode 100644 smile_base/data/warning_data.xml create mode 100644 smile_base/i18n/fr.po create mode 100644 smile_base/models/__init__.py create mode 100644 smile_base/models/api.py create mode 100644 smile_base/models/base.py create mode 100644 smile_base/models/fetchmail.py create mode 100644 smile_base/models/ir_actions.py create mode 100644 smile_base/models/ir_actions_server.py create mode 100644 smile_base/models/ir_mail_server.py create mode 100644 smile_base/models/ir_model.py create mode 100644 smile_base/models/ir_module_module.py create mode 100644 smile_base/models/language.py create mode 100644 smile_base/models/mail_mail.py create mode 100644 smile_base/models/mail_template.py create mode 100644 smile_base/models/module.py create mode 100644 smile_base/models/registry.py create mode 100644 smile_base/models/res_partner.py create mode 100644 smile_base/models/sql_db.py create mode 100644 smile_base/models/update.py create mode 100644 smile_base/security/base_security.xml create mode 100644 smile_base/security/res_users.xml create mode 100644 smile_base/static/description/icon.png create mode 100644 smile_base/static/description/index.html create mode 100644 smile_base/static/src/xml/base.xml create mode 100644 smile_base/tests/__init__.py create mode 100644 smile_base/tests/test_base.py create mode 100644 smile_base/tools/__init__.py create mode 100644 smile_base/tools/misc.py create mode 100644 smile_base/tools/sql.py create mode 100644 smile_base/views/ir_actions_server_view.xml create mode 100644 smile_base/views/ir_actions_view.xml create mode 100644 smile_base/views/template.xml create mode 100644 smile_base/wizard/__init__.py create mode 100644 smile_base/wizard/mail_compose_message.py diff --git a/smile_base/README.rst b/smile_base/README.rst new file mode 100644 index 000000000..02588c918 --- /dev/null +++ b/smile_base/README.rst @@ -0,0 +1,91 @@ +========== +Smile Base +========== + +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-Smile_SA%2Fodoo_addons-lightgray.png?logo=github + :target: https://github.com/Smile-SA/odoo_addons/tree/13.0/smile_base + :alt: Smile-SA/odoo_addons + +|badge2| |badge3| + +* Make French the default language if installed +* Disable the scheduled action "Update Notification" which sends companies and users info to Odoo S.A. +* Correct date and time format for French language +* Review the menu "Applications" +* Remove the menu "App store" and "Update modules" from apps.odoo.com. +* Add sequence and display window actions in IrValues +* Force to call unlink method at removal of remote object linked by a fields.many2one with ondelete='cascade' +* Add BaseModel.store_set_values and BaseModel._compute_store_set +* Improve BaseModel.load method performance +* Disable email sending/fetching by default + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Add this module to your addons, it will auto install. + +To enable email sending, add in your configuration file: + * enable_email_sending = True + +To enable email fetching, add in your configuration file: + * enable_email_fetching = True + +To enable sending of companies and users info to Odoo S.A., add in your configuration file: + * enable_publisher_warranty_contract_notification = True + +Changes done at migration +========================= + +The feature adding a colored ribbon to make your environments recognisable at +first glance was removed during migration to Odoo 12.0. +We recommand to instead install modules `web_environment_ribbon `_ and `server_environment_ir_config_parameter `_. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed feedback +`here `_. + +GDPR / EU Privacy +================= + +This addons does not collect any data and does not set any browser cookies. + +Credits +======= + +Authors +~~~~~~~ + +* Smile SA + +Contributors +~~~~~~~~~~~~ + +* Corentin Pouhet-Brunerie +* Majda EL MARIOULI + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the Smile SA. + +Since 1991 Smile has been a pioneer of technology and also the European expert in open source solutions. + +.. image:: https://avatars0.githubusercontent.com/u/572339?s=200&v=4 + :alt: Smile SA + :target: http://smile.fr + +This module is part of the `odoo-addons `_ project on GitHub. + +You are welcome to contribute. diff --git a/smile_base/__init__.py b/smile_base/__init__.py new file mode 100644 index 000000000..9c9c991eb --- /dev/null +++ b/smile_base/__init__.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import controllers +from . import models +from . import tools +from . import wizard + +from odoo.api import Environment, SUPERUSER_ID +from odoo import tools + + +def pre_init_hook(cr): + env = Environment(cr, SUPERUSER_ID, {}) + fetchmail = env['ir.module.module'].search([ + ('name', '=', 'fetchmail'), + ], limit=1) + if fetchmail.state != 'installed': + fetchmail.button_install() + + +def post_init_hook(cr, registry): + add_act_window_id_in_context(cr) + disable_update_notification_cron(cr) + set_default_lang(cr) + correct_datetime_format_fr(cr) + correct_datetime_format_eng(cr) + remove_menus(cr) + + +def add_act_window_id_in_context(cr): + env = Environment(cr, SUPERUSER_ID, {}) + env['ir.actions.act_window'].with_context(active_test=False).search([])._update_context() + + +def disable_update_notification_cron(cr): + env = Environment(cr, SUPERUSER_ID, {}) + cron = env.ref('mail.ir_cron_module_update_notification', False) + if cron: + cron.active = tools.config.get( + 'enable_publisher_warranty_contract_notification', False) + + +def set_default_lang(cr): + env = Environment(cr, SUPERUSER_ID, {}) + if env['res.lang'].search([('code', '=', 'fr_FR')], limit=1): + partner_lang_field_id = env.ref('base.field_res_partner__lang').id + value = env['ir.default'].search([('field_id', '=', partner_lang_field_id)], limit=1) + vals = { + 'field_id': partner_lang_field_id, + 'json_value': '"fr_FR"', + } + if value: + value.write(vals) + else: + value.create(vals) + + +def correct_datetime_format_fr(cr): + env = Environment(cr, SUPERUSER_ID, {}) + language = env['res.lang'].search([('code', '=', 'fr_FR')], limit=1) + if language: + language.write({ + 'date_format': '%d/%m/%Y', + 'time_format': '%H:%M:%S', + 'grouping': '[3, 3, 3, 3, 3]', + 'decimal_point': ',', + 'thousands_sep': ' ', + }) + + +def correct_datetime_format_eng(cr): + env = Environment(cr, SUPERUSER_ID, {}) + language = env['res.lang'].search([('code', '=', 'en_US')], limit=1) + if language: + language.write({ + 'date_format': '%m/%d/%Y', + 'time_format': '%H:%M:%S', + 'grouping': '[3, 3, 3, 3, 3]', + 'decimal_point': '.', + 'thousands_sep': ',', + }) + + +def remove_menus(cr): + env = Environment(cr, SUPERUSER_ID, {}) + for menu_id in ('base.module_mi', 'base.menu_module_updates'): + try: + env.ref(menu_id).unlink() + except ValueError: + pass + + diff --git a/smile_base/__manifest__.py b/smile_base/__manifest__.py new file mode 100644 index 000000000..b7dfbff0a --- /dev/null +++ b/smile_base/__manifest__.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Smile Base", + "version": "0.2.3", + "depends": [ + 'mail', + 'web_editor', + ], + "author": "Smile", + "license": 'AGPL-3', + "description": """""", + "summary": "", + "website": "", + "category": 'Tools', + "sequence": 20, + "data": [ + "security/base_security.xml", + "security/res_users.xml", + "data/warning_data.xml", + "views/ir_actions_view.xml", + "views/template.xml", + "views/ir_actions_server_view.xml", + ], + "qweb": [ + "static/src/xml/base.xml", + ], + "pre_init_hook": 'pre_init_hook', + "post_init_hook": 'post_init_hook', + "auto_install": True, + "installable": True, + "application": False, +} diff --git a/smile_base/controllers/__init__.py b/smile_base/controllers/__init__.py new file mode 100644 index 000000000..a6974e8fb --- /dev/null +++ b/smile_base/controllers/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import main diff --git a/smile_base/controllers/main.py b/smile_base/controllers/main.py new file mode 100644 index 000000000..ef901867a --- /dev/null +++ b/smile_base/controllers/main.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 + +from odoo import http +from odoo.http import request + +from odoo.addons.web.controllers.main import content_disposition + + +class Download(http.Controller): + """ + Example of utilisation: + + 1) Add a "Download" button of type "object" on your form view + + 2) Define the method for downloading the file + + from odoo import api, models + from odoo.tools import ustr + + + class StockMove(models.Model): + _inherit = 'stock.move' + + def _get_datas(self): + self.ensure_one() + return ustr("Stock n°%s") % self.id + + def button_get_file(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_url', + 'url': '/download/saveas?model=%(model)s&record_id + %(record_id)s&method=%(method)s&filename=%(filename)s' % { + 'filename': 'stock_infos.txt', + 'model': self._name, + 'record_id': self.id, + 'method': '_get_datas', + }, + 'target': 'self', + } + + """ + + @http.route('/download/saveas', type='http', auth="public") + def saveas(self, model, record_id, method, encoded=False, filename=None, + **kw): + """ Download link for files generated on the fly. + + :param str model: name of the model to fetch the data from + :param str record_id: id of the record from which to fetch the data + :param str method: name of the method used to fetch data + :param bool encoded: whether the data is encoded in base64 + :param str filename: the file's name, if any + :returns: :class:`werkzeug.wrappers.Response` + """ + Model = request.registry[model] + cr, uid, context = request.cr, request.uid, request.context + datas = getattr(Model, method)(cr, uid, int(record_id), context) + if not datas: + return request.not_found() + filecontent = datas[0] + if not filecontent: + return request.not_found() + if encoded: + filecontent = base64.b64decode(filecontent) + if not filename: + filename = '%s_%s' % (model.replace('.', '_'), record_id) + return request.make_response( + filecontent, [ + ('Content-Type', 'application/octet-stream'), + ('Content-Disposition', content_disposition(filename)), + ]) diff --git a/smile_base/data/warning_data.xml b/smile_base/data/warning_data.xml new file mode 100644 index 000000000..76544ee2f --- /dev/null +++ b/smile_base/data/warning_data.xml @@ -0,0 +1,16 @@ + + + + + + + Warning + + ir.actions.server + code + raise Warning(env['ir.translation']._get_source(None, 'code', user.lang, source='Feature not yet implemented')) + + + + + diff --git a/smile_base/i18n/fr.po b/smile_base/i18n/fr.po new file mode 100644 index 000000000..276d1dbf6 --- /dev/null +++ b/smile_base/i18n/fr.po @@ -0,0 +1,161 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * smile_base +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-08-09 15:54+0000\n" +"PO-Revision-Date: 2019-08-09 15:54+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: smile_base +#: model:ir.model,name:smile_base.model_ir_actions_act_window +msgid "Action Window" +msgstr "Action de fenêtre" + +#. module: smile_base +#: model:ir.model,name:smile_base.model_ir_actions_actions +msgid "Actions" +msgstr "" + +#. module: smile_base +#: model:ir.model,name:smile_base.model_base +msgid "Base" +msgstr "" + +#. module: smile_base +#: model_terms:ir.ui.view,arch_db:smile_base.view_server_action_form +msgid "Contextual Action" +msgstr "Action contextuelle" + +#. module: smile_base +#: model:ir.model,name:smile_base.model_mail_template +msgid "Email Templates" +msgstr "Modèles de courriels" + +#. module: smile_base +#: model:ir.model,name:smile_base.model_mail_compose_message +msgid "Email composition wizard" +msgstr "Assistant de composition de courriel" + +#. module: smile_base +#: code:addons/smile_base/models/res_partner.py:0 +#, python-format +msgid "Email is invalid." +msgstr "Le courriel est invalide." + +#. module: smile_base +#: code:addons/smile_base/models/base.py:40 +#, python-format +msgid "Error while validating constraint" +msgstr "Erreur lors de la validation de la contrainte" + +#. module: smile_base +#: code:addons/smile_base/models/mail_template.py:91 +#, python-format +msgid "Failed to render template %r using values %r" +msgstr "Échec du rendu du modèle %r à l'aide de valeurs %r" + +#. module: smile_base +#: model:res.groups,name:smile_base.group_hidden +msgid "Hidden Features" +msgstr "Fonctionnalités cachées" + +#. module: smile_base +#: model:ir.model,name:smile_base.model_fetchmail_server +msgid "Incoming Mail Server" +msgstr "Serveur d'emails entrants" + +#. module: smile_base +#: model:ir.model,name:smile_base.model_base_language_install +msgid "Install Language" +msgstr "Installation de langue" + +#. module: smile_base +#: model:ir.model,name:smile_base.model_base_language_export +msgid "Language Export" +msgstr "Export des traductions" + +#. module: smile_base +#: model_terms:ir.ui.view,arch_db:smile_base.login +msgid "Login" +msgstr "Identifiant" + +#. module: smile_base +#: model:ir.model,name:smile_base.model_ir_mail_server +msgid "Mail Server" +msgstr "Serveur d'email" + +#. module: smile_base +#: model:ir.model,name:smile_base.model_ir_model_access +msgid "Model Access" +msgstr "Modèle d'accès" + +#. module: smile_base +#: model:ir.model,name:smile_base.model_ir_module_module +msgid "Module" +msgstr "" + +#. module: smile_base +#: code:addons/smile_base/models/base.py:154 +#, python-format +msgid "New %s" +msgstr "Nouveau %s" + +#. module: smile_base +#: model:ir.model,name:smile_base.model_mail_mail +msgid "Outgoing Mails" +msgstr "Courriels à envoyer" + +#. module: smile_base +#: model:ir.model,name:smile_base.model_publisher_warranty_contract +msgid "Publisher Warranty Contract" +msgstr "Contrat de garantie éditeur" + +#. module: smile_base +#: model:ir.model.fields,field_description:smile_base.field_base_automation__window_actions +#: model:ir.model.fields,field_description:smile_base.field_ir_actions_act_url__window_actions +#: model:ir.model.fields,field_description:smile_base.field_ir_actions_act_window__window_actions +#: model:ir.model.fields,field_description:smile_base.field_ir_actions_act_window_close__window_actions +#: model:ir.model.fields,field_description:smile_base.field_ir_actions_actions__window_actions +#: model:ir.model.fields,field_description:smile_base.field_ir_actions_client__window_actions +#: model:ir.model.fields,field_description:smile_base.field_ir_actions_report__window_actions +#: model:ir.model.fields,field_description:smile_base.field_ir_actions_server__window_actions +#: model:ir.model.fields,field_description:smile_base.field_ir_cron__window_actions +msgid "Technical field" +msgstr "Champ technique" + +#. module: smile_base +#: model:ir.model,name:smile_base.model_base_update_translations +msgid "Update Translations" +msgstr "Mise à jour des traductio,s" + +#. module: smile_base +#: model_terms:ir.ui.view,arch_db:smile_base.act_report_xml_view +msgid "Visibility" +msgstr "Visibilité" + +#. module: smile_base +#: model:ir.actions.server,name:smile_base.warning_popup +msgid "Warning" +msgstr "Attention" + +#. module: smile_base +#: model:ir.model.fields,field_description:smile_base.field_base_automation__window_action_ids +#: model:ir.model.fields,field_description:smile_base.field_ir_actions_act_url__window_action_ids +#: model:ir.model.fields,field_description:smile_base.field_ir_actions_act_window__window_action_ids +#: model:ir.model.fields,field_description:smile_base.field_ir_actions_act_window_close__window_action_ids +#: model:ir.model.fields,field_description:smile_base.field_ir_actions_actions__window_action_ids +#: model:ir.model.fields,field_description:smile_base.field_ir_actions_client__window_action_ids +#: model:ir.model.fields,field_description:smile_base.field_ir_actions_report__window_action_ids +#: model:ir.model.fields,field_description:smile_base.field_ir_actions_server__window_action_ids +#: model:ir.model.fields,field_description:smile_base.field_ir_cron__window_action_ids +msgid "Window Actions" +msgstr "Actions de fenêtre" diff --git a/smile_base/models/__init__.py b/smile_base/models/__init__.py new file mode 100644 index 000000000..d3771479e --- /dev/null +++ b/smile_base/models/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import api +from . import base +from . import fetchmail +from . import ir_actions +from . import ir_actions_server +from . import ir_mail_server +from . import ir_model +from . import ir_module_module +from . import language +from . import mail_mail +from . import mail_template +from . import module +from . import registry +from . import sql_db +from . import update +from . import res_partner \ No newline at end of file diff --git a/smile_base/models/api.py b/smile_base/models/api.py new file mode 100644 index 000000000..9c7140d80 --- /dev/null +++ b/smile_base/models/api.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from odoo import api + +_logger = logging.getLogger(__name__) + +native_add_todo = api.Environment.add_to_compute + + +def add_to_compute(self, field, records): + if not self.registry: + _logger.warning('%s not recomputed (%s)' % (field, field.related)) + return + return native_add_todo(self, field, records) + + +api.Environment.add_to_compute = add_to_compute diff --git a/smile_base/models/base.py b/smile_base/models/base.py new file mode 100644 index 000000000..ff35173af --- /dev/null +++ b/smile_base/models/base.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import timedelta +from dateutil.relativedelta import relativedelta +import logging +from operator import and_, or_, sub +import psycopg2 +import pytz + +from odoo import api, tools, models, _ +from odoo.exceptions import UserError, ValidationError + +from ..tools import create_unique_index + +_logger = logging.getLogger(__name__) + +SET_OPERATORS = {"&": and_, "|": or_, "!": sub} +SQL2PYTHON_OPERATORS = { + "=": "==", + "<>": "!=", + "like": "in", + "ilike": "in", + "not like": "not in", + "not ilike": "not in", +} + + +class Base(models.AbstractModel): + _inherit = 'base' + + def _validate_fields(self, fields_to_validate, excluded_names=()): + if not self._context.get('no_validate'): + try: + super(Base, self)._validate_fields(fields_to_validate, excluded_names) + except ValidationError as e: + name = e.name.replace( + "%s\n\n" % _("Error while validating constraint"), ""). \ + replace("\nNone", "") + raise ValidationError(name) + + @api.model + def load(self, fields, data): + res = super(Base, self.with_context( + no_validate=True, defer_parent_store_computation=True) + ).load(fields, data) + ids = res['ids'] + if ids: + recs = self.browse(ids) + recs._validate_fields(fields) + self._parent_store_compute() + return res + + def unlink(self): + # Force to call unlink method at removal of remote object linked + # by a fields.many2one with ondelete='cascade' + if hasattr(self.pool[self._name], '_cascade_relations'): + self = self.with_context(active_test=False) + if 'unlink_in_cascade' not in self._context: + self = self.with_context( + unlink_in_cascade={self._name: list(self._ids)}) + for model, fnames in self.pool[self._name]. \ + _cascade_relations.items(): + domain = ['|'] * (len(fnames) - 1) + \ + [(fname, 'in', self._ids) for fname in fnames] + SubModel = self.env[model] + sub_models = SubModel.search(domain) + sub_model_ids = list(set(sub_models._ids) - set( + self._context['unlink_in_cascade'].get(model, []))) + if sub_model_ids: + self._context['unlink_in_cascade'].setdefault(model, []). \ + extend(sub_model_ids) + SubModel.browse(sub_model_ids).unlink() + if not self.exists(): + return True + return super(Base, self).unlink() + + def modified(self, fnames, create=False, before=False): + if self._context.get('recompute', True): + super(Base, self).modified(fnames, create, before) + + def _try_lock(self, warning=None): + try: + self._cr.execute("""SELECT id FROM "%s" WHERE id IN %%s + FOR UPDATE NOWAIT""" % self._table, + (tuple(self.ids),), log_exceptions=False) + except psycopg2.OperationalError: + # INFO: Early rollback to allow translations + # to work for the user feedback + self._cr.rollback() + if warning: + raise UserError(warning) + raise + + def open_wizard(self, **kwargs): + action = { + 'type': 'ir.actions.act_window', + 'res_model': self._name, + 'view_mode': 'form', + 'view_id': False, + 'res_id': self.ids and self.ids[0] or False, + 'domain': [], + 'target': 'new', + } + action.update(**kwargs) + return action + + @api.model + def _get_comparison_fields(self): + return [] + + def _compare(self, other): + """ + Compare an instance with another + Return {field: (previous_value, new_value)} + """ + self.ensure_one() + other.ensure_one() + diff = {} + comparison_fields = self._get_comparison_fields() + current_infos = self.read(comparison_fields)[0] + other_infos = other.read(comparison_fields)[0] + for field in comparison_fields: + if current_infos[field] != other_infos[field]: + diff[field] = (other_infos[field], current_infos[field]) + return diff + + def _get_comparison_logs(self, other): + + def get_values(items): + return map(lambda item: item and item[1], items) + + diff = self._compare(other) + logs = [] + for field_name in diff: + field = self._fields[field_name] + label = field.string + separator = ' -> ' + if field.type == 'many2one': + diff[field_name] = get_values(diff[field_name]) + if field.type == 'one2many': + diff[field_name] = get_values( + self.env[field.comodel_name]. + browse(diff[field_name]).name_get()) + label = _('New %s') % label + separator = ', ' + if field.type == 'selection': + selection = dict(field.selection) + diff[field_name] = [selection[key] for key in diff[field_name]] + log = separator.join(map(tools.ustr, diff[field_name])) + logs.append('%s: %s' % (label, log)) + return logs + + @api.model + def _create_unique_index(self, column, where_clause=None): + create_unique_index(self._cr, self._name, column, where_clause) + + @api.model + def _read_group_process_groupby(self, gb, query): + split = gb.split(':') + field_type = self._fields[split[0]].type + if field_type == 'datetime': + gb_function = split[1] if len(split) == 2 else None + tz_convert = field_type == 'datetime' and \ + self._context.get('tz') in pytz.all_timezones + qualified_field = self._inherits_join_calc( + self._table, split[0], query) + # Cfr: http://babel.pocoo.org/docs/dates/#date-fields + display_formats = { + 'minute': 'dd MMM yyyy HH:mm', + 'hour': 'dd MMM yyyy HH:mm', + 'day': 'dd MMM yyyy', # yyyy = normal year + 'week': "'W'w YYYY", # w YYYY = ISO week-year + 'month': 'MMMM yyyy', + 'quarter': 'QQQ yyyy', + 'year': 'yyyy', + } + time_intervals = { + 'minute': relativedelta(minutes=1), + 'hour': relativedelta(hours=1), + 'day': relativedelta(days=1), + 'week': timedelta(days=7), + 'month': relativedelta(months=1), + 'quarter': relativedelta(months=3), + 'year': relativedelta(years=1) + } + if tz_convert: + qualified_field = "timezone('%s', timezone('UTC',%s))" % \ + (self._context.get('tz', 'UTC'), qualified_field) + qualified_field = "date_trunc('%s', %s)" % \ + (gb_function or 'month', qualified_field) + return { + 'field': split[0], + 'groupby': gb, + 'type': field_type, + 'display_format': display_formats[gb_function or 'month'], + 'interval': time_intervals[gb_function or 'month'], + 'tz_convert': tz_convert, + 'qualified_field': qualified_field, + } + return super(Base, self)._read_group_process_groupby(gb, query) diff --git a/smile_base/models/fetchmail.py b/smile_base/models/fetchmail.py new file mode 100644 index 000000000..e27ea9046 --- /dev/null +++ b/smile_base/models/fetchmail.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models, tools +from odoo.addons.fetchmail.models.fetchmail import _logger + + +class FetchmailServer(models.Model): + _inherit = "fetchmail.server" + + def fetch_mail(self): + if not tools.config.get('enable_email_fetching'): + _logger.warning('Email fetching not enabled') + return False + return super(FetchmailServer, self).fetch_mail() diff --git a/smile_base/models/ir_actions.py b/smile_base/models/ir_actions.py new file mode 100644 index 000000000..5f116ebdd --- /dev/null +++ b/smile_base/models/ir_actions.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from collections import defaultdict + +from odoo import api, fields, models, tools +from odoo.exceptions import MissingError, AccessError +from odoo.tools.safe_eval import safe_eval + +from ..tools import unquote + + +class IrActionsActions(models.Model): + _inherit = 'ir.actions.actions' + + window_action_ids = fields.Many2many( + 'ir.actions.act_window', string="Window Actions", + compute='_get_window_action_ids', inverse='_set_window_action_ids') + window_actions = fields.Char('Technical field', readonly=True) + + @api.depends('window_actions') + def _get_window_action_ids(self): + ActWindow = self.env['ir.actions.act_window'] + for action in self: + ids = [] + if action.window_actions: + ids = list(map(int, filter( + bool, action.window_actions.split(',')))) + action.window_action_ids = ActWindow.browse(ids) + + def _set_window_action_ids(self): + for action in self: + ids = action.window_action_ids.ids or [] + action.window_actions = ',%s,' % ','.join(map(str, ids)) + + @api.model + @tools.ormcache_context( + 'frozenset(self.env.user.groups_id.ids)', 'model_name', + keys=('act_window_id',)) + def get_bindings(self, model_name): + """ Retrieve the list of actions bound to the given model. + + :return: a dict mapping binding types to a list of dict describing + actions, where the latter is given by calling the method + ``read`` on the action record. + """ + # DLE P19: Need to flush before doing the SELECT, which act as a search + # Test `test_bindings` + self.flush() + cr = self.env.cr + query = """ SELECT a.id, a.type, a.binding_type + FROM ir_actions a, ir_model m + WHERE a.binding_model_id=m.id AND m.model=%s + AND (a.window_actions IS NULL + OR a.window_actions like %s) + ORDER BY a.id """ + cr.execute( + query, [model_name, + '%%,%s,%%' % self._context.get('act_window_id', '')]) + + # discard unauthorized actions, and read action definitions + result = defaultdict(list) + user_groups = self.env.user.groups_id + for action_id, action_model, binding_type in cr.fetchall(): + try: + action = self.env[action_model].browse(action_id) + action_groups = getattr(action, 'groups_id', ()) + if action_groups and not action_groups & user_groups: + # the user may not perform this action + continue + result[binding_type].append(action.read()[0]) + except (AccessError, MissingError): + continue + + return result + + +class IrActionsActWindow(models.Model): + _inherit = 'ir.actions.act_window' + + def _update_context(self): + eval_dict = { + 'active_id': unquote("active_id"), + 'active_ids': unquote("active_ids"), + 'active_model': unquote("active_model"), + 'uid': unquote("uid"), + 'user': unquote("user"), + 'context': self._context, + } + try: + for act_window in self: + context = safe_eval( + act_window.context or '{}', eval_dict) or {} + if 'act_window_id' not in context: + act_window.context = act_window.context[:1] + \ + "'act_window_id': %s, " % act_window.id + \ + act_window.context[1:] + except Exception: + pass + + @api.model + def create(self, vals): + act_window = super(IrActionsActWindow, self).create(vals) + act_window._update_context() + return act_window + + def write(self, vals): + res = super(IrActionsActWindow, self).write(vals) + self._update_context() + return res + + def read(self, fields=None, load='_classic_read'): + results = super(IrActionsActWindow, self).read(fields, load) + # Evaluate context value with user + localdict = { + 'active_model': unquote('active_model'), + 'active_id': unquote('active_id'), + 'active_ids': unquote('active_ids'), + 'uid': unquote('uid'), + 'context': unquote('context'), + 'user': self.env.user, + } + for res in results: + if 'context' in res: + try: + with tools.mute_logger("odoo.tools.safe_eval"): + res['context'] = tools.ustr( + eval(res['context'], localdict)) + except Exception: + continue + return results diff --git a/smile_base/models/ir_actions_server.py b/smile_base/models/ir_actions_server.py new file mode 100644 index 000000000..d0aaef3a3 --- /dev/null +++ b/smile_base/models/ir_actions_server.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class IrActionsServer(models.Model): + _inherit = 'ir.actions.server' + + groups_id = fields.Many2many('res.groups', 'server_groups_rel', + 'server_id', 'group_id', string='Groups') diff --git a/smile_base/models/ir_mail_server.py b/smile_base/models/ir_mail_server.py new file mode 100644 index 000000000..5e36557e9 --- /dev/null +++ b/smile_base/models/ir_mail_server.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models, tools +from odoo.addons.base.models.ir_mail_server import _logger + + +class IrMailServer(models.Model): + _inherit = "ir.mail_server" + + @api.model + def send_email(self, message, mail_server_id=None, smtp_server=None, + smtp_port=None, smtp_user=None, smtp_password=None, + smtp_encryption=None, smtp_debug=False, smtp_session=None): + if not tools.config.get('enable_email_sending'): + _logger.warning('Email sending not enabled') + return False + return super(IrMailServer, self).send_email( + message, mail_server_id, smtp_server, smtp_port, smtp_user, + smtp_password, smtp_encryption, smtp_debug, smtp_session) + + def build_email(self, email_from, email_to, subject, body, email_cc=None, + email_bcc=None, reply_to=False, attachments=None, + message_id=None, references=None, object_id=False, + subtype='plain', headers=None, body_alternative=None, + subtype_alternative='plain'): + if tools.config.get('email_to'): + email_to = tools.email_split_and_format(tools.config['email_to']) + email_cc = None + email_bcc = None + msg = super(IrMailServer, self).build_email( + email_from, email_to, subject, body, email_cc, email_bcc, reply_to, + attachments, message_id, references, object_id, subtype, headers, + body_alternative, subtype_alternative) + return msg diff --git a/smile_base/models/ir_model.py b/smile_base/models/ir_model.py new file mode 100644 index 000000000..19a62debd --- /dev/null +++ b/smile_base/models/ir_model.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from functools import partial + +from odoo import api, models + + +class IrModelAccess(models.Model): + _inherit = 'ir.model.access' + + @api.model + def group_names_with_access(self, model_name, access_mode): + """ Translate group names in context or user lang. + """ + res = super(IrModelAccess, self).group_names_with_access( + model_name=model_name, access_mode=access_mode) + lang = self._context.get('lang') or self.env.user.lang + translated_res = [] + translate_group = partial( + self.env['ir.translation']._get_source, None, 'model', lang) + for complete_name in res: + if '/' in complete_name: + category_name, group_name = complete_name.split('/') + translated_name = "{}/{}".format( + translate_group(category_name) or category_name, + translate_group(group_name) or group_name) + else: + translated_name = '{}'.format( + translate_group(complete_name) or complete_name) + translated_res.append(translated_name) + return translated_res diff --git a/smile_base/models/ir_module_module.py b/smile_base/models/ir_module_module.py new file mode 100644 index 000000000..27137acd5 --- /dev/null +++ b/smile_base/models/ir_module_module.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class Module(models.Model): + _inherit = "ir.module.module" + + @api.model + def name_search(self, name='', args=None, operator='ilike', limit=100): + domain = [ + '|', + ('name', operator, name), + ('shortdesc', operator, name) + ] + (args or []) + recs = self.search(domain, limit=limit) + return recs.name_get() diff --git a/smile_base/models/language.py b/smile_base/models/language.py new file mode 100644 index 000000000..850eb92c1 --- /dev/null +++ b/smile_base/models/language.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class BaseLanguageExport(models.TransientModel): + _inherit = 'base.language.export' + + @api.model + def default_get(self, fields): + res = super(BaseLanguageExport, self).default_get(fields) + if self.env['res.lang'].search([('code', '=', 'fr_FR')]): + res.update({'lang': 'fr_FR', 'format': 'po'}) + return res + + +class BaseLanguageInstall(models.TransientModel): + _inherit = 'base.language.install' + + @api.model + def default_get(self, fields): + res = super(BaseLanguageInstall, self).default_get(fields) + if self.env['res.lang'].search([('code', '=', 'fr_FR')]): + res.update({'lang': 'fr_FR', 'overwrite': True}) + return res + + +class BaseUpdateTranslations(models.TransientModel): + _inherit = 'base.update.translations' + + @api.model + def default_get(self, fields): + res = super(BaseUpdateTranslations, self).default_get(fields) + if self.env['res.lang'].search([('code', '=', 'fr_FR')]): + res.update({'lang': 'fr_FR'}) + return res diff --git a/smile_base/models/mail_mail.py b/smile_base/models/mail_mail.py new file mode 100644 index 000000000..364d62fe9 --- /dev/null +++ b/smile_base/models/mail_mail.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models, tools +from odoo.addons.mail.models.mail_mail import _logger + + +class MailMail(models.Model): + _inherit = 'mail.mail' + + @api.model + def process_email_queue(self, ids=None): + if not tools.config.get('enable_email_sending'): + _logger.warning('Email sending not enabled') + return True + return super(MailMail, self).process_email_queue(ids) diff --git a/smile_base/models/mail_template.py b/smile_base/models/mail_template.py new file mode 100644 index 000000000..d435509e9 --- /dev/null +++ b/smile_base/models/mail_template.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import sys + +from odoo import api, models, tools, _ +from odoo.exceptions import UserError +from odoo.addons.mail.models.mail_render_mixin import \ + format_date, jinja_template_env, \ + jinja_safe_template_env, _logger + +if sys.version_info > (3,): + long = int + + +class MailTemplate(models.Model): + _inherit = "mail.template" + + def format_numeric(self, value, column, options=None): + try: + model, fieldname = column.split(',') + field = self.env[model]._fields[fieldname] + converter = self.env['ir.qweb.field.%s' % field.type] + return converter.value_to_html(value, field, options) + except Exception: + return value + + @api.model + def render_template(self, template_txt, model, res_ids, + post_process=False): + """ Render the given template text, replace mako expressions + ``${expr}`` with the result of evaluating these expressions + with an evaluation context containing: + + - ``user``: browse_record of the current user + - ``object``: record of the document record this mail is related to + - ``context``: the context passed to the mail composition wizard + + :param str template_txt: the template text to render + :param str model: model name of the document record + this mail is related to. + :param int res_ids: list of ids of document records + those mails are related to. + """ + multi_mode = True + if isinstance(res_ids, (int, long)): + multi_mode = False + res_ids = [res_ids] + + results = dict.fromkeys(res_ids, u"") + + # try to load the template + try: + mako_env = jinja_safe_template_env if self.env.context.get('safe') \ + else jinja_template_env + template = mako_env.from_string(tools.ustr(template_txt)) + except Exception: + _logger.info("Failed to load template %r", template_txt, + exc_info=True) + return multi_mode and results or results[res_ids[0]] + + # prepare template variables + # filter to avoid browsing [None] + records = self.env[model].browse(list(filter(None, res_ids))) + res_to_rec = dict.fromkeys(res_ids, None) + for record in records: + res_to_rec[record.id] = record + variables = { + 'format_date': lambda date, format=False, context=self._context: + format_date(self.env, date, format), + 'format_datetime': lambda dt, tz=False, format=False, + context=self._context: + tools.format_datetime(self.env, dt, tz, format), + 'format_amount': lambda amount, currency, context=self._context: + tools.format_amount(self.env, amount, currency), + 'format_numeric': lambda value, column, options=None: + self.format_numeric(value, column, options), # Added by Smile + 'user': self.env.user, + 'ctx': self._context, # context kw would clash with mako internals + } + for res_id, record in res_to_rec.items(): + variables['object'] = record + try: + render_result = template.render(variables) + except Exception: + _logger.info("Failed to render template %r using values %r" + % (template, variables), exc_info=True) + raise UserError( + _("Failed to render template %r using values %r") + % (template, variables)) + if render_result == u"False": + render_result = u"" + results[res_id] = render_result + + if post_process: + for res_id, result in results.items(): + results[res_id] = self.render_post_process(result) + + return multi_mode and results or results[res_ids[0]] diff --git a/smile_base/models/module.py b/smile_base/models/module.py new file mode 100644 index 000000000..ea0d3708a --- /dev/null +++ b/smile_base/models/module.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging +import os + +from odoo import api, models, modules, tools +from odoo.modules.module import load_information_from_description_file + +_logger = logging.getLogger(__name__) + + +class Module(models.Model): + _inherit = 'ir.module.module' + + def _get_all_dependencies(self): + all_dependencies = self.browse(self.ids) + parent_modules = self.browse(self.ids) + while parent_modules: + dependencies = self.browse() + for module in parent_modules: + for dependency in module.dependencies_id: + dependencies |= dependency.depend_id + parent_modules = dependencies - all_dependencies + all_dependencies |= parent_modules + return all_dependencies + + def load_data(self, kind='demo', mode='update', noupdate=False): + module_names = [module.name for module in self] + module_list = [module.name for module in self._get_all_dependencies()] + graph = modules.graph.Graph() + graph.add_modules(self._cr, module_list) + for module in graph: + if module.name in module_names: + self._load_data(module.name, kind, mode, noupdate) + + @api.model + def _load_data(self, module_name, kind='demo', mode='update', + noupdate=False): + cr = self._cr + info = load_information_from_description_file(module_name) + for filename in info.get(kind, []): + _logger.info('loading %s/%s...' % (module_name, filename)) + _, ext = os.path.splitext(filename) + pathname = os.path.join(module_name, filename) + with tools.file_open(pathname, 'rb') as fp: + if ext == '.sql': + tools.convert_sql_import(cr, fp) + elif ext == '.csv': + tools.convert_csv_import( + cr, module_name, pathname, fp.read(), + idref=None, mode=mode, noupdate=noupdate) + elif ext == '.xml': + tools.convert_xml_import( + cr, module_name, fp, + idref=None, mode=mode, noupdate=noupdate) + return True diff --git a/smile_base/models/registry.py b/smile_base/models/registry.py new file mode 100644 index 000000000..759195b2f --- /dev/null +++ b/smile_base/models/registry.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.modules.registry import Registry + +native_setup_models = Registry.setup_models + + +def new_setup_models(self, cr): + # Force to call unlink method at removal of remote object linked + # by a fields.many2one with ondelete='cascade' + native_setup_models(self, cr) + for RecordModel in self.models.values(): + for fieldname, field in RecordModel._fields.items(): + if field.type == 'many2one' and field.ondelete and \ + field.ondelete.lower() == 'cascade': + if field.comodel_name.startswith('mail.'): + continue + CoModel = self.get(field.comodel_name) + if not hasattr(CoModel, '_cascade_relations'): + setattr(CoModel, '_cascade_relations', {}) + CoModel._cascade_relations.setdefault( + RecordModel._name, set()).add(fieldname) + + +Registry.setup_models = new_setup_models diff --git a/smile_base/models/res_partner.py b/smile_base/models/res_partner.py new file mode 100644 index 000000000..563542a5e --- /dev/null +++ b/smile_base/models/res_partner.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import api, models, tools, _ +from odoo.exceptions import ValidationError + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + @api.constrains('email') + def _check_email_valid(self): + for partner in self: + if partner.email and \ + not tools.single_email_re.match(partner.email): + raise ValidationError(_('Email is invalid.')) diff --git a/smile_base/models/sql_db.py b/smile_base/models/sql_db.py new file mode 100644 index 000000000..efbe97214 --- /dev/null +++ b/smile_base/models/sql_db.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import traceback + +from odoo.sql_db import Cursor, check, _logger + +native_execute = Cursor.execute + + +@check +def execute(self, query, params=None, log_exceptions=None): + try: + return native_execute(self, query, params, log_exceptions) + except Exception as e: + _logger.error(e) + _logger.error('Traceback (most recent call last):\n' + ''.join( + traceback.format_stack())) + raise + + +Cursor.execute = execute diff --git a/smile_base/models/update.py b/smile_base/models/update.py new file mode 100644 index 000000000..ba938ca79 --- /dev/null +++ b/smile_base/models/update.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models, tools + + +class PublisherWarrantyContract(models.AbstractModel): + _inherit = "publisher_warranty.contract" + + def update_notification(self, cron_mode=True): + if not tools.config.get( + 'enable_publisher_warranty_contract_notification'): + return True + return super(PublisherWarrantyContract, self). \ + update_notification(cron_mode) diff --git a/smile_base/security/base_security.xml b/smile_base/security/base_security.xml new file mode 100644 index 000000000..bf947e643 --- /dev/null +++ b/smile_base/security/base_security.xml @@ -0,0 +1,11 @@ + + + + + + Hidden Features + + + + + diff --git a/smile_base/security/res_users.xml b/smile_base/security/res_users.xml new file mode 100644 index 000000000..79ba63413 --- /dev/null +++ b/smile_base/security/res_users.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/smile_base/static/description/icon.png b/smile_base/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..17984e2d08aac83e8271009bdce6cfdd09195231 GIT binary patch literal 9433 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 Ms(LDQN;dEQAN$d}kN^Mx literal 0 HcmV?d00001 diff --git a/smile_base/static/description/index.html b/smile_base/static/description/index.html new file mode 100644 index 000000000..162cb1ae5 --- /dev/null +++ b/smile_base/static/description/index.html @@ -0,0 +1,458 @@ + + + + + + +Smile Base + + + +
+

Smile Base

+ +

License: AGPL-3 Smile-SA/odoo_addons

+
    +
  • Make French the default language if installed
  • +
  • Disable the scheduled action "Update Notification" which sends companies and users info to Odoo S.A.
  • +
  • Correct date and time format for French language
  • +
  • Review the menu "Applications"
  • +
  • Remove the menu "App store" and "Update modules" from apps.odoo.com.
  • +
  • Add sequence and display window actions in IrValues
  • +
  • Force to call unlink method at removal of remote object linked by a fields.many2one with ondelete='cascade'
  • +
  • Add BaseModel.store_set_values and BaseModel._compute_store_set
  • +
  • Improve BaseModel.load method performance
  • +
  • Disable email sending/fetching by default
  • +
+

Table of contents

+ +
+

Usage

+

Add this module to your addons, it will auto install.

+
+
To enable email sending, add in your configuration file:
+
    +
  • enable_email_sending = True
  • +
+
+
To enable email fetching, add in your configuration file:
+
    +
  • enable_email_fetching = True
  • +
+
+
To enable sending of companies and users info to Odoo S.A., add in your configuration file:
+
    +
  • enable_publisher_warranty_contract_notification = True
  • +
+
+
+
+
+

Changes done at migration

+

The feature adding a colored ribbon to make your environments recognisable at +first glance was removed during migration to Odoo 12.0. +We recommand to instead install modules web_environment_ribbon and server_environment_ir_config_parameter.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed feedback +here.

+
+
+

GDPR / EU Privacy

+

This addons does not collect any data and does not set any browser cookies.

+
+
+

Credits

+
+

Authors

+
    +
  • Smile SA
  • +
+
+
+

Contributors

+
    +
  • Corentin Pouhet-Brunerie
  • +
  • Majda EL MARIOULI
  • +
+
+
+

Maintainers

+

This module is maintained by the Smile SA.

+

Since 1991 Smile has been a pioneer of technology and also the European expert in open source solutions.

+Smile SA +

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

+

You are welcome to contribute.

+
+
+
+ + diff --git a/smile_base/static/src/xml/base.xml b/smile_base/static/src/xml/base.xml new file mode 100644 index 000000000..0d76a38d4 --- /dev/null +++ b/smile_base/static/src/xml/base.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/smile_base/tests/__init__.py b/smile_base/tests/__init__.py new file mode 100644 index 000000000..7ae06db0b --- /dev/null +++ b/smile_base/tests/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_base + diff --git a/smile_base/tests/test_base.py b/smile_base/tests/test_base.py new file mode 100644 index 000000000..08de7eb35 --- /dev/null +++ b/smile_base/tests/test_base.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from contextlib import contextmanager + +from odoo.tests.common import TransactionCase +from odoo.tools.config import config +from odoo.addons.base.models.ir_mail_server import MailDeliveryException + + +@contextmanager +def config_to_enable_email_sending(enable_email_sending): + init_config = { + 'enable_email_sending': config.get('enable_email_sending'), + 'test_enable': config.get('test_enable'), + } + try: + config['enable_email_sending'] = enable_email_sending + config['test_enable'] = not enable_email_sending + yield config + finally: + for key, value in init_config.items(): + config[key] = value + + +class BaseTest(TransactionCase): + + def setUp(self): + super(BaseTest, self).setUp() + self.model = self.env['res.partner.category'] + + def test_unlink_cascade(self): + parent = self.model.create({'name': 'Parent'}) + child = self.model.create({'name': 'Child', 'parent_id': parent.id}) + self.assertTrue(parent.unlink()) + self.assertFalse(child.exists()) + + def _get_email(self): + vals = { + 'subject': 'The subject', + 'body': 'and the body', + 'email_from': 'admin@example.org', + 'email_to': 'demo@example.org', + } + return self.env['mail.mail'].create(vals) + + def test_disable_email_sending(self): + """ + I disable email sending in config file + I send an email + I check that no email was sent + """ + email = self._get_email() + with config_to_enable_email_sending(False): + email.send() + self.assertEquals('exception', email.state, + 'Email should be in exception!') + + def test_enable_email_sending(self): + """ + I enable email sending in config file + I send an email + I check that email was sent + """ + # I remove all outgoing mail server to be sure + # the exception will be raised + self.env['ir.mail_server'].search([]).unlink() + email = self._get_email() + with config_to_enable_email_sending(True): + with self.assertRaises(MailDeliveryException): + email.send(raise_exception=True) diff --git a/smile_base/tools/__init__.py b/smile_base/tools/__init__.py new file mode 100644 index 000000000..c61304474 --- /dev/null +++ b/smile_base/tools/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from .misc import * +from .sql import * diff --git a/smile_base/tools/misc.py b/smile_base/tools/misc.py new file mode 100644 index 000000000..520f2f394 --- /dev/null +++ b/smile_base/tools/misc.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import math +from six import string_types + + +def float_time_convert(float_val): + """ + Converts a float in time (hour, minute). + + @param float_val: float, obtained via widget float_time in Odoo interface + @return: (int, int), a tuple hours and minutes + """ + factor = float_val < 0 and -1 or 1 + val = abs(float_val) + return factor * int(math.floor(val)), int(round((val % 1) * 60)) + + +def float_to_strtime(float_time): + """ + :param hour: float + :param minute: float + :return: str + """ + return '{:02d}:{:02d}'.format(*float_time_convert(float_time)) + + +class unquote(str): + + def __getitem__(self, key): + return unquote('%s[%s]' % (self, key)) + + def __getattribute__(self, attr): + return unquote('%s.%s' % (self, attr)) + + def __call__(self, *args, **kwargs): + def format_args(k): + return isinstance(k, string_types) and '"%s"' % k or k + + def format_kwargs(t): + return '%s=%s' % ( + t[0], isinstance(t[1], string_types) and '"%s"' % t[1] or t[1]) + + params = [', '.join(map(format_args, args)), + ', '.join(map(format_kwargs, kwargs.items()))] + return unquote('%s(%s)' % (self, ', '.join(params))) + + def __repr__(self): + return self diff --git a/smile_base/tools/sql.py b/smile_base/tools/sql.py new file mode 100644 index 000000000..0fbaffa91 --- /dev/null +++ b/smile_base/tools/sql.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +_logger = logging.getLogger(__name__) + + +def create_unique_index(cr, table, column, where_clause=None): + if type(column) == list: + column = ','.join(column) + column_name = column.replace(' ', '').replace(',', '_') + index_name = 'uniq_%(table)s_%(column_name)s' % locals() + cr.execute("SELECT relname FROM pg_class WHERE relname=%s", (index_name,)) + if not cr.rowcount: + _logger.debug('Creating unique index %s' % index_name) + query = "CREATE UNIQUE INDEX %(index_name)s ON %(table)s (%(column)s)" + query += " WHERE %s" % (where_clause or "%(column)s IS NOT NULL") + query = query % locals() + cr.execute(query) diff --git a/smile_base/views/ir_actions_server_view.xml b/smile_base/views/ir_actions_server_view.xml new file mode 100644 index 000000000..3e1ca097e --- /dev/null +++ b/smile_base/views/ir_actions_server_view.xml @@ -0,0 +1,17 @@ + + + + actions server heritage + ir.actions.server + + + + + + + + + + + + \ No newline at end of file diff --git a/smile_base/views/ir_actions_view.xml b/smile_base/views/ir_actions_view.xml new file mode 100644 index 000000000..f52d94468 --- /dev/null +++ b/smile_base/views/ir_actions_view.xml @@ -0,0 +1,34 @@ + + + + + + ir.actions.report + ir.actions.report + + + + + + + + + + + + Server Action + ir.actions.server + + + + + + + + + + + + diff --git a/smile_base/views/template.xml b/smile_base/views/template.xml new file mode 100644 index 000000000..497224d6e --- /dev/null +++ b/smile_base/views/template.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/smile_base/wizard/__init__.py b/smile_base/wizard/__init__.py new file mode 100644 index 000000000..3c0280e18 --- /dev/null +++ b/smile_base/wizard/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import mail_compose_message diff --git a/smile_base/wizard/mail_compose_message.py b/smile_base/wizard/mail_compose_message.py new file mode 100644 index 000000000..dc15d2fdd --- /dev/null +++ b/smile_base/wizard/mail_compose_message.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# (C) 2019 Smile () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models, tools +from odoo.addons.mail.models import mail_template + + +class MailComposeMessage(models.TransientModel): + _inherit = 'mail.compose.message' + + def prepare_and_send(self, model, partner_ids, template_id, res_id, + composition_mode='mass_mail'): + """ + Prepare the message then send it. + + @param model: string, model name + @param partner_ids: int list, ids of message receivers + @param template_id: int, id of the email template + @param res_id: int, id of the record from where the email is sent + """ + def format_tz(dt, tz=False, format=False): + return mail_template.format_tz(self._model, self._cr, self._uid, + dt, tz, format, self._context) + template = self.env['mail.template'].browse(template_id) + # usefull to get template language + ctx = {'mail_auto_delete': template.auto_delete, + 'mail_notify_user_signature': False, + 'tpl_partners_only': False} + arg = { + 'object': self.env[model].browse(res_id), + 'user': self.env.user, + 'ctx': ctx, + 'format_tz': format_tz, + } + lang = mail_template.mako_template_env.from_string( + tools.ustr(template.lang)).render(arg) + + message = self.with_context(active_ids=None).create({ + 'model': model, + 'composition_mode': composition_mode, + 'partner_ids': [(6, 0, list(filter(None, partner_ids or [])))], + 'template_id': template_id, + 'notify': True, + 'res_id': res_id, + }) + message_lang = message.with_context(lang=lang) \ + if lang and lang != 'False' else message + value = message_lang.onchange_template_id( + template_id, composition_mode, model, res_id)['value'] + if value.get('attachment_ids') and ( + composition_mode == 'comment' or not template.report_template): + value['attachment_ids'] = [ + (4, attachment_id) + for attachment_id in value['attachment_ids']] + message.write(value) + message.send_mail()