diff --git a/server_environment_iap/__init__.py b/server_environment_iap/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/server_environment_iap/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/server_environment_iap/__manifest__.py b/server_environment_iap/__manifest__.py
new file mode 100644
index 0000000..38618f1
--- /dev/null
+++ b/server_environment_iap/__manifest__.py
@@ -0,0 +1,16 @@
+# Copyright 2021 Camptocamp SA
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+{
+ "name": "Server Environment IAP Account",
+ "summary": """
+ Override IAP Accounts from server environment file""",
+ "version": "14.0.1.0.0",
+ "license": "AGPL-3",
+ "author": "Camptocamp, Odoo Community Association (OCA)",
+ "website": "https://github.com/OCA/server-env",
+ "depends": ["iap", "server_environment"],
+ "data": [
+ "views/iap_views.xml",
+ ],
+}
diff --git a/server_environment_iap/models/__init__.py b/server_environment_iap/models/__init__.py
new file mode 100644
index 0000000..966d7e1
--- /dev/null
+++ b/server_environment_iap/models/__init__.py
@@ -0,0 +1 @@
+from . import iap_account
diff --git a/server_environment_iap/models/iap_account.py b/server_environment_iap/models/iap_account.py
new file mode 100644
index 0000000..043ca47
--- /dev/null
+++ b/server_environment_iap/models/iap_account.py
@@ -0,0 +1,70 @@
+# Copyright 2021 Camptocamp SA
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
+
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError
+
+from odoo.addons.server_environment.server_env import serv_config
+
+SECTION = "iap.account"
+
+
+class IapAccount(models.Model):
+
+ _inherit = "iap.account"
+
+ is_environment = fields.Boolean(
+ string="Defined by environment",
+ compute="_compute_is_environment",
+ help="If check, the value in the database will be ignored"
+ " and alternatively, the system will use the service name defined"
+ " in your odoo.cfg environment file.",
+ )
+
+ def _compute_is_environment(self):
+ for account in self:
+ account.is_environment = serv_config.has_option(
+ SECTION, account.service_name
+ )
+
+ @api.model
+ def get(self, service_name, force_create=True):
+ account = super().get(service_name, force_create=True)
+ if serv_config.has_option(SECTION, service_name):
+ cvalue = serv_config.get(SECTION, service_name)
+ if not cvalue:
+ # if service name is empty it's probably not a production instance,
+ # so we need to remove it from database
+ account.unlink()
+ raise UserError(
+ _("Service name %s is empty in " "server_environment_file")
+ % (service_name,)
+ )
+ if cvalue != account.account_token:
+ # we write in db on first access;
+ # should we have preloaded values in database at,
+ # server startup, modules loading their parameters
+ # from data files would break on unique key error.
+ account.account_token = cvalue
+ return account
+
+ @api.model
+ def create(self, vals):
+ service_name = vals.get("service_name")
+ if serv_config.has_option(SECTION, service_name):
+ # enforce account_token from config file
+ vals = dict(vals, account_token=serv_config.get(SECTION, service_name))
+ return super().create(vals)
+
+ def write(self, vals):
+ for rec in self:
+ service_name = vals.get("service_name") or rec.service_name
+ if serv_config.has_option(SECTION, service_name):
+ # enforce account_token from config file
+ newvals = dict(
+ vals, account_token=serv_config.get(SECTION, service_name)
+ )
+ else:
+ newvals = vals
+ super().write(newvals)
+ return True
diff --git a/server_environment_iap/readme/CONFIGURE.rst b/server_environment_iap/readme/CONFIGURE.rst
new file mode 100644
index 0000000..012053f
--- /dev/null
+++ b/server_environment_iap/readme/CONFIGURE.rst
@@ -0,0 +1,16 @@
+To configure this module, you need to add a section ``[iap.account]`` to
+you server_environment_files configurations, where the keys are service names
+as would normally be set in the Technical / IAP Accounts Odoo menu.
+
+When first using a value, the system will read it from the server environment file
+and override any value that would be present in the database, so the server environment file has precedence.
+
+When creating or modifying values that are in the server environment file, the
+module replace changes, enforcing the configuration value.
+
+For example you can use this module like that:
+
+.. code::
+
+ [iap.account]
+ partner_autocomplete=secret_token
diff --git a/server_environment_iap/readme/CONTRIBUTORS.rst b/server_environment_iap/readme/CONTRIBUTORS.rst
new file mode 100644
index 0000000..accaa82
--- /dev/null
+++ b/server_environment_iap/readme/CONTRIBUTORS.rst
@@ -0,0 +1 @@
+* Maksym Yankin (https://https://www.camptocamp.com)
diff --git a/server_environment_iap/readme/CREDITS.rst b/server_environment_iap/readme/CREDITS.rst
new file mode 100644
index 0000000..c08e96d
--- /dev/null
+++ b/server_environment_iap/readme/CREDITS.rst
@@ -0,0 +1,2 @@
+This module is maintained by:
+* Odoo Community Association
diff --git a/server_environment_iap/readme/DESCRIPTION.rst b/server_environment_iap/readme/DESCRIPTION.rst
new file mode 100644
index 0000000..5935572
--- /dev/null
+++ b/server_environment_iap/readme/DESCRIPTION.rst
@@ -0,0 +1 @@
+Override IAP Accounts from the server environment file.
diff --git a/server_environment_iap/readme/ROADMAP.rst b/server_environment_iap/readme/ROADMAP.rst
new file mode 100644
index 0000000..7b5b717
--- /dev/null
+++ b/server_environment_iap/readme/ROADMAP.rst
@@ -0,0 +1,2 @@
+It would be nice to set IAP Accounts in the server environment file, possibly make their key and value
+readonly in the user interface and remove them from database except production.
diff --git a/server_environment_iap/readme/USAGE.rst b/server_environment_iap/readme/USAGE.rst
new file mode 100644
index 0000000..44c0bc6
--- /dev/null
+++ b/server_environment_iap/readme/USAGE.rst
@@ -0,0 +1,2 @@
+Before using this module, you must be familiar with the
+server_environment module.
diff --git a/server_environment_iap/tests/__init__.py b/server_environment_iap/tests/__init__.py
new file mode 100644
index 0000000..210651a
--- /dev/null
+++ b/server_environment_iap/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_server_environment_iap
diff --git a/server_environment_iap/tests/config_iap_test.xml b/server_environment_iap/tests/config_iap_test.xml
new file mode 100644
index 0000000..77516b4
--- /dev/null
+++ b/server_environment_iap/tests/config_iap_test.xml
@@ -0,0 +1,6 @@
+
+
+ iap_from_config
+ value_from_xml
+
+
diff --git a/server_environment_iap/tests/test_server_environment_iap.py b/server_environment_iap/tests/test_server_environment_iap.py
new file mode 100644
index 0000000..9ad88e4
--- /dev/null
+++ b/server_environment_iap/tests/test_server_environment_iap.py
@@ -0,0 +1,101 @@
+# Copyright 2016-2018 ACSONE SA/NV
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo.exceptions import UserError
+from odoo.modules.module import get_resource_path
+from odoo.tests import tagged
+from odoo.tools import convert_file
+
+from odoo.addons.server_environment.tests.common import ServerEnvironmentCase
+
+from ..models import iap_account
+
+
+@tagged("post_install", "-at_install")
+class TestEnv(ServerEnvironmentCase):
+ def setUp(self):
+ super().setUp()
+ self.IAP = self.env["iap.account"]
+ self.env_config = (
+ "[iap.account]\n" "iap_from_config=config_value\n" "iap_empty=\n"
+ )
+ self.service_name = "iap_from_config"
+ self.account_token = "config_value"
+ self.some_service = "some.service"
+ self.some_token = "some.token"
+
+ def _load_xml(self, module, filepath):
+ convert_file(
+ self.env.cr,
+ module,
+ get_resource_path(module, filepath),
+ {},
+ mode="init",
+ noupdate=False,
+ kind="test",
+ )
+
+ def _search_account(self, service, token):
+ return self.IAP.search(
+ [("service_name", "=", service), ("account_token", "=", token)]
+ )
+
+ def test_empty(self):
+ """Empty config values cause error"""
+ with self.load_config(public=self.env_config, serv_config_class=iap_account):
+ with self.assertRaises(UserError):
+ self.IAP.get("iap_empty")
+ iap_nonexistant = self.IAP.get("iap_nonexistant")
+ self.assertTrue(iap_nonexistant.account_token)
+
+ def test_get_account(self):
+ """Get account data from config"""
+ with self.load_config(public=self.env_config, serv_config_class=iap_account):
+ # it's not in db
+ res = self._search_account(self.service_name, self.account_token)
+ self.assertFalse(res)
+ # read so it's created in db
+ account = self.IAP.get("iap_from_config")
+ self.assertEqual(account.account_token, "config_value")
+ self.assertEqual(len(account), 1)
+
+ def test_override_xmldata(self):
+ with self.load_config(public=self.env_config, serv_config_class=iap_account):
+ self._load_xml("server_environment_iap", "tests/config_iap_test.xml")
+ self.assertEqual(
+ self.IAP.get("iap_from_config").account_token, "config_value"
+ )
+
+ def test_set_param_1(self):
+ """We can't set account data that is in config file"""
+ with self.load_config(public=self.env_config, serv_config_class=iap_account):
+ # when creating, the value is overridden by config file
+ self.IAP.create(
+ {"service_name": "iap_from_config", "account_token": "new_value"}
+ )
+ acc = self.IAP.get("iap_from_config")
+ self.assertEqual(acc.account_token, "config_value")
+ # when writing, the value is overridden by config file
+ res = self._search_account(self.service_name, self.account_token)
+ self.assertEqual(len(res), 1)
+ res.write({"account_token": "new_value"})
+ acc = self.IAP.get("iap_from_config")
+ self.assertEqual(acc.account_token, "config_value")
+ # unlink works normally...
+ res = self._search_account(self.service_name, self.account_token)
+ self.assertEqual(len(res), 1)
+ res.unlink()
+ res = self._search_account(self.service_name, self.account_token)
+ self.assertEqual(len(res), 0)
+ # but the value is recreated when getting param again
+ acc = self.IAP.get("iap_from_config")
+ self.assertEqual(acc.account_token, "config_value")
+ self.assertEqual(len(acc), 1)
+
+ def test_set_param_2(self):
+ """We can set parameters that are not in config file"""
+ with self.load_config(public=self.env_config, serv_config_class=iap_account):
+ self.IAP.create(
+ {"service_name": "some.service", "account_token": "some.token"}
+ )
+ self.assertEqual(self.IAP.get("some.service").account_token, "some.token")
diff --git a/server_environment_iap/views/iap_views.xml b/server_environment_iap/views/iap_views.xml
new file mode 100644
index 0000000..ddcca96
--- /dev/null
+++ b/server_environment_iap/views/iap_views.xml
@@ -0,0 +1,34 @@
+
+
+
+
+ iap.account
+
+
+
+
+
+
+ {'readonly': [('is_environment', '=', True)]}
+
+
+ {'readonly': [('is_environment', '=', True)]}
+
+
+
+
+
+ iap.account
+
+
+
+
+
+
+
+
+
diff --git a/setup/server_environment_iap/odoo/addons/server_environment_iap b/setup/server_environment_iap/odoo/addons/server_environment_iap
new file mode 120000
index 0000000..e21dc23
--- /dev/null
+++ b/setup/server_environment_iap/odoo/addons/server_environment_iap
@@ -0,0 +1 @@
+../../../../server_environment_iap
\ No newline at end of file
diff --git a/setup/server_environment_iap/setup.py b/setup/server_environment_iap/setup.py
new file mode 100644
index 0000000..28c57bb
--- /dev/null
+++ b/setup/server_environment_iap/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)