diff --git a/server_environment_data_encryption/__init__.py b/server_environment_data_encryption/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/server_environment_data_encryption/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/server_environment_data_encryption/__manifest__.py b/server_environment_data_encryption/__manifest__.py
new file mode 100644
index 0000000..c1452cc
--- /dev/null
+++ b/server_environment_data_encryption/__manifest__.py
@@ -0,0 +1,13 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+{
+ "name": "Server Environment Data Encryption",
+ "version": "12.0.0.0.1",
+ "category": "Tools",
+ "website": "https://akretion.com/",
+ "author": "Akretion, Odoo Community Association (OCA)",
+ "license": "AGPL-3",
+ "application": False,
+ "installable": True,
+ "depends": ["server_environment", "data_encryption"],
+ "data": [],
+}
diff --git a/server_environment_data_encryption/models/__init__.py b/server_environment_data_encryption/models/__init__.py
new file mode 100644
index 0000000..6bd869a
--- /dev/null
+++ b/server_environment_data_encryption/models/__init__.py
@@ -0,0 +1 @@
+from . import server_env_mixin
diff --git a/server_environment_data_encryption/models/server_env_mixin.py b/server_environment_data_encryption/models/server_env_mixin.py
new file mode 100644
index 0000000..5579c4a
--- /dev/null
+++ b/server_environment_data_encryption/models/server_env_mixin.py
@@ -0,0 +1,165 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+import logging
+from odoo import api, models, _
+from odoo.exceptions import ValidationError
+from odoo.tools.config import config
+from lxml import etree
+from odoo.osv.orm import setup_modifiers
+
+
+_logger = logging.getLogger(__name__)
+
+
+class ServerEnvMixin(models.AbstractModel):
+ _inherit = "server.env.mixin"
+
+ def _compute_server_env_from_default(self, field_name, options):
+ "First return database encrypted value then default value"
+ self.ensure_one()
+ encrypted_data_name = "%s,%s" % (self._name, self.id)
+ env = self.env.context.get("environment", None)
+ vals = (
+ self.env["encrypted.data"]
+ .sudo()
+ ._get_json(encrypted_data_name, env=env)
+ )
+ if vals.get(field_name):
+ self[field_name] = vals[field_name]
+ else:
+ return super()._compute_server_env_from_default(
+ field_name, options
+ )
+
+ def _inverse_server_env(self, field_name):
+ """
+ When this module is installed, we store values into encrypted data
+ env instead of a default field in database (not env dependent).
+ """
+ is_editable_field = self._server_env_is_editable_fieldname(field_name)
+ encrypted_data_obj = self.env["encrypted.data"].sudo()
+ env = self.env.context.get("environment", None)
+ for record in self:
+ if record[is_editable_field]:
+ encrypted_data_name = "%s,%s" % (record._name, record.id)
+ values = encrypted_data_obj._get_json(
+ encrypted_data_name, env=env
+ )
+ new_val = {field_name: record[field_name]}
+ values.update(new_val)
+ encrypted_data_obj._store_json(
+ encrypted_data_name, values, env=env
+ )
+
+ def action_change_env_data_encrypted_fields(self):
+ action_id = self.env.context.get("params", {}).get("action")
+ if not action_id:
+ # We don't know which action we are using... take one random.
+ action_id = self.env['ir.actions.act_window'].search(
+ [('res_model', '=', self._name)], limit=1).id
+ action = self.env["ir.actions.act_window"].browse(action_id).read()[0]
+ action["view_mode"] = "form"
+ action["res_id"] = self.id
+ views_form = []
+ for view_id, view_type in action.get("views", []):
+ if view_type == "form":
+ views_form.append((view_id, view_type))
+ action["views"] = views_form
+ return action
+
+ def _get_extra_environment_info_div(self, current_env, extra_envs):
+ button_div = "
"
+ button_string = _("Define values for ")
+ for environment in extra_envs:
+ button = """
+
+ """.format(
+ button_string, environment, {"environment": environment}
+ )
+ button_div += "{}".format(button)
+ button_div += "
"
+ alert_string = _("Modify values for {} environment").format(
+ current_env
+ )
+ alert_type = (
+ current_env == config.get("running_env")
+ and "alert-info"
+ or "alert-warning"
+ )
+ elem = etree.fromstring(
+ """
+
+ """.format(
+ alert_type, alert_string, button_div
+ )
+ )
+ return elem
+
+ def _set_readonly_form_view(self, doc):
+ for field in doc.iter("field"):
+ env_fields = self._server_env_fields.keys()
+ field_name = field.get("name")
+ if field_name in env_fields:
+ continue
+ field.set("readonly", "1")
+ setup_modifiers(field, self.fields_get(field_name))
+
+ def _update_form_view_from_env(self, arch, view_type):
+ if view_type != "form":
+ return arch
+ current_env = self.env.context.get("environment") or config.get(
+ "running_env"
+ )
+ other_environments = [
+ key[15:]
+ for key, val in config.options.items()
+ if key.startswith("encryption_key_")
+ and val
+ and key[15:] != current_env
+ ]
+
+ if not current_env:
+ raise ValidationError(
+ _(
+ "you need to define the running_env entry in your odoo "
+ "configuration file"
+ )
+ )
+ doc = etree.XML(arch)
+ node = doc.xpath("//sheet")
+ if node:
+ node = node[0]
+ elem = self._get_extra_environment_info_div(
+ current_env, other_environments
+ )
+ node.insert(0, elem)
+
+ if current_env != config.get("running_env"):
+ self._set_readonly_form_view(doc)
+ arch = etree.tostring(doc, pretty_print=True, encoding="unicode")
+ else:
+ _logger.error(
+ "Missing sheet for form view on object {}".format(self._name)
+ )
+ return arch
+
+ @api.model
+ def fields_view_get(
+ self, view_id=None, view_type="form", toolbar=False, submenu=False
+ ):
+ res = super().fields_view_get(
+ view_id=view_id,
+ view_type=view_type,
+ toolbar=toolbar,
+ submenu=submenu,
+ )
+ res["arch"] = self._update_form_view_from_env(res["arch"], view_type)
+ return res
diff --git a/server_environment_data_encryption/readme/CONFIGURE.rst b/server_environment_data_encryption/readme/CONFIGURE.rst
new file mode 100644
index 0000000..9d0ad46
--- /dev/null
+++ b/server_environment_data_encryption/readme/CONFIGURE.rst
@@ -0,0 +1,24 @@
+In order to use this module properly, each environment should have their own encryption key
+and the production environment should have the keys of all environments.
+
+Example :
+Development environment ::
+
+ [options]
+ running_env=dev
+ encryption_key_dev=XXX
+
+Pre-production environment ::
+
+ [options]
+ running_env=preprod
+ encryption_key_preprod=YYY
+
+Production environment ::
+
+ [options]
+ running_env=prod
+ encryption_key_dev=XXX
+ encryption_key_preprod=YYY
+ encryption_key_prod=ZZZ
+
diff --git a/server_environment_data_encryption/readme/CONTRIBUTORS.rst b/server_environment_data_encryption/readme/CONTRIBUTORS.rst
new file mode 100644
index 0000000..86340ec
--- /dev/null
+++ b/server_environment_data_encryption/readme/CONTRIBUTORS.rst
@@ -0,0 +1,3 @@
+* Florian da Costa
+* Sébastien Beau
+* Benoît Guillot
diff --git a/server_environment_data_encryption/readme/DESCRIPTION.rst b/server_environment_data_encryption/readme/DESCRIPTION.rst
new file mode 100644
index 0000000..ebf4147
--- /dev/null
+++ b/server_environment_data_encryption/readme/DESCRIPTION.rst
@@ -0,0 +1,6 @@
+This module changes a little the behavior of server_environment modules.
+When Odoo does not find the value of the field in the configuration file,
+it will fallback on a Odoo encrypted field instead.
+Also it allows you
+to configure the environment dependent fields for all your environments
+from the production server.
diff --git a/server_environment_data_encryption/tests/__init__.py b/server_environment_data_encryption/tests/__init__.py
new file mode 100644
index 0000000..83ac8b8
--- /dev/null
+++ b/server_environment_data_encryption/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_server_environment_data_encrypt
diff --git a/server_environment_data_encryption/tests/fixtures/base.xml b/server_environment_data_encryption/tests/fixtures/base.xml
new file mode 100644
index 0000000..524c959
--- /dev/null
+++ b/server_environment_data_encryption/tests/fixtures/base.xml
@@ -0,0 +1,15 @@
+
diff --git a/server_environment_data_encryption/tests/fixtures/res1.xml b/server_environment_data_encryption/tests/fixtures/res1.xml
new file mode 100644
index 0000000..7ca515a
--- /dev/null
+++ b/server_environment_data_encryption/tests/fixtures/res1.xml
@@ -0,0 +1,21 @@
+
diff --git a/server_environment_data_encryption/tests/fixtures/res2.xml b/server_environment_data_encryption/tests/fixtures/res2.xml
new file mode 100644
index 0000000..032e108
--- /dev/null
+++ b/server_environment_data_encryption/tests/fixtures/res2.xml
@@ -0,0 +1,21 @@
+
diff --git a/server_environment_data_encryption/tests/test_server_environment_data_encrypt.py b/server_environment_data_encryption/tests/test_server_environment_data_encrypt.py
new file mode 100644
index 0000000..44be573
--- /dev/null
+++ b/server_environment_data_encryption/tests/test_server_environment_data_encrypt.py
@@ -0,0 +1,36 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+from odoo.addons.data_encryption.tests.common import CommonDataEncrypted
+from pathlib import Path
+
+
+class TestServerEnvDataEncrypted(CommonDataEncrypted):
+
+ # def test_store_data_no_superuser(self):
+ # self.env['server.env.mixin']._inverse_server_env('test')
+ # pass
+
+ def test_dynamic_view_current_env(self):
+ self.maxDiff = None
+ self.set_new_key_env("prod")
+ self.set_new_key_env("preprod")
+ mixin_obj = self.env["server.env.mixin"]
+ base_path = Path(__file__).parent / "fixtures" / "base.xml"
+ xml = base_path.read_text()
+ res_xml = mixin_obj._update_form_view_from_env(xml, "form")
+ expected_xml_path = Path(__file__).parent / "fixtures" / "res1.xml"
+ expected_xml = expected_xml_path.read_text()
+ self.assertEqual(res_xml, expected_xml)
+
+ def test_dynamic_view_other_env(self):
+ self.set_new_key_env("prod")
+ self.set_new_key_env("preprod")
+ mixin_obj = self.env["server.env.mixin"]
+ base_path = Path(__file__).parent / "fixtures" / "base.xml"
+ xml = base_path.read_text()
+ res_xml = mixin_obj.with_context(
+ environment="prod"
+ )._update_form_view_from_env(xml, "form")
+ expected_xml_path = Path(__file__).parent / "fixtures" / "res2.xml"
+ expected_xml = expected_xml_path.read_text()
+ self.assertEqual(res_xml, expected_xml)