+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+ {'readonly': [('check_domain','=',False)], 'required': [('check_domain','=',True)]}
+
+
+
+
+
+ mail.alias.tree.inherit
+ mail.alias
+
+
+
+
+
+
+
+
+
From 0a1252c0ee5b1a8da5f3c56bef7f411a2e5f2ff5 Mon Sep 17 00:00:00 2001
From: Ronald Portier
Date: Wed, 26 Mar 2025 10:14:35 +0100
Subject: [PATCH 2/7] [IMP] mail_alias_with_domain: pre-commit stuff
---
mail_alias_with_domain/models/mail_alias.py | 5 ++++-
mail_alias_with_domain/models/mail_thread.py | 4 ++--
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/mail_alias_with_domain/models/mail_alias.py b/mail_alias_with_domain/models/mail_alias.py
index 4c71110647..aea75ce408 100644
--- a/mail_alias_with_domain/models/mail_alias.py
+++ b/mail_alias_with_domain/models/mail_alias.py
@@ -13,7 +13,10 @@ def generate_hash(value):
class Alias(models.Model):
_inherit = "mail.alias"
- alias_domain = fields.Char(inverse="_inverse_alias_domain", store=True,)
+ alias_domain = fields.Char(
+ inverse="_inverse_alias_domain",
+ store=True,
+ )
alias_display_name = fields.Char()
alias_name = fields.Char(compute="_compute_alias_name_and_hash", store=True)
alias_hash = fields.Char(compute="_compute_alias_name_and_hash", store=True)
diff --git a/mail_alias_with_domain/models/mail_thread.py b/mail_alias_with_domain/models/mail_thread.py
index 0945cbbd88..9d17795784 100644
--- a/mail_alias_with_domain/models/mail_thread.py
+++ b/mail_alias_with_domain/models/mail_thread.py
@@ -16,8 +16,8 @@ class MailThread(models.AbstractModel):
def message_route(
self, message, message_dict, model=None, thread_id=None, custom_values=None
):
- """ Prepare message_dict by extending recipients
- with found aliases mails base on alias and domain
+ """Prepare message_dict by extending recipients
+ with found aliases mails base on alias and domain
"""
try:
maching_aliases = self._find_alias_with_domain(message_dict)
From da8d655844b7d1c5f3bad47a07d23ca76fbb50bd Mon Sep 17 00:00:00 2001
From: Ronald Portier
Date: Wed, 26 Mar 2025 11:22:36 +0100
Subject: [PATCH 3/7] [MIG] mail_alias_with_domain: migrate to 16.0
---
mail_alias_with_domain/README.rst | 131 +++++
mail_alias_with_domain/__init__.py | 3 +-
mail_alias_with_domain/__manifest__.py | 11 +-
mail_alias_with_domain/models/__init__.py | 2 +-
mail_alias_with_domain/models/mail_alias.py | 138 ++++--
mail_alias_with_domain/models/mail_thread.py | 59 ++-
mail_alias_with_domain/post_init_hook.py | 10 +
.../readme/CONTRIBUTORS.rst | 7 +
mail_alias_with_domain/readme/DESCRIPTION.rst | 41 ++
mail_alias_with_domain/readme/USAGE.rst | 4 +
.../static/description/index.html | 466 ++++++++++++++++++
mail_alias_with_domain/tests/__init__.py | 2 +-
.../tests/test_mail_thread.py | 152 ++++--
.../views/mail_alias_views.xml | 69 +--
14 files changed, 923 insertions(+), 172 deletions(-)
create mode 100644 mail_alias_with_domain/README.rst
create mode 100644 mail_alias_with_domain/post_init_hook.py
create mode 100644 mail_alias_with_domain/readme/CONTRIBUTORS.rst
create mode 100644 mail_alias_with_domain/readme/DESCRIPTION.rst
create mode 100644 mail_alias_with_domain/readme/USAGE.rst
create mode 100644 mail_alias_with_domain/static/description/index.html
diff --git a/mail_alias_with_domain/README.rst b/mail_alias_with_domain/README.rst
new file mode 100644
index 0000000000..d8b60734e9
--- /dev/null
+++ b/mail_alias_with_domain/README.rst
@@ -0,0 +1,131 @@
+======================
+Mail Alias With Domain
+======================
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:3c7166c28331e6e457d77bd71f2af2420004c8b92ad4b8d745f80ad1e6884603
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
+ :target: https://odoo-community.org/page/development-status
+ :alt: Beta
+.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+ :alt: License: AGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github
+ :target: https://github.com/OCA/social/tree/16.0/mail_alias_with_domain
+ :alt: OCA/social
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/social-16-0/social-16-0-mail_alias_with_domain
+ :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/social&target_branch=16.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This module adds possibility to process aliases together with domain.
+
+For example, suppose we have 3 companies in odoo.
+Each company wants to have an alias where customers can send the bills.
+invoice@company1.com
+invoice@company2.com
+invoice@company3.com
+
+In odoo, aliases are unique, and this module extends this functionality in
+such a way that you can have many of the same aliases but with different domains.
+
+Note that when an incoming mail can be linked to an alias with a domain,
+this will be the only alias used. However when an incoming mail can be
+linked to multiple aliasses that have a domain, it is possible to have
+multiple used.
+
+FOR DEVELOPERS
+
+In the default alias system, only the local part of an email address (the part
+before the @) is used to link an incoming email to an alias. This happens in the
+message_route method of the mail.thread model.
+
+Aliasses in standard Odoo store the alias_name field without domain.
+
+To still be able to use a domain name, we need a trick. What we will do is:
+
+* Replace the alias_name in the user interface with an alias_entry field, where a
+ complete email address can be entered.
+
+* If an alias is entered as a complete email address, this will be stored in the
+ alias_name as __at__. For instance alex__at__example.com.
+ alias_name is therefore changed from a writable field to a stored computed field.
+
+* The computation of alias_domain will be enhanced to take full email addresses into
+ account.
+
+* If an incoming mail can be linked to a full email address alias, we will write a
+ context key pointing to this alias. The search method of mail.alias will be overriden
+ to check for this key, and then not search at all, but just return the alias
+ requested.
+
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Usage
+=====
+
+To use this module, you need to:
+
+Got to the mail aliasses and check which aliasses you want to link to a specific
+domain.
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues `_.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+`feedback `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+~~~~~~~
+
+* Solvti
+* Therp BV
+
+Contributors
+~~~~~~~~~~~~
+
+* `Solvti sp. z o.o. `_:
+
+ * Jakub Wiselka
+
+* `Therp `_:
+
+ * Ronald Portier (ronald@therp.nl)
+
+Maintainers
+~~~~~~~~~~~
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+This module is part of the `OCA/social `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/mail_alias_with_domain/__init__.py b/mail_alias_with_domain/__init__.py
index b66e6bd134..53d63dae48 100644
--- a/mail_alias_with_domain/__init__.py
+++ b/mail_alias_with_domain/__init__.py
@@ -1,3 +1,4 @@
-# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import models
+from .post_init_hook import init_alias_entry
diff --git a/mail_alias_with_domain/__manifest__.py b/mail_alias_with_domain/__manifest__.py
index 8f4a4a07d5..715bc8b151 100644
--- a/mail_alias_with_domain/__manifest__.py
+++ b/mail_alias_with_domain/__manifest__.py
@@ -1,16 +1,17 @@
# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl)
+# Copyright 2025 Therp BV (https://therp.nl)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Mail Alias With Domain",
- "summary": """
- Extend alias fnctionality by giving possibility
- to setup alias with custom domain""",
- "author": "Solvti, Odoo Community Association (OCA)",
+ "summary": "Allow simple mail alias to be combined with a mail domain",
+ "author": "Solvti, Therp BV, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/social",
- "version": "13.0.1.0.0",
+ "version": "16.0.1.0.0",
"license": "AGPL-3",
"application": False,
"installable": True,
+ "post_init_hook": "init_alias_entry",
"depends": ["mail"],
"data": ["views/mail_alias_views.xml"],
}
diff --git a/mail_alias_with_domain/models/__init__.py b/mail_alias_with_domain/models/__init__.py
index 7e891a0d4d..06aaa650a8 100644
--- a/mail_alias_with_domain/models/__init__.py
+++ b/mail_alias_with_domain/models/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import mail_alias
from . import mail_thread
diff --git a/mail_alias_with_domain/models/mail_alias.py b/mail_alias_with_domain/models/mail_alias.py
index aea75ce408..63b48cccb2 100644
--- a/mail_alias_with_domain/models/mail_alias.py
+++ b/mail_alias_with_domain/models/mail_alias.py
@@ -1,61 +1,101 @@
-# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl)
-
-import hashlib
+# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl).
+# Copyright 2025 Therp BV (https://therp.nl).
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models
-def generate_hash(value):
- mail_hash = hashlib.md5(value.encode("utf-8")).hexdigest()
- return mail_hash
-
-
class Alias(models.Model):
_inherit = "mail.alias"
- alias_domain = fields.Char(
- inverse="_inverse_alias_domain",
- store=True,
- )
- alias_display_name = fields.Char()
- alias_name = fields.Char(compute="_compute_alias_name_and_hash", store=True)
- alias_hash = fields.Char(compute="_compute_alias_name_and_hash", store=True)
- check_domain = fields.Boolean(
- help=(
- "Determines whether alias should be processed together with domain.\n"
- "If checked domain will be taken into account during mail processing.\n\n"
- "Alias name is genereted as follow: 'HASH+alias_name'\n\n"
- "*HASH = hash(alias_display_name + alias_domain)"
- ),
+ @api.depends("alias_name")
+ def _compute_alias_domain(self):
+ alias_with_domain = self.filtered(
+ lambda r: r.alias_name and "__at__" in r.alias_name
+ )
+ for alias in alias_with_domain:
+ alias.alias_domain = alias.alias_name.split("__at__")[1]
+ alias_without_domain = self - alias_with_domain
+ if alias_without_domain:
+ super(Alias, alias_without_domain)._compute_alias_domain()
+ return None
+
+ alias_entry = fields.Char(
+ help="This will be used to enter an email, complete with domain",
)
- def _inverse_alias_domain(self):
- pass
-
- @api.depends("alias_domain", "alias_display_name", "check_domain", "alias_name")
- def _compute_alias_name_and_hash(self):
- for rec in self:
- if rec.check_domain and rec.alias_display_name and rec.alias_domain:
- alias_hash = generate_hash(rec.alias_display_name + rec.alias_domain)
- rec.alias_hash = alias_hash
- rec.alias_name = alias_hash + "+" + rec.alias_display_name
- elif rec.alias_name or rec.alias_display_name:
- name = rec.alias_display_name or rec.alias_name
- rec.alias_hash = False
- rec.alias_name = rec.alias_display_name = rec._clean_and_make_unique(
- name, alias_ids=rec.ids
- )
- else:
- rec.alias_hash = rec.alias_name = False
+ @api.model
+ def search(self, domain, **kwargs):
+ """If mail alias in context, return this as result."""
+ matching_alias = self.env.context.get("matching_alias", False)
+ if matching_alias:
+ return matching_alias
+ return super().search(domain, **kwargs)
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ self._patch_alias_vals(vals)
+ records = super().create(vals_list)
+ records._synchronize_alias_entry_with_name()
+ return records
def write(self, vals):
- name = vals.get("alias_name") or vals.get("alias_display_name")
- if name and not vals.get("check_domain") and not self.check_domain:
- vals["alias_name"] = vals[
- "alias_display_name"
- ] = self._clean_and_make_unique(name, alias_ids=self.ids)
- if vals.get("check_domain") is False:
- vals["alias_domain"] = (
- self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")
+ self._patch_alias_vals(vals)
+ result = super().write(vals)
+ self._synchronize_alias_entry_with_name()
+ return result
+
+ def _synchronize_alias_entry_with_name(self):
+ """In case alias created/written without alias_entry, complete entry field."""
+ for this in self:
+ if not this.alias_name:
+ alias_entry = False
+ elif "__at__" in this.alias_name:
+ alias_entry = this.alias_name.replace("__at__", "@")
+ else:
+ alias_entry = this.alias_name
+ if this.alias_entry != alias_entry:
+ super(Alias, this).write({"alias_entry": alias_entry})
+ return None
+
+ @api.model
+ def _patch_alias_vals(self, vals):
+ """If vals contains alias_entry, add corresponding alias_name."""
+ alias_entry = vals.get("alias_entry", False)
+ if alias_entry:
+ default_domain = self._get_default_domain()
+ if "@" not in alias_entry:
+ alias_name = alias_entry
+ elif default_domain and default_domain in alias_entry:
+ alias_name = alias_entry.split("@")[0]
+ else:
+ alias_name = alias_entry.replace("@", "__at__")
+ vals["alias_name"] = alias_name
+
+ @api.model
+ def _get_default_domain(self):
+ """get default domain."""
+ ICP = self.env["ir.config_parameter"].sudo()
+ return ICP.get_param("mail.catchall.domain")
+
+ @api.model
+ def get_clean_email(self, email):
+ """Users tend to pollute emails with extra info. get just the email."""
+ # In Odoo 17.0 there is a new method parse_contact_from_email in
+ # odoo/tools/mail.py that we could use for this purpose.
+ if email:
+ # 1. Replace special characters with spaces.
+ cleaned = (
+ email.replace('"', " ")
+ .replace("<", " ")
+ .replace(">", " ")
+ .replace(",", " ")
)
- return super().write(vals)
+ # 2. Split on whitespace
+ parts = cleaned.split()
+ # 3. Find the part with an '@' if any and assume it is the real email.
+ for part in parts:
+ if "@" in part:
+ return part.lower()
+ return False # Else module partner_email_check would raise ValidationError.
diff --git a/mail_alias_with_domain/models/mail_thread.py b/mail_alias_with_domain/models/mail_thread.py
index 9d17795784..1954b577ba 100644
--- a/mail_alias_with_domain/models/mail_thread.py
+++ b/mail_alias_with_domain/models/mail_thread.py
@@ -1,13 +1,8 @@
# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl)
-
-import logging
-
+# Copyright 2025 Therp BV (https://therp.nl)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, models, tools
-from .mail_alias import generate_hash
-
-_logger = logging.getLogger(__name__)
-
class MailThread(models.AbstractModel):
_inherit = "mail.thread"
@@ -16,29 +11,39 @@ class MailThread(models.AbstractModel):
def message_route(
self, message, message_dict, model=None, thread_id=None, custom_values=None
):
- """Prepare message_dict by extending recipients
- with found aliases mails base on alias and domain
- """
- try:
- maching_aliases = self._find_alias_with_domain(message_dict)
- if maching_aliases:
- recipients = (
- f"{message_dict['recipients']},"
- f"{','.join(maching_aliases.mapped('display_name'))}"
+ """Check for a recipient that can be linked to a full domain alias."""
+ if not self.env.context.get("matching_alias", False):
+ matching_alias = self._find_alias_with_domain(message_dict)
+ if matching_alias:
+ # Call super with extra context.
+ return (
+ super()
+ .with_context(matching_alias=matching_alias)
+ .message_route(
+ message,
+ message_dict,
+ model=model,
+ thread_id=thread_id,
+ custom_values=custom_values,
+ )
)
- message_dict["recipients"] = recipients
- except Exception as e:
- _logger.error(f"Unexpected error during processing alias with domain: {e}")
return super().message_route(
- message, message_dict, model, thread_id, custom_values
+ message,
+ message_dict,
+ model=model,
+ thread_id=thread_id,
+ custom_values=custom_values,
)
def _find_alias_with_domain(self, message_dict):
+ """Find all aliasses that match."""
+ Alias = self.env["mail.alias"]
emails = {email for email in (tools.email_split(message_dict["recipients"]))}
- hash_list = list(
- map(generate_hash, [email.replace("@", "") for email in emails])
- )
- match = self.env["mail.alias"].search(
- [("check_domain", "=", True), ("alias_hash", "in", hash_list)]
- )
- return match
+ alias_names = []
+ for email in emails:
+ clean_email = Alias.get_clean_email(email)
+ if not clean_email:
+ continue
+ alias_name = clean_email.replace("@", "__at__")
+ alias_names.append(alias_name)
+ return Alias.search([("alias_name", "in", alias_names)])
diff --git a/mail_alias_with_domain/post_init_hook.py b/mail_alias_with_domain/post_init_hook.py
new file mode 100644
index 0000000000..dbd35d0b6a
--- /dev/null
+++ b/mail_alias_with_domain/post_init_hook.py
@@ -0,0 +1,10 @@
+# Copyright 2025 Therp BV (https://therp.nl)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+
+
+def init_alias_entry(cr, registry):
+ cr.execute(
+ "UPDATE mail_alias"
+ " SET alias_entry = alias_name"
+ " WHERE alias_entry IS NULL AND NOT alias_name IS NULL"
+ )
diff --git a/mail_alias_with_domain/readme/CONTRIBUTORS.rst b/mail_alias_with_domain/readme/CONTRIBUTORS.rst
new file mode 100644
index 0000000000..167a32718c
--- /dev/null
+++ b/mail_alias_with_domain/readme/CONTRIBUTORS.rst
@@ -0,0 +1,7 @@
+* `Solvti sp. z o.o. `_:
+
+ * Jakub Wiselka
+
+* `Therp `_:
+
+ * Ronald Portier (ronald@therp.nl)
diff --git a/mail_alias_with_domain/readme/DESCRIPTION.rst b/mail_alias_with_domain/readme/DESCRIPTION.rst
new file mode 100644
index 0000000000..cc5a4e2ede
--- /dev/null
+++ b/mail_alias_with_domain/readme/DESCRIPTION.rst
@@ -0,0 +1,41 @@
+This module adds possibility to process aliases together with domain.
+
+For example, suppose we have 3 companies in odoo.
+Each company wants to have an alias where customers can send the bills.
+invoice@company1.com
+invoice@company2.com
+invoice@company3.com
+
+In odoo, aliases are unique, and this module extends this functionality in
+such a way that you can have many of the same aliases but with different domains.
+
+Note that when an incoming mail can be linked to an alias with a domain,
+this will be the only alias used. However when an incoming mail can be
+linked to multiple aliasses that have a domain, it is possible to have
+multiple used.
+
+FOR DEVELOPERS
+
+In the default alias system, only the local part of an email address (the part
+before the @) is used to link an incoming email to an alias. This happens in the
+message_route method of the mail.thread model.
+
+Aliasses in standard Odoo store the alias_name field without domain.
+
+To still be able to use a domain name, we need a trick. What we will do is:
+
+* Replace the alias_name in the user interface with an alias_entry field, where a
+ complete email address can be entered.
+
+* If an alias is entered as a complete email address, this will be stored in the
+ alias_name as __at__. For instance alex__at__example.com.
+ alias_name is therefore changed from a writable field to a stored computed field.
+
+* The computation of alias_domain will be enhanced to take full email addresses into
+ account.
+
+* If an incoming mail can be linked to a full email address alias, we will write a
+ context key pointing to this alias. The search method of mail.alias will be overriden
+ to check for this key, and then not search at all, but just return the alias
+ requested.
+
diff --git a/mail_alias_with_domain/readme/USAGE.rst b/mail_alias_with_domain/readme/USAGE.rst
new file mode 100644
index 0000000000..93d8c7603e
--- /dev/null
+++ b/mail_alias_with_domain/readme/USAGE.rst
@@ -0,0 +1,4 @@
+To use this module, you need to:
+
+Got to the mail aliasses and check which aliasses you want to link to a specific
+domain.
diff --git a/mail_alias_with_domain/static/description/index.html b/mail_alias_with_domain/static/description/index.html
new file mode 100644
index 0000000000..62251c8583
--- /dev/null
+++ b/mail_alias_with_domain/static/description/index.html
@@ -0,0 +1,466 @@
+
+
+
+
+
+
+Mail Alias With Domain
+
+
+
+
+
Mail Alias With Domain
+
+
+
+
This module adds possibility to process aliases together with domain.
In odoo, aliases are unique, and this module extends this functionality in
+such a way that you can have many of the same aliases but with different domains.
+
Note that when an incoming mail can be linked to an alias with a domain,
+this will be the only alias used. However when an incoming mail can be
+linked to multiple aliasses that have a domain, it is possible to have
+multiple used.
+
FOR DEVELOPERS
+
In the default alias system, only the local part of an email address (the part
+before the @) is used to link an incoming email to an alias. This happens in the
+message_route method of the mail.thread model.
+
Aliasses in standard Odoo store the alias_name field without domain.
+
To still be able to use a domain name, we need a trick. What we will do is:
+
+
Replace the alias_name in the user interface with an alias_entry field, where a
+complete email address can be entered.
+
If an alias is entered as a complete email address, this will be stored in the
+alias_name as <localpart>__at__<domain>. For instance alex__at__example.com.
+alias_name is therefore changed from a writable field to a stored computed field.
+
The computation of alias_domain will be enhanced to take full email addresses into
+account.
+
If an incoming mail can be linked to a full email address alias, we will write a
+context key pointing to this alias. The search method of mail.alias will be overriden
+to check for this key, and then not search at all, but just return the alias
+requested.
Bugs are tracked on GitHub Issues.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+feedback.
+
Do not contact contributors directly about support or help with technical issues.
OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
This module is part of the OCA/social project on GitHub.
Bugs are tracked on GitHub Issues.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
-feedback.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
-
This module is part of the OCA/social project on GitHub.
+
This module is part of the OCA/social project on GitHub.