+
+\
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..8b56a7ceaa
--- /dev/null
+++ b/base_comment_template/security/security.xml
@@ -0,0 +1,10 @@
+
+
+
+ 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..d6da6e30f9
--- /dev/null
+++ b/base_comment_template/static/description/index.html
@@ -0,0 +1,555 @@
+
+
+
+
+
+
README.rst
+
+
+
+
+
+
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..209138b36b
--- /dev/null
+++ b/base_comment_template/tests/fake_models.py
@@ -0,0 +1,24 @@
+# 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 fields, models
+
+
+class TestResUsers(models.Model):
+ _name = "test.res.users"
+ _description = "Test User"
+ _inherits = {"res.partner": "partner_id"}
+ _inherit = ["comment.template"]
+
+ partner_id = fields.Many2one(
+ "res.partner",
+ required=True,
+ ondelete="restrict",
+ bypass_search_access=True,
+ index=True,
+ string="Related Partner",
+ help="Partner-related data of the user",
+ )
+ login = fields.Char(required=True, help="Used to log into the system")
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..c5fb2a5d8f
--- /dev/null
+++ b/base_comment_template/tests/test_base_comment_template.py
@@ -0,0 +1,217 @@
+# 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.orm.model_classes import add_to_registry
+from odoo.tests import common
+from odoo.tools.misc import mute_logger
+
+
+class TestCommentTemplate(common.TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ from .fake_models import TestResUsers
+
+ add_to_registry(cls.registry, TestResUsers)
+ cls.registry._setup_models__(cls.env.cr, ["test.res.users"])
+ cls.registry.init_models(
+ cls.env.cr,
+ ["test.res.users"],
+ {"models_to_check": True},
+ )
+ cls.addClassCleanup(cls.registry.__delitem__, "test.res.users")
+
+ cls.test_user_obj = cls.env["ir.model"].search(
+ [("model", "=", "test.res.users")], limit=1
+ )
+ cls.user = cls.env["test.res.users"].create(
+ {
+ "name": "Test User",
+ "login": "test_user",
+ "email": "test_user@example.com",
+ }
+ )
+ cls.user2 = cls.env["test.res.users"].create(
+ {
+ "name": "Test User 2",
+ "login": "test_user_2",
+ "email": "test_user_2@example.com",
+ }
+ )
+ cls.partner_id = cls.env["res.partner"].create({"name": "Test Partner"})
+ cls.partner2_id = cls.env["res.partner"].create({"name": "Test Partner 2"})
+ cls.ResPartnerCategory = cls.env["res.partner.category"]
+ 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.test_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.test_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),
+ ]
+
+ def test_template_model_ids(self):
+ self.assertIn(
+ self.test_user_obj.model, self.before_template_id.mapped("model_ids.model")
+ )
+ self.assertEqual(len(self.before_template_id.model_ids), 1)
+ self.assertIn(
+ self.test_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_global_template(self):
+ # Need to force _compute because only trigger when partner_id have changed
+ global_template = self.env["base.comment.template"].create(
+ {
+ "name": "Top template",
+ "text": "Text before lines",
+ "models": self.test_user_obj.model,
+ "company_id": self.company.id,
+ }
+ )
+ self.user._compute_comment_template_ids()
+ # Check getting default comment template
+ self.assertNotIn(global_template, self.user.comment_template_ids)
+ global_template.global_template = True
+ self.user._compute_comment_template_ids()
+ self.assertIn(global_template, 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 = f"[('id', 'in', {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 = f"Test comment render {self.user.name}"
+ self.before_template_id.text = "Test comment render {{object.name}}"
+ 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_category = self.ResPartnerCategory.create({"name": "Ambassador"})
+ # Adding translated terms
+ ctx = dict(lang="ro_RO")
+ partner_category.with_context(**ctx).write({"name": "Ambasador"})
+ self.user.partner_id.category_id = partner_category
+ self.before_template_id.text = "Test comment render {{object.category_id.name}}"
+
+ expected_en_text = "Test comment render Ambassador"
+ expected_ro_text = "Test comment render Ambasador"
+ 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..3dec351df4
--- /dev/null
+++ b/base_comment_template/views/base_comment_template_view.xml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
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..d0d6704227
--- /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..b6217df3f3
--- /dev/null
+++ b/base_comment_template/wizard/base_comment_template_preview_views.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+