+
+\
diff --git a/base_comment_template/security/ir.model.access.csv b/base_comment_template/security/ir.model.access.csv
new file mode 100644
index 0000000000..acfeb4685f
--- /dev/null
+++ b/base_comment_template/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_base_comment_template,access_base_comment_template no one,model_base_comment_template,base.group_no_one,1,1,1,1
+access_base_comment_template_preview,access.base.comment.template.preview,model_base_comment_template_preview,base.group_user,1,1,1,0
diff --git a/base_comment_template/security/security.xml b/base_comment_template/security/security.xml
new file mode 100644
index 0000000000..440091427b
--- /dev/null
+++ b/base_comment_template/security/security.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ Base comment multi-company
+
+
+
+ ['|',('company_id','=',False),('company_id','in',company_ids)]
+
+
+
diff --git a/base_comment_template/static/description/icon.png b/base_comment_template/static/description/icon.png
new file mode 100644
index 0000000000..3a0328b516
Binary files /dev/null and b/base_comment_template/static/description/icon.png differ
diff --git a/base_comment_template/static/description/index.html b/base_comment_template/static/description/index.html
new file mode 100644
index 0000000000..487b5e8f01
--- /dev/null
+++ b/base_comment_template/static/description/index.html
@@ -0,0 +1,549 @@
+
+
+
+
+
+
Base Comments Templates
+
+
+
+
+
+
diff --git a/base_comment_template/tests/__init__.py b/base_comment_template/tests/__init__.py
new file mode 100644
index 0000000000..e198115e4e
--- /dev/null
+++ b/base_comment_template/tests/__init__.py
@@ -0,0 +1,2 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+from . import test_base_comment_template
diff --git a/base_comment_template/tests/fake_models.py b/base_comment_template/tests/fake_models.py
new file mode 100644
index 0000000000..02ee0ca234
--- /dev/null
+++ b/base_comment_template/tests/fake_models.py
@@ -0,0 +1,34 @@
+# Copyright 2017 LasLabs Inc.
+# Copyright 2018 ACSONE
+# Copyright 2018 Camptocamp
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
+
+from odoo import models
+
+
+def setup_test_model(env, model_cls):
+ """Pass a test model class and initialize it.
+
+ Courtesy of SBidoul from https://github.com/OCA/mis-builder :)
+ """
+ model_cls._build_model(env.registry, env.cr)
+ env.registry.setup_models(env.cr)
+ env.registry.init_models(
+ env.cr, [model_cls._name], dict(env.context, update_custom_fields=True)
+ )
+
+
+def teardown_test_model(env, model_cls):
+ """Pass a test model class and deinitialize it.
+
+ Courtesy of SBidoul from https://github.com/OCA/mis-builder :)
+ """
+ if not getattr(model_cls, "_teardown_no_delete", False):
+ del env.registry.models[model_cls._name]
+ env.registry.setup_models(env.cr)
+
+
+class ResUsers(models.Model):
+ _name = "res.users"
+ _inherit = ["res.users", "comment.template"]
+ _teardown_no_delete = True
diff --git a/base_comment_template/tests/test_base_comment_template.py b/base_comment_template/tests/test_base_comment_template.py
new file mode 100644
index 0000000000..d13a521685
--- /dev/null
+++ b/base_comment_template/tests/test_base_comment_template.py
@@ -0,0 +1,186 @@
+# Copyright 2020 NextERP Romania SRL
+# Copyright 2021 Tecnativa - Víctor Martínez
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+from odoo import Command
+from odoo.exceptions import ValidationError
+from odoo.tests import common
+from odoo.tools.misc import mute_logger
+
+from .fake_models import ResUsers, setup_test_model, teardown_test_model
+
+
+class TestCommentTemplate(common.TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ setup_test_model(cls.env, ResUsers)
+ cls.user_obj = cls.env.ref("base.model_res_users")
+ cls.user = cls.env.ref("base.user_demo")
+ cls.user2 = cls.env.ref("base.demo_user0")
+ cls.partner_id = cls.env.ref("base.res_partner_12")
+ cls.partner2_id = cls.env.ref("base.res_partner_10")
+ cls.ResPartnerTitle = cls.env["res.partner.title"]
+ cls.main_company = cls.env.ref("base.main_company")
+ cls.company = cls.env["res.company"].create({"name": "Test company"})
+ cls.before_template_id = cls.env["base.comment.template"].create(
+ {
+ "name": "Top template",
+ "text": "Text before lines",
+ "models": cls.user_obj.model,
+ "company_id": cls.company.id,
+ }
+ )
+ cls.after_template_id = cls.env["base.comment.template"].create(
+ {
+ "name": "Bottom template",
+ "position": "after_lines",
+ "text": "Text after lines",
+ "models": cls.user_obj.model,
+ "company_id": cls.company.id,
+ }
+ )
+ cls.user.partner_id.base_comment_template_ids = [
+ (4, cls.before_template_id.id),
+ (4, cls.after_template_id.id),
+ ]
+
+ @classmethod
+ def tearDownClass(cls):
+ teardown_test_model(cls.env, ResUsers)
+ return super().tearDownClass()
+
+ def test_template_model_ids(self):
+ self.assertIn(
+ self.user_obj.model, self.before_template_id.mapped("model_ids.model")
+ )
+ self.assertEqual(len(self.before_template_id.model_ids), 1)
+ self.assertIn(
+ self.user_obj.model, self.after_template_id.mapped("model_ids.model")
+ )
+ self.assertEqual(len(self.after_template_id.model_ids), 1)
+
+ def test_template_models_constrains(self):
+ with self.assertRaises(ValidationError):
+ self.env["base.comment.template"].create(
+ {
+ "name": "Custom template",
+ "text": "Text",
+ "models": "incorrect.model",
+ "company_id": self.company.id,
+ }
+ )
+
+ def test_template_display_name(self):
+ self.assertEqual(
+ self.before_template_id.display_name,
+ "Top template (Top)",
+ )
+ self.assertEqual(
+ self.after_template_id.display_name,
+ "Bottom template (Bottom)",
+ )
+
+ def test_general_template(self):
+ # Need to force _compute because only trigger when partner_id have changed
+ self.user._compute_comment_template_ids()
+ # Check getting default comment template
+ self.assertTrue(self.before_template_id in self.user.comment_template_ids)
+ self.assertTrue(self.after_template_id in self.user.comment_template_ids)
+
+ def test_partner_template(self):
+ self.partner2_id.base_comment_template_ids = [
+ (4, self.before_template_id.id),
+ (4, self.after_template_id.id),
+ ]
+ self.assertTrue(
+ self.before_template_id in self.partner2_id.base_comment_template_ids
+ )
+ self.assertTrue(
+ self.after_template_id in self.partner2_id.base_comment_template_ids
+ )
+
+ def test_partner_template_domain(self):
+ # Check getting the comment template if domain is set
+ self.partner2_id.base_comment_template_ids = [
+ (4, self.before_template_id.id),
+ (4, self.after_template_id.id),
+ ]
+ self.before_template_id.domain = "[('id', 'in', %s)]" % self.user.ids
+ self.assertTrue(
+ self.before_template_id in self.partner2_id.base_comment_template_ids
+ )
+ self.assertTrue(
+ self.before_template_id not in self.partner_id.base_comment_template_ids
+ )
+
+ def test_render_comment_text(self):
+ expected_text = "Test comment render %s" % self.user.name
+ self.before_template_id.text = "Test comment render {{object.name}}"
+ with self.with_user(self.user.login):
+ self.assertEqual(
+ self.user.render_comment(self.before_template_id), expected_text
+ )
+
+ def test_render_comment_text_(self):
+ ro_RO_lang = (
+ self.env["res.lang"]
+ .with_context(active_test=False)
+ .search([("code", "=", "ro_RO")])
+ )
+ with mute_logger("odoo.addons.base.models.ir_translation"):
+ self.env["base.language.install"].create(
+ {"overwrite": True, "lang_ids": [(6, 0, [ro_RO_lang.id])]}
+ ).lang_install()
+
+ module = self.env.ref("base.module_test_translation_import")
+ export = self.env["base.language.export"].create(
+ {"lang": "ro_RO", "format": "po", "modules": [Command.set([module.id])]}
+ )
+ export.act_getfile()
+ po_file = export.data
+ self.assertIsNotNone(po_file)
+
+ partner_title = self.ResPartnerTitle.create(
+ {"name": "Ambassador", "shortcut": "Amb."}
+ )
+ # Adding translated terms
+ ctx = dict(lang="ro_RO")
+ partner_title.with_context(**ctx).write(
+ {"name": "Ambasador", "shortcut": "Amb."}
+ )
+ self.user.partner_id.title = partner_title
+ self.before_template_id.text = "Test comment render {{object.title.name}}"
+
+ expected_en_text = "Test comment render Ambassador"
+ expected_ro_text = "Test comment render Ambasador"
+ with self.with_user(self.user.login):
+ self.assertEqual(
+ self.user.render_comment(self.before_template_id), expected_en_text
+ )
+ self.assertEqual(
+ self.user.with_context(**ctx).render_comment(self.before_template_id),
+ expected_ro_text,
+ )
+
+ def test_partner_template_wizaard(self):
+ partner_preview = (
+ self.env["base.comment.template.preview"]
+ .with_context(default_base_comment_template_id=self.before_template_id.id)
+ .create({})
+ )
+ self.assertTrue(partner_preview)
+ default = (
+ self.env["base.comment.template.preview"]
+ .with_context(default_base_comment_template_id=self.before_template_id.id)
+ .default_get(partner_preview._fields)
+ )
+ self.assertTrue(default.get("base_comment_template_id"))
+ resource_ref = partner_preview._selection_target_model()
+ self.assertTrue(len(resource_ref) >= 2)
+ partner_preview._compute_no_record()
+ self.assertTrue(partner_preview.no_record)
+
+ def test_partner_commercial_fields(self):
+ self.assertTrue(
+ "base_comment_template_ids" in self.env["res.partner"]._commercial_fields()
+ )
diff --git a/base_comment_template/views/base_comment_template_view.xml b/base_comment_template/views/base_comment_template_view.xml
new file mode 100644
index 0000000000..362947d0a5
--- /dev/null
+++ b/base_comment_template/views/base_comment_template_view.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/base_comment_template/views/res_partner_view.xml b/base_comment_template/views/res_partner_view.xml
new file mode 100644
index 0000000000..d1fd4c7c2e
--- /dev/null
+++ b/base_comment_template/views/res_partner_view.xml
@@ -0,0 +1,28 @@
+
+
+ res.partner
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/base_comment_template/wizard/__init__.py b/base_comment_template/wizard/__init__.py
new file mode 100644
index 0000000000..9a0d64bbf6
--- /dev/null
+++ b/base_comment_template/wizard/__init__.py
@@ -0,0 +1 @@
+from . import base_comment_template_preview
diff --git a/base_comment_template/wizard/base_comment_template_preview.py b/base_comment_template/wizard/base_comment_template_preview.py
new file mode 100644
index 0000000000..0146390757
--- /dev/null
+++ b/base_comment_template/wizard/base_comment_template_preview.py
@@ -0,0 +1,85 @@
+from odoo import api, fields, models
+from odoo.tools.safe_eval import safe_eval
+
+from odoo.addons.base.models.res_partner import _lang_get
+
+
+class BaseCommentTemplatePreview(models.TransientModel):
+ _name = "base.comment.template.preview"
+ _description = "Base Comment Template Preview"
+
+ @api.model
+ def _selection_target_model(self):
+ models = self.env["ir.model"].search([("is_comment_template", "=", True)])
+ return [(model.model, model.name) for model in models]
+
+ @api.model
+ def default_get(self, fields):
+ result = super().default_get(fields)
+ base_comment_template_id = self.env.context.get(
+ "default_base_comment_template_id"
+ )
+ if not base_comment_template_id or "resource_ref" not in fields:
+ return result
+ base_comment_template = self.env["base.comment.template"].browse(
+ base_comment_template_id
+ )
+ result["model_ids"] = base_comment_template.model_ids
+ domain = safe_eval(base_comment_template.domain)
+ model = (
+ base_comment_template.model_ids[0]
+ if base_comment_template.model_ids
+ else False
+ )
+ res = self.env[model.model].search(domain, limit=1)
+ if res:
+ result["resource_ref"] = f"{model.model},{res.id}"
+ return result
+
+ base_comment_template_id = fields.Many2one(
+ "base.comment.template", required=True, ondelete="cascade"
+ )
+ lang = fields.Selection(_lang_get, string="Template Preview Language")
+ engine = fields.Selection(
+ [
+ ("inline_template", "Inline template"),
+ ("qweb", "QWeb"),
+ ("qweb_view", "QWeb view"),
+ ],
+ string="Template Preview Engine",
+ default="inline_template",
+ )
+ model_ids = fields.Many2many(
+ "ir.model", related="base_comment_template_id.model_ids"
+ )
+ model_id = fields.Many2one("ir.model")
+ body = fields.Char(compute="_compute_base_comment_template_fields")
+ resource_ref = fields.Reference(
+ string="Record reference", selection="_selection_target_model"
+ )
+ no_record = fields.Boolean(compute="_compute_no_record")
+
+ @api.depends("model_id")
+ def _compute_no_record(self):
+ for preview in self:
+ domain = safe_eval(self.base_comment_template_id.domain)
+ preview.no_record = (
+ (self.env[preview.model_id.model].search_count(domain) == 0)
+ if preview.model_id
+ else True
+ )
+
+ @api.depends("lang", "resource_ref", "engine")
+ def _compute_base_comment_template_fields(self):
+ for wizard in self:
+ if (
+ wizard.model_id
+ and wizard.resource_ref
+ and wizard.lang
+ and wizard.engine
+ ):
+ wizard.body = wizard.resource_ref.with_context(
+ lang=wizard.lang
+ ).render_comment(self.base_comment_template_id, engine=wizard.engine)
+ else:
+ wizard.body = wizard.base_comment_template_id.text
diff --git a/base_comment_template/wizard/base_comment_template_preview_views.xml b/base_comment_template/wizard/base_comment_template_preview_views.xml
new file mode 100644
index 0000000000..c53194c690
--- /dev/null
+++ b/base_comment_template/wizard/base_comment_template_preview_views.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+