Compare commits
No commits in common. '14.0' and '13.0' have entirely different histories.
-
2.DINAR/build-date.txt
-
5.DINAR/config.yaml
-
19.DINAR/image/src/addons.yaml
-
91.gitignore
-
3.isort.cfg
-
12.pre-commit-config.yaml
-
3.pylintrc
-
2.pylintrc-mandatory
-
47.travis.yml
-
23LICENSE
-
19README.md
-
1build-date.txt
-
49email_headers/README.md
-
32email_headers/__init__.py
-
41email_headers/__manifest__.py
-
14email_headers/data/ir_config_parameter_data.xml
-
9email_headers/migrations/12.0.1.2.0/post-migration.py
-
21email_headers/models/__init__.py
-
585email_headers/models/mail.py
-
1email_headers/tests/__init__.py
-
353email_headers/tests/test_email.py
-
22email_headers/views/ir_mail_server_views.xml
-
67mail_multi_website/README.rst
-
43mail_multi_website/__init__.py
-
44mail_multi_website/__manifest__.py
-
13mail_multi_website/doc/changelog.rst
-
73mail_multi_website/doc/index.rst
-
36mail_multi_website/doc/src/index.html
-
1mail_multi_website/doc/src/info.yaml
-
123mail_multi_website/i18n/mail_multi_website.pot
-
BINmail_multi_website/images/main.jpg
-
7mail_multi_website/models/__init__.py
-
20mail_multi_website/models/ir_property.py
-
13mail_multi_website/models/mail_message.py
-
185mail_multi_website/models/mail_template.py
-
34mail_multi_website/models/mail_thread.py
-
50mail_multi_website/models/res_users.py
-
12mail_multi_website/models/website.py
-
BINmail_multi_website/static/description/icon.png
-
134mail_multi_website/static/description/index.html
-
4mail_multi_website/tests/__init__.py
-
63mail_multi_website/tests/test_fetch.py
-
11mail_multi_website/tests/test_mail_model.py
-
145mail_multi_website/tests/test_render.py
-
72mail_multi_website/tests/test_send.py
-
14mail_multi_website/views/website_views.xml
-
2mail_multi_website/wizard/__init__.py
-
22mail_multi_website/wizard/mail_compose_message.py
-
38mail_private/README.rst
-
3mail_private/__init__.py
-
26mail_private/__manifest__.py
-
19mail_private/doc/changelog.rst
-
26mail_private/doc/index.rst
-
27mail_private/full_composer_wizard.xml
-
143mail_private/i18n/es.po
-
143mail_private/i18n/it.po
-
138mail_private/i18n/mail_private.pot
-
143mail_private/i18n/pt.po
-
143mail_private/i18n/pt_BR.po
-
BINmail_private/images/mail_private_image.png
-
67mail_private/models.py
-
BINmail_private/static/description/check_recipients.png
-
BINmail_private/static/description/icon.png
-
118mail_private/static/description/index.html
-
BINmail_private/static/description/result_message.png
-
290mail_private/static/src/js/mail_private.js
-
72mail_private/static/src/js/test_private.js
-
35mail_private/static/src/xml/mail_private.xml
-
22mail_private/template.xml
-
3mail_private/tests/__init__.py
-
28mail_private/tests/test_js.py
-
4oca_dependencies.txt
@ -1 +1 @@ |
|||
new repo readme files |
|||
October 11, 2020 |
@ -1 +1,20 @@ |
|||
# see https://github.com/Tecnativa/doodba#optodoocustomsrcaddonsyaml |
|||
--- |
|||
ENV: |
|||
DEFAULT_REPO_PATTERN: https://github.com/OCA/{}.git |
|||
|
|||
web: |
|||
- "*" |
|||
|
|||
--- |
|||
ENV: |
|||
DEFAULT_REPO_PATTERN: https://github.com/itpp-labs/{}.git |
|||
|
|||
access-addons: |
|||
- "*" |
|||
|
|||
misc-addons: |
|||
- "*" |
|||
|
|||
website-addons: |
|||
- "*" |
@ -0,0 +1,91 @@ |
|||
# Byte-compiled / optimized / DLL files |
|||
__pycache__/ |
|||
*.py[cod] |
|||
*$py.class |
|||
|
|||
# C extensions |
|||
*.so |
|||
|
|||
# Distribution / packaging |
|||
.Python |
|||
env/ |
|||
build/ |
|||
develop-eggs/ |
|||
dist/ |
|||
downloads/ |
|||
eggs/ |
|||
.eggs/ |
|||
lib/ |
|||
lib64/ |
|||
parts/ |
|||
sdist/ |
|||
var/ |
|||
*.egg-info/ |
|||
.installed.cfg |
|||
*.egg |
|||
*.pyc |
|||
*~ |
|||
|
|||
# PyInstaller |
|||
# Usually these files are written by a python script from a template |
|||
# before PyInstaller builds the exe, so as to inject date/other infos into it. |
|||
*.manifest |
|||
*.spec |
|||
|
|||
# Installer logs |
|||
pip-log.txt |
|||
pip-delete-this-directory.txt |
|||
|
|||
# Unit test / coverage reports |
|||
htmlcov/ |
|||
.tox/ |
|||
.coverage |
|||
.coverage.* |
|||
.cache |
|||
nosetests.xml |
|||
coverage.xml |
|||
*,cover |
|||
.hypothesis/ |
|||
|
|||
# Translations |
|||
*.mo |
|||
*.pot |
|||
|
|||
# Django stuff: |
|||
*.log |
|||
local_settings.py |
|||
|
|||
# Flask stuff: |
|||
instance/ |
|||
.webassets-cache |
|||
|
|||
# Scrapy stuff: |
|||
.scrapy |
|||
|
|||
# Sphinx documentation |
|||
docs/_build/ |
|||
|
|||
# PyBuilder |
|||
target/ |
|||
|
|||
# IPython Notebook |
|||
.ipynb_checkpoints |
|||
|
|||
# pyenv |
|||
.python-version |
|||
|
|||
# celery beat schedule file |
|||
celerybeat-schedule |
|||
|
|||
# dotenv |
|||
.env |
|||
|
|||
# virtualenv |
|||
venv/ |
|||
ENV/ |
|||
|
|||
# Spyder project settings |
|||
.spyderproject |
|||
|
|||
# Rope project settings |
|||
.ropeproject |
@ -0,0 +1,47 @@ |
|||
language: python |
|||
|
|||
python: |
|||
- "3.6" |
|||
|
|||
#dist: trusty |
|||
sudo: false |
|||
cache: pip |
|||
|
|||
addons: |
|||
# odoo 12.0+ uses command "INSERT ... ON CONFLICT ...", which is available in 9.5+ only |
|||
postgresql: "9.5" |
|||
apt: |
|||
packages: |
|||
- expect-dev # provides unbuffer utility |
|||
- python-lxml # because pip installation is slow |
|||
|
|||
env: |
|||
global: |
|||
- VERSION="13.0" TESTS="0" LINT_CHECK="0" UNIT_TEST="0" |
|||
- PYLINT_ODOO_JSLINTRC="/home/travis/maintainer-quality-tools/travis/cfg/.jslintrc" |
|||
|
|||
matrix: |
|||
- LINT_CHECK="1" |
|||
- CHECK_TAGS="1" |
|||
- TESTS="1" ODOO_REPO="odoo/odoo" MAKEPOT="1" |
|||
- TESTS="1" ODOO_REPO="OCA/OCB" |
|||
|
|||
install: |
|||
- pip install anybox.testing.openerp |
|||
- git clone https://github.com/it-projects-llc/maintainer-quality-tools.git |
|||
${HOME}/maintainer-quality-tools |
|||
- export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} |
|||
- travis_install_nightly |
|||
|
|||
script: |
|||
- travis_run_tests |
|||
|
|||
after_success: |
|||
- travis_after_tests_success |
|||
|
|||
notifications: |
|||
email: false |
|||
webhooks: |
|||
on_failure: change |
|||
urls: |
|||
- "https://ci.it-projects.info/travis/on_failure/change" |
@ -0,0 +1,23 @@ |
|||
The MIT License (MIT) |
|||
|
|||
Copyright 2020 IT-Projects Labs |
|||
Copyright 2015-2020 IT-Projects LLC |
|||
Copyright 2014-2015 Ivan Yelizariev |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in |
|||
all copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|||
THE SOFTWARE. |
@ -0,0 +1 @@ |
|||
April 3, 2020 |
@ -0,0 +1,49 @@ |
|||
# Robust Mails |
|||
|
|||
This module is used to improve email deliverability and make sure that replies find |
|||
their way to the correct thread in Odoo. |
|||
|
|||
Options: |
|||
|
|||
- Force the `From` and `Reply-To` addresses of outgoing email |
|||
- Generate a thread-specific `Reply-To` address for outgoing emails so that losing the |
|||
headers used to identify the correct thread won't be a problem any more. |
|||
|
|||
## Gotcha |
|||
|
|||
To make the automatic bounce message work when using thread-specific `Reply-To` |
|||
addresses, you should define the actual catchall alias in a system parameter called |
|||
`mail.catchall.alias.custom` and change the `mail.catchall.alias` to something |
|||
completely random that will never be used, or alternatively remove it. |
|||
|
|||
The reason is this: when Odoo is looking for a route for an incoming email that has lost |
|||
its headers, it won't check whether the email was sent to `catchall@whatever.com` but |
|||
instead it will see if the local part of that address contains the word `catchall`. And |
|||
this isn't a good thing when the address is something like |
|||
`catchall+123abc@whatever.com`. That's why we had to skip the default catchall |
|||
evaluation and redo it in a later phase. |
|||
|
|||
## Database-specific Settings |
|||
|
|||
| Setting | Purpose | Default value | |
|||
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------- | |
|||
| email_headers.strip_mail_message_ids | Office 365 emails may add whitespaces before the Message-Id's. This feature removes them. | "True" | |
|||
| email_headers.prioritize_replyto_over_headers | When "True", Odoo will prioritize the (unique) Reply-To address of an incoming email and only then look at the `References` and `In-Reply-To` headers. | "True" | |
|||
| mail.catchall.alias | The default catchall alias. See "Gotcha" for more information. | "catchall" | |
|||
| mail.catchall.alias.custom | The new catchall alias setting. See "Gotcha" for more information. Will be set automatically upon module installation. | mail.catchall.alias value | |
|||
|
|||
## Debugging |
|||
|
|||
### Decode and decrypt a message id |
|||
|
|||
```python |
|||
from odoo.addons.email_headers.models.mail import decode_msg_id |
|||
decode_msg_id(<encrypted and base32/64 encoded message database id>, self.env) |
|||
``` |
|||
|
|||
### Encrypt and encode a message id |
|||
|
|||
```python |
|||
from odoo.addons.email_headers.models.mail import encode_msg_id |
|||
encode_msg_id(<message database id>, self.env) |
|||
``` |
@ -0,0 +1,32 @@ |
|||
############################################################################## |
|||
# |
|||
# Author: Avoin.Systems |
|||
# Copyright 2017 Avoin.Systems |
|||
# |
|||
# This program is free software: you can redistribute it and/or modify |
|||
# it under the terms of the GNU Affero General Public License as |
|||
# published by the Free Software Foundation, either version 3 of the |
|||
# License, or (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
# noinspection PyUnresolvedReferences |
|||
from . import models |
|||
from odoo import SUPERUSER_ID, api |
|||
|
|||
|
|||
def set_catchall_alias(cr, registry): |
|||
env = api.Environment(cr, SUPERUSER_ID, {}) |
|||
icp = env["ir.config_parameter"] |
|||
custom_alias = icp.get_param("mail.catchall.alias.custom") |
|||
if not custom_alias: |
|||
original_alias = icp.get_param("mail.catchall.alias", "catchall") |
|||
icp.set_param("mail.catchall.alias.custom", original_alias) |
|||
icp.set_param("mail.catchall.alias", "Use mail.catchall.alias.custom") |
@ -0,0 +1,41 @@ |
|||
############################################################################## |
|||
# |
|||
# Author: Avoin.Systems |
|||
# Copyright 2017 Avoin.Systems |
|||
# |
|||
# This program is free software: you can redistribute it and/or modify |
|||
# it under the terms of the GNU Affero General Public License as |
|||
# published by the Free Software Foundation, either version 3 of the |
|||
# License, or (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
# noinspection PyStatementEffect |
|||
{ |
|||
"name": "Robust Mails", |
|||
"version": "13.0.1.2.0", |
|||
"license": "AGPL-3", |
|||
"summary": """ |
|||
Adds fields on outgoing email server that allows you to better control the |
|||
outgoing email headers and Reply-To addresses. |
|||
""", |
|||
"data": ["data/ir_config_parameter_data.xml", "views/ir_mail_server_views.xml"], |
|||
"author": "Avoin.Systems", |
|||
"website": "https://avoin.systems", |
|||
"category": "Email", |
|||
"depends": ["mail"], |
|||
"external_dependencies": { |
|||
"python": ["Crypto.Cipher.AES"], # pip3 install pycryptodome |
|||
"bin": [], |
|||
}, |
|||
"installable": True, |
|||
"post_init_hook": "set_catchall_alias", |
|||
} |
@ -0,0 +1,14 @@ |
|||
<?xml version="1.0" encoding="utf-8" ?> |
|||
<odoo noupdate="1"> |
|||
<record id="prioritize_replyto_over_headers" model="ir.config_parameter"> |
|||
<field name="key">email_headers.prioritize_replyto_over_headers</field> |
|||
<field name="value">True</field> |
|||
</record> |
|||
<!-- The original field was called just strip_message_ids, but since |
|||
it's manually set in some production databases, it's risky to |
|||
define here. It might break a database upgrade. --> |
|||
<record id="strip_mail_message_ids" model="ir.config_parameter"> |
|||
<field name="key">email_headers.strip_mail_message_ids</field> |
|||
<field name="value">True</field> |
|||
</record> |
|||
</odoo> |
@ -0,0 +1,9 @@ |
|||
def migrate(cr, version): |
|||
if not version: |
|||
return |
|||
|
|||
cr.execute( |
|||
"UPDATE ir_mail_server " |
|||
"SET reply_to_method = 'alias' " |
|||
"WHERE reply_to_alias IS TRUE" |
|||
) |
@ -0,0 +1,21 @@ |
|||
############################################################################## |
|||
# |
|||
# Author: Avoin.Systems |
|||
# Copyright 2018 Avoin.Systems |
|||
# |
|||
# This program is free software: you can redistribute it and/or modify |
|||
# it under the terms of the GNU Affero General Public License as |
|||
# published by the Free Software Foundation, either version 3 of the |
|||
# License, or (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
# noinspection PyUnresolvedReferences |
|||
from . import mail |
@ -0,0 +1,585 @@ |
|||
############################################################################## |
|||
# |
|||
# Author: Avoin.Systems |
|||
# Copyright 2018 Avoin.Systems |
|||
# |
|||
# This program is free software: you can redistribute it and/or modify |
|||
# it under the terms of the GNU Affero General Public License as |
|||
# published by the Free Software Foundation, either version 3 of the |
|||
# License, or (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
import base64 |
|||
import binascii |
|||
import logging |
|||
import random |
|||
import re |
|||
import string |
|||
from email.message import Message |
|||
from email.utils import formataddr, parseaddr |
|||
|
|||
from Crypto.Cipher import AES |
|||
|
|||
from odoo import api, fields, models, tools |
|||
from odoo.tools import frozendict |
|||
|
|||
from odoo.addons.base.models.ir_mail_server import encode_rfc2822_address_header |
|||
|
|||
_logger = logging.getLogger(__name__) |
|||
|
|||
|
|||
MESSAGE_PREFIX = "msg-" |
|||
|
|||
|
|||
def random_string(length): |
|||
return "".join( |
|||
random.choice(string.ascii_lowercase + string.digits) for _ in range(length) |
|||
) |
|||
|
|||
|
|||
def get_key(env): |
|||
return env["ir.config_parameter"].get_param("database.secret", "noneedtobestrong")[ |
|||
:16 |
|||
] |
|||
|
|||
|
|||
def get_cipher(env): |
|||
return AES.new( |
|||
get_key(env).encode("utf-8"), mode=AES.MODE_CBC, iv=b"veryverysecret81" |
|||
) |
|||
|
|||
|
|||
def encode_msg_id(msg_id, env): |
|||
id_padded = "%016d" % msg_id |
|||
encrypted = get_cipher(env).encrypt(id_padded.encode("utf-8")) |
|||
return base64.b32encode(encrypted).decode("utf-8") |
|||
|
|||
|
|||
# Remove in Odoo 14 |
|||
def encode_msg_id_legacy(msg_id, env): |
|||
id_padded = "%016d" % msg_id |
|||
encrypted = get_cipher(env).encrypt(id_padded.encode("utf-8")) |
|||
return base64.urlsafe_b64encode(encrypted).decode("utf-8") |
|||
|
|||
|
|||
def decode_msg_id(encoded_encrypted_id, env): |
|||
|
|||
try: |
|||
# Some email clients don't respect the original Reply-To address case |
|||
# and might make them lowercase. Make the encoded ID uppercase. |
|||
encrypted = base64.b32decode(encoded_encrypted_id.encode("utf-8").upper()) |
|||
except binascii.Error: |
|||
# Fall back to base64, which was used by the previous versions. |
|||
# This can be removed in Odoo 14. |
|||
try: |
|||
encrypted = base64.urlsafe_b64decode(encoded_encrypted_id.encode("utf-8")) |
|||
except binascii.Error: |
|||
_logger.error( |
|||
"Unable to decode the message ID. The input value " |
|||
"is invalid and cannot be decoded. " |
|||
"Encoded value: {}".format(encoded_encrypted_id) |
|||
) |
|||
raise |
|||
|
|||
try: |
|||
id_str = get_cipher(env).decrypt(encrypted).decode("utf-8") |
|||
except UnicodeDecodeError: |
|||
_logger.error( |
|||
"Unable to decrypt the message ID. The input value " |
|||
"probably wasn't encrypted with the same key. Encoded " |
|||
"value: {}".format(encoded_encrypted_id) |
|||
) |
|||
raise |
|||
|
|||
return int(id_str) |
|||
|
|||
|
|||
class MailServer(models.Model): |
|||
_inherit = "ir.mail_server" |
|||
|
|||
reply_to_method = fields.Selection( |
|||
[("default", "Odoo Default"), ("alias", "Alias"), ("msg_id", "Message ID")], |
|||
"Reply-To Method", |
|||
default="default", |
|||
help="Odoo Default: Don't add any unique identifiers into the\n" |
|||
"Reply-To address.\n" |
|||
"\n" |
|||
"Alias: Find or generate an email alias for the Reply-To field of\n " |
|||
"every outgoing message so the responses will be automatically \n" |
|||
"routed to the correct thread even if the email client (Yes, \n" |
|||
"I'm looking at you, Microsoft Outlook) decides to drop the \n" |
|||
"References, In-Reply-To and Message-ID fields.\n\n" |
|||
"The alias will then be used to generate a RFC 5233 sub-address\n" |
|||
"using the Force From Address field as a base, eg.\n" |
|||
"odoo@mycompany.fi would become odoo+adf9bacd98732@mycompany.fi\n" |
|||
"\n" |
|||
"Note that this method has a flaw: if the headers have dropped\n" |
|||
"and Odoo can't connect the reply to any message in the thread,\n" |
|||
"it will automatically connect it to the first message in the \n" |
|||
"thread which often is an internal note and the reply will also\n" |
|||
"be marked as an internal note even when it should be a comment." |
|||
"\n\n" |
|||
"Message ID: Include a prefix and the message ID in encrypted\n" |
|||
"and base32 encoded format in the Reply-To\n" |
|||
"address to that Odoo will be able to directly connect the\n" |
|||
"reply to the original message. Note that in this mode the\n" |
|||
"Reply-To address has a priority over References and\n" |
|||
"In-Reply-To headers.", |
|||
) |
|||
|
|||
force_email_reply_to = fields.Char("Force Reply-To Address",) |
|||
|
|||
force_email_reply_to_name = fields.Char("Force Reply-To Name",) |
|||
|
|||
force_email_reply_to_domain = fields.Char("Force Reply-To Domain",) |
|||
|
|||
force_email_from = fields.Char("Force From Address",) |
|||
|
|||
force_email_sender = fields.Char("Force Sender Address",) |
|||
|
|||
prioritize_reply_to_over_msgid = fields.Boolean( |
|||
"Prioritize Reply-To Over Email Headers", |
|||
default=True, |
|||
help="If this field is selected, the unique Reply-To address " |
|||
"generated by the Message ID method will be prioritized " |
|||
"over the email headers (default Odoo behavior) in incoming " |
|||
"emails. This is recommended when the Reply-To method is set to " |
|||
"Message ID.", |
|||
) |
|||
|
|||
headers_example = fields.Text( |
|||
"Example Headers", compute="_compute_headers_example", store=False, |
|||
) |
|||
|
|||
# TODO Implement field input validators |
|||
def _get_reply_to_address(self, alias, original_from_name): |
|||
self.ensure_one() |
|||
|
|||
force_email_from = encode_rfc2822_address_header(self.force_email_from) |
|||
|
|||
# Split the From address |
|||
from_address = force_email_from.split("@") |
|||
|
|||
reply_to_addr = "{alias}@{domain}".format( |
|||
alias=alias if alias else from_address[0], |
|||
domain=self.force_email_reply_to_domain or from_address[1], |
|||
) |
|||
|
|||
if self.force_email_reply_to_name: |
|||
reply_to = formataddr((self.force_email_reply_to_name, reply_to_addr)) |
|||
elif original_from_name: |
|||
reply_to = formataddr((original_from_name, reply_to_addr)) |
|||
else: |
|||
reply_to = reply_to_addr |
|||
|
|||
return encode_rfc2822_address_header(reply_to) |
|||
|
|||
@api.depends( |
|||
"force_email_sender", |
|||
"force_email_reply_to", |
|||
"force_email_reply_to_domain", |
|||
"force_email_from", |
|||
"force_email_reply_to_name", |
|||
"reply_to_method", |
|||
) |
|||
def _compute_headers_example(self): |
|||
for server in self: |
|||
example = [] |
|||
if server.force_email_sender: |
|||
example.append("Sender: {}".format(server.force_email_sender)) |
|||
|
|||
if server.force_email_reply_to: |
|||
example.append("Reply-To: {}".format(server.force_email_reply_to)) |
|||
elif server.force_email_from and server.reply_to_method != "default": |
|||
reply_to_pair = server.force_email_from.split("@") |
|||
|
|||
if server.reply_to_method == "alias": |
|||
token = "{}+1d278g1082bca" |
|||
elif server.reply_to_method == "msg_id": |
|||
token = "{}+" + MESSAGE_PREFIX + "p2IxKkfEKugl16juheTT0g==" |
|||
else: |
|||
token = "INVALID" |
|||
_logger.error( |
|||
"Invalid reply_to_method found: " + server.reply_to_method |
|||
) |
|||
|
|||
# noinspection PyProtectedMember |
|||
reply_to = server._get_reply_to_address( |
|||
token.format(reply_to_pair[0]), "Original From Person" |
|||
) |
|||
example.append("Reply-To: {}".format(reply_to)) |
|||
else: |
|||
example.append("Reply-To: Odoo default") |
|||
|
|||
if server.force_email_from: |
|||
example.append( |
|||
"From: {}".format( |
|||
formataddr(("Original From Person", server.force_email_from)) |
|||
) |
|||
) |
|||
else: |
|||
example.append("From: Odoo default") |
|||
|
|||
server.headers_example = "\n".join(example) |
|||
|
|||
@api.model |
|||
def send_email( |
|||
self, |
|||
message, |
|||
mail_server_id=None, |
|||
smtp_server=None, |
|||
smtp_port=None, |
|||
smtp_user=None, |
|||
smtp_password=None, |
|||
smtp_encryption=None, |
|||
smtp_debug=False, |
|||
smtp_session=None, |
|||
): |
|||
|
|||
# Get SMTP Server Details from Mail Server |
|||
mail_server = None |
|||
if mail_server_id: |
|||
mail_server = self.sudo().browse(mail_server_id) |
|||
elif not smtp_server: |
|||
mail_server = self.sudo().search([], order="sequence", limit=1) |
|||
|
|||
# Note that Odoo already has the ability to use a fixed From address |
|||
# by settings "email_from" in the Odoo settings. This is however a |
|||
# secondary option and here email_from always overrides that. |
|||
if mail_server.force_email_from: |
|||
original_from_name = parseaddr(message["From"])[0] |
|||
force_email_from = encode_rfc2822_address_header( |
|||
mail_server.force_email_from |
|||
) |
|||
del message["From"] |
|||
message["From"] = formataddr((original_from_name, force_email_from)) |
|||
|
|||
if mail_server.reply_to_method == "alias": |
|||
# Find or create an email alias |
|||
alias = self.find_or_create_alias(force_email_from.split("@")) |
|||
# noinspection PyProtectedMember |
|||
reply_to = mail_server._get_reply_to_address(alias, original_from_name,) |
|||
del message["Reply-To"] |
|||
message["Reply-To"] = reply_to |
|||
|
|||
elif mail_server.reply_to_method == "msg_id": |
|||
odoo_msg_id = message.get("Message-Id") |
|||
if odoo_msg_id: |
|||
# The message_id isn't unique. Prefer the one that has a |
|||
# model set and only pick the first record. Odoo does |
|||
# almost the same thing in mail.thread.message_route(). |
|||
odoo_msg = ( |
|||
self.sudo() |
|||
.env["mail.message"] |
|||
.search( |
|||
[("message_id", "=", odoo_msg_id)], order="model", limit=1 |
|||
) |
|||
) |
|||
|
|||
encrypted_id = encode_msg_id(odoo_msg.id, self.env) |
|||
# noinspection PyProtectedMember |
|||
reply_to = mail_server._get_reply_to_address( |
|||
"{}+{}{}".format( |
|||
force_email_from.split("@")[0], MESSAGE_PREFIX, encrypted_id |
|||
), |
|||
original_from_name, |
|||
) |
|||
|
|||
_logger.info( |
|||
'Generated a new reply-to address "{}" for ' |
|||
'Message-Id "{}".'.format(reply_to, odoo_msg_id) |
|||
) |
|||
|
|||
del message["Reply-To"] |
|||
message["Reply-To"] = reply_to |
|||
else: |
|||
_logger.warning( |
|||
"Couldn't get Message-Id from the message {}. The " |
|||
"reply might not find its way to the correct thread.".format( |
|||
message.as_string() |
|||
) |
|||
) |
|||
|
|||
if mail_server.force_email_reply_to: |
|||
del message["Reply-To"] |
|||
message["Reply-To"] = encode_rfc2822_address_header( |
|||
mail_server.force_email_reply_to |
|||
) |
|||
|
|||
if mail_server.force_email_sender: |
|||
del message["Sender"] |
|||
message["Sender"] = encode_rfc2822_address_header( |
|||
mail_server.force_email_sender |
|||
) |
|||
|
|||
return super(MailServer, self).send_email( |
|||
message, |
|||
mail_server_id, |
|||
smtp_server, |
|||
smtp_port, |
|||
smtp_user, |
|||
smtp_password, |
|||
smtp_encryption, |
|||
smtp_debug, |
|||
smtp_session, |
|||
) |
|||
|
|||
def find_or_create_alias(self, from_address): |
|||
|
|||
record_id, record_model_name = self.resolve_record() |
|||
if not record_id or not record_model_name: |
|||
# Can't create an alias if we don't know the related record |
|||
return False |
|||
|
|||
if record_model_name not in self.env: |
|||
_logger.error( |
|||
"Unable to find or create an alias for outgoing " |
|||
"email: invalid_model name {}.".format(record_model_name) |
|||
) |
|||
return False |
|||
|
|||
# Find an alias |
|||
alias_model_id = ( |
|||
self.env["ir.model"].search([("model", "=", record_model_name)]).id |
|||
) |
|||
# noinspection PyPep8Naming |
|||
Alias = self.env["mail.alias"] |
|||
existing_aliases = Alias.search( |
|||
[ |
|||
("alias_model_id", "=", alias_model_id), |
|||
( |
|||
"alias_name", |
|||
"like", |
|||
"{from_address}+".format(from_address=from_address[0]), |
|||
), |
|||
("alias_force_thread_id", "=", record_id), |
|||
("alias_contact", "=", "everyone"), # TODO: check from record |
|||
] |
|||
) |
|||
|
|||
if existing_aliases: |
|||
return existing_aliases[0].alias_name |
|||
|
|||
# Create a new alias |
|||
alias = Alias.create( |
|||
{ |
|||
"alias_model_id": alias_model_id, |
|||
"alias_name": "{from_address}+{random_string}".format( |
|||
from_address=from_address[0], random_string=random_string(8) |
|||
), |
|||
"alias_force_thread_id": record_id, |
|||
"alias_contact": "everyone", |
|||
} |
|||
) |
|||
|
|||
return alias.alias_name |
|||
|
|||
def resolve_record(self): |
|||
ctx = self.env.context |
|||
# Don't ever use active_id or active_model from the context here. |
|||
# It might not be the one that you expect. Go ahead and try, open |
|||
# a sales order, go to the related purchase order and send the PO. |
|||
record_id = ctx.get("default_res_id") |
|||
record_model_name = ctx.get("default_model") |
|||
|
|||
# If incoming_routes isn't enough, we can use ctx['incoming_to'] to |
|||
# find a alias directly without active_id and active_model_name. |
|||
routes = ctx.get("incoming_routes", []) |
|||
if (not record_id or not record_model_name) and routes and len(routes) > 0: |
|||
route = routes[0] |
|||
record_model_name = route[0] |
|||
record_id = route[1] |
|||
|
|||
return record_id, record_model_name |
|||
|
|||
@api.model |
|||
def encrypt_message_id(self, message_id): |
|||
""" |
|||
A helper encryption method for debugging mail delivery issues. |
|||
:param message_id: The id of the `mail.message` |
|||
:return: The id of the `mail.message` encrypted and base64 encoded |
|||
""" |
|||
return encode_msg_id(message_id, self.env) |
|||
|
|||
@api.model |
|||
def decrypt_message_id(self, encrypted_id): |
|||
""" |
|||
A helper decryption method for debugging mail delivery issues. |
|||
:param encrypted_id: The encrypted and base64 encoded id of |
|||
the `mail.message` to be decrypted |
|||
:return: The id of the `mail.message` |
|||
""" |
|||
return decode_msg_id(encrypted_id, self.env) |
|||
|
|||
|
|||
class MailThread(models.AbstractModel): |
|||
|
|||
_inherit = "mail.thread" |
|||
|
|||
""" |
|||
The process for incoming emails goes something like this: |
|||
1. message_process (processing the incoming message) |
|||
2. message_parse (parsing the email message) |
|||
3. message_route (decides how to route the email) |
|||
4. message_route_process (executes the route) |
|||
5. message_post (posts the message to a thread) |
|||
""" |
|||
|
|||
@api.model |
|||
def message_parse(self, message, save_original=False): |
|||
email_to = tools.decode_message_header(message, "To") |
|||
email_to_localpart = (tools.email_split(email_to) or [""])[0].split("@", 1)[0] |
|||
|
|||
config_params = self.env["ir.config_parameter"].sudo() |
|||
|
|||
# Check if the To part contains the prefix and a base32/64 encoded string |
|||
# Remove the "24," part when migrating to Odoo 14. |
|||
prefix_in_to = email_to_localpart and re.search( |
|||
r".*" + MESSAGE_PREFIX + "(?P<odoo_id>.{24,32}$)", email_to_localpart |
|||
) |
|||
|
|||
prioritize_replyto_over_headers = config_params.get_param( |
|||
"email_headers.prioritize_replyto_over_headers", "True" |
|||
) |
|||
prioritize_replyto_over_headers = ( |
|||
True if prioritize_replyto_over_headers != "False" else False |
|||
) |
|||
|
|||
# If the msg prefix part is found in the To part, find the parent |
|||
# message and inject the Message-Id to the In-Reply-To part and |
|||
# remove References because it by default takes priority over |
|||
# In-Reply-To. We want the unique Reply-To address have the priority. |
|||
if prefix_in_to and prioritize_replyto_over_headers: |
|||
message_id_encrypted = prefix_in_to.group("odoo_id") |
|||
try: |
|||
message_id = decode_msg_id(message_id_encrypted, self.env) |
|||
parent_id = self.env["mail.message"].browse(message_id) |
|||
if parent_id: |
|||
# See unit test test_reply_to_method_msg_id_priority |
|||
del message["References"] |
|||
del message["In-Reply-To"] |
|||
message["In-Reply-To"] = parent_id.message_id |
|||
else: |
|||
_logger.warning( |
|||
"Received an invalid mail.message database id in incoming " |
|||
"email sent to {}. The email type (comment, note) might " |
|||
"be wrong.".format(email_to) |
|||
) |
|||
except UnicodeDecodeError: |
|||
_logger.warning( |
|||
"Unique Reply-To address of an incoming email couldn't be " |
|||
"decrypted. Falling back to default Odoo behavior." |
|||
) |
|||
|
|||
res = super(MailThread, self).message_parse(message, save_original) |
|||
|
|||
strip_message_id = config_params.get_param( |
|||
"email_headers.strip_mail_message_ids", "True" |
|||
) |
|||
strip_message_id = True if strip_message_id != "False" else False |
|||
|
|||
if not strip_message_id == "True": |
|||
return res |
|||
|
|||
# When Odoo compares message_id to the one stored in the database when determining |
|||
# whether or not the incoming message is a reply to another one, the message_id search |
|||
# parameter is stripped before the search. But Odoo does not do anything of the sort when |
|||
# a message is created, meaning if some email software (for example Outlook, |
|||
# for no particular reason) includes anything strippable at the start of the Message-Id, |
|||
# any replies to that message in the future will not find their way correctly, as the |
|||
# search yields nothing. |
|||
# |
|||
# Example of what happened before. The first one is the original Message-Id, and thus also |
|||
# the ID that gets stored on the mail.message as the `message_id` |
|||
# '\r\n <AM6PR05MB4933DE6BCAD68A037185EBCFFBAF0@AM6PR05MB4933.eurprd05.prod.outlook.com>' |
|||
# But when trying to find this message, Odoo takes the above message_id and strips it, |
|||
# which results in: |
|||
# '<AM6PR05MB4933DE6BCAD68A037185EBCFFBAF0@AM6PR05MB4933.eurprd05.prod.outlook.com>' |
|||
# And then the search is done for an exact match, which will fail. |
|||
# |
|||
# Odoo doesn't, so we must strip the message_ids before they are stored in the database |
|||
mail_message_id = res.get("message_id", "") |
|||
if mail_message_id: |
|||
mail_message_id = mail_message_id.strip() |
|||
res["message_id"] = mail_message_id |
|||
return res |
|||
|
|||
@api.model |
|||
def message_route_process(self, message, message_dict, routes): |
|||
ctx = self.env.context.copy() |
|||
ctx["incoming_routes"] = routes |
|||
ctx["incoming_to"] = message_dict.get("to") |
|||
self.env.context = frozendict(ctx) |
|||
return super(MailThread, self).message_route_process( |
|||
message, message_dict, routes |
|||
) |
|||
|
|||
@api.model |
|||
def message_route( |
|||
self, message, message_dict, model=None, thread_id=None, custom_values=None |
|||
): |
|||
|
|||
# NOTE! If you're going to backport this module to Odoo 11 or Odoo 10, |
|||
# you will have to create the mail_bounce_catchall email template |
|||
# because it was introduced only in Odoo 12. |
|||
|
|||
if not isinstance(message, Message): |
|||
raise TypeError("message must be an " "email.message.Message at this point") |
|||
|
|||
try: |
|||
route = super(MailThread, self).message_route( |
|||
message, message_dict, model, thread_id, custom_values |
|||
) |
|||
except ValueError: |
|||
|
|||
# If the headers that connect the incoming message to a thread in |
|||
# Odoo have disappeared at some point and the message was sent to |
|||
# the catchall address (with a sub-addressing suffix), we will |
|||
# skip the default catchall check and perform it here for |
|||
# mail.catchall.alias.custom. We do this because the alias check |
|||
# if done AFTER the catchall check by default and it may cause |
|||
# Odoo to send a bounce message to the sender who sent the email to |
|||
# the correct thread-specific address. |
|||
|
|||
catchall_alias = ( |
|||
self.env["ir.config_parameter"] |
|||
.sudo() |
|||
.get_param("mail.catchall.alias.custom") |
|||
) |
|||
|
|||
email_to = tools.decode_message_header(message, "To") |
|||
email_to_localpart = ( |
|||
(tools.email_split(email_to) or [""])[0].split("@", 1)[0].lower() |
|||
) |
|||
|
|||
message_id = message.get("Message-Id") |
|||
email_from = tools.decode_message_header(message, "From") |
|||
|
|||
# check it does not directly contact catchall |
|||
if catchall_alias and catchall_alias in email_to_localpart: |
|||
_logger.info( |
|||
"Routing mail from %s to %s with Message-Id %s: " |
|||
"direct write to catchall, bounce", |
|||
email_from, |
|||
email_to, |
|||
message_id, |
|||
) |
|||
body = self.env.ref("mail.mail_bounce_catchall").render( |
|||
{"message": message}, engine="ir.qweb" |
|||
) |
|||
self._routing_create_bounce_email( |
|||
email_from, body, message, reply_to=self.env.user.company_id.email |
|||
) |
|||
return [] |
|||
else: |
|||
raise |
|||
|
|||
return route |
@ -0,0 +1 @@ |
|||
from . import test_email |
@ -0,0 +1,353 @@ |
|||
# No need to translate tests |
|||
# pylint: disable=translation-required |
|||
from email.message import EmailMessage |
|||
|
|||
import mock |
|||
|
|||
import odoo |
|||
from odoo import SUPERUSER_ID |
|||
from odoo.tests import TransactionCase |
|||
|
|||
from odoo.addons.base.models.ir_mail_server import IrMailServer |
|||
|
|||
from ..models.mail import ( |
|||
MESSAGE_PREFIX, |
|||
encode_msg_id, |
|||
encode_msg_id_legacy, |
|||
random_string, |
|||
) |
|||
|
|||
|
|||
@odoo.tests.tagged("post_install", "-at_install") |
|||
class TestEmail(TransactionCase): |
|||
def setUp(self): |
|||
super(TestEmail, self).setUp() |
|||
|
|||
self.partner = self.env["res.partner"].create({"name": "Test Dude"}) |
|||
self.partner2 = self.env["res.partner"].create({"name": "Dudette"}) |
|||
self.demo_user = self.env.ref("base.user_demo") |
|||
self.subtype_comment = self.env.ref("mail.mt_comment") |
|||
self.subtype_note = self.env.ref("mail.mt_note") |
|||
|
|||
self.MailMessage = self.env["mail.message"] |
|||
self.ConfigParam = self.env["ir.config_parameter"] |
|||
|
|||
# Create server configuration |
|||
self.outgoing_server = self.env["ir.mail_server"].create( |
|||
{ |
|||
"name": "Outgoing SMTP Server for Unit Tests", |
|||
"sequence": 1, |
|||
"smtp_host": "localhost", |
|||
"smtp_port": "9999", |
|||
"smtp_encryption": "none", |
|||
"smtp_user": "doesnt", |
|||
"smtp_pass": "exist", |
|||
"reply_to_method": "msg_id", |
|||
"force_email_reply_to_domain": "example.com", |
|||
"force_email_from": "odoo@example.com", |
|||
} |
|||
) |
|||
|
|||
@staticmethod |
|||
def create_email_message(): |
|||
message = EmailMessage() |
|||
message[ |
|||
"Content-Type" |
|||
] = 'multipart/mixed; boundary="===============2590914155756834027=="' |
|||
message["MIME-Version"] = "1.0" |
|||
message[ |
|||
"Message-Id" |
|||
] = "<CAD-eYi=a264_3DcrYSDU5yc_fwYoHonZ3H+{}@mail.gmail.com>".format( |
|||
random_string(6) |
|||
) |
|||
message["Subject"] = "1" |
|||
message["From"] = "Miku Laitinen <miku@avoin.systems>" |
|||
message["Reply-To"] = "YourCompany Eurooppa <sales@avoin.onmicrosoft.com>" |
|||
message["To"] = '"Erik N. French" <ErikNFrench@armyspy.com>' |
|||
message["Date"] = "Mon, 06 May 2019 14:16:38 -0000" |
|||
return message |
|||
|
|||
def test_reply_to_method_msg_id(self): |
|||
|
|||
# Make administrator follow the partner |
|||
self.partner.message_subscribe([self.env.user.partner_id.id]) |
|||
|
|||
# Send a message to the followers of the partner |
|||
thread_msg = self.partner.with_user(self.demo_user).message_post( |
|||
body="dummy message.", message_type="comment", subtype="mail.mt_comment" |
|||
) |
|||
|
|||
# Make sure the message headers look right.. or not |
|||
# mail_msg = thread_msg.notification_ids[0] |
|||
|
|||
# Get the encoded message address |
|||
encoded_msg_id = encode_msg_id(thread_msg.id, self.env) |
|||
|
|||
# Try to read an incoming email |
|||
message = self.create_email_message() |
|||
del message["To"] |
|||
message["To"] = '"Erik N. French" <ErikNFrench+{}{}@armyspy.com>'.format( |
|||
MESSAGE_PREFIX, encoded_msg_id |
|||
) |
|||
|
|||
thread_id = self.env["mail.thread"].message_process( |
|||
model=False, message=message.as_string() |
|||
) |
|||
self.assertEqual( |
|||
thread_msg.res_id, |
|||
thread_id, |
|||
"The incoming email wasn't connected to the correct thread", |
|||
) |
|||
|
|||
# Make sure the message is a comment |
|||
incoming_msg1 = self.MailMessage.search( |
|||
[("message_id", "=", message["Message-Id"])] |
|||
) |
|||
self.assertEqual( |
|||
incoming_msg1.message_type, |
|||
"email", |
|||
"The incoming message was created as a type {} instead of a email.".format( |
|||
incoming_msg1.message_type |
|||
), |
|||
) |
|||
self.assertEqual( |
|||
incoming_msg1.subtype_id, |
|||
self.subtype_comment, |
|||
"The incoming message was created as a subtype {} instead of a comment.".format( |
|||
incoming_msg1.subtype_id |
|||
), |
|||
) |
|||
|
|||
# Try to read another incoming email |
|||
message = self.create_email_message() |
|||
del message["To"] |
|||
message["To"] = '"Erik N. French" <ErikNFrench+{}HURDURLUR@armyspy.com>'.format( |
|||
MESSAGE_PREFIX |
|||
) |
|||
message["In-Reply-To"] = thread_msg.message_id |
|||
|
|||
thread_id = self.env["mail.thread"].message_process( |
|||
model=False, message=message.as_string() |
|||
) |
|||
self.assertEqual( |
|||
thread_msg.res_id, |
|||
thread_id, |
|||
"The incoming email wasn't connected to the correct thread", |
|||
) |
|||
|
|||
# Make sure the message is a comment |
|||
incoming_msg2 = self.MailMessage.search( |
|||
[("message_id", "=", message["Message-Id"])] |
|||
) |
|||
self.assertEqual( |
|||
incoming_msg2.message_type, |
|||
"email", |
|||
"The incoming message was created as a type {} instead of a email.".format( |
|||
incoming_msg2.message_type |
|||
), |
|||
) |
|||
self.assertEqual( |
|||
incoming_msg2.subtype_id, |
|||
self.subtype_comment, |
|||
"The incoming message was created as a subtype {} instead of a comment.".format( |
|||
incoming_msg2.subtype_id |
|||
), |
|||
) |
|||
|
|||
def test_reply_to_method_msg_id_priority(self): |
|||
""" |
|||
In this test we will inject the wrong Message-Id to the incoming |
|||
email messages References-header and see if Odoo will prioritize |
|||
the custom Reply-To address over the References-header. It should. |
|||
:return: |
|||
""" |
|||
|
|||
# Make administrator follow the partner |
|||
self.partner.message_subscribe([self.env.user.partner_id.id]) |
|||
|
|||
# Send a message to the followers of the partner |
|||
thread_msg = self.partner.with_user(self.demo_user).message_post( |
|||
body="dummy message X.", message_type="comment", subtype="mail.mt_comment" |
|||
) |
|||
|
|||
# Get the encoded message address |
|||
encoded_msg_id = encode_msg_id(thread_msg.id, self.env) |
|||
|
|||
# Send another message to the followers of the partner |
|||
thread_msg2 = self.partner2.with_user(self.demo_user).message_post( |
|||
body="dummy message X.", message_type="comment", subtype="mail.mt_comment" |
|||
) |
|||
|
|||
# Try to read an incoming email |
|||
message = self.create_email_message() |
|||
del message["To"] |
|||
del message["References"] |
|||
message["To"] = '"Erik N. French" <ErikNFrench+{}{}@armyspy.com>'.format( |
|||
MESSAGE_PREFIX, encoded_msg_id |
|||
) |
|||
|
|||
# Inject the wrong References |
|||
message["References"] = thread_msg2.message_id |
|||
|
|||
thread_id = self.env["mail.thread"].message_process( |
|||
model=False, message=message.as_string() |
|||
) |
|||
self.assertEqual( |
|||
thread_msg.res_id, |
|||
thread_id, |
|||
"The incoming email wasn't connected to the correct thread", |
|||
) |
|||
|
|||
def test_reply_to_method_msg_id_notification(self): |
|||
|
|||
# Make administrator follow the partner |
|||
self.partner2.message_subscribe([self.env.user.partner_id.id]) |
|||
|
|||
# Send a message to the followers of the partner |
|||
thread_msg = self.partner2.with_user(self.demo_user).message_post( |
|||
body="dummy message 2.", message_type="comment", subtype="mail.mt_note" |
|||
) |
|||
|
|||
# Get the encoded message address |
|||
encoded_msg_id = encode_msg_id(thread_msg.id, self.env) |
|||
|
|||
# Try to read an incoming email |
|||
message = self.create_email_message() |
|||
del message["To"] |
|||
message["To"] = '"Erik N. French" <ErikNFrench+{}{}@armyspy.com>'.format( |
|||
MESSAGE_PREFIX, encoded_msg_id |
|||
) |
|||
|
|||
thread_id = self.env["mail.thread"].message_process( |
|||
model=False, message=message.as_string() |
|||
) |
|||
self.assertEqual( |
|||
thread_msg.res_id, |
|||
thread_id, |
|||
"The incoming email wasn't connected to the correct thread", |
|||
) |
|||
|
|||
# Make sure the message is a note |
|||
incoming_msg1 = self.MailMessage.search( |
|||
[("message_id", "=", message["Message-Id"])] |
|||
) |
|||
self.assertEqual( |
|||
incoming_msg1.message_type, |
|||
"email", |
|||
"The incoming message was created as a type {} instead of a email.".format( |
|||
incoming_msg1.message_type |
|||
), |
|||
) |
|||
self.assertEqual( |
|||
incoming_msg1.subtype_id, |
|||
self.subtype_note, |
|||
"The incoming message was created as a subtype {} instead of a note.".format( |
|||
incoming_msg1.subtype_id |
|||
), |
|||
) |
|||
|
|||
def test_reply_to_method_msg_id_legacy(self): |
|||
# REMOVE this test when porting to Odoo 14 |
|||
|
|||
# Make administrator follow the partner |
|||
self.partner2.message_subscribe([self.env.user.partner_id.id]) |
|||
|
|||
# Send a message to the followers of the partner |
|||
thread_msg = self.partner2.with_user(self.demo_user).message_post( |
|||
body="dummy message 2.", message_type="comment", subtype="mail.mt_note" |
|||
) |
|||
|
|||
# Get the encoded message address |
|||
encoded_msg_id = encode_msg_id_legacy(thread_msg.id, self.env) |
|||
|
|||
# Try to read an incoming email |
|||
message = self.create_email_message() |
|||
del message["To"] |
|||
message["To"] = '"Erik N. French" <ErikNFrench+{}{}@armyspy.com>'.format( |
|||
MESSAGE_PREFIX, encoded_msg_id |
|||
) |
|||
|
|||
thread_id = self.env["mail.thread"].message_process( |
|||
model=False, message=message.as_string() |
|||
) |
|||
self.assertEqual( |
|||
thread_msg.res_id, |
|||
thread_id, |
|||
"The incoming email wasn't connected to the correct thread", |
|||
) |
|||
|
|||
def test_reply_to_method_msg_id_lowercase(self): |
|||
# Make administrator follow the partner |
|||
self.partner2.message_subscribe([self.env.user.partner_id.id]) |
|||
|
|||
# Send a message to the followers of the partner |
|||
thread_msg = self.partner2.with_user(self.demo_user).message_post( |
|||
body="dummy message 2.", message_type="comment", subtype="mail.mt_note" |
|||
) |
|||
|
|||
# Get the encoded message address |
|||
encoded_msg_id = encode_msg_id(thread_msg.id, self.env).lower() |
|||
|
|||
# Try to read an incoming email |
|||
message = self.create_email_message() |
|||
del message["To"] |
|||
message["To"] = '"Erik N. French" <ErikNFrench+{}{}@armyspy.com>'.format( |
|||
MESSAGE_PREFIX, encoded_msg_id |
|||
) |
|||
|
|||
thread_id = self.env["mail.thread"].message_process( |
|||
model=False, message=message.as_string() |
|||
) |
|||
self.assertEqual( |
|||
thread_msg.res_id, |
|||
thread_id, |
|||
"The incoming email wasn't connected to the correct thread", |
|||
) |
|||
|
|||
def test_outgoing_msg_id(self): |
|||
# Make administrator follow the partner |
|||
self.partner2.message_subscribe([SUPERUSER_ID]) |
|||
|
|||
with mock.patch.object(IrMailServer, "send_email") as send_email: |
|||
# Send a message to the followers of the partner |
|||
thread_msg = self.partner2.with_user(self.demo_user).message_post( |
|||
body="dummy message 3.", |
|||
message_type="comment", |
|||
subtype="mail.mt_comment", |
|||
) |
|||
|
|||
# Get the encoded message address |
|||
encoded_msg_id = encode_msg_id(thread_msg.id, self.env) |
|||
|
|||
self.assertTrue( |
|||
send_email.called, |
|||
"IrMailServer.send_email wasn't called when sending outgoing email", |
|||
) |
|||
|
|||
message = send_email.call_args[0][0] |
|||
|
|||
reply_to_address = "{}{}@{}".format( |
|||
MESSAGE_PREFIX, |
|||
encoded_msg_id, |
|||
self.outgoing_server.force_email_reply_to_domain, |
|||
) |
|||
|
|||
# Make sure the subaddress is correct in the Reply-To field |
|||
self.assertIn( |
|||
reply_to_address, |
|||
message["Reply-To"], |
|||
"Reply-To address didn't contain the correct subaddress", |
|||
) |
|||
|
|||
# Make sure the author name is in the Reply-To field |
|||
self.assertIn( |
|||
thread_msg.author_id.name, |
|||
message["Reply-To"], |
|||
"Reply-To address didn't contain the author name", |
|||
) |
|||
|
|||
self.assertIn( |
|||
self.outgoing_server.force_email_from, |
|||
message["From"], |
|||
"From address didn't contain the configure From-address", |
|||
) |
@ -0,0 +1,22 @@ |
|||
<?xml version="1.0" encoding="utf-8" ?> |
|||
<odoo> |
|||
<!-- FORM: ir.mail.server --> |
|||
<record id="ir_mail_server_form_email_headers" model="ir.ui.view"> |
|||
<field name="name">ir.mail_server.form.email.headers</field> |
|||
<field name="model">ir.mail_server</field> |
|||
<field name="inherit_id" ref="base.ir_mail_server_form" /> |
|||
<field name="arch" type="xml"> |
|||
<xpath expr="//group[last()]" position="after"> |
|||
<group name="advanced" string="Smart Headers"> |
|||
<field name="force_email_sender" /> |
|||
<field name="force_email_reply_to" /> |
|||
<field name="force_email_reply_to_name" /> |
|||
<field name="force_email_reply_to_domain" /> |
|||
<field name="force_email_from" /> |
|||
<field name="reply_to_method" /> |
|||
<field name="headers_example" /> |
|||
</group> |
|||
</xpath> |
|||
</field> |
|||
</record> |
|||
</odoo> |
@ -0,0 +1,67 @@ |
|||
.. image:: https://img.shields.io/badge/license-MIT-blue.svg |
|||
:target: https://opensource.org/licenses/MIT |
|||
:alt: License: MIT |
|||
|
|||
===================== |
|||
Multi-Brand Mailing |
|||
===================== |
|||
|
|||
Mail-related stuff for multi-website support |
|||
|
|||
* Makes following field in ``res.users`` website-dependent: |
|||
|
|||
* ``email`` |
|||
* ``signature`` |
|||
|
|||
* Makes following fields in ``mail.template`` website-dependent: |
|||
|
|||
* ``body_html`` |
|||
* ``mail_server_id`` |
|||
* ``report_template`` |
|||
|
|||
* Overrides ``mail.template``'s ``render_template`` method to add ``website`` |
|||
variable. It may cause incompatibility with other modules that redefine that |
|||
method too. |
|||
|
|||
Roadmap |
|||
======= |
|||
|
|||
* ``body_html`` becomes untranslatable after module installation |
|||
|
|||
Credits |
|||
======= |
|||
|
|||
Contributors |
|||
------------ |
|||
* `Ivan Yelizariev <https://it-projects.info/team/yelizariev>`__ |
|||
|
|||
Sponsors |
|||
-------- |
|||
* `e-thos SSII <http://www.e-thos.fr/>`__ |
|||
|
|||
Maintainers |
|||
----------- |
|||
* `IT-Projects LLC <https://it-projects.info>`__ |
|||
|
|||
To get a guaranteed support |
|||
you are kindly requested to purchase the module |
|||
at `odoo apps store <https://apps.odoo.com/apps/modules/13.0/mail_multi_website/>`__. |
|||
|
|||
Thank you for understanding! |
|||
|
|||
`IT-Projects Team <https://www.it-projects.info/team>`__ |
|||
|
|||
Further information |
|||
=================== |
|||
|
|||
Demo: http://runbot.it-projects.info/demo/mail-addons/13.0 |
|||
|
|||
HTML Description: https://apps.odoo.com/apps/modules/13.0/mail_multi_website/ |
|||
|
|||
Usage instructions: `<doc/index.rst>`_ |
|||
|
|||
Changelog: `<doc/changelog.rst>`_ |
|||
|
|||
Notifications on updates: `via Atom <https://github.com/it-projects-llc/mail-addons/commits/13.0/mail_multi_website.atom>`_, `by Email <https://blogtrottr.com/?subscribe=https://github.com/it-projects-llc/mail-addons/commits/13.0/mail_multi_website.atom>`_ |
|||
|
|||
Tested on Odoo 13.0 ca67c83e8d36ececaf97a7579c3ff2529b3e227c |
@ -0,0 +1,43 @@ |
|||
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
from . import models |
|||
from . import wizard |
|||
from .tests import test_mail_model |
|||
|
|||
|
|||
def post_init_hook(cr, registry): |
|||
from odoo import api, SUPERUSER_ID |
|||
|
|||
env = api.Environment(cr, SUPERUSER_ID, {}) |
|||
|
|||
env.cr.execute("ALTER TABLE res_users ADD COLUMN email VARCHAR") |
|||
|
|||
# fill new email column with values from partner |
|||
for user in env["res.users"].with_context(active_test=False).search([]): |
|||
email = user.partner_id.email |
|||
if email: |
|||
user._force_default("email", email) |
|||
|
|||
|
|||
def uninstall_hook(cr, registry): |
|||
from odoo import api, SUPERUSER_ID |
|||
|
|||
env = api.Environment(cr, SUPERUSER_ID, {}) |
|||
|
|||
# remove properties |
|||
field_ids = [ |
|||
env.ref("base.field_res_users__email").id, |
|||
env.ref("base.field_res_users__signature").id, |
|||
env.ref("mail.field_mail_template__body_html").id, |
|||
env.ref("mail.field_mail_template__mail_server_id").id, |
|||
env.ref("mail.field_mail_template__report_template").id, |
|||
] |
|||
env["ir.property"].search([("fields_id", "in", field_ids)]).unlink() |
|||
|
|||
# copy emails from user to partner |
|||
cr.execute("SELECT partner_id,email FROM res_users") |
|||
for partner_id, default_email in cr.fetchall(): |
|||
env["res.partner"].browse(partner_id).email = default_email |
|||
|
|||
# email field is computed (related) and not needed if mail_multi_website is not installed |
|||
env.cr.execute("ALTER TABLE res_users DROP COLUMN email") |
@ -0,0 +1,44 @@ |
|||
# Copyright 2018,2020 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
{ |
|||
"name": """Multi-Brand Mailing""", |
|||
"summary": """Use single Backend to manage several Websites""", |
|||
"category": "Discuss", |
|||
# "live_test_url": "http://apps.it-projects.info/shop/product/website-multi-company?version=11.0", |
|||
"images": ["images/main.jpg"], |
|||
"version": "13.0.2.0.0", |
|||
"application": False, |
|||
"author": "IT-Projects LLC, Ivan Yelizariev", |
|||
"support": "apps@itpp.dev", |
|||
"website": "https://twitter.com/OdooFree", |
|||
"license": "Other OSI approved licence", # MIT |
|||
"price": 115.00, |
|||
"currency": "EUR", |
|||
"depends": [ |
|||
"ir_config_parameter_multi_company", |
|||
"web_website", |
|||
"mail", |
|||
"test_mail", |
|||
], |
|||
"external_dependencies": {"python": [], "bin": []}, |
|||
"data": ["views/website_views.xml"], |
|||
"demo": [], |
|||
"qweb": [], |
|||
"post_load": None, |
|||
"pre_init_hook": None, |
|||
"post_init_hook": "post_init_hook", |
|||
"uninstall_hook": "uninstall_hook", |
|||
"auto_install": False, |
|||
"installable": True, |
|||
# "demo_title": "Email Addresses per Website", |
|||
# "demo_addons": [ |
|||
# ], |
|||
# "demo_addons_hidden": [ |
|||
# ], |
|||
# "demo_url": "DEMO-URL", |
|||
# "demo_summary": "Use single Backend to manage several Websites", |
|||
# "demo_images": [ |
|||
# "images/MAIN_IMAGE", |
|||
# ] |
|||
} |
@ -0,0 +1,13 @@ |
|||
`2.0.0` |
|||
------- |
|||
- **Improvement:** adapt module to latest version of ``web_website`` module |
|||
|
|||
`1.0.1` |
|||
------- |
|||
|
|||
- **Fix:** Issue with module uninstallation |
|||
|
|||
`1.0.0` |
|||
------- |
|||
|
|||
- **Init version** |
@ -0,0 +1,73 @@ |
|||
===================== |
|||
Multi-Brand Mailing |
|||
===================== |
|||
|
|||
Installation |
|||
============ |
|||
|
|||
* `Install <https://odoo-development.readthedocs.io/en/latest/odoo/usage/install-module.html>`__ this module in a usual way |
|||
|
|||
Configuration |
|||
============= |
|||
|
|||
|
|||
Access to websites |
|||
------------------ |
|||
|
|||
* Go to menu ``[[ Settings ]] >> Users & Companies >> Users`` |
|||
* Select a user |
|||
* Grant access ``[x] Multi Websites for Backend`` |
|||
* Configure **Allowed Websites** |
|||
|
|||
User's email per website |
|||
------------------------ |
|||
|
|||
* Refresh page if you just granted your user access to websites |
|||
* Use top right-hand corner button with current website name to switch between websites |
|||
* Use top right-hand corner button with user name and avatar to open |
|||
Preference popup. When you edit **Email** field, it will be saved as a value |
|||
for current website. |
|||
|
|||
Email template per website |
|||
-------------------------- |
|||
|
|||
* Refresh page if you just granted your user access to websites |
|||
* `Activate Developer Mode <https://odoo-development.readthedocs.io/en/latest/odoo/usage/debug-mode.html>`__ |
|||
* Use top right-hand corner button with current website name to switch between websites |
|||
* Go to menu ``[[ Settings ]] >> Technical >> Email >> Templates`` |
|||
* When you edit template, following fields will be saved as a value for current website: |
|||
|
|||
* **Body** |
|||
* **Outgoing Mail Server** |
|||
* **Optional report to print and attach** |
|||
|
|||
* Additional variable ``website`` is available to configure rest fields (**Subject**, **From**, etc.) |
|||
|
|||
Note. If related record (e.g. ``sale.order``) has field ``company_id`` or ``website_id`` those values will be used instead of currently selected in Website / Company Switchers |
|||
|
|||
Alias domain per website |
|||
------------------------ |
|||
|
|||
Configure ``mail.catchall.domain`` per website. See Documentation of the module `Context-dependent values in System Parameters <https://apps.odoo.com/apps/modules/10.0/ir_config_parameter_multi_company>`__. |
|||
|
|||
Outgoing mails servers per website |
|||
---------------------------------- |
|||
|
|||
If each domain has different Outgoing Mail Server you need following adjustments |
|||
|
|||
* Go to menu ``[[ Website ]] >> Configuration >> Websites`` |
|||
* In each Website specify field **Outgoing Mails** |
|||
|
|||
Properties |
|||
---------- |
|||
|
|||
To review properties by website use menu ``[[ Settings ]] >> Technical >> Parameters >> Company Properties``. See **How it works** in Documentation of module `Website Switcher in Backend <https://apps.odoo.com/apps/modules/10.0/web_website>`__. |
|||
|
|||
Usage |
|||
===== |
|||
|
|||
When you work from backend, Email for current website is used. |
|||
|
|||
When a user do something on website (e.g. purchase products) and some mail is sent, then email address for that website will be used (mostly Administrator's email address). |
|||
|
|||
When email is sent, template's value like body, subject, etc. for current values are used. |
@ -0,0 +1,36 @@ |
|||
|
|||
<section class="oe_container"> |
|||
<div class="oe_row oe_spaced"> |
|||
<div class="oe_span12"> |
|||
|
|||
<div class="alert alert-info oe_mt32" style="padding:0.3em 0.6em; font-size: 150%;"> |
|||
<i class="fa fa-hand-o-right"></i><b> Key features: </b> |
|||
<ul class="list-unstyled"> |
|||
|
|||
<li> |
|||
<i class="fa fa-check-square-o text-primary"></i> |
|||
Separate Mail Templates per Website |
|||
</li> |
|||
|
|||
<li> |
|||
<i class="fa fa-check-square-o text-primary"></i> |
|||
<em>From</em> address in email has address for current Website |
|||
</li> |
|||
|
|||
<li> |
|||
<i class="fa fa-check-square-o text-primary"></i> |
|||
<em>Reply-To</em> address in email has domain of current Website |
|||
</li> |
|||
|
|||
<li> |
|||
<i class="fa fa-check-square-o text-primary"></i> |
|||
User's signature per Website |
|||
</li> |
|||
|
|||
</ul> |
|||
</div> |
|||
|
|||
</div> |
|||
</div> |
|||
</section> |
|||
|
@ -0,0 +1 @@ |
|||
slogan: Use single Backend to manage several Websites |
@ -0,0 +1,123 @@ |
|||
# Translation of Odoo Server. |
|||
# This file contains the translation of the following modules: |
|||
# * mail_multi_website |
|||
# |
|||
msgid "" |
|||
msgstr "" |
|||
"Project-Id-Version: Odoo Server 12.0\n" |
|||
"Report-Msgid-Bugs-To: \n" |
|||
"Last-Translator: <>\n" |
|||
"Language-Team: \n" |
|||
"MIME-Version: 1.0\n" |
|||
"Content-Type: text/plain; charset=UTF-8\n" |
|||
"Content-Transfer-Encoding: \n" |
|||
"Plural-Forms: \n" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model.fields,field_description:mail_multi_website.field_email_template_preview__body_html |
|||
#: model:ir.model.fields,field_description:mail_multi_website.field_mail_template__body_html |
|||
msgid "Body" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model.fields,field_description:mail_multi_website.field_mail_test_simple__company_id |
|||
msgid "Company" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model,name:mail_multi_website.model_ir_property |
|||
msgid "Company Property" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model.fields,help:mail_multi_website.field_website__mail_server_id |
|||
msgid "Default outgoing mail server" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model.fields,field_description:mail_multi_website.field_res_users__email_multi_website |
|||
msgid "Email Multi Website" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model,name:mail_multi_website.model_mail_template |
|||
msgid "Email Templates" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model,name:mail_multi_website.model_mail_thread |
|||
msgid "Email Thread" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model,name:mail_multi_website.model_mail_compose_message |
|||
msgid "Email composition wizard" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: code:addons/mail_multi_website/models/mail_template.py:112 |
|||
#, python-format |
|||
msgid "Failed to render template %r using values %r" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model,name:mail_multi_website.model_mail_message |
|||
msgid "Message" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model.fields,field_description:mail_multi_website.field_res_users__email |
|||
msgid "Multi Website Email" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model.fields,help:mail_multi_website.field_email_template_preview__mail_server_id |
|||
#: model:ir.model.fields,help:mail_multi_website.field_mail_template__mail_server_id |
|||
msgid "Optional preferred server for outgoing mails. If not set, the highest priority one will be used." |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model.fields,field_description:mail_multi_website.field_email_template_preview__report_template |
|||
#: model:ir.model.fields,field_description:mail_multi_website.field_mail_template__report_template |
|||
msgid "Optional report to print and attach (Multi-Website)" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model.fields,field_description:mail_multi_website.field_email_template_preview__mail_server_id |
|||
#: model:ir.model.fields,field_description:mail_multi_website.field_mail_template__mail_server_id |
|||
msgid "Outgoing Mail Server (Multi-Website)" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model.fields,field_description:mail_multi_website.field_website__mail_server_id |
|||
msgid "Outgoing Mails" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model.fields,field_description:mail_multi_website.field_mail_compose_message__mail_server_id |
|||
#: model:ir.model.fields,field_description:mail_multi_website.field_mail_mail__mail_server_id |
|||
#: model:ir.model.fields,field_description:mail_multi_website.field_mail_message__mail_server_id |
|||
msgid "Outgoing mail server" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model.fields,field_description:mail_multi_website.field_res_users__signature |
|||
msgid "Signature" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model,name:mail_multi_website.model_mail_test_simple |
|||
msgid "Simple Chatter Model" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model,name:mail_multi_website.model_res_users |
|||
msgid "Users" |
|||
msgstr "" |
|||
|
|||
#. module: mail_multi_website |
|||
#: model:ir.model,name:mail_multi_website.model_website |
|||
#: model:ir.model.fields,field_description:mail_multi_website.field_mail_test_simple__website_id |
|||
msgid "Website" |
|||
msgstr "" |
|||
|
After Width: 750 | Height: 371 | Size: 285 KiB |
@ -0,0 +1,7 @@ |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
from . import res_users |
|||
from . import ir_property |
|||
from . import mail_template |
|||
from . import mail_thread |
|||
from . import mail_message |
|||
from . import website |
@ -0,0 +1,20 @@ |
|||
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
from odoo import models |
|||
|
|||
|
|||
class IrProperty(models.Model): |
|||
_inherit = "ir.property" |
|||
|
|||
def write(self, vals): |
|||
res = super(IrProperty, self).write(vals) |
|||
field_object_list = [ |
|||
self.env.ref("base.field_res_users__email"), |
|||
self.env.ref("mail.field_mail_template__body_html"), |
|||
self.env.ref("mail.field_mail_template__mail_server_id"), |
|||
self.env.ref("mail.field_mail_template__report_template"), |
|||
] |
|||
for fobj in field_object_list: |
|||
self._update_db_value_website_dependent(fobj) |
|||
return res |
@ -0,0 +1,13 @@ |
|||
# Copyright 2018,2020 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
# License OPL-1 (https://www.odoo.com/documentation/user/13.0/legal/licenses/licenses.html#odoo-apps) for derivative work. |
|||
from odoo import fields, models |
|||
|
|||
|
|||
class Message(models.Model): |
|||
_inherit = "mail.message" |
|||
|
|||
def _default_mail_server_id(self): |
|||
return self.env.website.mail_server_id.id |
|||
|
|||
mail_server_id = fields.Many2one(default=_default_mail_server_id) |
@ -0,0 +1,185 @@ |
|||
# Copyright 2018,2020 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
# License OPL-1 (https://www.odoo.com/documentation/user/13.0/legal/licenses/licenses.html#odoo-apps) for derivative work. |
|||
import logging |
|||
|
|||
from odoo import _, api, fields, models, tools |
|||
from odoo.exceptions import UserError |
|||
|
|||
from odoo.addons.mail.models.mail_template import format_date, format_datetime |
|||
|
|||
_logger = logging.getLogger(__name__) |
|||
FIELDS = ["body_html", "mail_server_id", "report_template"] |
|||
|
|||
try: |
|||
from odoo.addons.mail.models.mail_template import ( |
|||
mako_safe_template_env, |
|||
mako_template_env, |
|||
) |
|||
except ImportError: |
|||
_logger.warning("jinja2 not available, templating features will not work!") |
|||
|
|||
|
|||
class MailTemplate(models.Model): |
|||
|
|||
_inherit = ["mail.template", "website_dependent.mixin"] |
|||
_name = "mail.template" |
|||
|
|||
body_html = fields.Html(company_dependent=True, website_dependent=True) |
|||
mail_server_id = fields.Many2one( |
|||
string="Outgoing Mail Server (Multi-Website)", |
|||
company_dependent=True, |
|||
website_dependent=True, |
|||
) |
|||
report_template = fields.Many2one( |
|||
string="Optional report to print and attach (Multi-Website)", |
|||
company_dependent=True, |
|||
website_dependent=True, |
|||
) |
|||
|
|||
def generate_email(self, res_ids, fields=None): |
|||
"""Remove mail_server_id when not set to recompute in _default_mail_server_id in mail.message""" |
|||
multi_mode = True |
|||
if isinstance(res_ids, int): |
|||
multi_mode = False |
|||
res = super(MailTemplate, self).generate_email(res_ids, fields=fields) |
|||
if not multi_mode: |
|||
list_of_dict = {0: res} |
|||
else: |
|||
list_of_dict = res |
|||
|
|||
for _unused, data in list_of_dict.items(): |
|||
if "mail_server_id" in data and not data.get("mail_server_id"): |
|||
del data["mail_server_id"] |
|||
|
|||
return res |
|||
|
|||
@api.model |
|||
def _render_template(self, template_txt, model, res_ids, post_process=False): |
|||
"""Override to add website to context""" |
|||
multi_mode = True |
|||
if isinstance(res_ids, int): |
|||
multi_mode = False |
|||
res_ids = [res_ids] |
|||
|
|||
results = dict.fromkeys(res_ids, u"") |
|||
|
|||
# try to load the template |
|||
try: |
|||
mako_env = ( |
|||
mako_safe_template_env |
|||
if self.env.context.get("safe") |
|||
else mako_template_env |
|||
) |
|||
template = mako_env.from_string(tools.ustr(template_txt)) |
|||
except Exception: |
|||
_logger.info("Failed to load template %r", template_txt, exc_info=True) |
|||
return multi_mode and results or results[res_ids[0]] |
|||
|
|||
# prepare template variables |
|||
records = self.env[model].browse( |
|||
it for it in res_ids if it |
|||
) # filter to avoid browsing [None] |
|||
res_to_rec = dict.fromkeys(res_ids, None) |
|||
for record in records: |
|||
res_to_rec[record.id] = record |
|||
variables = { |
|||
"format_date": lambda date, date_format=False, lang_code=False: format_date( |
|||
self.env, date, date_format, lang_code |
|||
), |
|||
"format_datetime": lambda dt, tz=False, dt_format=False, lang_code=False: format_datetime( |
|||
self.env, dt, tz, dt_format, lang_code |
|||
), |
|||
"format_amount": lambda amount, currency, lang_code=False: tools.format_amount( |
|||
self.env, amount, currency, lang_code |
|||
), |
|||
"format_duration": lambda value: tools.format_duration(value), |
|||
"user": self.env.user, |
|||
"ctx": self._context, # context kw would clash with mako internals |
|||
} |
|||
|
|||
# [NEW] Check website and company context |
|||
company = self.env["res.company"] # empty value |
|||
|
|||
company_id = self.env.context.get("force_company") |
|||
if company_id: |
|||
company = self.env["res.company"].sudo().browse(company_id) |
|||
|
|||
website = self.env.website |
|||
# [/NEW] |
|||
|
|||
for res_id, record in res_to_rec.items(): |
|||
# [NEW] Check website and company context |
|||
record_company = company |
|||
if not record_company: |
|||
if hasattr(record, "company_id") and record.company_id: |
|||
record_company = record.company_id |
|||
|
|||
record_website = website |
|||
if hasattr(record, "website_id") and record.website_id: |
|||
record_website = record.website_id |
|||
|
|||
if ( |
|||
record_company |
|||
and record_website |
|||
and record_website.company_id != company |
|||
): |
|||
# company and website are incompatible, so keep only company |
|||
record_website = self.env["website"] # empty value |
|||
|
|||
record_context = dict( |
|||
force_company=record_company.id, website_id=record_website.id |
|||
) |
|||
variables["website"] = record_website |
|||
# [/NEW] |
|||
|
|||
variables["object"] = record |
|||
try: |
|||
render_result = template.render(variables) |
|||
except Exception: |
|||
_logger.info( |
|||
"Failed to render template %r using values %r" |
|||
% (template, variables), |
|||
exc_info=True, |
|||
) |
|||
raise UserError( |
|||
_("Failed to render template %r using values %r") |
|||
% (template, variables) |
|||
) |
|||
if render_result == u"False": |
|||
render_result = u"" |
|||
results[res_id] = render_result |
|||
|
|||
if post_process: |
|||
for res_id, result in results.items(): |
|||
results[res_id] = self.with_context( |
|||
**record_context |
|||
).render_post_process(result) |
|||
|
|||
return multi_mode and results or results[res_ids[0]] |
|||
|
|||
@api.model |
|||
def create(self, vals): |
|||
res = super(MailTemplate, self).create(vals) |
|||
# make value company independent |
|||
for f in FIELDS: |
|||
res._force_default(f, vals.get(f)) |
|||
return res |
|||
|
|||
def write(self, vals): |
|||
res = super(MailTemplate, self).write(vals) |
|||
|
|||
# TODO: will it work with OCA's partner_firstname module? |
|||
if "name" in vals: |
|||
fields_to_update = FIELDS |
|||
else: |
|||
fields_to_update = [f for f in FIELDS if f in vals] |
|||
for f in fields_to_update: |
|||
self._update_properties_label(f) |
|||
|
|||
return res |
|||
|
|||
def _auto_init(self): |
|||
for f in FIELDS: |
|||
self._auto_init_website_dependent(f) |
|||
return super(MailTemplate, self)._auto_init() |
@ -0,0 +1,34 @@ |
|||
# Copyright 2018,2020 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
# License OPL-1 (https://www.odoo.com/documentation/user/13.0/legal/licenses/licenses.html#odoo-apps) for derivative work. |
|||
from odoo import api, models, tools |
|||
|
|||
|
|||
class MailThread(models.AbstractModel): |
|||
_inherit = "mail.thread" |
|||
|
|||
@api.model |
|||
def _message_route_process(self, message, message_dict, routes): |
|||
rcpt_tos = ",".join( |
|||
[ |
|||
tools.decode_message_header(message, "Delivered-To"), |
|||
tools.decode_message_header(message, "To"), |
|||
tools.decode_message_header(message, "Cc"), |
|||
tools.decode_message_header(message, "Resent-To"), |
|||
tools.decode_message_header(message, "Resent-Cc"), |
|||
] |
|||
) |
|||
rcpt_tos_websiteparts = [ |
|||
e.split("@")[1].lower() for e in tools.email_split(rcpt_tos) |
|||
] |
|||
website = ( |
|||
self.env["website"].sudo().search([("domain", "in", rcpt_tos_websiteparts)]) |
|||
) |
|||
company = website.mapped("company_id") |
|||
new_self = self.with_context( |
|||
allowed_website_ids=website.ids, allowed_company_ids=company.ids |
|||
) |
|||
|
|||
return super(MailThread, new_self)._message_route_process( |
|||
message, message_dict, routes |
|||
) |
@ -0,0 +1,50 @@ |
|||
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
import logging |
|||
|
|||
from odoo import api, fields, models |
|||
|
|||
_logger = logging.getLogger(__name__) |
|||
FIELD_NAME = "email" |
|||
FIELDS = ["signature"] |
|||
ALL_FIELDS = [FIELD_NAME] + FIELDS |
|||
|
|||
|
|||
class User(models.Model): |
|||
|
|||
_inherit = ["res.users", "website_dependent.mixin"] |
|||
_name = "res.users" |
|||
|
|||
signature = fields.Html(company_dependent=True, website_dependent=True) |
|||
|
|||
# extra field to detach email field from res.partner |
|||
email = fields.Char( |
|||
string="Multi Website Email", |
|||
company_dependent=True, |
|||
website_dependent=True, |
|||
inherited=False, |
|||
related=None, |
|||
readonly=False, |
|||
) |
|||
|
|||
@api.model |
|||
def create(self, vals): |
|||
res = super(User, self).create(vals) |
|||
# make value company independent |
|||
res._force_default(FIELD_NAME, vals.get("email")) |
|||
for f in FIELDS: |
|||
res._force_default(f, vals.get(f)) |
|||
return res |
|||
|
|||
def write(self, vals): |
|||
res = super(User, self).write(vals) |
|||
# TODO: will it work with OCA's partner_firstname module? |
|||
if any(k in vals for k in ["name"] + FIELDS): |
|||
for f in ALL_FIELDS: |
|||
self._update_properties_label(f) |
|||
return res |
|||
|
|||
def _auto_init(self): |
|||
for f in FIELDS: |
|||
self._auto_init_website_dependent(f) |
|||
return super(User, self)._auto_init() |
@ -0,0 +1,12 @@ |
|||
# Copyright 2017 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
|
|||
from odoo import fields, models |
|||
|
|||
|
|||
class Website(models.Model): |
|||
_inherit = "website" |
|||
|
|||
mail_server_id = fields.Many2one( |
|||
"ir.mail_server", "Outgoing Mails", help="Default outgoing mail server" |
|||
) |
After Width: 100 | Height: 100 | Size: 2.1 KiB |
@ -0,0 +1,134 @@ |
|||
|
|||
<section class="container"> |
|||
<div class="row"> |
|||
<div class="col-md-offset-1 col-md-8 mb64 mt64"> |
|||
<div class=""> |
|||
<h2 class="display-3">Multi-Brand Mailing</h2> |
|||
<h4 class="text-default">Use single Backend to manage several Websites</h4> |
|||
<p class="text-default mt16">Version: v<b>13.0</b>.2.0.0</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 text-right"> |
|||
<p> |
|||
<img src="https://itpp.dev/images/favicon.png"/> |
|||
<br/>Tested and maintained by |
|||
<br/><b>IT Projects Labs</b> |
|||
<br/>Assitance: <a href="mailto:apps@itpp.dev">apps@itpp.dev</a> |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
|
|||
|
|||
|
|||
<section class="oe_container"> |
|||
<div class="oe_row oe_spaced"> |
|||
<div class="oe_span12"> |
|||
|
|||
<div class="alert alert-info oe_mt32" style="padding:0.3em 0.6em; font-size: 150%;"> |
|||
<i class="fa fa-hand-o-right"></i><b> Key features: </b> |
|||
<ul class="list-unstyled"> |
|||
|
|||
<li> |
|||
<i class="fa fa-check-square-o text-primary"></i> |
|||
Separate Mail Templates per Website |
|||
</li> |
|||
|
|||
<li> |
|||
<i class="fa fa-check-square-o text-primary"></i> |
|||
<em>From</em> address in email has address for current Website |
|||
</li> |
|||
|
|||
<li> |
|||
<i class="fa fa-check-square-o text-primary"></i> |
|||
<em>Reply-To</em> address in email has domain of current Website |
|||
</li> |
|||
|
|||
<li> |
|||
<i class="fa fa-check-square-o text-primary"></i> |
|||
User's signature per Website |
|||
</li> |
|||
|
|||
</ul> |
|||
</div> |
|||
|
|||
</div> |
|||
</div> |
|||
</section> |
|||
|
|||
|
|||
|
|||
|
|||
<section class="container text-center"> |
|||
<div class="mt-4 mb-3"> |
|||
<h2>Let our expertise work for you!</h2> |
|||
<h4 class="d-none d-md-block">(Doors and windows below are clickable)</h4> |
|||
<div class="mb32 d-md-none"> |
|||
<ul class="list-unstyled"> |
|||
<li><a target="_blank" href="https://apps.odoo.com/apps/browse?repo_maintainer_id=59928">All modules</a></li> |
|||
<li><a target="_blank" href="https://apps.odoo.com/apps/browse?price=Free&order=Downloads&repo_maintainer_id=59928">Free Modules</a></li> |
|||
<li><a target="_blank" href="https://apps.odoo.com/apps/modules/category/Point%20of%20Sale/browse?repo_maintainer_id=59928">All POS modules</a></li> |
|||
<li><a target="_blank" href="https://apps.odoo.com/apps/modules/category/Discuss/browse?repo_maintainer_id=59928">All Mail modules</a></li> |
|||
<li><a target="_blank" href="https://apps.odoo.com/apps/modules/category/Website/browse?repo_maintainer_id=59928">All Website modules</a></li> |
|||
<li><a target="_blank" href="https://apps.odoo.com/apps/modules/category/eCommerce/browse?repo_maintainer_id=59928">All eCommerce modules</a></li> |
|||
<li><a target="_blank" href="https://apps.odoo.com/apps/modules/browse?search=sync+studio&repo_maintainer_id=59928">Sync Studio</a></li> |
|||
<li><a target="_blank" href="https://apps.odoo.com/apps/modules/14.0/openapi/">REST API</a></li> |
|||
<li><a target="_blank" href="https://apps.odoo.com/apps/modules/14.0/base_automation_webhook/">Webhooks</a></li> |
|||
<li><a target="_blank" href="https://apps.odoo.com/apps/modules/14.0/pos_product_available/">POS: show product qty</a></li> |
|||
<li><a target="_blank" href="https://apps.odoo.com/apps/modules/14.0/web_login_background/">Web Login Background</a></li> |
|||
</ul> |
|||
</div> |
|||
<div class="mb64 d-none d-xl-block"> |
|||
<img class="img img-responsive" src="https://itpp.dev/images/apps/;-)/itpp-labs-1100.jpg" usemap="#itpp-xl" /> |
|||
<map name="itpp-xl"> |
|||
<area alt="Free Modules" target="_blank" href="https://apps.odoo.com/apps/browse?price=Free&order=Downloads&repo_maintainer_id=59928" shape="rect" class="" coords="900,220,1080,625" /> |
|||
<area alt="Sync Studio" target="_blank" href="https://apps.odoo.com/apps/modules/browse?search=sync+studio&repo_maintainer_id=59928" shape="rect" class="" coords="552,186,721,274" /> |
|||
<area alt="REST API" target="_blank" href="https://apps.odoo.com/apps/modules/14.0/openapi/" shape="rect" class="" coords="646,279,720,365" /> |
|||
<area alt="Webhooks" target="_blank" href="https://apps.odoo.com/apps/modules/14.0/base_automation_webhook/" shape="rect" class="" coords="646,370,720,458" /> |
|||
<area alt="POS: show product qty" target="_blank" href="https://apps.odoo.com/apps/modules/14.0/pos_product_available/" shape="rect" class="" coords="82,373,251,459" /> |
|||
<area alt="All modules" target="_blank" href="https://apps.odoo.com/apps/browse?repo_maintainer_id=59928" shape="rect" class="" coords="326,267,381,456" /> |
|||
<area alt="All POS modules" target="_blank" href="https://apps.odoo.com/apps/modules/category/Point%20of%20Sale/browse?repo_maintainer_id=59928" shape="rect" class="" coords="82,191,154,276" /> |
|||
<area alt="All Mail modules" target="_blank" href="https://apps.odoo.com/apps/modules/category/Discuss/browse?repo_maintainer_id=59928" shape="rect" class="" coords="82,282,155,368" /> |
|||
<area alt="All Website modules" target="_blank" href="https://apps.odoo.com/apps/modules/category/Website/browse?repo_maintainer_id=59928" shape="rect" class="" coords="176,282,250,368" /> |
|||
<area alt="All eCommerce modules" target="_blank" href="https://apps.odoo.com/apps/modules/category/eCommerce/browse?repo_maintainer_id=59928" shape="rect" class="" coords="175,189,250,275" /> |
|||
<area alt="Ivan Yelizariev" target="_blank" href="https://apps.odoo.com/apps/modules/browse?search=Ivan+Yelizariev&repo_maintainer_id=59928" shape="rect" class="" coords="554,280,625,456" /> |
|||
<area alt="Web Login Background" target="_blank" href="https://apps.odoo.com/apps/modules/14.0/web_login_background/" shape="rect" class="" coords="420,267,477,456" /> |
|||
</map> |
|||
</div> |
|||
<div class="mb64 d-none d-lg-block d-xl-none"> |
|||
<img class="img img-responsive" src="https://itpp.dev/images/apps/;-)/itpp-labs-930.jpg" usemap="#itpp-lg" /> |
|||
<map name="itpp-lg"> |
|||
<area alt="Free Modules" target="_blank" href="https://apps.odoo.com/apps/browse?price=Free&order=Downloads&repo_maintainer_id=59928" shape="rect" class="" coords="760,186,913,528" /> |
|||
<area alt="Sync Studio" target="_blank" href="https://apps.odoo.com/apps/modules/browse?search=sync+studio&repo_maintainer_id=59928" shape="rect" class="" coords="466,157,609,231" /> |
|||
<area alt="REST API" target="_blank" href="https://apps.odoo.com/apps/modules/14.0/openapi/" shape="rect" class="" coords="546,235,608,308" /> |
|||
<area alt="Webhooks" target="_blank" href="https://apps.odoo.com/apps/modules/14.0/base_automation_webhook/" shape="rect" class="" coords="546,312,608,387" /> |
|||
<area alt="POS: show product qty" target="_blank" href="https://apps.odoo.com/apps/modules/14.0/pos_product_available/" shape="rect" class="" coords="69,315,212,388" /> |
|||
<area alt="All modules" target="_blank" href="https://apps.odoo.com/apps/browse?repo_maintainer_id=59928" shape="rect" class="" coords="275,225,322,385" /> |
|||
<area alt="All POS modules" target="_blank" href="https://apps.odoo.com/apps/modules/category/Point%20of%20Sale/browse?repo_maintainer_id=59928" shape="rect" class="" coords="69,161,130,233" /> |
|||
<area alt="All Mail modules" target="_blank" href="https://apps.odoo.com/apps/modules/category/Discuss/browse?repo_maintainer_id=59928" shape="rect" class="" coords="69,238,131,311" /> |
|||
<area alt="All Website modules" target="_blank" href="https://apps.odoo.com/apps/modules/category/Website/browse?repo_maintainer_id=59928" shape="rect" class="" coords="148,238,211,311" /> |
|||
<area alt="All eCommerce modules" target="_blank" href="https://apps.odoo.com/apps/modules/category/eCommerce/browse?repo_maintainer_id=59928" shape="rect" class="" coords="147,159,211,232" /> |
|||
<area alt="Ivan Yelizariev" target="_blank" href="https://apps.odoo.com/apps/modules/browse?search=Ivan+Yelizariev&repo_maintainer_id=59928" shape="rect" class="" coords="468,236,528,385" /> |
|||
<area alt="Web Login Background" target="_blank" href="https://apps.odoo.com/apps/modules/14.0/web_login_background/" shape="rect" class="" coords="355,225,403,385" /> |
|||
</map> |
|||
</div> |
|||
<div class="mb64 d-lg-none"> |
|||
<img class="img img-responsive" src="https://itpp.dev/images/apps/;-)/itpp-labs-690.jpg" usemap="#itpp-md" /> |
|||
<map name="itpp-md"> |
|||
<area alt="Free Modules" target="_blank" href="https://apps.odoo.com/apps/browse?price=Free&order=Downloads&repo_maintainer_id=59928" shape="rect" class="" coords="564,138,677,392" /> |
|||
<area alt="Sync Studio" target="_blank" href="https://apps.odoo.com/apps/modules/browse?search=sync+studio&repo_maintainer_id=59928" shape="rect" class="" coords="346,116,452,171" /> |
|||
<area alt="REST API" target="_blank" href="https://apps.odoo.com/apps/modules/14.0/openapi/" shape="rect" class="" coords="405,175,451,228" /> |
|||
<area alt="Webhooks" target="_blank" href="https://apps.odoo.com/apps/modules/14.0/base_automation_webhook/" shape="rect" class="" coords="405,232,451,287" /> |
|||
<area alt="POS: show product qty" target="_blank" href="https://apps.odoo.com/apps/modules/14.0/pos_product_available/" shape="rect" class="" coords="51,233,157,287" /> |
|||
<area alt="All modules" target="_blank" href="https://apps.odoo.com/apps/browse?repo_maintainer_id=59928" shape="rect" class="" coords="204,167,238,286" /> |
|||
<area alt="All POS modules" target="_blank" href="https://apps.odoo.com/apps/modules/category/Point%20of%20Sale/browse?repo_maintainer_id=59928" shape="rect" class="" coords="51,119,96,173" /> |
|||
<area alt="All Mail modules" target="_blank" href="https://apps.odoo.com/apps/modules/category/Discuss/browse?repo_maintainer_id=59928" shape="rect" class="" coords="51,176,97,230" /> |
|||
<area alt="All Website modules" target="_blank" href="https://apps.odoo.com/apps/modules/category/Website/browse?repo_maintainer_id=59928" shape="rect" class="" coords="110,176,156,230" /> |
|||
<area alt="All eCommerce modules" target="_blank" href="https://apps.odoo.com/apps/modules/category/eCommerce/browse?repo_maintainer_id=59928" shape="rect" class="" coords="109,118,156,172" /> |
|||
<area alt="Ivan Yelizariev" target="_blank" href="https://apps.odoo.com/apps/modules/browse?search=Ivan+Yelizariev&repo_maintainer_id=59928" shape="rect" class="" coords="347,175,392,286" /> |
|||
<area alt="Web Login Background" target="_blank" href="https://apps.odoo.com/apps/modules/14.0/web_login_background/" shape="rect" class="" coords="263,167,299,286" /> |
|||
</map> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
|
@ -0,0 +1,4 @@ |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
from . import test_send |
|||
from . import test_render |
|||
from . import test_fetch |
@ -0,0 +1,63 @@ |
|||
# Copyright 2018,2020 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
# License OPL-1 (https://www.odoo.com/documentation/user/13.0/legal/licenses/licenses.html#odoo-apps) for derivative work. |
|||
from odoo.tools import mute_logger |
|||
|
|||
from odoo.addons.test_mail.data.test_mail_data import MAIL_TEMPLATE |
|||
from odoo.addons.test_mail.tests.test_mail_mail import TestMail |
|||
|
|||
|
|||
# TODO: Shall we use TestMailgateway class instead? |
|||
class TestFetch(TestMail): |
|||
at_install = True |
|||
post_install = True |
|||
|
|||
def setUp(self): |
|||
super(TestFetch, self).setUp() |
|||
self.email_from = '"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>' |
|||
self.website = self.env["website"].create( |
|||
{"name": "Test Website", "domain": "example.com"} |
|||
) |
|||
self.company = self.env["res.company"].create({"name": "New Test Website"}) |
|||
self.website.company_id = self.company |
|||
|
|||
# copy-paste from mail.tests.test_mail_gateway |
|||
mail_test_model = self.env["ir.model"]._get("mail.test.simple") |
|||
# groups@.. will cause the creation of new mail.test.simple |
|||
self.alias = self.env["mail.alias"].create( |
|||
{ |
|||
"alias_name": "groups", |
|||
"alias_user_id": False, |
|||
"alias_model_id": mail_test_model.id, |
|||
"alias_contact": "everyone", |
|||
} |
|||
) |
|||
|
|||
@mute_logger("odoo.addons.mail.models.mail_thread", "odoo.models") |
|||
def test_fetch_multi_website(self): |
|||
""" Incoming email on an alias creating a new record + message_new + message details """ |
|||
new_groups = self.format_and_process( |
|||
MAIL_TEMPLATE, |
|||
self.email_from, |
|||
"groups@example.com", |
|||
subject="My Frogs", |
|||
target_model="mail.test.simple", |
|||
) |
|||
|
|||
# Test: one group created by mailgateway administrator |
|||
self.assertEqual( |
|||
len(new_groups), |
|||
1, |
|||
"message_process: a new mail.test should have been created", |
|||
) |
|||
self.assertEqual( |
|||
new_groups.website_id, |
|||
self.website, |
|||
"New record is created with wrong website", |
|||
) |
|||
self.assertEqual( |
|||
new_groups.company_id, |
|||
self.company, |
|||
"New record is created with wrong company", |
|||
) |
@ -0,0 +1,11 @@ |
|||
# Copyright 2018,2020 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
# License OPL-1 (https://www.odoo.com/documentation/user/13.0/legal/licenses/licenses.html#odoo-apps) for derivative work. |
|||
from odoo import fields, models |
|||
|
|||
|
|||
class MailTest(models.Model): |
|||
_inherit = "mail.test.simple" |
|||
|
|||
company_id = fields.Many2one("res.company", default=lambda self: self.env.company) |
|||
website_id = fields.Many2one("website", default=lambda self: self.env.website) |
@ -0,0 +1,145 @@ |
|||
# Copyright 2018,2020 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
# License OPL-1 (https://www.odoo.com/documentation/user/13.0/legal/licenses/licenses.html#odoo-apps) for derivative work. |
|||
import base64 |
|||
|
|||
from odoo.addons.test_mail.tests.test_mail_mail import TestMail |
|||
|
|||
|
|||
class TestRender(TestMail): |
|||
at_install = True |
|||
post_install = True |
|||
|
|||
def setUp(self): |
|||
super(TestRender, self).setUp() |
|||
|
|||
self.original_email = self.env.user.email |
|||
self.original_company = self.env.user.company_id |
|||
self.email = "superadmin@second-website.example" |
|||
self.assertNotEqual(self.original_email, self.email) |
|||
|
|||
self.website = self.env.ref("website.website2") |
|||
self.company = self.env["res.company"].create({"name": "New Test Website"}) |
|||
self.website.company_id = self.company |
|||
self.mail_server_id = self.env["ir.mail_server"].create( |
|||
{"name": "mail server", "smtp_host": "mail.example.com"} |
|||
) |
|||
self.website.mail_server_id = self.mail_server_id |
|||
|
|||
user_admin = self.env.ref("base.user_admin") |
|||
# copy-paste from mail.tests.test_mail_template |
|||
self._attachments = [ |
|||
{ |
|||
"name": "first.txt", |
|||
"datas": base64.b64encode(b"My first attachment"), |
|||
"res_model": "res.partner", |
|||
"res_id": user_admin.partner_id.id, |
|||
}, |
|||
{ |
|||
"name": "second.txt", |
|||
"datas": base64.b64encode(b"My second attachment"), |
|||
"res_model": "res.partner", |
|||
"res_id": user_admin.partner_id.id, |
|||
}, |
|||
] |
|||
|
|||
self.partner_1 = self.env["res.partner"].create({"name": "partner_1"}) |
|||
self.partner_2 = self.env["res.partner"].create({"name": "partner_2"}) |
|||
self.email_1 = "test1@example.com" |
|||
self.email_2 = "test2@example.com" |
|||
self.email_3 = self.partner_1.email |
|||
self.email_template = self.env["mail.template"].create( |
|||
{ |
|||
"model_id": self.env["ir.model"]._get("mail.test").id, |
|||
"name": "Pigs Template", |
|||
"subject": "${website.name}", |
|||
"body_html": "${object.description}", |
|||
"user_signature": False, |
|||
"attachment_ids": [ |
|||
(0, 0, self._attachments[0]), |
|||
(0, 0, self._attachments[1]), |
|||
], |
|||
"partner_to": "%s,%s" |
|||
% (self.partner_2.id, self.user_employee.partner_id.id), |
|||
"email_to": "{}, {}".format(self.email_1, self.email_2), |
|||
"email_cc": "%s" % self.email_3, |
|||
} |
|||
) |
|||
|
|||
def switch_user_website(self): |
|||
# add website to allowed |
|||
self.env.user.write( |
|||
dict( |
|||
backend_website_ids=[(4, self.website.id)], |
|||
backend_website_id=self.website.id, |
|||
company_id=self.company.id, |
|||
company_ids=[(4, self.company.id)], |
|||
) |
|||
) |
|||
|
|||
def test_website_in_render_variables(self): |
|||
"""Mail values are per website""" |
|||
|
|||
self.env.user.backend_website_id = None |
|||
TestModel = self.env["mail.test"].with_context( |
|||
{"mail_create_nolog": True, "mail_create_nosubscribe": True} |
|||
) |
|||
self.test_pigs = TestModel.create( |
|||
{ |
|||
"name": "Pigs", |
|||
"description": "Fans of Pigs, unite !", |
|||
"alias_name": "pigs", |
|||
"alias_contact": "followers", |
|||
} |
|||
) |
|||
|
|||
# sending without website |
|||
mail_id = self.email_template.send_mail(self.test_pigs.id) |
|||
mail = self.env["mail.mail"].browse(mail_id) |
|||
self.assertEqual(mail.subject, "") |
|||
self.assertFalse(mail.mail_server_id) |
|||
|
|||
# sending from frontend |
|||
self.test_pigs.company_id = None |
|||
mail_id = self.email_template.with_context( |
|||
wdb=True, allowed_website_ids=self.website.ids |
|||
).send_mail(self.test_pigs.id) |
|||
mail = self.env["mail.mail"].browse(mail_id) |
|||
self.assertEqual(mail.subject, self.website.name) |
|||
self.assertEqual(mail.mail_server_id, self.mail_server_id) |
|||
|
|||
# copy-pasted tests |
|||
self.assertEqual(mail.email_to, self.email_template.email_to) |
|||
# for some reason self.email_template.email_cc might return u'False' |
|||
self.assertEqual( |
|||
mail.email_cc or "False", self.email_template.email_cc or "False" |
|||
) |
|||
self.assertEqual( |
|||
mail.recipient_ids, self.partner_2 | self.user_employee.partner_id |
|||
) |
|||
|
|||
# sending from frontend |
|||
self.switch_user_website() |
|||
mail_id = self.email_template.send_mail(self.test_pigs.id) |
|||
mail = self.env["mail.mail"].browse(mail_id) |
|||
self.assertEqual(mail.subject, self.website.name) |
|||
|
|||
def _test_message_post_with_template(self): |
|||
# It's deactivated, because workaround is based on checking host value in get_current_website() |
|||
"""Simulate sending email on eCommerce checkout""" |
|||
self.switch_user_website() |
|||
self.env.user.email = self.email |
|||
self.env.user.invalidate_cache() |
|||
self.env.user.invalidate_cache() |
|||
self.assertEqual(self.env.user.email, self.email) |
|||
# switch admin user back |
|||
self.env.user.company_id = self.original_company |
|||
self.env.user.invalidate_cache() |
|||
self.assertEqual(self.env.user.email, self.original_email) |
|||
|
|||
self.test_pigs.with_context( |
|||
allowed_website_ids=self.website.ids |
|||
).message_post_with_template(self.email_template.id) |
|||
message = self.env["mail.message"].search([], order="id desc", limit=1) |
|||
self.assertIn("<%s>" % self.email, message.email_from) |
@ -0,0 +1,72 @@ |
|||
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
from odoo.tests.common import TransactionCase |
|||
|
|||
|
|||
class TestSendMail(TransactionCase): |
|||
at_install = True |
|||
post_install = True |
|||
|
|||
def setUp(self): |
|||
super(TestSendMail, self).setUp() |
|||
self.website = self.env.ref("website.website2") |
|||
self.company = self.env["res.company"].create({"name": "New Test Website"}) |
|||
self.original_email = self.env.user.email |
|||
self.original_company = self.env.user.company_id |
|||
self.email = "superadmin@second-website.example" |
|||
# Check that current email is set and differs |
|||
self.assertTrue(self.email) |
|||
self.assertNotEqual(self.original_email, self.email) |
|||
self.website.company_id = self.company |
|||
|
|||
def switch_user_website(self): |
|||
# add website to allowed |
|||
self.env.user.write( |
|||
dict( |
|||
backend_website_ids=[(4, self.website.id)], |
|||
backend_website_id=self.website.id, |
|||
company_id=self.company.id, |
|||
company_ids=[(4, self.company.id)], |
|||
) |
|||
) |
|||
|
|||
def test_multi_email(self): |
|||
"""User has email addresses per website""" |
|||
self.switch_user_website() |
|||
# update user's email |
|||
self.env.user.email = self.email |
|||
# Check that writing works |
|||
self.env.user.flush() |
|||
self.env.user.invalidate_cache() |
|||
self.assertEqual( |
|||
self.env.user.email, |
|||
self.email, |
|||
"Write methods doesn't work (Field is not in registry?)", |
|||
) |
|||
|
|||
# changing company will automatically update website value to empty value |
|||
self.env.user.company_id = self.original_company |
|||
self.env.user.invalidate_cache() |
|||
self.assertEqual( |
|||
self.env.user.email, |
|||
self.original_email, |
|||
"Multi-email doesn't work on switching websites", |
|||
) |
|||
|
|||
def test_multi_email_partner(self): |
|||
"""Partner doesn't have email addresses per website""" |
|||
original_email = "original@email1" |
|||
new_email = "new@email2" |
|||
partner = self.env["res.partner"].create( |
|||
{"name": "test", "email": original_email} |
|||
) |
|||
self.switch_user_website() |
|||
# update partner's email |
|||
partner.email = new_email |
|||
self.assertEqual(partner.email, new_email) |
|||
# changing company will automatically update website value to empty value |
|||
self.env.user.company_id = self.original_company |
|||
self.env.user.invalidate_cache() |
|||
self.assertEqual( |
|||
partner.email, new_email, "Partner's email must not be Multi-website" |
|||
) |
@ -0,0 +1,14 @@ |
|||
<?xml version="1.0" encoding="utf-8" ?> |
|||
<!-- Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
License MIT (https://opensource.org/licenses/MIT). --> |
|||
<odoo> |
|||
<record id="view_website_multi_mail_form" model="ir.ui.view"> |
|||
<field name="model">website</field> |
|||
<field name="inherit_id" ref="website.view_website_form" /> |
|||
<field name="arch" type="xml"> |
|||
<xpath expr="//field[@name='default_lang_id']" position="after"> |
|||
<field name="mail_server_id" /> |
|||
</xpath> |
|||
</field> |
|||
</record> |
|||
</odoo> |
@ -0,0 +1,2 @@ |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
from . import mail_compose_message |
@ -0,0 +1,22 @@ |
|||
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
from odoo import api, models |
|||
from odoo.http import request |
|||
|
|||
|
|||
class MailComposer(models.TransientModel): |
|||
|
|||
_inherit = "mail.compose.message" |
|||
|
|||
@api.model |
|||
def create(self, vals): |
|||
"""Workaround for https://github.com/odoo/odoo/pull/26589""" |
|||
if "website_id" not in self.env.context: |
|||
website = ( |
|||
request and hasattr(request, "website") and request.website or None |
|||
) |
|||
if not website: |
|||
website = self.env["website"].get_current_website() |
|||
if website: |
|||
self = self.with_context(website_id=website.id) |
|||
return super(MailComposer, self).create(vals) |
@ -0,0 +1,38 @@ |
|||
.. image:: https://itpp.dev/images/infinity-readme.png |
|||
:alt: Tested and maintained by IT Projects Labs |
|||
:target: https://itpp.dev |
|||
|
|||
.. image:: https://img.shields.io/badge/license-MIT-blue.svg |
|||
:target: https://opensource.org/licenses/MIT |
|||
:alt: License: MIT |
|||
|
|||
==================== |
|||
Internal Messaging |
|||
==================== |
|||
|
|||
Send private messages to specified recipients, regardless of who are in followers list. |
|||
|
|||
Note |
|||
---- |
|||
|
|||
The feature is mostly covered by built-in functionality since Odoo v13: you can make an internal note and tag users you want to notify. |
|||
|
|||
Questions? |
|||
========== |
|||
|
|||
To get an assistance on this module contact us by email :arrow_right: help@itpp.dev |
|||
|
|||
Contributors |
|||
============ |
|||
* Pavel Romanchenko |
|||
|
|||
|
|||
Further information |
|||
=================== |
|||
|
|||
Odoo Apps Store: https://apps.odoo.com/apps/modules/13.0/mail_private/ |
|||
|
|||
|
|||
Notifications on updates: `via Atom <https://github.com/it-projects-llc/mail-addons/commits/13.0/mail_private.atom>`_, `by Email <https://blogtrottr.com/?subscribe=https://github.com/it-projects-llc/mail-addons/commits/13.0/mail_private.atom>`_ |
|||
|
|||
Tested on `Odoo 12.0 <https://github.com/odoo/odoo/commit/5240bc2303816348837425b88fc7ee3ff7de2336>`_ |
@ -0,0 +1,3 @@ |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
|
|||
from . import models |
@ -0,0 +1,26 @@ |
|||
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> |
|||
# Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/> |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
{ |
|||
"name": """Internal Messaging""", |
|||
"summary": """Send private messages to specified recipients, regardless of who are in followers list.""", |
|||
"category": "Discuss", |
|||
"images": [], |
|||
"version": "13.0.1.1.0", |
|||
"application": False, |
|||
"author": "IT-Projects LLC, Pavel Romanchenko", |
|||
"support": "help@itpp.dev", |
|||
"website": "https://twitter.com/OdooFree", |
|||
"license": "Other OSI approved licence", # MIT |
|||
"depends": ["mail"], |
|||
"external_dependencies": {"python": [], "bin": []}, |
|||
"data": ["template.xml", "full_composer_wizard.xml"], |
|||
"demo": [], |
|||
"qweb": ["static/src/xml/mail_private.xml"], |
|||
"post_load": None, |
|||
"pre_init_hook": None, |
|||
"post_init_hook": None, |
|||
"uninstall_hook": None, |
|||
"auto_install": False, |
|||
"installable": True, |
|||
} |
@ -0,0 +1,19 @@ |
|||
`1.1.0` |
|||
------- |
|||
|
|||
- **New**: internal users are flagged automatically. |
|||
|
|||
`1.0.2` |
|||
------- |
|||
|
|||
- **FIX:** Error when choosing a message template |
|||
|
|||
`1.0.1` |
|||
------- |
|||
|
|||
- **PORT:** Odoo 10 support. |
|||
|
|||
`1.0.0` |
|||
------- |
|||
|
|||
- Init version |
@ -0,0 +1,26 @@ |
|||
==================== |
|||
Internal Messaging |
|||
==================== |
|||
|
|||
Installation |
|||
============ |
|||
|
|||
Nothing special is needed to install this module. |
|||
|
|||
Usage |
|||
===== |
|||
|
|||
To send a message to specified recipients: |
|||
|
|||
* Click on the ``[Send internal message]``. |
|||
|
|||
* Choose the recipients that should receive a message by ticking the checkboxes. |
|||
|
|||
* To add more recipients click on the ``[Open the full mail composer]`` on the right upper corner of the message block and choose recipients in the **Recipient** field. |
|||
|
|||
* Click on the ``[Send]`` button. |
|||
|
|||
Uninstallation |
|||
============== |
|||
|
|||
Nothing special is needed to uninstall this module. |
@ -0,0 +1,27 @@ |
|||
<?xml version="1.0" encoding="utf-8" ?> |
|||
<!--Copyright 2017 Artyom Losev <https://github.com/ArtyomLosev> |
|||
Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/> |
|||
License MIT (https://opensource.org/licenses/MIT).--> |
|||
<odoo> |
|||
<record model="ir.ui.view" id="email_compose_message_wizard_form_private"> |
|||
<field name="name">mail.compose.message.form.private</field> |
|||
<field name="model">mail.compose.message</field> |
|||
<field name="groups_id" eval="[(4,ref('base.group_user'))]" /> |
|||
<field name="inherit_id" ref="mail.email_compose_message_wizard_form" /> |
|||
<field name="arch" type="xml"> |
|||
<data> |
|||
<xpath expr="//field[@name='active_domain']" position="after"> |
|||
<field name="is_private" invisible="1" /> |
|||
</xpath> |
|||
<xpath |
|||
expr="//div[@groups='base.group_user']/span[2]" |
|||
position="attributes" |
|||
> |
|||
<attribute name="attrs"> |
|||
{'invisible': [('is_private', '=', True)]} |
|||
</attribute> |
|||
</xpath> |
|||
</data> |
|||
</field> |
|||
</record> |
|||
</odoo> |
@ -0,0 +1,143 @@ |
|||
# Translation of Odoo Server. |
|||
# This file contains the translation of the following modules: |
|||
# * mail_private |
|||
# |
|||
# Translators: |
|||
# Randall Castro <rcastro@treintaycinco.com>, 2018 |
|||
msgid "" |
|||
msgstr "" |
|||
"Project-Id-Version: Odoo Server 10.0\n" |
|||
"Report-Msgid-Bugs-To: \n" |
|||
"POT-Creation-Date: 2017-12-19 16:49+0000\n" |
|||
"PO-Revision-Date: 2017-12-19 16:49+0000\n" |
|||
"Last-Translator: Randall Castro <rcastro@treintaycinco.com>, 2018\n" |
|||
"Language-Team: Spanish (https://www.transifex.com/it-projects-llc/" |
|||
"teams/76080/es/)\n" |
|||
"Language: es\n" |
|||
"MIME-Version: 1.0\n" |
|||
"Content-Type: text/plain; charset=UTF-8\n" |
|||
"Content-Transfer-Encoding: \n" |
|||
"Plural-Forms: nplurals=2; plural=(n != 1);\n" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:43 |
|||
#, python-format |
|||
msgid "Check the first one" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:30 |
|||
#, python-format |
|||
msgid "Click on Private mail creating button" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:35 |
|||
#, python-format |
|||
msgid "Dummy action" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#: model:ir.model,name:mail_private.model_mail_compose_message |
|||
msgid "Email composition wizard" |
|||
msgstr "Asistente para crear email" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:18 |
|||
#, python-format |
|||
msgid "Go to the list of subscribers" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:26 |
|||
#, python-format |
|||
msgid "Go to user page" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#: model:ir.model,name:mail_private.model_mail_message |
|||
msgid "Message" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:22 |
|||
#, python-format |
|||
msgid "Select a user" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:14 |
|||
#, python-format |
|||
msgid "Select channel settings" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#: model:ir.model.fields,field_description:mail_private.field_mail_compose_message__is_private |
|||
#: model:ir.model.fields,field_description:mail_private.field_mail_mail__is_private |
|||
#: model:ir.model.fields,field_description:mail_private.field_mail_message__is_private |
|||
msgid "Send Internal Message" |
|||
msgstr "Enviar mensaje interno" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:9 |
|||
#, python-format |
|||
msgid "Send a message to specified recipients only" |
|||
msgstr "Enviar un mensaje sólo a los destinatarios especificados" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:52 |
|||
#: code:addons/mail_private/static/src/js/test_private.js:55 |
|||
#, python-format |
|||
msgid "Send email" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:9 |
|||
#, python-format |
|||
msgid "Send internal message" |
|||
msgstr "Enviar mensaje interno" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:15 |
|||
#, python-format |
|||
msgid "To: Followers of" |
|||
msgstr "Para: Seguidores de" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:26 |
|||
#, python-format |
|||
msgid "Uncheck all" |
|||
msgstr "Desmarcar todos" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:39 |
|||
#, fuzzy, python-format |
|||
msgid "Uncheck all Followers" |
|||
msgstr "Desmarcar todos" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:46 |
|||
#, python-format |
|||
msgid "Write some email" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:20 |
|||
#, python-format |
|||
msgid "this document" |
|||
msgstr "este documento" |
@ -0,0 +1,143 @@ |
|||
# Translation of Odoo Server. |
|||
# This file contains the translation of the following modules: |
|||
# * mail_private |
|||
# |
|||
# Translators: |
|||
# Translation Bot <i18n-bot@it-projects.info>, 2017 |
|||
msgid "" |
|||
msgstr "" |
|||
"Project-Id-Version: Odoo Server 10.0\n" |
|||
"Report-Msgid-Bugs-To: \n" |
|||
"POT-Creation-Date: 2017-07-17 20:20+0000\n" |
|||
"PO-Revision-Date: 2017-07-17 20:20+0000\n" |
|||
"Last-Translator: Translation Bot <i18n-bot@it-projects.info>, 2017\n" |
|||
"Language-Team: Italian (https://www.transifex.com/it-projects-llc/" |
|||
"teams/76080/it/)\n" |
|||
"Language: it\n" |
|||
"MIME-Version: 1.0\n" |
|||
"Content-Type: text/plain; charset=UTF-8\n" |
|||
"Content-Transfer-Encoding: \n" |
|||
"Plural-Forms: nplurals=2; plural=(n != 1);\n" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:43 |
|||
#, python-format |
|||
msgid "Check the first one" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:30 |
|||
#, python-format |
|||
msgid "Click on Private mail creating button" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:35 |
|||
#, python-format |
|||
msgid "Dummy action" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#: model:ir.model,name:mail_private.model_mail_compose_message |
|||
msgid "Email composition wizard" |
|||
msgstr "Componi Email" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:18 |
|||
#, python-format |
|||
msgid "Go to the list of subscribers" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:26 |
|||
#, python-format |
|||
msgid "Go to user page" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#: model:ir.model,name:mail_private.model_mail_message |
|||
msgid "Message" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:22 |
|||
#, python-format |
|||
msgid "Select a user" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:14 |
|||
#, python-format |
|||
msgid "Select channel settings" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#: model:ir.model.fields,field_description:mail_private.field_mail_compose_message__is_private |
|||
#: model:ir.model.fields,field_description:mail_private.field_mail_mail__is_private |
|||
#: model:ir.model.fields,field_description:mail_private.field_mail_message__is_private |
|||
msgid "Send Internal Message" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:9 |
|||
#, python-format |
|||
msgid "Send a message to specified recipients only" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:52 |
|||
#: code:addons/mail_private/static/src/js/test_private.js:55 |
|||
#, python-format |
|||
msgid "Send email" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:9 |
|||
#, python-format |
|||
msgid "Send internal message" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:15 |
|||
#, python-format |
|||
msgid "To: Followers of" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:26 |
|||
#, python-format |
|||
msgid "Uncheck all" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:39 |
|||
#, python-format |
|||
msgid "Uncheck all Followers" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:46 |
|||
#, python-format |
|||
msgid "Write some email" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:20 |
|||
#, python-format |
|||
msgid "this document" |
|||
msgstr "" |
@ -0,0 +1,138 @@ |
|||
# Translation of Odoo Server. |
|||
# This file contains the translation of the following modules: |
|||
# * mail_private |
|||
# |
|||
msgid "" |
|||
msgstr "" |
|||
"Project-Id-Version: Odoo Server 12.0\n" |
|||
"Report-Msgid-Bugs-To: \n" |
|||
"Last-Translator: <>\n" |
|||
"Language-Team: \n" |
|||
"MIME-Version: 1.0\n" |
|||
"Content-Type: text/plain; charset=UTF-8\n" |
|||
"Content-Transfer-Encoding: \n" |
|||
"Plural-Forms: \n" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:43 |
|||
#, python-format |
|||
msgid "Check the first one" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:30 |
|||
#, python-format |
|||
msgid "Click on Private mail creating button" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:35 |
|||
#, python-format |
|||
msgid "Dummy action" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#: model:ir.model,name:mail_private.model_mail_compose_message |
|||
msgid "Email composition wizard" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:18 |
|||
#, python-format |
|||
msgid "Go to the list of subscribers" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:26 |
|||
#, python-format |
|||
msgid "Go to user page" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#: model:ir.model,name:mail_private.model_mail_message |
|||
msgid "Message" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:22 |
|||
#, python-format |
|||
msgid "Select a user" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:14 |
|||
#, python-format |
|||
msgid "Select channel settings" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#: model:ir.model.fields,field_description:mail_private.field_mail_compose_message__is_private |
|||
#: model:ir.model.fields,field_description:mail_private.field_mail_mail__is_private |
|||
#: model:ir.model.fields,field_description:mail_private.field_mail_message__is_private |
|||
msgid "Send Internal Message" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:9 |
|||
#, python-format |
|||
msgid "Send a message to specified recipients only" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:52 |
|||
#: code:addons/mail_private/static/src/js/test_private.js:55 |
|||
#, python-format |
|||
msgid "Send email" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:9 |
|||
#, python-format |
|||
msgid "Send internal message" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:15 |
|||
#, python-format |
|||
msgid "To: Followers of" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:26 |
|||
#, python-format |
|||
msgid "Uncheck all" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:39 |
|||
#, python-format |
|||
msgid "Uncheck all Followers" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:46 |
|||
#, python-format |
|||
msgid "Write some email" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:20 |
|||
#, python-format |
|||
msgid "this document" |
|||
msgstr "" |
|||
|
@ -0,0 +1,143 @@ |
|||
# Translation of Odoo Server. |
|||
# This file contains the translation of the following modules: |
|||
# * mail_private |
|||
# |
|||
# Translators: |
|||
# Translation Bot <i18n-bot@it-projects.info>, 2017 |
|||
msgid "" |
|||
msgstr "" |
|||
"Project-Id-Version: Odoo Server 10.0\n" |
|||
"Report-Msgid-Bugs-To: \n" |
|||
"POT-Creation-Date: 2017-12-15 13:31+0000\n" |
|||
"PO-Revision-Date: 2017-12-15 13:31+0000\n" |
|||
"Last-Translator: Translation Bot <i18n-bot@it-projects.info>, 2017\n" |
|||
"Language-Team: Portuguese (https://www.transifex.com/it-projects-llc/" |
|||
"teams/76080/pt/)\n" |
|||
"Language: pt\n" |
|||
"MIME-Version: 1.0\n" |
|||
"Content-Type: text/plain; charset=UTF-8\n" |
|||
"Content-Transfer-Encoding: \n" |
|||
"Plural-Forms: nplurals=2; plural=(n != 1);\n" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:43 |
|||
#, python-format |
|||
msgid "Check the first one" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:30 |
|||
#, python-format |
|||
msgid "Click on Private mail creating button" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:35 |
|||
#, python-format |
|||
msgid "Dummy action" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#: model:ir.model,name:mail_private.model_mail_compose_message |
|||
msgid "Email composition wizard" |
|||
msgstr "Assistente de composição de Email" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:18 |
|||
#, python-format |
|||
msgid "Go to the list of subscribers" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:26 |
|||
#, python-format |
|||
msgid "Go to user page" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#: model:ir.model,name:mail_private.model_mail_message |
|||
msgid "Message" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:22 |
|||
#, python-format |
|||
msgid "Select a user" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:14 |
|||
#, python-format |
|||
msgid "Select channel settings" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#: model:ir.model.fields,field_description:mail_private.field_mail_compose_message__is_private |
|||
#: model:ir.model.fields,field_description:mail_private.field_mail_mail__is_private |
|||
#: model:ir.model.fields,field_description:mail_private.field_mail_message__is_private |
|||
msgid "Send Internal Message" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:9 |
|||
#, python-format |
|||
msgid "Send a message to specified recipients only" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:52 |
|||
#: code:addons/mail_private/static/src/js/test_private.js:55 |
|||
#, python-format |
|||
msgid "Send email" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:9 |
|||
#, python-format |
|||
msgid "Send internal message" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:15 |
|||
#, python-format |
|||
msgid "To: Followers of" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:26 |
|||
#, python-format |
|||
msgid "Uncheck all" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:39 |
|||
#, python-format |
|||
msgid "Uncheck all Followers" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:46 |
|||
#, python-format |
|||
msgid "Write some email" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:20 |
|||
#, python-format |
|||
msgid "this document" |
|||
msgstr "" |
@ -0,0 +1,143 @@ |
|||
# Translation of Odoo Server. |
|||
# This file contains the translation of the following modules: |
|||
# * mail_private |
|||
# |
|||
# Translators: |
|||
# Translation Bot <i18n-bot@it-projects.info>, 2017 |
|||
msgid "" |
|||
msgstr "" |
|||
"Project-Id-Version: Odoo Server 10.0\n" |
|||
"Report-Msgid-Bugs-To: \n" |
|||
"POT-Creation-Date: 2017-12-15 13:31+0000\n" |
|||
"PO-Revision-Date: 2017-12-15 13:31+0000\n" |
|||
"Last-Translator: Translation Bot <i18n-bot@it-projects.info>, 2017\n" |
|||
"Language-Team: Portuguese (Brazil) (https://www.transifex.com/it-projects-" |
|||
"llc/teams/76080/pt_BR/)\n" |
|||
"Language: pt_BR\n" |
|||
"MIME-Version: 1.0\n" |
|||
"Content-Type: text/plain; charset=UTF-8\n" |
|||
"Content-Transfer-Encoding: \n" |
|||
"Plural-Forms: nplurals=2; plural=(n > 1);\n" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:43 |
|||
#, python-format |
|||
msgid "Check the first one" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:30 |
|||
#, python-format |
|||
msgid "Click on Private mail creating button" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:35 |
|||
#, python-format |
|||
msgid "Dummy action" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#: model:ir.model,name:mail_private.model_mail_compose_message |
|||
msgid "Email composition wizard" |
|||
msgstr "Assistente de Composição de Email" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:18 |
|||
#, python-format |
|||
msgid "Go to the list of subscribers" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:26 |
|||
#, python-format |
|||
msgid "Go to user page" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#: model:ir.model,name:mail_private.model_mail_message |
|||
msgid "Message" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:22 |
|||
#, python-format |
|||
msgid "Select a user" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:14 |
|||
#, python-format |
|||
msgid "Select channel settings" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#: model:ir.model.fields,field_description:mail_private.field_mail_compose_message__is_private |
|||
#: model:ir.model.fields,field_description:mail_private.field_mail_mail__is_private |
|||
#: model:ir.model.fields,field_description:mail_private.field_mail_message__is_private |
|||
msgid "Send Internal Message" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:9 |
|||
#, python-format |
|||
msgid "Send a message to specified recipients only" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:52 |
|||
#: code:addons/mail_private/static/src/js/test_private.js:55 |
|||
#, python-format |
|||
msgid "Send email" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:9 |
|||
#, python-format |
|||
msgid "Send internal message" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:15 |
|||
#, python-format |
|||
msgid "To: Followers of" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:26 |
|||
#, python-format |
|||
msgid "Uncheck all" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:39 |
|||
#, python-format |
|||
msgid "Uncheck all Followers" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/js/test_private.js:46 |
|||
#, python-format |
|||
msgid "Write some email" |
|||
msgstr "" |
|||
|
|||
#. module: mail_private |
|||
#. openerp-web |
|||
#: code:addons/mail_private/static/src/xml/mail_private.xml:20 |
|||
#, python-format |
|||
msgid "this document" |
|||
msgstr "" |
After Width: 766 | Height: 387 | Size: 33 KiB |
@ -0,0 +1,67 @@ |
|||
# Copyright 2016 x620 <https://github.com/x620> |
|||
# Copyright 2017 Artyom Losev <https://github.com/ArtyomLosev> |
|||
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
# Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/> |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
from odoo import fields, models |
|||
|
|||
|
|||
class MailComposeMessage(models.TransientModel): |
|||
_inherit = "mail.compose.message" |
|||
|
|||
is_private = fields.Boolean(string="Send Internal Message") |
|||
|
|||
|
|||
class MailThread(models.AbstractModel): |
|||
_inherit = "mail.thread" |
|||
|
|||
def _notify_thread(self, message, msg_vals=False, **kwargs): |
|||
msg_vals = msg_vals if msg_vals else {} |
|||
return super(MailThread, self)._notify_thread(message, msg_vals) |
|||
|
|||
def _notify_compute_recipients(self, message, msg_vals): |
|||
recipient_data = super(MailThread, self)._notify_compute_recipients( |
|||
message, msg_vals |
|||
) |
|||
if "is_private" in message._context: |
|||
pids = ( |
|||
[x for x in msg_vals.get("partner_ids")] |
|||
if "partner_ids" in msg_vals |
|||
else self.sudo().partner_ids.ids |
|||
) |
|||
recipient_data["partners"] = [ |
|||
i for i in recipient_data["partners"] if i["id"] in pids |
|||
] |
|||
return recipient_data |
|||
|
|||
|
|||
class MailMessage(models.Model): |
|||
_inherit = "mail.message" |
|||
|
|||
is_private = fields.Boolean(string="Send Internal Message") |
|||
|
|||
def send_recepients_for_internal_message(self, model, domain): |
|||
result = [] |
|||
default_resource = self.env[model].search(domain) |
|||
follower_ids = default_resource.message_follower_ids |
|||
internal_ids = self.get_internal_users_ids() |
|||
|
|||
recipient_ids = [r.partner_id for r in follower_ids if r.partner_id] |
|||
|
|||
for recipient in recipient_ids: |
|||
result.append( |
|||
{ |
|||
"checked": recipient.user_ids.id in internal_ids, |
|||
"partner_id": recipient.id, |
|||
"full_name": recipient.name, |
|||
"name": recipient.name, |
|||
"email_address": recipient.email, |
|||
"reason": "Recipient", |
|||
} |
|||
) |
|||
|
|||
return result |
|||
|
|||
def get_internal_users_ids(self): |
|||
internal_users_ids = self.env["res.users"].search([("share", "=", False)]).ids |
|||
return internal_users_ids |
After Width: 1092 | Height: 692 | Size: 108 KiB |
After Width: 100 | Height: 100 | Size: 2.1 KiB |
@ -0,0 +1,118 @@ |
|||
<section class="oe_container"> |
|||
<div class="oe_row oe_spaced"> |
|||
<div class="oe_span12"> |
|||
<h2 class="oe_slogan">Internal Messaging</h2> |
|||
<h3 class="oe_slogan">Send private messages to specified recipients, regardless of who are in followers list.</h3> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
|
|||
<section class="oe_container"> |
|||
<div class="oe_row oe_spaced"> |
|||
<div class="oe_span12"> |
|||
<p class="oe_mt32"> |
|||
<div class="alert alert-warning" style="padding:0.6em 0.6em; font-size: 120%;"> |
|||
By default, to send a private message to specific recipient(s) you need to delete other followers included to the same document. The module allows to send private messages to recipients you specified, regardless of who are in the followers list. |
|||
</div> |
|||
</p> |
|||
<div class="alert alert-success" style="padding:0.3em 0.6em; font-size: 120%;"> |
|||
<ul style="padding-left: 1.2em;"> |
|||
<li style="margin-top: 0.3em;"> |
|||
It simplify internal communication in leads, when you need to send some private messages to your colleagues before reply to a customer. |
|||
</li> |
|||
<li style="margin-top: 0.3em;"> |
|||
It helps in records like project tasks: in a task with many participants, you can easily send message and only specified colleagues will be notified. |
|||
</li> |
|||
<li style="margin-top: 0.3em;"> |
|||
It allows to have clean inbox for everybody. It's very essential, because people often neglect important message in Inbox full of useless messages. |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
|
|||
<section class="oe_container oe_dark"> |
|||
<div class="oe_row oe_spaced"> |
|||
<div class="oe_span12 text-center"> |
|||
<h3 class="oe_slogan">How it works</h3> |
|||
<p class="oe_mt32"> |
|||
<font style="font-size: 120%;"> |
|||
Click <b>Send internal message</b>, choose the recipient(s) and send a message. |
|||
</font> |
|||
</p> |
|||
<div class="oe_screenshot" align="center"> |
|||
<img style="max-width: 80%" src="check_recipients.png?1"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
|
|||
<section class="oe_container"> |
|||
<div class="oe_row oe_spaced"> |
|||
<div class="oe_span12 text-center"> |
|||
<p class="oe_mt32"> |
|||
<font style="font-size: 120%;"> |
|||
As a result, the message is sent to the corresponding recipient(s). Other followers will not receive the notification. |
|||
</font> |
|||
</p> |
|||
<div class="oe_screenshot" align="center"> |
|||
<img style="max-width: 80%" src="result_message.png?2"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
|
|||
|
|||
<section class="oe_container"> |
|||
<div class="oe_row oe_spaced"> |
|||
<div class="oe_span8"> |
|||
<h2>Need our service?</h2> |
|||
<p class="oe_mt32">Contact us by <a href="mailto:apps@it-projects.info">email</a> or fill out <a href="https://www.it-projects.info/page/website.contactus " target="_blank">request form</a></p> |
|||
<ul> |
|||
<li><a href="mailto:apps@it-projects.info">apps@it-projects.info <i class="fa fa-envelope-o"></i></a></li> |
|||
<li><a href="https://www.it-projects.info/page/website.contactus " target="_blank">https://www.it-projects.info/page/website.contactus <i class="fa fa-list-alt"></i></a></li> |
|||
<li><a href="https://m.me/itprojectsllc" target="_blank">https://m.me/itprojectsllc <i class="fa fa-facebook-square"></i></a></li> |
|||
<li>skype@it-projects.info <i class="fa fa-skype"></i></li> |
|||
</ul> |
|||
</div> |
|||
<div class="oe_span4"> |
|||
<div class="stamp" style="width:200px;"> |
|||
<div style="margin-top: 15px; |
|||
position: relative; |
|||
font-family:'Vollkorn', serif; |
|||
font-size: 16px; |
|||
line-height: 25px; |
|||
text-transform: uppercase; |
|||
font-weight: bold; |
|||
color: #75526b; |
|||
border: 3px dashed #75526b; |
|||
float: left; |
|||
padding: 4px 12px; |
|||
-webkit-transform: rotate(6deg); |
|||
-o-transform: rotate(6deg); |
|||
-moz-transform: rotate(6deg); |
|||
-ms-transform: rotate(6deg);"> |
|||
Tested on Odoo<br/>11.0 community |
|||
</div> |
|||
<div style="margin-top: 15px; |
|||
position: relative; |
|||
font-family:'Vollkorn', serif; |
|||
font-size: 16px; |
|||
line-height: 25px; |
|||
text-transform: uppercase; |
|||
font-weight: bold; |
|||
color: #75526b; |
|||
border: 3px dashed #75526b; |
|||
float: left; |
|||
padding: 4px 12px; |
|||
-webkit-transform: rotate(-7deg); |
|||
-o-transform: rotate(-7deg); |
|||
-moz-transform: rotate(-7deg); |
|||
-ms-transform: rotate(-7deg);"> |
|||
Tested on Odoo<br/>11.0 enterprise |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</section> |
After Width: 1154 | Height: 838 | Size: 94 KiB |
@ -0,0 +1,290 @@ |
|||
/* Copyright 2016-2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
Copyright 2016 manavi <https://github.com/manawi>
|
|||
Copyright 2017-2018 Artyom Losev <https://github.com/ArtyomLosev>
|
|||
Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
|
|||
Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/>
|
|||
License MIT (https://opensource.org/licenses/MIT). */
|
|||
odoo.define("mail_private", function (require) { |
|||
"use strict"; |
|||
|
|||
var Chatter = require("mail.Chatter"); |
|||
var ChatterComposer = require("mail.composer.Chatter"); |
|||
var session = require("web.session"); |
|||
|
|||
var rpc = require("web.rpc"); |
|||
var mailUtils = require("mail.utils"); |
|||
|
|||
Chatter.include({ |
|||
init: function () { |
|||
this._super.apply(this, arguments); |
|||
this.private = false; |
|||
this.events["click .oe_compose_post_private"] = |
|||
"on_open_composer_private_message"; |
|||
}, |
|||
|
|||
on_open_composer_private_message: function (event) { |
|||
var self = this; |
|||
this.fetch_recipients_for_internal_message().then(function (data) { |
|||
self._openComposer({ |
|||
is_private: true, |
|||
suggested_partners: data, |
|||
}); |
|||
}); |
|||
}, |
|||
|
|||
_openComposer: function (options) { |
|||
var self = this; |
|||
var old_composer = this._composer; |
|||
// Create the new composer
|
|||
this._composer = new ChatterComposer( |
|||
this, |
|||
this.record.model, |
|||
options.suggested_partners || [], |
|||
{ |
|||
commandsEnabled: false, |
|||
context: this.context, |
|||
inputMinHeight: 50, |
|||
isLog: options && options.isLog, |
|||
recordName: this.recordName, |
|||
defaultBody: |
|||
old_composer && |
|||
old_composer.$input && |
|||
old_composer.$input.val(), |
|||
defaultMentionSelections: |
|||
old_composer && old_composer.getMentionListenerSelections(), |
|||
attachmentIds: |
|||
(old_composer && old_composer.get("attachment_ids")) || [], |
|||
is_private: options.is_private, |
|||
} |
|||
); |
|||
this._composer.on("input_focused", this, function () { |
|||
this._composer.mentionSetPrefetchedPartners( |
|||
this._mentionSuggestions || [] |
|||
); |
|||
}); |
|||
this._composer.insertAfter(this.$(".o_chatter_topbar")).then(function () { |
|||
// Destroy existing composer
|
|||
if (old_composer) { |
|||
old_composer.destroy(); |
|||
} |
|||
self._composer.focus(); |
|||
self._composer.on("post_message", self, function (messageData) { |
|||
if (options.is_private) { |
|||
self._composer.options.isLog = true; |
|||
} |
|||
self._discardOnReload(messageData).then(function () { |
|||
self._disableComposer(); |
|||
self.fields.thread |
|||
.postMessage(messageData) |
|||
.then(function () { |
|||
self._closeComposer(true); |
|||
if (self._reloadAfterPost(messageData)) { |
|||
self.trigger_up("reload"); |
|||
} else if (messageData.attachment_ids.length) { |
|||
self._reloadAttachmentBox(); |
|||
self.trigger_up("reload", { |
|||
fieldNames: ["message_attachment_count"], |
|||
keepChanges: true, |
|||
}); |
|||
} |
|||
}) |
|||
.guardedCatch(function () { |
|||
self._enableComposer(); |
|||
}); |
|||
}); |
|||
}); |
|||
var toggle_post_private = self._composer.options.is_private || false; |
|||
self._composer.on( |
|||
"need_refresh", |
|||
self, |
|||
self.trigger_up.bind(self, "reload") |
|||
); |
|||
self._composer.on( |
|||
"close_composer", |
|||
null, |
|||
self._closeComposer.bind(self, true) |
|||
); |
|||
|
|||
self.$el.addClass("o_chatter_composer_active"); |
|||
self.$( |
|||
".o_chatter_button_new_message, .o_chatter_button_log_note, .oe_compose_post_private" |
|||
).removeClass("o_active"); |
|||
self.$(".o_chatter_button_new_message").toggleClass( |
|||
"o_active", |
|||
!self._composer.options.isLog && !self._composer.options.is_private |
|||
); |
|||
self.$(".o_chatter_button_log_note").toggleClass( |
|||
"o_active", |
|||
self._composer.options.isLog && !options.is_private |
|||
); |
|||
self.$(".oe_compose_post_private").toggleClass( |
|||
"o_active", |
|||
toggle_post_private |
|||
); |
|||
}); |
|||
}, |
|||
|
|||
fetch_recipients_for_internal_message: function () { |
|||
var self = this; |
|||
self.result = {}; |
|||
var follower_ids_domain = [["id", "=", self.context.default_res_id]]; |
|||
return rpc |
|||
.query({ |
|||
model: "mail.message", |
|||
method: "send_recepients_for_internal_message", |
|||
args: [[], self.context.default_model, follower_ids_domain], |
|||
}) |
|||
.then(function (res) { |
|||
return _.filter(res, function (obj) { |
|||
return obj.partner_id !== session.partner_id; |
|||
}); |
|||
}); |
|||
}, |
|||
}); |
|||
|
|||
ChatterComposer.include({ |
|||
init: function (parent, model, suggestedPartners, options) { |
|||
this._super(parent, model, suggestedPartners, options); |
|||
this.events["click .oe_composer_uncheck"] = "on_uncheck_recipients"; |
|||
if (typeof options.is_private === "undefined") { |
|||
// Otherwise it causes an error in context creating function
|
|||
options.is_private = false; |
|||
} |
|||
}, |
|||
|
|||
_preprocessMessage: function () { |
|||
var self = this; |
|||
var def = $.Deferred(); |
|||
this._super().then(function (message) { |
|||
message = _.extend(message, { |
|||
subtype: "mail.mt_comment", |
|||
message_type: "comment", |
|||
context: _.defaults({}, self.context, session.user_context), |
|||
}); |
|||
|
|||
// Subtype
|
|||
if (self.options.isLog) { |
|||
message.subtype = "mail.mt_note"; |
|||
} |
|||
|
|||
if (self.options.is_private) { |
|||
message.context.is_private = true; |
|||
message.channel_ids = self.get_checked_channel_ids(); |
|||
} |
|||
|
|||
// Partner_ids
|
|||
if (self.options.isLog) { |
|||
def.resolve(message); |
|||
} else { |
|||
var check_suggested_partners = self._getCheckedSuggestedPartners(); |
|||
self._checkSuggestedPartners(check_suggested_partners).then( |
|||
function (partnerIDs) { |
|||
message.partner_ids = (message.partner_ids || []).concat( |
|||
partnerIDs |
|||
); |
|||
// Update context
|
|||
message.context = _.defaults({}, message.context, { |
|||
mail_post_autofollow: true, |
|||
}); |
|||
def.resolve(message); |
|||
} |
|||
); |
|||
} |
|||
}); |
|||
return def; |
|||
}, |
|||
|
|||
on_uncheck_recipients: function () { |
|||
this.$(".o_composer_suggested_partners input:checked").each(function () { |
|||
$(this).prop("checked", false); |
|||
}); |
|||
}, |
|||
|
|||
_onOpenFullComposer: function () { |
|||
if (!this._doCheckAttachmentUpload()) { |
|||
return false; |
|||
} |
|||
var self = this; |
|||
var recipientDoneDef = $.Deferred(); |
|||
this.trigger_up("discard_record_changes", { |
|||
proceed: function () { |
|||
if (self.options.isLog) { |
|||
recipientDoneDef.resolve([]); |
|||
} else { |
|||
var checkedSuggestedPartners = self._getCheckedSuggestedPartners(); |
|||
self._checkSuggestedPartners(checkedSuggestedPartners).then( |
|||
recipientDoneDef.resolve.bind(recipientDoneDef) |
|||
); |
|||
} |
|||
}, |
|||
}); |
|||
recipientDoneDef.then(function (partnerIDs) { |
|||
var context = { |
|||
default_parent_id: self.id, |
|||
default_body: mailUtils.getTextToHTML(self.$input.val()), |
|||
default_attachment_ids: _.pluck(self.get("attachment_ids"), "id"), |
|||
default_partner_ids: partnerIDs, |
|||
default_is_log: self.options.isLog, |
|||
mail_post_autofollow: true, |
|||
is_private: self.options.is_private, |
|||
}; |
|||
|
|||
if (self.options && self.options.is_private) { |
|||
context.default_is_private = self.options.is_private; |
|||
} |
|||
|
|||
if (self.context.default_model && self.context.default_res_id) { |
|||
context.default_model = self.context.default_model; |
|||
context.default_res_id = self.context.default_res_id; |
|||
} |
|||
var action = { |
|||
type: "ir.actions.act_window", |
|||
res_model: "mail.compose.message", |
|||
view_mode: "form", |
|||
view_type: "form", |
|||
views: [[false, "form"]], |
|||
target: "new", |
|||
context: context, |
|||
}; |
|||
self.do_action(action, { |
|||
on_close: self.trigger.bind(self, "need_refresh"), |
|||
}).then(self.trigger.bind(self, "close_composer")); |
|||
}); |
|||
}, |
|||
|
|||
_getCheckedSuggestedPartners: function () { |
|||
var checked_partners = this._super(this, arguments); |
|||
// Workaround: odoo code works only when all partners are checked intially,
|
|||
// while may select only some of them (internal recepients)
|
|||
_.each(checked_partners, function (partner) { |
|||
partner.checked = true; |
|||
}); |
|||
checked_partners = _.uniq( |
|||
_.filter(checked_partners, function (obj) { |
|||
return obj.reason !== "Channel"; |
|||
}) |
|||
); |
|||
this.get_checked_channel_ids(); |
|||
return checked_partners; |
|||
}, |
|||
|
|||
get_checked_channel_ids: function () { |
|||
var self = this; |
|||
var checked_channels = []; |
|||
this.$(".o_composer_suggested_partners input:checked").each(function () { |
|||
var full_name = $(this).data("fullname"); |
|||
checked_channels = checked_channels.concat( |
|||
_.filter(self.suggested_partners, function (item) { |
|||
return full_name === item.full_name; |
|||
}) |
|||
); |
|||
}); |
|||
checked_channels = _.uniq( |
|||
_.filter(checked_channels, function (obj) { |
|||
return obj.reason === "Channel"; |
|||
}) |
|||
); |
|||
return _.pluck(checked_channels, "channel_id"); |
|||
}, |
|||
}); |
|||
}); |
@ -0,0 +1,72 @@ |
|||
/* Copyright 2018-2019 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> |
|||
Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/>
|
|||
License MIT (https://opensource.org/licenses/MIT).*/
|
|||
odoo.define("mail_private.tour", function (require) { |
|||
"use strict"; |
|||
|
|||
var core = require("web.core"); |
|||
var tour = require("web_tour.tour"); |
|||
var _t = core._t; |
|||
|
|||
var email = "mail_private test email"; |
|||
var steps = [ |
|||
tour.STEPS.SHOW_APPS_MENU_ITEM, |
|||
{ |
|||
trigger: ".fa.fa-cog.o_mail_channel_settings", |
|||
content: _t("Select channel settings"), |
|||
position: "bottom", |
|||
}, |
|||
{ |
|||
trigger: '.nav-link:contains("Members")', |
|||
content: _t("Go to the list of subscribers"), |
|||
position: "bottom", |
|||
}, |
|||
{ |
|||
trigger: '.o_data_cell:contains("YourCompany, Marc Demo")', |
|||
content: _t("Select a user"), |
|||
position: "bottom", |
|||
}, |
|||
{ |
|||
trigger: '.o_form_uri.o_field_widget:contains("YourCompany, Marc Demo")', |
|||
content: _t("Go to user page"), |
|||
position: "bottom", |
|||
}, |
|||
{ |
|||
trigger: "button.oe_compose_post_private", |
|||
content: _t("Click on Private mail creating button"), |
|||
position: "bottom", |
|||
}, |
|||
{ |
|||
// For some reason (due to tricky renderings) button.oe_composer_uncheck could not be find by the tour manager
|
|||
trigger: ".o_control_panel.o_breadcrumb_full li.active", |
|||
content: _t("Dummy action"), |
|||
}, |
|||
{ |
|||
trigger: "button.oe_composer_uncheck", |
|||
extra_trigger: "button.oe_composer_uncheck", |
|||
content: _t("Uncheck all Followers"), |
|||
timeout: 10000, |
|||
}, |
|||
{ |
|||
trigger: "div.o_composer_suggested_partners", |
|||
content: _t("Check the first one"), |
|||
}, |
|||
{ |
|||
trigger: "textarea.o_composer_text_field:first", |
|||
content: _t("Write some email"), |
|||
run: function () { |
|||
$("textarea.o_composer_text_field:first").val(email); |
|||
}, |
|||
}, |
|||
{ |
|||
trigger: ".o_composer_send .o_composer_button_send", |
|||
content: _t("Send email"), |
|||
}, |
|||
{ |
|||
trigger: ".o_mail_thread .o_thread_message:contains(" + email + ")", |
|||
content: _t("Send email"), |
|||
}, |
|||
]; |
|||
|
|||
tour.register("mail_private_tour", {test: true, url: "/web"}, steps); |
|||
}); |
@ -0,0 +1,35 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<!--Copyright 2016-2017 Ivan Yelizariev <https://it-projects.info/team/yelizariev> |
|||
Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/> |
|||
License MIT (https://opensource.org/licenses/MIT).--> |
|||
<template> |
|||
<t t-extend="mail.chatter.Buttons"> |
|||
<t t-jquery="button[title='Send a message']" t-operation="after"> |
|||
<button |
|||
class="btn btn-link oe_compose_post_private" |
|||
title="Send a message to specified recipients only" |
|||
> |
|||
Send internal message |
|||
</button> |
|||
</t> |
|||
</t> |
|||
<t t-extend="mail.chatter.Composer"> |
|||
<t t-jquery="small[class='o_chatter_composer_info']" t-operation="replace"> |
|||
<small class="o_chatter_composer_info" t-if="!widget.options.is_private"> |
|||
To: Followers of |
|||
<t t-if="widget.options.record_name"> |
|||
"<t t-esc="widget.options.record_name" />" |
|||
</t> |
|||
<t t-if="!widget.options.record_name">this document</t> |
|||
</small> |
|||
</t> |
|||
<t t-jquery="div[class='o_composer_suggested_partners']" t-operation="after"> |
|||
<button |
|||
class="btn btn-link oe_composer_uncheck" |
|||
t-if="widget.options.is_private" |
|||
> |
|||
Uncheck all |
|||
</button> |
|||
</t> |
|||
</t> |
|||
</template> |
@ -0,0 +1,22 @@ |
|||
<?xml version="1.0" encoding="utf-8" ?> |
|||
<!--Copyright 2016 x620 <https://github.com/x620> |
|||
Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/> |
|||
License MIT (https://opensource.org/licenses/MIT).--> |
|||
<odoo> |
|||
<template |
|||
id="assets_backend" |
|||
name="mail_private_assets_backend" |
|||
inherit_id="web.assets_backend" |
|||
> |
|||
<xpath expr="." position="inside"> |
|||
<script |
|||
type="text/javascript" |
|||
src="/mail_private/static/src/js/mail_private.js" |
|||
/> |
|||
<script |
|||
type="text/javascript" |
|||
src="/mail_private/static/src/js/test_private.js" |
|||
/> |
|||
</xpath> |
|||
</template> |
|||
</odoo> |
@ -0,0 +1,3 @@ |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
|
|||
from . import test_js |
@ -0,0 +1,28 @@ |
|||
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> |
|||
# Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/> |
|||
# License MIT (https://opensource.org/licenses/MIT). |
|||
|
|||
# import odoo.tests |
|||
|
|||
|
|||
# @odoo.tests.common.at_install(True) |
|||
# @odoo.tests.common.post_install(True) |
|||
# class TestUi(odoo.tests.HttpCase): |
|||
# def test_01_mail_private(self): |
|||
# # needed because tests are run before the module is marked as |
|||
# # installed. In js web will only load qweb coming from modules |
|||
# # that are returned by the backend in module_boot. Without |
|||
# # this you end up with js, css but no qweb. |
|||
# cr = self.registry.cursor() |
|||
# self.env["ir.module.module"].search( |
|||
# [("name", "=", "mail_private")], limit=1 |
|||
# ).state = "installed" |
|||
# cr._lock.release() |
|||
|
|||
# self.phantom_js( |
|||
# "/web", |
|||
# "odoo.__DEBUG__.services['web_tour.tour'].run('mail_private_tour', 1000)", |
|||
# "odoo.__DEBUG__.services['web_tour.tour'].tours.mail_private_tour.ready", |
|||
# login="admin", |
|||
# timeout=90, |
|||
# ) |
@ -0,0 +1,4 @@ |
|||
OCA_web https://github.com/it-projects-llc/web.git |
|||
access-addons https://github.com/it-projects-llc/access-addons.git |
|||
misc-addons https://github.com/it-projects-llc/misc-addons |
|||
website-addons https://github.com/it-projects-llc/website-addons |