diff --git a/trp_external_user/README.rst b/trp_external_user/README.rst
new file mode 100644
index 00000000..a8e04211
--- /dev/null
+++ b/trp_external_user/README.rst
@@ -0,0 +1,17 @@
+External users
+==============
+
+Define a group 'External Users' that only have access to selected partners,
+usually their own organizations. These partners are indicated on the user's user
+form. By default, this module only grants read access to these partners.
+
+On a permission level, it seems necessary that the partners also need read access
+to the database company partner, which this module also allows. Keep this in mind
+when building functionality on top of this module that allows interaction with
+the partner model. By means of precaution, this module does define separate
+records for reading and writing so as to prevent modifications of the company
+partner (even if global write access on partners are not granted to the external
+users group in this module).
+
+This module also disables the default assignment of the 'user' and 'partner manager'
+groups to new users.
diff --git a/trp_external_user/__init__.py b/trp_external_user/__init__.py
new file mode 100644
index 00000000..c32fd62b
--- /dev/null
+++ b/trp_external_user/__init__.py
@@ -0,0 +1,2 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+from . import models
diff --git a/trp_external_user/__manifest__.py b/trp_external_user/__manifest__.py
new file mode 100644
index 00000000..635f49e4
--- /dev/null
+++ b/trp_external_user/__manifest__.py
@@ -0,0 +1,19 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+{
+ "name": "External users",
+ "summary": "Allow external users on your system",
+ "version": "13.0.1.0.0",
+ "author": "Therp BV, Sunflower IT",
+ "category": "External users",
+ "website": "https://therp.nl",
+ "depends": ["base", "portal"],
+ "demo": ["demo/res_users.xml"],
+ "data": [
+ "data/ir_module_category.xml",
+ "security/res_groups.xml",
+ "security/ir.model.access.csv",
+ "security/ir_rule.xml",
+ "views/res_users.xml",
+ ],
+ "license": "AGPL-3",
+}
diff --git a/trp_external_user/data/ir_module_category.xml b/trp_external_user/data/ir_module_category.xml
new file mode 100644
index 00000000..2bc6741d
--- /dev/null
+++ b/trp_external_user/data/ir_module_category.xml
@@ -0,0 +1,8 @@
+
+
+
+ External users
+ External users
+ 60
+
+
diff --git a/trp_external_user/demo/res_users.xml b/trp_external_user/demo/res_users.xml
new file mode 100644
index 00000000..8f49166f
--- /dev/null
+++ b/trp_external_user/demo/res_users.xml
@@ -0,0 +1,9 @@
+
+
+
+ external
+ External user
+
+
+
+
diff --git a/trp_external_user/models/__init__.py b/trp_external_user/models/__init__.py
new file mode 100644
index 00000000..26984bbe
--- /dev/null
+++ b/trp_external_user/models/__init__.py
@@ -0,0 +1,2 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+from . import res_users
diff --git a/trp_external_user/models/res_users.py b/trp_external_user/models/res_users.py
new file mode 100644
index 00000000..0623aca3
--- /dev/null
+++ b/trp_external_user/models/res_users.py
@@ -0,0 +1,39 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+from odoo import api, fields, models
+
+
+class ResUsers(models.Model):
+ _inherit = "res.users"
+
+ @api.model
+ def _default_groups(self):
+ """Disable default assignment of group 'users' and 'partner manager'"""
+ filter_groups = self.env.ref("base.group_user") + self.env.ref(
+ "base.group_partner_manager"
+ )
+ return (
+ super(ResUsers, self)
+ ._default_groups()
+ .filtered(lambda x: not x & filter_groups)
+ )
+
+ @api.depends("groups_id")
+ def _compute_is_external_user(self):
+ for this in self:
+ this.is_external_user = (
+ self.env["ir.model.access"]
+ .with_user(this)
+ .check_groups("trp_external_user.group_external_user")
+ )
+
+ external_user_partner_ids = fields.Many2many(
+ "res.partner",
+ "trp_external_user_partner_id_rel",
+ "user_id",
+ "partner_id",
+ "External access to related partners",
+ )
+ is_external_user = fields.Boolean(
+ "Is external user", compute="_compute_is_external_user"
+ )
+ groups_id = fields.Many2many(default=lambda self: self._default_groups())
diff --git a/trp_external_user/security/ir.model.access.csv b/trp_external_user/security/ir.model.access.csv
new file mode 100644
index 00000000..52a69fe8
--- /dev/null
+++ b/trp_external_user/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"
+"external_access_partner","Read access on partners for external users","base.model_res_partner","trp_external_user.group_external_user",1,0,0,0
+"external_access_mail_message","Read/create access on mail messages for external users","mail.model_mail_message","trp_external_user.group_external_user",1,0,1,0
diff --git a/trp_external_user/security/ir_rule.xml b/trp_external_user/security/ir_rule.xml
new file mode 100644
index 00000000..d9654ff2
--- /dev/null
+++ b/trp_external_user/security/ir_rule.xml
@@ -0,0 +1,35 @@
+
+
+
+ Read access to own partners plus company partner
+
+
+
+
+
+ ['|', '|',
+ ('id', '=', user.company_id.partner_id.id),
+ ('id', '=', user.partner_id.id),
+ ('id', 'in', user.external_user_partner_ids.ids),
+ ]
+
+
+
+ Access to own partners
+
+
+ ['|',
+ ('id', '=', user.partner_id.id),
+ ('id', 'in', user.external_user_partner_ids.ids),
+ ]
+
+
+
+ Access to own user
+
+
+ [
+ ('id', '=', user.id),
+ ]
+
+
diff --git a/trp_external_user/security/res_groups.xml b/trp_external_user/security/res_groups.xml
new file mode 100644
index 00000000..13f6b6d3
--- /dev/null
+++ b/trp_external_user/security/res_groups.xml
@@ -0,0 +1,11 @@
+
+
+
+ External users
+
+
+
+
diff --git a/trp_external_user/static/description/icon.png b/trp_external_user/static/description/icon.png
new file mode 100644
index 00000000..4c7ab302
Binary files /dev/null and b/trp_external_user/static/description/icon.png differ
diff --git a/trp_external_user/tests/__init__.py b/trp_external_user/tests/__init__.py
new file mode 100644
index 00000000..b3ab0c98
--- /dev/null
+++ b/trp_external_user/tests/__init__.py
@@ -0,0 +1,2 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+from . import test_trp_external_user
diff --git a/trp_external_user/tests/test_trp_external_user.py b/trp_external_user/tests/test_trp_external_user.py
new file mode 100644
index 00000000..03f0befc
--- /dev/null
+++ b/trp_external_user/tests/test_trp_external_user.py
@@ -0,0 +1,33 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+from odoo.exceptions import AccessError
+from odoo.tests.common import TransactionCase
+
+
+class TestTrpExternalUser(TransactionCase):
+ def test_trp_external_user(self):
+ external_user = self.env.ref("trp_external_user.user_external")
+ # check some permissions
+ with self.assertRaises(AccessError):
+ self.env.ref("base.user_root").with_user(external_user).name
+ self.assertEqual(
+ self.env.ref("base.main_partner").name,
+ self.env.ref("base.main_partner").with_user(external_user).name,
+ )
+ self.assertEqual(
+ self.env.ref("base.res_partner_2").name,
+ self.env.ref("base.res_partner_2").with_user(external_user).name,
+ )
+ self.assertEqual(
+ external_user.name, external_user.with_user(external_user).name
+ )
+ # check flag
+ self.assertTrue(external_user.is_external_user)
+ self.assertFalse(self.env.ref("base.user_root").is_external_user)
+ # check user creation
+ test_user = self.env["res.users"].create(
+ {"login": "some login", "name": "Some name"}
+ )
+ self.assertNotIn(self.env.ref("base.group_user"), test_user.groups_id)
+ self.assertNotIn(
+ self.env.ref("base.group_partner_manager"), test_user.groups_id
+ )
diff --git a/trp_external_user/views/res_users.xml b/trp_external_user/views/res_users.xml
new file mode 100644
index 00000000..831afe60
--- /dev/null
+++ b/trp_external_user/views/res_users.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ res.users
+
+
+
+
+
+
+
+
+
+