Browse Source

Merge pull request #260 from yelizariev/12.0-pre-commit-cleanups

commit is created by 👷‍♂️ Merge Bot: https://odoo-devops.readthedocs.io/en/latest/git/github-merge-bot.html
pull/264/head
Mitchell Admin 5 years ago
committed by GitHub
parent
commit
4af1aa2db1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .DINAR/image/README.md
  2. 1
      .DINAR/image/src/addons.yaml
  3. 13
      .github/workflows/main.yml
  4. 2
      .isort.cfg
  5. 19
      .travis.yml
  6. 24
      mail_all/__manifest__.py
  7. 77
      mail_all/static/src/js/mail_all.js
  8. 32
      mail_all/static/src/js/test_mail_all.js
  9. 11
      mail_all/static/src/xml/menu.xml
  10. 17
      mail_all/tests/test_js.py
  11. 22
      mail_all/views/templates.xml
  12. 23
      mail_archives/__manifest__.py
  13. 159
      mail_archives/static/src/js/archives.js
  14. 12
      mail_archives/static/src/xml/menu.xml
  15. 1
      mail_archives/tests/__init__.py
  16. 17
      mail_archives/tests/test_js.py
  17. 20
      mail_archives/views/templates.xml
  18. 1
      mail_base/__init__.py
  19. 18
      mail_base/__manifest__.py
  20. 7
      mail_base/controllers/main.py
  21. 21
      mail_base/models.py
  22. 1
      mail_base/tests/__init__.py
  23. 7
      mail_base/tests/test_default.py
  24. 12
      mail_base/views/templates.xml
  25. 26
      mail_check_immediately/__manifest__.py
  26. 51
      mail_check_immediately/models.py
  27. 53
      mail_check_immediately/static/src/js/main.js
  28. 14
      mail_check_immediately/static/src/xml/main.xml
  29. 11
      mail_check_immediately/views.xml
  30. 4
      mail_fix_553/__manifest__.py
  31. 2
      mail_fix_553/data.xml
  32. 160
      mail_fix_553/mail_fix_553.py
  33. 33
      mail_move_message/__manifest__.py
  34. 5
      mail_move_message/controllers/main.py
  35. 3
      mail_move_message/data/mail_move_message_data.xml
  36. 517
      mail_move_message/mail_move_message_models.py
  37. 135
      mail_move_message/mail_move_message_views.xml
  38. 5
      mail_move_message/static/src/css/mail_move_message.css
  39. 149
      mail_move_message/static/src/js/mail_move_message.js
  40. 11
      mail_move_message/static/src/xml/mail_move_message_main.xml
  41. 1
      mail_move_message/tests/__init__.py
  42. 13
      mail_move_message/tests/test_mail_move.py
  43. 18
      mail_multi_website/__init__.py
  44. 15
      mail_multi_website/__manifest__.py
  45. 12
      mail_multi_website/models/ir_property.py
  46. 8
      mail_multi_website/models/mail_message.py
  47. 112
      mail_multi_website/models/mail_template.py
  48. 29
      mail_multi_website/models/mail_thread.py
  49. 18
      mail_multi_website/models/res_users.py
  50. 6
      mail_multi_website/models/website.py
  51. 53
      mail_multi_website/tests/test_fetch.py
  52. 16
      mail_multi_website/tests/test_mail_model.py
  53. 147
      mail_multi_website/tests/test_render.py
  54. 49
      mail_multi_website/tests/test_send.py
  55. 6
      mail_multi_website/views/website_views.xml
  56. 12
      mail_multi_website/wizard/mail_compose_message.py
  57. 23
      mail_private/__manifest__.py
  58. 15
      mail_private/full_composer_wizard.xml
  59. 79
      mail_private/models.py
  60. 485
      mail_private/static/src/js/mail_private.js
  61. 67
      mail_private/static/src/js/test_private.js
  62. 15
      mail_private/static/src/xml/mail_private.xml
  63. 19
      mail_private/template.xml
  64. 16
      mail_private/tests/test_js.py
  65. 22
      mail_recovery/__manifest__.py
  66. 13
      mail_recovery/data.xml
  67. 15
      mail_recovery/static/src/js/mail_recovery.js
  68. 16
      mail_reply/__manifest__.py
  69. 76
      mail_reply/static/src/js/mail_reply.js
  70. 14
      mail_reply/static/src/xml/reply_button.xml
  71. 15
      mail_reply/templates.xml
  72. 1
      mail_reply/tests/__init__.py
  73. 7
      mail_reply/tests/test_default.py
  74. 23
      mail_sent/__manifest__.py
  75. 38
      mail_sent/models.py
  76. 185
      mail_sent/static/src/js/sent.js
  77. 15
      mail_sent/static/src/xml/menu.xml
  78. 1
      mail_sent/tests/__init__.py
  79. 17
      mail_sent/tests/test_js.py
  80. 14
      mail_sent/views/templates.xml
  81. 16
      mail_to/__manifest__.py
  82. 15
      mail_to/static/src/xml/recipient.xml
  83. 12
      mail_to/templates.xml
  84. 47
      mail_to/tests/test_default.py
  85. 23
      mailgun/__manifest__.py
  86. 16
      mailgun/controllers/main.py
  87. 6
      mailgun/data/ir_cron_data.xml
  88. 20
      mailgun/models/ir_config_parameter.py
  89. 19
      mailgun/models/mail_thread.py
  90. 22
      res_partner_company_messages/__manifest__.py
  91. 18
      res_partner_company_messages/models.py
  92. 2
      res_partner_company_messages/views.xml
  93. 16
      res_partner_mails_count/__manifest__.py
  94. 15
      res_partner_mails_count/models.py
  95. 44
      res_partner_mails_count/static/src/js/res_partner_mails_count_tour.js
  96. 35
      res_partner_mails_count/templates.xml
  97. 2
      res_partner_mails_count/tests/__init__.py
  98. 67
      res_partner_mails_count/tests/test_mail.py
  99. 17
      res_partner_mails_count/tests/test_phantom.py
  100. 2
      res_partner_mails_count/views/res_partner_mails_count.xml

6
.DINAR/image/README.md

@ -1,2 +1,4 @@
This folder is attached on image building as `custom/` folder in [doobba](https://github.com/Tecnativa/doodba#image-usage).
Few additional [files](https://github.com/itpp-labs/DINAR/tree/master/embedded-files/.DINAR/image) are attached temporary on image building.
This folder is attached on image building as `custom/` folder in
[doobba](https://github.com/Tecnativa/doodba#image-usage). Few additional
[files](https://github.com/itpp-labs/DINAR/tree/master/embedded-files/.DINAR/image) are
attached temporary on image building.

1
.DINAR/image/src/addons.yaml

@ -1,5 +1,4 @@
# see https://github.com/Tecnativa/doodba#optodoocustomsrcaddonsyaml
---
ENV:
DEFAULT_REPO_PATTERN: https://github.com/it-projects-llc/{}.git

13
.github/workflows/main.yml

@ -6,11 +6,14 @@ on:
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send notifications to Telegram
run: curl -s -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage -d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} -d text="${MESSAGE}" >> /dev/null
env:
MESSAGE: "Issue ${{ github.event.action }}: \n${{ github.event.issue.html_url }}"
- name: Send notifications to Telegram
run:
curl -s -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN
}}/sendMessage -d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} -d text="${MESSAGE}"
>> /dev/null
env:
MESSAGE:
"Issue ${{ github.event.action }}: \n${{ github.event.issue.html_url }}"

2
.isort.cfg

@ -9,4 +9,4 @@ line_length=88
known_odoo=odoo
known_odoo_addons=odoo.addons
sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER
known_third_party=
known_third_party=requests,simplejson,werkzeug

19
.travis.yml

@ -12,23 +12,24 @@ addons:
postgresql: "9.5"
apt:
packages:
- expect-dev # provides unbuffer utility
- python-lxml # because pip installation is slow
- expect-dev # provides unbuffer utility
- python-lxml # because pip installation is slow
env:
global:
- VERSION="12.0" TESTS="0" LINT_CHECK="0" UNIT_TEST="0"
- PYLINT_ODOO_JSLINTRC="/home/travis/maintainer-quality-tools/travis/cfg/.jslintrc"
- VERSION="12.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"
- 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
- 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

24
mail_all/__manifest__.py

@ -7,34 +7,24 @@
"summary": """Checkout all messages where you have access""",
"category": "Discuss",
# "live_test_url": "",
"images": ['images/1.jpg'],
"images": ["images/1.jpg"],
"version": "12.0.1.0.1",
"application": False,
"author": "IT-Projects LLC, Pavel Romanchenko",
"support": "apps@it-projects.info",
"website": "https://it-projects.info",
"license": "LGPL-3",
'price': 40.00,
'currency': 'EUR',
"depends": [
"mail"
],
"price": 40.00,
"currency": "EUR",
"depends": ["mail"],
"external_dependencies": {"python": [], "bin": []},
"data": [
"views/templates.xml",
],
"qweb": [
"static/src/xml/menu.xml",
],
"data": ["views/templates.xml"],
"qweb": ["static/src/xml/menu.xml"],
"demo": [],
"post_load": None,
"pre_init_hook": None,
"post_init_hook": None,
"uninstall_hook": None,
'installable': True,
"installable": True,
"auto_install": False,
}

77
mail_all/static/src/js/mail_all.js

@ -2,49 +2,50 @@
# Copyright 2017-2018 Artyom Losev <https://it-projects.info/team/ArtyomLosev>
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). */
odoo.define('mail_all.all', function (require) {
"use strict";
odoo.define("mail_all.all", function(require) {
"use strict";
var core = require('web.core');
var Manager = require('mail.Manager');
var Mailbox = require('mail.model.Mailbox');
var core = require("web.core");
var Manager = require("mail.Manager");
var Mailbox = require("mail.model.Mailbox");
var _t = core._t;
var _t = core._t;
Manager.include({
_updateMailboxesFromServer: function (data) {
var self = this;
this._super(data);
if (!_.find(this.getThreads(), function(th){
return th.getID() === 'mailbox_channel_all';
})) {
this._addMailbox({
id: 'channel_all',
name: _t("All Messages"),
mailboxCounter: 0,
});
}
},
Manager.include({
_updateMailboxesFromServer: function(data) {
var self = this;
this._super(data);
if (
!_.find(this.getThreads(), function(th) {
return th.getID() === "mailbox_channel_all";
})
) {
this._addMailbox({
id: "channel_all",
name: _t("All Messages"),
mailboxCounter: 0,
});
}
},
_makeMessage: function (data) {
var message = this._super(data);
message._addThread('mailbox_channel_all');
return message;
},
});
_makeMessage: function(data) {
var message = this._super(data);
message._addThread("mailbox_channel_all");
return message;
},
});
Mailbox.include({
_getThreadDomain: function () {
if (this._id === 'mailbox_channel_all') {
return [];
}
return this._super();
},
});
Mailbox.include({
_getThreadDomain: function() {
if (this._id === "mailbox_channel_all") {
return [];
}
return this._super();
},
});
return {
'Manager': Manager,
'Mailbox': Mailbox,
return {
Manager: Manager,
Mailbox: Mailbox,
};
});

32
mail_all/static/src/js/test_mail_all.js

@ -1,29 +1,33 @@
/* # Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). */
odoo.define('mail_all.tour', function (require) {
odoo.define("mail_all.tour", function(require) {
"use strict";
var tour = require("web_tour.tour");
var core = require('web.core');
var core = require("web.core");
var _t = core._t;
var steps = [{
var steps = [
{
trigger: 'a.full[href="#"]',
content: _t("Click to open app list"),
position: 'bottom',
}, {
position: "bottom",
},
{
trigger: 'a.dropdown-item.o_app:contains("Discuss")',
content: _t("Click to enter menu discuss"),
position: 'bottom',
}, {
position: "bottom",
},
{
content: _t("Open All Messages"),
trigger: '.o_channel_name.mail_all',
}, {
trigger: ".o_channel_name.mail_all",
},
{
content: _t("Check that All Messages are opened"),
trigger: '.o_mail_discuss_title_main.o_mail_mailbox_title_all.o_mail_discuss_item.o_active',
}];
tour.register('tour_mail_all', { test: true, url: '/web' }, steps);
trigger:
".o_mail_discuss_title_main.o_mail_mailbox_title_all.o_mail_discuss_item.o_active",
},
];
tour.register("tour_mail_all", {test: true, url: "/web"}, steps);
});

11
mail_all/static/src/xml/menu.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<!--# Copyright 2018 Artyom Losev <https://it-projects.info/team/ArtyomLosev>
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). -->
@ -6,8 +6,13 @@
<!--Inherit Sidebar and add All messages menu item after Starred -->
<t t-extend="mail.discuss.Sidebar">
<t t-jquery="div[data-thread-id=mailbox_starred]" t-operation="after">
<div t-attf-class="o_mail_discuss_title_main o_mail_mailbox_title_all o_mail_discuss_item #{(activeThreadID == 'channel_all') ? 'o_active': ''}" data-thread-id="mailbox_channel_all">
<span class="o_channel_name mail_all"> <i class="fa fa-database"/> All messages </span>
<div
t-attf-class="o_mail_discuss_title_main o_mail_mailbox_title_all o_mail_discuss_item #{(activeThreadID == 'channel_all') ? 'o_active': ''}"
data-thread-id="mailbox_channel_all"
>
<span class="o_channel_name mail_all"> <i
class="fa fa-database"
/> All messages </span>
</div>
</t>
</t>

17
mail_all/tests/test_js.py

@ -9,16 +9,19 @@ import odoo.tests
@odoo.tests.common.at_install(True)
@odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase):
def test_01_mail_all(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.
self.env['ir.module.module'].search([('name', '=', 'mail_all')], limit=1).state = 'installed'
self.env["ir.module.module"].search(
[("name", "=", "mail_all")], limit=1
).state = "installed"
link = '/web#action=%s' % self.ref('mail.action_discuss')
self.phantom_js(link,
"odoo.__DEBUG__.services['web_tour.tour'].run('tour_mail_all', 1000)",
"odoo.__DEBUG__.services['web_tour.tour'].tours.tour_mail_all.ready",
login="admin")
link = "/web#action=%s" % self.ref("mail.action_discuss")
self.phantom_js(
link,
"odoo.__DEBUG__.services['web_tour.tour'].run('tour_mail_all', 1000)",
"odoo.__DEBUG__.services['web_tour.tour'].tours.tour_mail_all.ready",
login="admin",
)

22
mail_all/views/templates.xml

@ -1,16 +1,24 @@
<?xml version="1.0"?>
<?xml version="1.0" ?>
<!--# Copyright 2016 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2018 Artyom Losev <https://it-projects.info/team/ArtyomLosev>
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). -->
<odoo>
<template id="mail_all_assets_backend"
name="mail_all_assets_backend"
inherit_id="web.assets_backend">
<template
id="mail_all_assets_backend"
name="mail_all_assets_backend"
inherit_id="web.assets_backend"
>
<xpath expr="." position="inside">
<link rel="stylesheet" href="/mail_all/static/src/css/mail_all.css"/>
<script src="/mail_all/static/src/js/mail_all.js" type="text/javascript"></script>
<script src="/mail_all/static/src/js/test_mail_all.js" type="text/javascript"></script>
<link rel="stylesheet" href="/mail_all/static/src/css/mail_all.css" />
<script
src="/mail_all/static/src/js/mail_all.js"
type="text/javascript"
/>
<script
src="/mail_all/static/src/js/test_mail_all.js"
type="text/javascript"
/>
</xpath>
</template>
</odoo>

23
mail_archives/__manifest__.py

@ -2,25 +2,16 @@
"name": "Mail archives",
"summary": """Adds menu to find old messages""",
"category": "Discuss",
"images": ['images/1.jpg'],
"images": ["images/1.jpg"],
"version": "12.0.1.0.1",
"author": "IT-Projects LLC, Pavel Romanchenko",
"support": "apps@it-projects.info",
"website": "https://it-projects.info",
"license": "LGPL-3",
'price': 40.00,
'currency': 'EUR',
"depends": [
"mail",
],
"data": [
"views/templates.xml",
],
"qweb": [
"static/src/xml/menu.xml",
],
'installable': True,
"price": 40.00,
"currency": "EUR",
"depends": ["mail"],
"data": ["views/templates.xml"],
"qweb": ["static/src/xml/menu.xml"],
"installable": True,
}

159
mail_archives/static/src/js/archives.js

@ -1,89 +1,92 @@
odoo.define('mail_archives.archives', function (require) {
"use strict";
odoo.define("mail_archives.archives", function(require) {
"use strict";
var core = require('web.core');
var session = require('web.session');
var Manager = require('mail.Manager');
var Mailbox = require('mail.model.Mailbox');
var SearchableThread = require('mail.model.SearchableThread');
var core = require("web.core");
var session = require("web.session");
var Manager = require("mail.Manager");
var Mailbox = require("mail.model.Mailbox");
var SearchableThread = require("mail.model.SearchableThread");
var _t = core._t;
var _t = core._t;
Manager.include({
_updateMailboxesFromServer: function (data) {
var self = this;
this._super(data);
if (!_.find(this.getThreads(), function(th){
return th.getID() === 'mailbox_channel_archive';
})) {
this._addMailbox({
id: 'channel_archive',
name: _t("Archive"),
mailboxCounter: 0,
});
}
},
});
Manager.include({
_updateMailboxesFromServer: function(data) {
var self = this;
this._super(data);
if (
!_.find(this.getThreads(), function(th) {
return th.getID() === "mailbox_channel_archive";
})
) {
this._addMailbox({
id: "channel_archive",
name: _t("Archive"),
mailboxCounter: 0,
});
}
},
});
SearchableThread.include({
_fetchMessages: function (pDomain, loadMore) {
var self = this;
if (this._id !== 'mailbox_channel_archive') {
return this._super(pDomain, loadMore);
}
SearchableThread.include({
_fetchMessages: function(pDomain, loadMore) {
var self = this;
if (this._id !== "mailbox_channel_archive") {
return this._super(pDomain, loadMore);
}
// this is a copy-paste from super method
var domain = this._getThreadDomain();
var cache = this._getCache(pDomain);
if (pDomain) {
domain = domain.concat(pDomain || []);
}
if (loadMore) {
var minMessageID = cache.messages[0].getID();
domain = [['id', '<', minMessageID]].concat(domain);
}
return this._rpc({
model: 'mail.message',
method: 'message_fetch',
args: [domain],
kwargs: this._getFetchMessagesKwargs(),
}).then(function (messages) {
// except this function. It adds the required thread to downloaded messages
_.each(messages, function(m){
m.channel_ids.push('mailbox_channel_archive');
});
if (!cache.allHistoryLoaded) {
cache.allHistoryLoaded = messages.length < self._FETCH_LIMIT;
// This is a copy-paste from super method
var domain = this._getThreadDomain();
var cache = this._getCache(pDomain);
if (pDomain) {
domain = domain.concat(pDomain || []);
}
cache.loaded = true;
_.each(messages, function (message) {
self.call('mail_service', 'addMessage', message, {
silent: true,
domain: pDomain,
if (loadMore) {
var minMessageID = cache.messages[0].getID();
domain = [["id", "<", minMessageID]].concat(domain);
}
return this._rpc({
model: "mail.message",
method: "message_fetch",
args: [domain],
kwargs: this._getFetchMessagesKwargs(),
}).then(function(messages) {
// Except this function. It adds the required thread to downloaded messages
_.each(messages, function(m) {
m.channel_ids.push("mailbox_channel_archive");
});
if (!cache.allHistoryLoaded) {
cache.allHistoryLoaded = messages.length < self._FETCH_LIMIT;
}
cache.loaded = true;
_.each(messages, function(message) {
self.call("mail_service", "addMessage", message, {
silent: true,
domain: pDomain,
});
});
cache = self._getCache(pDomain || []);
return cache.messages;
});
cache = self._getCache(pDomain || []);
return cache.messages;
});
},
});
},
});
Mailbox.include({
_getThreadDomain: function () {
if (this._id === 'mailbox_channel_archive') {
return ['|','|',
['partner_ids', 'in', [session.partner_id]],
['author_id', 'in', [session.partner_id]],
['channel_ids.channel_partner_ids', 'in', [session.partner_id]],
];
}
return this._super();
},
});
return {
'Manager': Manager,
'Mailbox': Mailbox,
};
Mailbox.include({
_getThreadDomain: function() {
if (this._id === "mailbox_channel_archive") {
return [
"|",
"|",
["partner_ids", "in", [session.partner_id]],
["author_id", "in", [session.partner_id]],
["channel_ids.channel_partner_ids", "in", [session.partner_id]],
];
}
return this._super();
},
});
return {
Manager: Manager,
Mailbox: Mailbox,
};
});

12
mail_archives/static/src/xml/menu.xml

@ -1,11 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<template>
<!--Inherit Sidebar and add Archive menu item after Starred -->
<t t-extend="mail.discuss.Sidebar">
<t t-jquery="div[data-thread-id=mailbox_starred]" t-operation="after">
<div t-attf-class="o_mail_discuss_title_main o_mail_discuss_item #{(activeThreadID == 'channel_archive') ? 'o_active': ''}"
data-thread-id="mailbox_channel_archive">
<span class="o_channel_name mail_archives"> <i class="fa fa-archive"/> Archive </span>
<div
t-attf-class="o_mail_discuss_title_main o_mail_discuss_item #{(activeThreadID == 'channel_archive') ? 'o_active': ''}"
data-thread-id="mailbox_channel_archive"
>
<span class="o_channel_name mail_archives"> <i
class="fa fa-archive"
/> Archive </span>
</div>
</t>
</t>

1
mail_archives/tests/__init__.py

@ -1,2 +1 @@
from . import test_js

17
mail_archives/tests/test_js.py

@ -1,18 +1,20 @@
import odoo.tests
from werkzeug import url_encode
import odoo.tests
@odoo.tests.common.at_install(True)
@odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase):
def test_01_mail_archives(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.
self.env['ir.module.module'].search([('name', '=', 'mail_archives')], limit=1).state = 'installed'
self.env["ir.module.module"].search(
[("name", "=", "mail_archives")], limit=1
).state = "installed"
# wait till page loaded and then click and wait again
code = """
@ -20,5 +22,10 @@ class TestUi(odoo.tests.HttpCase):
console.log($(".mail_archives").length && 'ok' || 'error');
}, 3000);
"""
link = '/web#%s' % url_encode({'action': 'mail.action_discuss'})
self.phantom_js(link, code, "odoo.__DEBUG__.services['web_tour.tour'].tours.mail_tour.ready", login="admin")
link = "/web#%s" % url_encode({"action": "mail.action_discuss"})
self.phantom_js(
link,
code,
"odoo.__DEBUG__.services['web_tour.tour'].tours.mail_tour.ready",
login="admin",
)

20
mail_archives/views/templates.xml

@ -1,12 +1,20 @@
<?xml version="1.0"?>
<?xml version="1.0" ?>
<openerp>
<data>
<template id="res_partner_mails_count_assets_backend"
name="res_partner_mails_count_assets_backend"
inherit_id="web.assets_backend">
<template
id="res_partner_mails_count_assets_backend"
name="res_partner_mails_count_assets_backend"
inherit_id="web.assets_backend"
>
<xpath expr="." position="inside">
<link rel="stylesheet" href="/mail_archives/static/src/css/archives.css"/>
<script src="/mail_archives/static/src/js/archives.js" type="text/javascript"></script>
<link
rel="stylesheet"
href="/mail_archives/static/src/css/archives.css"
/>
<script
src="/mail_archives/static/src/js/archives.js"
type="text/javascript"
/>
</xpath>
</template>
</data>

1
mail_base/__init__.py

@ -1,3 +1,2 @@
from . import models
from . import controllers

18
mail_base/__manifest__.py

@ -4,21 +4,13 @@
"category": "Discuss",
"images": [],
"version": "11.0.1.0.2",
"author": "IT-Projects LLC, Pavel Romanchenko",
"support": "apps@it-projects.info",
"website": "https://it-projects.info",
"license": "LGPL-3",
'price': 9.00,
'currency': 'EUR',
"depends": [
"base",
"mail"
],
"data": [
"views/templates.xml",
],
'installable': False,
"price": 9.00,
"currency": "EUR",
"depends": ["base", "mail"],
"data": ["views/templates.xml"],
"installable": False,
}

7
mail_base/controllers/main.py

@ -1,5 +1,6 @@
from openerp.http import request
from openerp.addons.bus.controllers.main import BusController
from odoo.http import request
from odoo.addons.bus.controllers.main import BusController
class MailChatController(BusController):
@ -9,6 +10,6 @@ class MailChatController(BusController):
def _poll(self, dbname, channels, last, options):
if request.session.uid:
channels.append((request.db, 'mail_base.mail_sent'))
channels.append((request.db, "mail_base.mail_sent"))
return super(MailChatController, self)._poll(dbname, channels, last, options)

21
mail_base/models.py

@ -1,30 +1,31 @@
from openerp import api, models
from odoo import api, models
class MailMessage(models.Model):
_inherit = 'mail.message'
_inherit = "mail.message"
@api.multi
def write(self, values):
if values.get('needaction_partner_ids'):
if not values.get('partner_ids'):
values['partner_ids'] = []
for triplet in values.get('needaction_partner_ids'):
if values.get("needaction_partner_ids"):
if not values.get("partner_ids"):
values["partner_ids"] = []
for triplet in values.get("needaction_partner_ids"):
if triplet[0] == 6:
for id in triplet[2]:
values['partner_ids'].append((4, id, False))
values["partner_ids"].append((4, id, False))
return super(MailMessage, self).write(values)
class MailComposer(models.TransientModel):
_inherit = 'mail.compose.message'
_inherit = "mail.compose.message"
@api.multi
def send_mail(self, auto_commit=False):
res = super(MailComposer, self).send_mail(auto_commit=auto_commit)
notification = {}
self.env['bus.bus'].sendone((self._cr.dbname, 'mail_base.mail_sent'), notification)
self.env["bus.bus"].sendone(
(self._cr.dbname, "mail_base.mail_sent"), notification
)
return res

1
mail_base/tests/__init__.py

@ -1,2 +1 @@
from . import test_default

7
mail_base/tests/test_default.py

@ -4,7 +4,6 @@ import odoo.tests
@odoo.tests.common.at_install(False)
@odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase):
def test_01_mail_base(self):
# wait till page loaded
code = """
@ -12,5 +11,7 @@ class TestUi(odoo.tests.HttpCase):
console.log('ok');
}, 1000);
"""
link = '/web#action=%s' % self.ref('mail.mail_channel_action_client_chat')
self.phantom_js(link, code, "odoo.__DEBUG__.services['mail_base.base']", login="admin")
link = "/web#action=%s" % self.ref("mail.mail_channel_action_client_chat")
self.phantom_js(
link, code, "odoo.__DEBUG__.services['mail_base.base']", login="admin"
)

12
mail_base/views/templates.xml

@ -1,11 +1,13 @@
<?xml version="1.0"?>
<?xml version="1.0" ?>
<openerp>
<data>
<template id="mail_base_assets_backend"
name="mail_base_assets_backend"
inherit_id="web.assets_backend">
<template
id="mail_base_assets_backend"
name="mail_base_assets_backend"
inherit_id="web.assets_backend"
>
<xpath expr="." position="inside">
<script src="/mail_base/static/lib/base.js" type="text/javascript"></script>
<script src="/mail_base/static/lib/base.js" type="text/javascript" />
</xpath>
</template>
</data>

26
mail_check_immediately/__manifest__.py

@ -1,19 +1,15 @@
{
'name': 'Check mail immediately',
'version': '1.0.1',
'author': 'IT-Projects LLC, Ivan Yelizariev',
'license': 'LGPL-3',
"name": "Check mail immediately",
"vesion": "12.0.1.0.1",
"author": "IT-Projects LLC, Ivan Yelizariev",
"license": "LGPL-3",
"category": "Discuss",
"support": "apps@it-projects.info",
'website': 'https://twitter.com/yelizariev',
'price': 9.00,
'currency': 'EUR',
'depends': ['base', 'web', 'fetchmail', 'mail'],
'data': [
'views.xml',
],
'qweb': [
"static/src/xml/main.xml",
],
'installable': False
"website": "https://twitter.com/yelizariev",
"price": 9.00,
"currency": "EUR",
"depends": ["base", "web", "fetchmail", "mail"],
"data": ["views.xml"],
"qweb": ["static/src/xml/main.xml"],
"installable": False,
}

51
mail_check_immediately/models.py

@ -1,15 +1,12 @@
import datetime
from openerp.tools.translate import _
from openerp import tools
from openerp import exceptions
from openerp import models, fields, api
from odoo import api, exceptions, fields, models, tools
from odoo.tools.translate import _
class FetchMailServer(models.Model):
_inherit = 'fetchmail.server'
_name = 'fetchmail.server'
_inherit = "fetchmail.server"
_name = "fetchmail.server"
_last_updated = None
@ -19,36 +16,54 @@ class FetchMailServer(models.Model):
if not self._last_updated:
self._last_updated = tools.datetime.now()
src_tstamp_str = self._last_updated.strftime(tools.misc.DEFAULT_SERVER_DATETIME_FORMAT)
src_tstamp_str = self._last_updated.strftime(
tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
)
src_format = tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
dst_format = tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
dst_tz_name = self._context.get('tz') or self.env.user.tz
_now = tools.misc.server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name)
dst_tz_name = self._context.get("tz") or self.env.user.tz
_now = tools.misc.server_to_local_timestamp(
src_tstamp_str, src_format, dst_format, dst_tz_name
)
return _now
@api.model
def _fetch_mails(self):
if self._context.get('run_fetchmail_manually'):
if self._context.get("run_fetchmail_manually"):
# if interval less than 5 seconds
if self._last_updated and (datetime.datetime.now() - self._last_updated) < datetime.timedelta(0, 5):
raise exceptions.Warning(_('Error'), _('Task can be started no earlier than 5 seconds.'))
if self._last_updated and (
datetime.datetime.now() - self._last_updated
) < datetime.timedelta(0, 5):
raise exceptions.Warning(
_("Error"), _("Task can be started no earlier than 5 seconds.")
)
super(FetchMailServer, self)._fetch_mails()
res = self.env['fetchmail.server'].sudo().with_context(tz=self.env.user.tz).search([('state', '=', 'done')])
res = (
self.env["fetchmail.server"]
.sudo()
.with_context(tz=self.env.user.tz)
.search([("state", "=", "done")])
)
if res:
res[0].run_time = self._run_time()
class FetchMailImmediately(models.AbstractModel):
_name = 'fetch_mail.imm'
_name = "fetch_mail.imm"
@api.model
def get_last_update_time(self):
res = self.env['fetchmail.server'].sudo().with_context(tz=self.env.user.tz).search([('state', '=', 'done')])
res = (
self.env["fetchmail.server"]
.sudo()
.with_context(tz=self.env.user.tz)
.search([("state", "=", "done")])
)
array = [r.run_time for r in res]
if array:
return array[0]
@ -58,8 +73,8 @@ class FetchMailImmediately(models.AbstractModel):
@api.model
def run_fetchmail_manually(self):
fetchmail_task = self.env.ref('fetchmail.ir_cron_mail_gateway_action')
fetchmail_model = self.env['fetchmail.server'].sudo()
fetchmail_task = self.env.ref("fetchmail.ir_cron_mail_gateway_action")
fetchmail_model = self.env["fetchmail.server"].sudo()
fetchmail_task._try_lock()
fetchmail_model.with_context(run_fetchmail_manually=True)._fetch_mails()

53
mail_check_immediately/static/src/js/main.js

@ -1,14 +1,13 @@
openerp.mail_check_immediately = function(instance, local) {
"use strict";
instance.mail.Wall.include({
init: function(){
init: function() {
this._super.apply(this, arguments);
var _this = this;
this.imm_model = new instance.web.Model('fetch_mail.imm');
this.events['click a.oe_fetch_new_mails'] = function(){
this.imm_model = new instance.web.Model("fetch_mail.imm");
this.events["click a.oe_fetch_new_mails"] = function() {
_this.run_fetchmail_manually();
};
},
@ -16,40 +15,46 @@ openerp.mail_check_immediately = function(instance, local) {
start: function() {
var _this = this;
this._super();
this.get_last_fetched_time();
this.get_time_loop = setInterval(function(){
this.get_time_loop = setInterval(function() {
_this.get_last_fetched_time();
}, 30000);
},
run_fetchmail_manually: function(){
run_fetchmail_manually: function() {
var _this = this;
this.imm_model.call('run_fetchmail_manually', {context: new instance.web.CompoundContext()}).then(function(){
_this.get_last_fetched_time();
});
this.imm_model
.call("run_fetchmail_manually", {
context: new instance.web.CompoundContext(),
})
.then(function() {
_this.get_last_fetched_time();
});
},
get_last_fetched_time: function(){
get_last_fetched_time: function() {
var _this = this;
this.imm_model.call('get_last_update_time', {context: new instance.web.CompoundContext()}).then(function(res){
var value;
if (res)
value = $.timeago(res);
value = value || 'undefined';
_this.$el.find('span.oe_view_manager_fetch_mail_imm_field').html(value);
});
this.imm_model
.call("get_last_update_time", {
context: new instance.web.CompoundContext(),
})
.then(function(res) {
var value = null;
if (res) value = $.timeago(res);
value = value || "undefined";
_this.$el
.find("span.oe_view_manager_fetch_mail_imm_field")
.html(value);
});
},
destroy: function(){
destroy: function() {
clearInterval(this.get_time_loop);
this._super.apply(this, arguments);
}
this._super.apply(this, arguments);
},
});
};

14
mail_check_immediately/static/src/xml/main.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<templates>
<t t-name="fetch_mail_immediately.header">
<tr class="oe_header_row">
@ -6,19 +6,23 @@
<div class="oe_view_manager_fetch_mail_imm">
<em>
<span>Mails fetched:</span>
<a href="#" class="oe_fetch_new_mails" title="Click to fetch mails now">
<span class="oe_view_manager_fetch_mail_imm_field"></span>
<a
href="#"
class="oe_fetch_new_mails"
title="Click to fetch mails now"
>
<span class="oe_view_manager_fetch_mail_imm_field" />
</a>
</em>
</div>
</td>
<td></td>
<td />
</tr>
</t>
<t t-extend="mail.wall">
<t t-jquery="tr.oe_header_row_top" t-operation="after">
<t t-call="fetch_mail_immediately.header">
<t t-set="colspan" t-value="2"/>
<t t-set="colspan" t-value="2" />
</t>
</t>
</t>

11
mail_check_immediately/views.xml

@ -1,8 +1,15 @@
<openerp>
<data>
<template id="assets_backend_inherited_check_mail" name="Check mail immediately bar" inherit_id="web.assets_backend">
<template
id="assets_backend_inherited_check_mail"
name="Check mail immediately bar"
inherit_id="web.assets_backend"
>
<xpath expr="." position="inside">
<script type="text/javascript" src="/mail_check_immediately/static/src/js/main.js"></script>
<script
type="text/javascript"
src="/mail_check_immediately/static/src/js/main.js"
/>
</xpath>
</template>
</data>

4
mail_fix_553/__manifest__.py

@ -2,11 +2,11 @@
"name": "Fix mail error 553",
"version": "0.3",
"author": "IT-Projects LLC, Ivan Yelizariev",
'license': 'LGPL-3',
"license": "LGPL-3",
"category": "Discuss",
"support": "apps@it-projects.info",
"website": "https://yelizariev.github.io",
"depends": ["base", "mail"],
"data": ["data.xml"],
'installable': False
"installable": False,
}

2
mail_fix_553/data.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<openerp>
<data noupdate="1">
<!-- Catchall Email Alias -->

160
mail_fix_553/mail_fix_553.py

@ -1,15 +1,14 @@
import base64
import logging
import re
from email.utils import formataddr
from openerp import tools
from openerp import SUPERUSER_ID
from openerp.addons.base.ir.ir_mail_server import MailDeliveryException
from openerp.osv import osv
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools.translate import _
from odoo import SUPERUSER_ID, tools
from odoo.osv import osv
from odoo.tools.safe_eval import safe_eval as eval
from odoo.tools.translate import _
from odoo.addons.base.ir.ir_mail_server import MailDeliveryException
_logger = logging.getLogger(__name__)
@ -17,7 +16,9 @@ _logger = logging.getLogger(__name__)
class MailMail(osv.Model):
_inherit = "mail.mail"
def send(self, cr, uid, ids, auto_commit=False, raise_exception=False, context=None):
def send(
self, cr, uid, ids, auto_commit=False, raise_exception=False, context=None
):
# copy-paste from addons/mail/mail_mail.py
""" Sends the selected emails immediately, ignoring their current
state (mails that have already been sent should not be passed
@ -35,50 +36,83 @@ class MailMail(osv.Model):
"""
# NEW STUFF
catchall_alias = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.catchall.alias_from", context=context)
catchall_alias_name = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.catchall.name_alias_from", context=context)
catchall_domain = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.catchall.domain", context=context)
correct_email_from = r'@%s>?\s*$' % catchall_domain
default_email_from = '%s@%s' % (catchall_alias, catchall_domain)
catchall_alias = self.pool["ir.config_parameter"].get_param(
cr, uid, "mail.catchall.alias_from", context=context
)
catchall_alias_name = self.pool["ir.config_parameter"].get_param(
cr, uid, "mail.catchall.name_alias_from", context=context
)
catchall_domain = self.pool["ir.config_parameter"].get_param(
cr, uid, "mail.catchall.domain", context=context
)
correct_email_from = r"@%s>?\s*$" % catchall_domain
default_email_from = "{}@{}".format(catchall_alias, catchall_domain)
context = dict(context or {})
ir_mail_server = self.pool.get('ir.mail_server')
ir_attachment = self.pool['ir.attachment']
ir_mail_server = self.pool.get("ir.mail_server")
ir_attachment = self.pool["ir.attachment"]
for mail in self.browse(cr, SUPERUSER_ID, ids, context=context):
try:
# TDE note: remove me when model_id field is present on mail.message - done here to avoid doing it multiple times in the sub method
if mail.model:
model_id = self.pool['ir.model'].search(cr, SUPERUSER_ID, [('model', '=', mail.model)], context=context)[0]
model = self.pool['ir.model'].browse(cr, SUPERUSER_ID, model_id, context=context)
model_id = self.pool["ir.model"].search(
cr, SUPERUSER_ID, [("model", "=", mail.model)], context=context
)[0]
model = self.pool["ir.model"].browse(
cr, SUPERUSER_ID, model_id, context=context
)
else:
model = None
if model:
context['model_name'] = model.name
context["model_name"] = model.name
# load attachment binary data with a separate read(), as prefetching all
# `datas` (binary field) could bloat the browse cache, triggerring
# soft/hard mem limits with temporary data.
attachment_ids = [a.id for a in mail.attachment_ids]
attachments = [(a['datas_fname'], base64.b64decode(a['datas']))
for a in ir_attachment.read(cr, SUPERUSER_ID, attachment_ids,
['datas_fname', 'datas'])]
attachments = [
(a["datas_fname"], base64.b64decode(a["datas"]))
for a in ir_attachment.read(
cr, SUPERUSER_ID, attachment_ids, ["datas_fname", "datas"]
)
]
# specific behavior to customize the send email for notified partners
email_list = []
if mail.email_to:
email_list.append(self.send_get_email_dict(cr, uid, mail, context=context))
email_list.append(
self.send_get_email_dict(cr, uid, mail, context=context)
)
for partner in mail.recipient_ids:
email_list.append(self.send_get_email_dict(cr, uid, mail, partner=partner, context=context))
email_list.append(
self.send_get_email_dict(
cr, uid, mail, partner=partner, context=context
)
)
# headers
headers = {}
bounce_alias = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.bounce.alias", context=context)
catchall_domain = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.catchall.domain", context=context)
bounce_alias = self.pool["ir.config_parameter"].get_param(
cr, uid, "mail.bounce.alias", context=context
)
catchall_domain = self.pool["ir.config_parameter"].get_param(
cr, uid, "mail.catchall.domain", context=context
)
if bounce_alias and catchall_domain:
if mail.model and mail.res_id:
headers['Return-Path'] = '%s-%d-%s-%d@%s' % (bounce_alias, mail.id, mail.model, mail.res_id, catchall_domain)
headers["Return-Path"] = "%s-%d-%s-%d@%s" % (
bounce_alias,
mail.id,
mail.model,
mail.res_id,
catchall_domain,
)
else:
headers['Return-Path'] = '%s-%d@%s' % (bounce_alias, mail.id, catchall_domain)
headers["Return-Path"] = "%s-%d@%s" % (
bounce_alias,
mail.id,
catchall_domain,
)
if mail.headers:
try:
headers.update(eval(mail.headers))
@ -88,7 +122,7 @@ class MailMail(osv.Model):
# Writing on the mail object may fail (e.g. lock on user) which
# would trigger a rollback *after* actually sending the email.
# To avoid sending twice the same email, provoke the failure earlier
mail.write({'state': 'exception'})
mail.write({"state": "exception"})
mail_sent = False
# build an RFC2822 email.message.Message object and send it without queuing
@ -104,58 +138,78 @@ class MailMail(osv.Model):
msg = ir_mail_server.build_email(
email_from=email_from, # NEW STUFF
email_to=email.get('email_to'),
subject=email.get('subject'),
body=email.get('body'),
body_alternative=email.get('body_alternative'),
email_to=email.get("email_to"),
subject=email.get("subject"),
body=email.get("body"),
body_alternative=email.get("body_alternative"),
email_cc=tools.email_split(mail.email_cc),
reply_to=mail.reply_to,
attachments=attachments,
message_id=mail.message_id,
references=mail.references,
object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
subtype='html',
subtype_alternative='plain',
headers=headers)
object_id=mail.res_id
and ("{}-{}".format(mail.res_id, mail.model)),
subtype="html",
subtype_alternative="plain",
headers=headers,
)
try:
res = ir_mail_server.send_email(cr, uid, msg,
mail_server_id=mail.mail_server_id.id,
context=context)
res = ir_mail_server.send_email(
cr,
uid,
msg,
mail_server_id=mail.mail_server_id.id,
context=context,
)
except AssertionError as error:
if error.message == ir_mail_server.NO_VALID_RECIPIENT:
if str(error) == ir_mail_server.NO_VALID_RECIPIENT:
# No valid recipient found for this particular
# mail item -> ignore error to avoid blocking
# delivery to next recipients, if any. If this is
# the only recipient, the mail will show as failed.
_logger.warning("Ignoring invalid recipients for mail.mail %s: %s",
mail.message_id, email.get('email_to'))
_logger.warning(
"Ignoring invalid recipients for mail.mail %s: %s",
mail.message_id,
email.get("email_to"),
)
else:
raise
if res:
mail.write({'state': 'sent', 'message_id': res})
mail.write({"state": "sent", "message_id": res})
mail_sent = True
# /!\ can't use mail.state here, as mail.refresh() will cause an error
# see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
if mail_sent:
_logger.info('Mail with ID %r and Message-Id %r successfully sent', mail.id, mail.message_id)
self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=mail_sent)
_logger.info(
"Mail with ID %r and Message-Id %r successfully sent",
mail.id,
mail.message_id,
)
self._postprocess_sent_message(
cr, uid, mail, context=context, mail_sent=mail_sent
)
except MemoryError:
# prevent catching transient MemoryErrors, bubble up to notify user or abort cron job
# instead of marking the mail as failed
_logger.exception('MemoryError while processing mail with ID %r and Msg-Id %r. '
'Consider raising the --limit-memory-hard startup option',
mail.id, mail.message_id)
_logger.exception(
"MemoryError while processing mail with ID %r and Msg-Id %r. "
"Consider raising the --limit-memory-hard startup option",
mail.id,
mail.message_id,
)
raise
except Exception as e:
_logger.exception('failed sending mail.mail %s', mail.id)
mail.write({'state': 'exception'})
self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=False)
_logger.exception("failed sending mail.mail %s", mail.id)
mail.write({"state": "exception"})
self._postprocess_sent_message(
cr, uid, mail, context=context, mail_sent=False
)
if raise_exception:
if isinstance(e, AssertionError):
# get the args of the original error, wrap into a value and throw a MailDeliveryException
# that is an except_orm, with name and value as arguments
value = '. '.join(e.args)
value = ". ".join(e.args)
raise MailDeliveryException(_("Mail Delivery Failed"), value)
raise

33
mail_move_message/__manifest__.py

@ -6,25 +6,18 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
{
'name': 'Mail Relocation',
'version': '11.0.1.0.6',
'author': 'IT-Projects LLC, Ivan Yelizariev, Pavel Romanchenko',
'license': 'LGPL-3',
'category': 'Discuss',
'images': ['images/m1.png'],
"name": "Mail Relocation",
"version": "11.0.1.0.6",
"author": "IT-Projects LLC, Ivan Yelizariev, Pavel Romanchenko",
"license": "LGPL-3",
"category": "Discuss",
"images": ["images/m1.png"],
"support": "apps@it-projects.info",
'website': 'https://twitter.com/yelizariev',
'price': 100.00,
'currency': 'EUR',
'depends': [
'mail_all',
],
'data': [
'mail_move_message_views.xml',
'data/mail_move_message_data.xml',
],
'qweb': [
'static/src/xml/mail_move_message_main.xml',
],
'installable': False,
"website": "https://twitter.com/yelizariev",
"price": 100.00,
"currency": "EUR",
"depends": ["mail_all"],
"data": ["mail_move_message_views.xml", "data/mail_move_message_data.xml"],
"qweb": ["static/src/xml/mail_move_message_main.xml"],
"installable": False,
}

5
mail_move_message/controllers/main.py

@ -4,6 +4,7 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo.http import request
from odoo.addons.bus.controllers.main import BusController
@ -14,6 +15,6 @@ class MailChatController(BusController):
def _poll(self, dbname, channels, last, options):
if request.session.uid:
channels.append((request.db, 'mail_move_message'))
channels.append((request.db, 'mail_move_message.delete_message'))
channels.append((request.db, "mail_move_message"))
channels.append((request.db, "mail_move_message.delete_message"))
return super(MailChatController, self)._poll(dbname, channels, last, options)

3
mail_move_message/data/mail_move_message_data.xml

@ -1,8 +1,7 @@
<?xml version="1.0"?>
<?xml version="1.0" ?>
<!--# Copyright 2016 Ildar Nasyrov <https://it-projects.info/team/iledarn>
# Copyright 2017 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).-->
<odoo>
<record id="mail_relocation_models" model="ir.config_parameter">
<field name="key">mail_relocation_models</field>

517
mail_move_message/mail_move_message_models.py

@ -5,32 +5,34 @@
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo import api
from odoo import fields
from odoo import models
from odoo import api, exceptions, fields, models
from odoo.tools import email_split
from odoo.tools.translate import _
from odoo import exceptions
class Wizard(models.TransientModel):
_name = 'mail_move_message.wizard'
_description = 'Mail move message wizard'
_name = "mail_move_message.wizard"
_description = "Mail move message wizard"
@api.model
def _model_selection(self):
selection = []
config_parameters = self.env['ir.config_parameter']
model_names = config_parameters.sudo().get_param('mail_relocation_models')
model_names = model_names.split(',') if model_names else []
if 'default_message_id' in self.env.context:
message = self.env['mail.message'].browse(self.env.context['default_message_id'])
config_parameters = self.env["ir.config_parameter"]
model_names = config_parameters.sudo().get_param("mail_relocation_models")
model_names = model_names.split(",") if model_names else []
if "default_message_id" in self.env.context:
message = self.env["mail.message"].browse(
self.env.context["default_message_id"]
)
if message.model and message.model not in model_names:
model_names.append(message.model)
if message.moved_from_model and message.moved_from_model not in model_names:
model_names.append(message.moved_from_model)
if model_names:
selection = [(m.model, m.display_name) for m in self.env['ir.model'].search([('model', 'in', model_names)])]
selection = [
(m.model, m.display_name)
for m in self.env["ir.model"].search([("model", "in", model_names)])
]
return selection
@api.model
@ -40,70 +42,107 @@ class Wizard(models.TransientModel):
available_models = self._model_selection()
if len(available_models):
record = self.env[available_models[0][0]].search([], limit=1)
res['model_record'] = len(record) and (available_models[0][0] + ',' + str(record.id)) or False
res["model_record"] = (
len(record) and (available_models[0][0] + "," + str(record.id)) or False
)
if 'message_id' in res:
message = self.env['mail.message'].browse(res['message_id'])
if "message_id" in res:
message = self.env["mail.message"].browse(res["message_id"])
email_from = message.email_from
parts = email_split(email_from.replace(' ', ','))
parts = email_split(email_from.replace(" ", ","))
if parts:
email = parts[0]
name = email_from.find(email) != -1 and email_from[:email_from.index(email)].replace('"', '').replace('<', '').strip() or email_from
name = (
email_from.find(email) != -1
and email_from[: email_from.index(email)]
.replace('"', "")
.replace("<", "")
.strip()
or email_from
)
else:
name, email = email_from
res['message_name_from'] = name
res['message_email_from'] = email
res['partner_id'] = message.author_id.id
if message.author_id and self.env.uid not in [u.id for u in message.author_id.user_ids]:
res['filter_by_partner'] = True
if message.author_id and res.get('model'):
res_id = self.env[res['model']].search([], order='id desc', limit=1)
res["message_name_from"] = name
res["message_email_from"] = email
res["partner_id"] = message.author_id.id
if message.author_id and self.env.uid not in [
u.id for u in message.author_id.user_ids
]:
res["filter_by_partner"] = True
if message.author_id and res.get("model"):
res_id = self.env[res["model"]].search([], order="id desc", limit=1)
if res_id:
res['res_id'] = res_id[0].id
res["res_id"] = res_id[0].id
config_parameters = self.env['ir.config_parameter']
res['move_followers'] = config_parameters.sudo().get_param('mail_relocation_move_followers')
config_parameters = self.env["ir.config_parameter"]
res["move_followers"] = config_parameters.sudo().get_param(
"mail_relocation_move_followers"
)
res['uid'] = self.env.uid
res["uid"] = self.env.uid
return res
message_id = fields.Many2one('mail.message', string='Message')
message_body = fields.Html(related='message_id.body', string='Message to move', readonly=True)
message_from = fields.Char(related='message_id.email_from', string='From', readonly=True)
message_subject = fields.Char(related='message_id.subject', string='Subject', readonly=True)
message_moved_by_message_id = fields.Many2one('mail.message', related='message_id.moved_by_message_id', string='Moved with', readonly=True)
message_moved_by_user_id = fields.Many2one('res.users', related='message_id.moved_by_user_id', string='Moved by', readonly=True)
message_is_moved = fields.Boolean(string='Is Moved', related='message_id.is_moved', readonly=True)
parent_id = fields.Many2one('mail.message', string='Search by name', )
model_record = fields.Reference(selection="_model_selection", string='Record')
model = fields.Char(compute="_compute_model_res_id", string='Model')
res_id = fields.Integer(compute="_compute_model_res_id", string='Record ID')
can_move = fields.Boolean('Can move', compute='_compute_get_can_move')
move_back = fields.Boolean('MOVE TO ORIGIN', help='Move message and submessages to original place')
partner_id = fields.Many2one('res.partner', string='Author')
filter_by_partner = fields.Boolean('Filter Records by partner')
message_id = fields.Many2one("mail.message", string="Message")
message_body = fields.Html(
related="message_id.body", string="Message to move", readonly=True
)
message_from = fields.Char(
related="message_id.email_from", string="From", readonly=True
)
message_subject = fields.Char(
related="message_id.subject", string="Subject", readonly=True
)
message_moved_by_message_id = fields.Many2one(
"mail.message",
related="message_id.moved_by_message_id",
string="Moved with",
readonly=True,
)
message_moved_by_user_id = fields.Many2one(
"res.users",
related="message_id.moved_by_user_id",
string="Moved by",
readonly=True,
)
message_is_moved = fields.Boolean(
string="Is Moved", related="message_id.is_moved", readonly=True
)
parent_id = fields.Many2one("mail.message", string="Search by name",)
model_record = fields.Reference(selection="_model_selection", string="Record")
model = fields.Char(compute="_compute_model_res_id", string="Model")
res_id = fields.Integer(compute="_compute_model_res_id", string="Record ID")
can_move = fields.Boolean("Can move", compute="_compute_get_can_move")
move_back = fields.Boolean(
"MOVE TO ORIGIN", help="Move message and submessages to original place"
)
partner_id = fields.Many2one("res.partner", string="Author")
filter_by_partner = fields.Boolean("Filter Records by partner")
message_email_from = fields.Char()
message_name_from = fields.Char()
# FIXME message_to_read should be True even if current message or any his childs are unread
message_to_read = fields.Boolean(compute='_compute_is_read', string="Unread message",
help="Service field shows that this message were unread when moved")
message_to_read = fields.Boolean(
compute="_compute_is_read",
string="Unread message",
help="Service field shows that this message were unread when moved",
)
uid = fields.Integer()
move_followers = fields.Boolean(
'Move Followers',
"Move Followers",
help="Add followers of current record to a new record.\n"
"You must use this option, if new record has restricted access.\n"
"You can change default value for this option at Settings/System Parameters")
"You must use this option, if new record has restricted access.\n"
"You can change default value for this option at Settings/System Parameters",
)
@api.multi
@api.depends('model_record')
@api.depends("model_record")
def _compute_model_res_id(self):
for rec in self:
rec.model = rec.model_record and rec.model_record._name or False
rec.res_id = rec.model_record and rec.model_record.id or False
@api.depends('message_id')
@api.depends("message_id")
@api.multi
def _compute_get_can_move(self):
for r in self:
@ -111,32 +150,43 @@ class Wizard(models.TransientModel):
@api.multi
def _compute_is_read(self):
messages = self.env['mail.message'].sudo().browse(self.message_id.all_child_ids.ids + [self.message_id.id])
messages = (
self.env["mail.message"]
.sudo()
.browse(self.message_id.all_child_ids.ids + [self.message_id.id])
)
self.message_to_read = True in [m.needaction for m in messages]
@api.multi
def get_can_move_one(self):
self.ensure_one()
# message was not moved before OR message is a top message of previous move
self.can_move = not self.message_id.moved_by_message_id or self.message_id.moved_by_message_id.id == self.message_id.id
self.can_move = (
not self.message_id.moved_by_message_id
or self.message_id.moved_by_message_id.id == self.message_id.id
)
@api.onchange('move_back')
@api.onchange("move_back")
def on_change_move_back(self):
if not self.move_back:
return
self.parent_id = self.message_id.moved_from_parent_id
message = self.message_id
if message.is_moved:
self.model_record = self.env[message.moved_from_model].browse(message.moved_from_res_id)
self.model_record = self.env[message.moved_from_model].browse(
message.moved_from_res_id
)
@api.onchange('parent_id', 'model_record')
@api.onchange("parent_id", "model_record")
def update_move_back(self):
model = self.message_id.moved_from_model
self.move_back = self.parent_id == self.message_id.moved_from_parent_id \
and self.res_id == self.message_id.moved_from_res_id \
self.move_back = (
self.parent_id == self.message_id.moved_from_parent_id
and self.res_id == self.message_id.moved_from_res_id
and (self.model == model or (not self.model and not model))
)
@api.onchange('parent_id')
@api.onchange("parent_id")
def on_change_parent_id(self):
if self.parent_id and self.parent_id.model:
self.model = self.parent_id.model
@ -145,24 +195,26 @@ class Wizard(models.TransientModel):
self.model = None
self.res_id = None
@api.onchange('model', 'filter_by_partner', 'partner_id')
@api.onchange("model", "filter_by_partner", "partner_id")
def on_change_partner(self):
domain = {'res_id': [('id', '!=', self.message_id.res_id)]}
domain = {"res_id": [("id", "!=", self.message_id.res_id)]}
if self.model and self.filter_by_partner and self.partner_id:
fields = self.env[self.model].fields_get(False)
contact_field = False
for n, f in fields.items():
if f['type'] == 'many2one' and f['relation'] == 'res.partner':
if f["type"] == "many2one" and f["relation"] == "res.partner":
contact_field = n
break
if contact_field:
domain['res_id'].append((contact_field, '=', self.partner_id.id))
domain["res_id"].append((contact_field, "=", self.partner_id.id))
if self.model:
res_id = self.env[self.model].search(domain['res_id'], order='id desc', limit=1)
res_id = self.env[self.model].search(
domain["res_id"], order="id desc", limit=1
)
self.res_id = res_id and res_id[0].id
else:
self.res_id = None
return {'domain': domain}
return {"domain": domain}
@api.multi
def check_access(self):
@ -172,16 +224,18 @@ class Wizard(models.TransientModel):
@api.multi
def check_access_one(self):
self.ensure_one()
operation = 'write'
operation = "write"
if not (self.model and self.res_id):
return True
model_obj = self.env[self.model]
mids = model_obj.browse(self.res_id).exists()
if hasattr(model_obj, 'check_mail_message_access'):
if hasattr(model_obj, "check_mail_message_access"):
model_obj.check_mail_message_access(mids.ids, operation)
else:
self.env['mail.thread'].check_mail_message_access(mids.ids, operation, model_name=self.model)
self.env["mail.thread"].check_mail_message_access(
mids.ids, operation, model_name=self.model
)
@api.multi
def open_moved_by_message_id(self):
@ -189,44 +243,60 @@ class Wizard(models.TransientModel):
for r in self:
message_id = r.message_moved_by_message_id.id
return {
'type': 'ir.actions.act_window',
'res_model': 'mail_move_message.wizard',
'view_mode': 'form',
'view_type': 'form',
'views': [[False, 'form']],
'target': 'new',
'context': {'default_message_id': message_id},
"type": "ir.actions.act_window",
"res_model": "mail_move_message.wizard",
"view_mode": "form",
"view_type": "form",
"views": [[False, "form"]],
"target": "new",
"context": {"default_message_id": message_id},
}
@api.multi
def move(self):
for r in self:
if not r.model:
raise exceptions.except_orm(_('Record field is empty!'), _('Select a record for relocation first'))
raise exceptions.except_orm(
_("Record field is empty!"),
_("Select a record for relocation first"),
)
for r in self:
r.check_access()
if not r.parent_id or not (r.parent_id.model == r.model and
r.parent_id.res_id == r.res_id):
if not r.parent_id or not (
r.parent_id.model == r.model and r.parent_id.res_id == r.res_id
):
# link with the first message of record
parent = self.env['mail.message'].search([('model', '=', r.model), ('res_id', '=', r.res_id)], order='id', limit=1)
parent = self.env["mail.message"].search(
[("model", "=", r.model), ("res_id", "=", r.res_id)],
order="id",
limit=1,
)
r.parent_id = parent.id or None
r.message_id.move(r.parent_id.id, r.res_id, r.model, r.move_back, r.move_followers, r.message_to_read, r.partner_id)
r.message_id.move(
r.parent_id.id,
r.res_id,
r.model,
r.move_back,
r.move_followers,
r.message_to_read,
r.partner_id,
)
if r.model in ['mail.message', 'mail.channel', False]:
if r.model in ["mail.message", "mail.channel", False]:
return {
'name': 'Chess game page',
'type': 'ir.actions.act_url',
'url': '/web',
'target': 'self',
"name": "Chess game page",
"type": "ir.actions.act_url",
"url": "/web",
"target": "self",
}
return {
'name': _('Record'),
'view_type': 'form',
'view_mode': 'form',
'res_model': r.model,
'res_id': r.res_id,
'views': [(False, 'form')],
'type': 'ir.actions.act_window',
"name": _("Record"),
"view_type": "form",
"view_mode": "form",
"res_model": r.model,
"res_id": r.res_id,
"views": [(False, "form")],
"type": "ir.actions.act_window",
}
@api.multi
@ -240,8 +310,10 @@ class Wizard(models.TransientModel):
msg_id = self.message_id.id
# Send notification
notification = {'id': msg_id}
self.env['bus.bus'].sendone((self._cr.dbname, 'mail_move_message.delete_message'), notification)
notification = {"id": msg_id}
self.env["bus.bus"].sendone(
(self._cr.dbname, "mail_move_message.delete_message"), notification
)
self.message_id.unlink()
return {}
@ -256,20 +328,34 @@ class Wizard(models.TransientModel):
self.ensure_one()
self.message_id.set_message_done()
self.message_id.child_ids.set_message_done()
return {'type': 'ir.actions.act_window_close'}
return {"type": "ir.actions.act_window_close"}
class MailMessage(models.Model):
_inherit = 'mail.message'
is_moved = fields.Boolean('Is moved')
moved_from_res_id = fields.Integer('Related Document ID (Original)')
moved_from_model = fields.Char('Related Document Model (Original)')
moved_from_parent_id = fields.Many2one('mail.message', 'Parent Message (Original)', ondelete='set null')
moved_by_message_id = fields.Many2one('mail.message', 'Moved by message', ondelete='set null', help='Top message, that initate moving this message')
moved_by_user_id = fields.Many2one('res.users', 'Moved by user', ondelete='set null')
all_child_ids = fields.One2many('mail.message', string='All childs', compute='_compute_get_all_childs', help='all childs, including subchilds')
moved_as_unread = fields.Boolean('Was Unread', default=False)
_inherit = "mail.message"
is_moved = fields.Boolean("Is moved")
moved_from_res_id = fields.Integer("Related Document ID (Original)")
moved_from_model = fields.Char("Related Document Model (Original)")
moved_from_parent_id = fields.Many2one(
"mail.message", "Parent Message (Original)", ondelete="set null"
)
moved_by_message_id = fields.Many2one(
"mail.message",
"Moved by message",
ondelete="set null",
help="Top message, that initate moving this message",
)
moved_by_user_id = fields.Many2one(
"res.users", "Moved by user", ondelete="set null"
)
all_child_ids = fields.One2many(
"mail.message",
string="All childs",
compute="_compute_get_all_childs",
help="all childs, including subchilds",
)
moved_as_unread = fields.Boolean("Was Unread", default=False)
@api.multi
def _compute_get_all_childs(self, include_myself=True):
@ -283,30 +369,59 @@ class MailMessage(models.Model):
if include_myself:
ids.append(self.id)
while True:
new_ids = self.search([('parent_id', 'in', ids), ('id', 'not in', ids)]).ids
new_ids = self.search([("parent_id", "in", ids), ("id", "not in", ids)]).ids
if new_ids:
ids = ids + new_ids
continue
break
moved_childs = self.search([('moved_by_message_id', '=', self.id)]).ids
moved_childs = self.search([("moved_by_message_id", "=", self.id)]).ids
self.all_child_ids = ids + moved_childs
@api.multi
def move_followers(self, model, ids):
fol_obj = self.env['mail.followers']
fol_obj = self.env["mail.followers"]
for message in self:
followers = fol_obj.sudo().search([('res_model', '=', message.model),
('res_id', '=', message.res_id)])
followers = fol_obj.sudo().search(
[("res_model", "=", message.model), ("res_id", "=", message.res_id)]
)
for f in followers:
self.env[model].browse(ids).message_subscribe([f.partner_id.id], [s.id for s in f.subtype_ids])
self.env[model].browse(ids).message_subscribe(
[f.partner_id.id], [s.id for s in f.subtype_ids]
)
@api.multi
def move(self, parent_id, res_id, model, move_back, move_followers=False, message_to_read=False, author=False):
def move(
self,
parent_id,
res_id,
model,
move_back,
move_followers=False,
message_to_read=False,
author=False,
):
for r in self:
r.move_one(parent_id, res_id, model, move_back, move_followers=move_followers, message_to_read=message_to_read, author=author)
r.move_one(
parent_id,
res_id,
model,
move_back,
move_followers=move_followers,
message_to_read=message_to_read,
author=author,
)
@api.multi
def move_one(self, parent_id, res_id, model, move_back, move_followers=False, message_to_read=False, author=False):
def move_one(
self,
parent_id,
res_id,
model,
move_back,
move_followers=False,
message_to_read=False,
author=False,
):
self.ensure_one()
if parent_id == self.id:
# if for any reason method is called to move message with parent
@ -314,116 +429,126 @@ class MailMessage(models.Model):
# building message tree
return
if not self.author_id:
self.write({
'author_id': author.id,
})
self.write({"author_id": author.id})
vals = {}
if move_back:
# clear variables if we move everything back
vals['is_moved'] = False
vals['moved_by_user_id'] = None
vals['moved_by_message_id'] = None
vals['moved_from_res_id'] = None
vals['moved_from_model'] = None
vals['moved_from_parent_id'] = None
vals['moved_as_unread'] = None
vals["is_moved"] = False
vals["moved_by_user_id"] = None
vals["moved_by_message_id"] = None
vals["moved_from_res_id"] = None
vals["moved_from_model"] = None
vals["moved_from_parent_id"] = None
vals["moved_as_unread"] = None
else:
vals['parent_id'] = parent_id
vals['res_id'] = res_id
vals['model'] = model
vals['is_moved'] = True
vals['moved_by_user_id'] = self.env.user.id
vals['moved_by_message_id'] = self.id
vals['moved_as_unread'] = message_to_read
vals["parent_id"] = parent_id
vals["res_id"] = res_id
vals["model"] = model
vals["is_moved"] = True
vals["moved_by_user_id"] = self.env.user.id
vals["moved_by_message_id"] = self.id
vals["moved_as_unread"] = message_to_read
# Update record_name in message
vals['record_name'] = self._get_record_name(vals)
vals["record_name"] = self._get_record_name(vals)
# unread message remains unread after moving back to origin
if self.moved_as_unread and move_back:
notification = {
'mail_message_id': self.id,
'res_partner_id': self.env.user.partner_id.id,
'is_read': False,
"mail_message_id": self.id,
"res_partner_id": self.env.user.partner_id.id,
"is_read": False,
}
self.write({
'notification_ids': [(0, 0, notification)],
})
self.write({"notification_ids": [(0, 0, notification)]})
for r in self.all_child_ids:
r_vals = vals.copy()
if not r.is_moved:
# moved_from_* variables contain not last, but original
# reference
r_vals['moved_from_parent_id'] = r.parent_id.id or r.env.context.get('uid')
r_vals['moved_from_res_id'] = r.res_id or r.id
r_vals['moved_from_model'] = r.model or r._name
r_vals["moved_from_parent_id"] = r.parent_id.id or r.env.context.get(
"uid"
)
r_vals["moved_from_res_id"] = r.res_id or r.id
r_vals["moved_from_model"] = r.model or r._name
elif move_back:
r_vals['parent_id'] = r.moved_from_parent_id.id
r_vals['res_id'] = r.moved_from_res_id
r_vals['model'] = (r.moved_from_model and r.moved_from_model not in ['mail.message', 'mail.channel', False]) and r.moved_from_model
r_vals['record_name'] = r_vals['model'] and self.env[r.moved_from_model].browse(r.moved_from_res_id).name
r_vals["parent_id"] = r.moved_from_parent_id.id
r_vals["res_id"] = r.moved_from_res_id
r_vals["model"] = (
r.moved_from_model
and r.moved_from_model
not in ["mail.message", "mail.channel", False]
) and r.moved_from_model
r_vals["record_name"] = (
r_vals["model"]
and self.env[r.moved_from_model].browse(r.moved_from_res_id).name
)
if move_followers:
r.sudo().move_followers(r_vals.get('model'), r_vals.get('res_id'))
r.sudo().move_followers(r_vals.get("model"), r_vals.get("res_id"))
r.sudo().write(r_vals)
r.attachment_ids.sudo().write({
'res_id': r_vals.get('res_id'),
'res_model': r_vals.get('model')
})
r.attachment_ids.sudo().write(
{"res_id": r_vals.get("res_id"), "res_model": r_vals.get("model")}
)
# Send notification
notification = {
'id': self.id,
'res_id': vals.get('res_id'),
'model': vals.get('model'),
'is_moved': vals['is_moved'],
'record_name': 'record_name' in vals and vals['record_name'],
"id": self.id,
"res_id": vals.get("res_id"),
"model": vals.get("model"),
"is_moved": vals["is_moved"],
"record_name": "record_name" in vals and vals["record_name"],
}
self.env['bus.bus'].sendone((self._cr.dbname, 'mail_move_message'), notification)
self.env["bus.bus"].sendone(
(self._cr.dbname, "mail_move_message"), notification
)
@api.multi
def name_get(self):
context = self.env.context
if not (context or {}).get('extended_name'):
if not (context or {}).get("extended_name"):
return super(MailMessage, self).name_get()
reads = self.read(['record_name', 'model', 'res_id'])
reads = self.read(["record_name", "model", "res_id"])
res = []
for record in reads:
name = record['record_name'] or ''
extended_name = ' [%s] ID %s' % (record.get('model', 'UNDEF'), record.get('res_id', 'UNDEF'))
res.append((record['id'], name + extended_name))
name = record["record_name"] or ""
extended_name = " [{}] ID {}".format(
record.get("model", "UNDEF"), record.get("res_id", "UNDEF"),
)
res.append((record["id"], name + extended_name))
return res
@api.multi
def message_format(self):
message_values = super(MailMessage, self).message_format()
message_index = {message['id']: message for message in message_values}
message_index = {message["id"]: message for message in message_values}
for item in self:
msg = message_index.get(item.id)
if msg:
msg['is_moved'] = item.is_moved
msg["is_moved"] = item.is_moved
return message_values
class MailMoveMessageConfiguration(models.TransientModel):
_inherit = 'res.config.settings'
_inherit = "res.config.settings"
model_ids = fields.Many2many(comodel_name='ir.model', string='Models')
move_followers = fields.Boolean('Move Followers')
model_ids = fields.Many2many(comodel_name="ir.model", string="Models")
move_followers = fields.Boolean("Move Followers")
@api.model
def get_values(self):
res = super(MailMoveMessageConfiguration, self).get_values()
config_parameters = self.env["ir.config_parameter"].sudo()
model_names = config_parameters.sudo().get_param('mail_relocation_models')
model_names = model_names.split(',')
model_ids = self.env['ir.model'].sudo().search([('model', 'in', model_names)])
model_names = config_parameters.sudo().get_param("mail_relocation_models")
model_names = model_names.split(",")
model_ids = self.env["ir.model"].sudo().search([("model", "in", model_names)])
res.update(
model_ids=[m.id for m in model_ids],
move_followers=config_parameters.sudo().get_param('mail_relocation_move_followers'),
move_followers=config_parameters.sudo().get_param(
"mail_relocation_move_followers"
),
)
return res
@ -432,38 +557,50 @@ class MailMoveMessageConfiguration(models.TransientModel):
super(MailMoveMessageConfiguration, self).set_values()
config_parameters = self.env["ir.config_parameter"].sudo()
for record in self:
model_names = ','.join([x.model for x in record.model_ids])
config_parameters.set_param("mail_relocation_models", model_names or '')
config_parameters.set_param("mail_relocation_move_followers", record.move_followers or '')
model_names = ",".join([x.model for x in record.model_ids])
config_parameters.set_param("mail_relocation_models", model_names or "")
config_parameters.set_param(
"mail_relocation_move_followers", record.move_followers or ""
)
class ResPartner(models.Model):
_inherit = 'res.partner'
_inherit = "res.partner"
@api.model
def create(self, vals):
res = super(ResPartner, self).create(vals)
if 'update_message_author' in self.env.context and 'email' in vals:
mail_message_obj = self.env['mail.message']
if "update_message_author" in self.env.context and "email" in vals:
mail_message_obj = self.env["mail.message"]
# Escape special SQL characters in email_address to avoid invalid matches
email_address = (vals['email'].replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_'))
email_address = (
vals["email"]
.replace("\\", "\\\\")
.replace("%", "\\%")
.replace("_", "\\_")
)
email_brackets = "<%s>" % email_address
messages = mail_message_obj.search([
'|',
('email_from', '=ilike', email_address),
('email_from', 'ilike', email_brackets),
('author_id', '=', False)
])
messages = mail_message_obj.search(
[
"|",
("email_from", "=ilike", email_address),
("email_from", "ilike", email_brackets),
("author_id", "=", False),
]
)
if messages:
messages.sudo().write({'author_id': res.id})
messages.sudo().write({"author_id": res.id})
return res
@api.model
def default_get(self, default_fields):
contextual_self = self
if 'mail_move_message' in self.env.context and self.env.context['mail_move_message']:
if (
"mail_move_message" in self.env.context
and self.env.context["mail_move_message"]
):
contextual_self = self.with_context(
default_name=self.env.context['message_name_from'] or '',
default_email=self.env.context['message_email_from'] or '',
default_name=self.env.context["message_name_from"] or "",
default_email=self.env.context["message_email_from"] or "",
)
return super(ResPartner, contextual_self).default_get(default_fields)

135
mail_move_message/mail_move_message_views.xml

@ -1,15 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!--# Copyright 2016 Ildar Nasyrov <https://it-projects.info/team/iledarn>
# Copyright 2016 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).-->
<odoo>
<template id="assets_backend" name="custom bar assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/mail_move_message/static/src/css/mail_move_message.css"/>
<script type="text/javascript" src="/mail_move_message/static/src/js/mail_move_message.js"></script>
<link
rel="stylesheet"
href="/mail_move_message/static/src/css/mail_move_message.css"
/>
<script
type="text/javascript"
src="/mail_move_message/static/src/js/mail_move_message.js"
/>
</xpath>
</template>
@ -19,51 +24,76 @@
<field name="arch" type="xml">
<form string="Move Message">
<field name="can_move" invisible="1"/>
<field name="message_is_moved" invisible="1"/>
<field name="message_name_from" invisible="1"/>
<field name="message_email_from" invisible="1"/>
<field name="message_to_read" invisible="1"/>
<field name="uid" invisible="1"/>
<field name="can_move" invisible="1" />
<field name="message_is_moved" invisible="1" />
<field name="message_name_from" invisible="1" />
<field name="message_email_from" invisible="1" />
<field name="message_to_read" invisible="1" />
<field name="uid" invisible="1" />
<p attrs="{'invisible':[('can_move', '!=', False)]}">You cannot move this message. It was already moved with a message bellow. Open one and apply changes there.</p>
<p
attrs="{'invisible':[('can_move', '!=', False)]}"
>You cannot move this message. It was already moved with a message bellow. Open one and apply changes there.</p>
<group attrs="{'invisible':[('can_move', '!=', False)]}">
<field name="message_moved_by_message_id" context="{'extended_name':1}"/>
<field name="message_moved_by_user_id"/>
<button name="open_moved_by_message_id" string="Open message" type="object" class="oe_highlight"/>
<field
name="message_moved_by_message_id"
context="{'extended_name':1}"
/>
<field name="message_moved_by_user_id" />
<button
name="open_moved_by_message_id"
string="Open message"
type="object"
class="oe_highlight"
/>
</group>
<group attrs="{'invisible':[('can_move', '=', False)]}" colspan="2">
<label for="model_record"/>
<label for="model_record" />
<div>
<field name="model_record" class="oe_inline"/>
<field name="model" invisible="1"/>
<field name="res_id" invisible="1"/>
<field name="model_record" class="oe_inline" />
<field name="model" invisible="1" />
<field name="res_id" invisible="1" />
</div>
<label for="filter_by_partner"/>
<label for="filter_by_partner" />
<div>
<field name="filter_by_partner" class="oe_inline"/>
<field name="partner_id" class="oe_inline"/>
<button string="Create Partner" attrs="{'invisible':[('partner_id','!=',False)]}"
<field name="filter_by_partner" class="oe_inline" />
<field name="partner_id" class="oe_inline" />
<button
string="Create Partner"
attrs="{'invisible':[('partner_id','!=',False)]}"
class="oe_highlight oe_inline ml32"
special="quick_create" model="res.partner" field="partner_id" context="{'force_email':True,'default_email':message_email_from,'default_name':message_name_from, 'update_message_author':True}" />
special="quick_create"
model="res.partner"
field="partner_id"
context="{'force_email':True,'default_email':message_email_from,'default_name':message_name_from, 'update_message_author':True}"
/>
</div>
<label for="move_back" attrs="{'invisible':[('message_is_moved','=',False)]}"/>
<label
for="move_back"
attrs="{'invisible':[('message_is_moved','=',False)]}"
/>
<div attrs="{'invisible':[('message_is_moved','=',False)]}">
<field name="move_back"/>
<field name="move_back" />
</div>
<label for="move_followers"/>
<label for="move_followers" />
<div>
<field name="move_followers"/>
<field name="move_followers" />
</div>
</group>
<button name="move" string="Move" type="object" class="oe_highlight" attrs="{'invisible':[('can_move', '=', False)]}"/>
<button
name="move"
string="Move"
type="object"
class="oe_highlight"
attrs="{'invisible':[('can_move', '=', False)]}"
/>
<button string="Close" class="" special="cancel" />
<separator string="Message"/>
<separator string="Message" />
<group>
<field name="message_subject"/>
<field name="message_from"/>
<field name="message_id" invisible="1"/>
<field name="message_subject" />
<field name="message_from" />
<field name="message_id" invisible="1" />
</group>
<div class="openerp mail_move_message">
<div class="oe_mail">
@ -71,18 +101,30 @@
<div class="oe_msg_content">
<div class="oe_msg_body">
<!-- use built-in css for messages -->
<field name="message_body"/>
<field name="message_body" />
</div>
</div>
</div>
</div>
</div>
<footer>
<button name="read_close" string="Mark as read and Close" type="object" class="oe_highlight"/> or
<button special="cancel" string="Close" class="oe_link"/>
<button
name="read_close"
string="Mark as read and Close"
type="object"
class="oe_highlight"
/> or
<button special="cancel" string="Close" class="oe_link" />
<button name="delete" string="Delete message" type="object" class="oe_highlight pull-right" confirm="Do you really want to delete this message?" attrs="{'invisible':[('uid','!=',1)]}"/>
<button
name="delete"
string="Delete message"
type="object"
class="oe_highlight pull-right"
confirm="Do you really want to delete this message?"
attrs="{'invisible':[('uid','!=',1)]}"
/>
</footer>
</form>
</field>
@ -92,30 +134,35 @@
<record id="view_mail_move_message_config_settings" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="55"/>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="priority" eval="55" />
<field name="inherit_id" ref="base.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block" data-string="Mail Relocation" string="Mail Relocation" data-key="mail_move_message">
<div
class="app_settings_block"
data-string="Mail Relocation"
string="Mail Relocation"
data-key="mail_move_message"
>
<h2>Mail Relocation</h2>
<div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-6 o_setting_box" id="crm_lead">
<div class="o_setting_left_pane">
</div>
<div class="o_setting_right_pane">
<label for="model_ids"/>
<label for="model_ids" />
<div class="text-muted">
Add models to be used for message relocation
</div>
<field name="model_ids" widget="many2many_tags"/>
<field name="model_ids" widget="many2many_tags" />
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box" id="crm_lead">
<div class="o_setting_left_pane">
<field name="move_followers"/>
<field name="move_followers" />
</div>
<div class="o_setting_right_pane">
<label for="move_followers"/>
<label for="move_followers" />
</div>
</div>
</div>
@ -128,7 +175,7 @@
<field name="name">Mail Relocation</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.config.settings</field>
<field name="view_id" ref="view_mail_move_message_config_settings"/>
<field name="view_id" ref="view_mail_move_message_config_settings" />
<field name="view_mode">form</field>
<field name="target">inline</field>
<field name="context">{'module' : 'mail_move_message'}</field>

5
mail_move_message/static/src/css/mail_move_message.css

@ -1,6 +1,7 @@
i.oe_moved {
color: #ED6F6A;
text-shadow: 0px 1px #961b1b,0px -1px #961b1b, -1px 0px #961b1b, 1px 0px #961b1b, 0px 3px 3px rgba(0,0,0,0.1);
color: #ed6f6a;
text-shadow: 0px 1px #961b1b, 0px -1px #961b1b, -1px 0px #961b1b, 1px 0px #961b1b,
0px 3px 3px rgba(0, 0, 0, 0.1);
}
.mail_move_message {
width: 864px;

149
mail_move_message/static/src/js/mail_move_message.js

@ -1,58 +1,58 @@
/*Copyright 2016 Ildar Nasyrov <https://it-projects.info/team/iledarn>
/* Copyright 2016 Ildar Nasyrov <https://it-projects.info/team/iledarn>
# Copyright 2016 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2016 Pavel Romanchenko
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). */
odoo.define('mail_move_message.relocate', function (require) {
odoo.define("mail_move_message.relocate", function(require) {
"use strict";
var bus = require('bus.bus').bus;
var chat_manager = require('mail_base.base').chat_manager;
var thread = require('mail.ChatThread');
var chatter = require('mail.Chatter');
var rpc = require('web.rpc');
var Basicmodel = require('web.BasicModel');
var view_dialogs = require('web.view_dialogs');
var field_utils_format = require('web.field_utils').format;
var BasicRenderer = require('web.BasicRenderer');
var core = require('web.core');
var form_widget = require('web.FormRenderer');
var session = require('web.Session');
var FormController = require('web.FormController');
var FormView = require('web.FormView');
var FormRenderer = require('web.FormRenderer');
var dialogs = require('web.view_dialogs');
var Dialog = require('web.Dialog');
var relational_fields = require('web.relational_fields');
var Widget = require('web.Widget');
var bus = require("bus.bus").bus;
var chat_manager = require("mail_base.base").chat_manager;
var thread = require("mail.ChatThread");
var chatter = require("mail.Chatter");
var rpc = require("web.rpc");
var Basicmodel = require("web.BasicModel");
var view_dialogs = require("web.view_dialogs");
var field_utils_format = require("web.field_utils").format;
var BasicRenderer = require("web.BasicRenderer");
var core = require("web.core");
var form_widget = require("web.FormRenderer");
var session = require("web.Session");
var FormController = require("web.FormController");
var FormView = require("web.FormView");
var FormRenderer = require("web.FormRenderer");
var dialogs = require("web.view_dialogs");
var Dialog = require("web.Dialog");
var relational_fields = require("web.relational_fields");
var Widget = require("web.Widget");
var _t = core._t;
thread.include({
init: function(){
init: function() {
this._super.apply(this, arguments);
// Add click reaction in the events of the thread object
this.events['click .oe_move'] = function(event) {
var message_id = $(event.currentTarget).data('message-id');
this.events["click .oe_move"] = function(event) {
var message_id = $(event.currentTarget).data("message-id");
this.trigger("move_message", message_id);
};
},
on_move_message: function(message_id){
on_move_message: function(message_id) {
var action = {
name: _t('Relocate Message'),
type: 'ir.actions.act_window',
res_model: 'mail_move_message.wizard',
view_mode: 'form',
view_type: 'form',
views: [[false, 'form']],
target: 'new',
context: {'default_message_id': message_id},
name: _t("Relocate Message"),
type: "ir.actions.act_window",
res_model: "mail_move_message.wizard",
view_mode: "form",
view_type: "form",
views: [[false, "form"]],
target: "new",
context: {default_message_id: message_id},
};
this.do_action(action, {
'on_close': function(){}
on_close: function() {},
});
}
},
});
chatter.include({
@ -61,39 +61,39 @@ odoo.define('mail_move_message.relocate', function (require) {
// For show wizard in the form
if (this.fields.thread && this.fields.thread.thread) {
var thread = this.fields.thread.thread;
thread.on('move_message', this, thread.on_move_message);
thread.on("move_message", this, thread.on_move_message);
}
return $.when(result).done(function() {});
}
},
});
var ChatAction = core.action_registry.get('mail.chat.instant_messaging');
var ChatAction = core.action_registry.get("mail.chat.instant_messaging");
ChatAction.include({
start: function() {
var result = this._super.apply(this, arguments);
// For show wizard in the channels
this.thread.on('move_message', this, this.thread.on_move_message);
this.thread.on("move_message", this, this.thread.on_move_message);
return $.when(result).done(function() {});
}
},
});
// override methods of chat manager
// Override methods of chat manager
var chat_manager_super_make_message = chat_manager.make_message;
chat_manager.make_message = function(data){
chat_manager.make_message = function(data) {
var msg = chat_manager_super_make_message(data);
// Mark msg as moved after reload
msg.is_moved = data.is_moved || false;
return msg;
};
var chat_manager_super_on_notification = chat_manager.on_notification;
chat_manager.on_notification = function(notifications){
chat_manager.on_notification = function(notifications) {
chat_manager_super_on_notification(notifications);
var self = this;
_.each(notifications, function (notification) {
_.each(notifications, function(notification) {
var model = notification[0][1];
var message_id = notification[1].id;
var message = chat_manager.get_message(message_id);
if (model === 'mail_move_message' && message) {
if (model === "mail_move_message" && message) {
message.res_id = notification[1].res_id;
message.model = notification[1].model;
message.record_name = notification[1].record_name;
@ -102,51 +102,60 @@ odoo.define('mail_move_message.relocate', function (require) {
// Update cache and accordingly message in the thread
self.add_to_cache(message, []);
// Call thread.on_update_message(message)
chat_manager.bus.trigger('update_message', message);
} else if (model === 'mail_move_message.delete_message') {
_.each(message.channel_ids, function(ch){
chat_manager.bus.trigger("update_message", message);
} else if (model === "mail_move_message.delete_message") {
_.each(message.channel_ids, function(ch) {
self.remove_message_from_channel(ch, message);
})
chat_manager.bus.trigger('update_message', message);
});
chat_manager.bus.trigger("update_message", message);
}
});
};
Basicmodel.include({
applyDefaultValues: function (recordID, values, options) {
delete values.model
return this._super(recordID, values, options)
}
applyDefaultValues: function(recordID, values, options) {
delete values.model;
return this._super(recordID, values, options);
},
});
FormController.include({
_onButtonClicked: function(event){
if(event.data.attrs.special === 'quick_create' && event.data.attrs.field === 'partner_id'){
_onButtonClicked: function(event) {
if (
event.data.attrs.special === "quick_create" &&
event.data.attrs.field === "partner_id"
) {
var self = this;
var field_data = event.data.record.data;
this.on_saved = function(record, bool) {
var values = [{
id: record.res_id,
display_name: record.data.display_name,
}];
var values = [
{
id: record.res_id,
display_name: record.data.display_name,
},
];
};
var wid = self.initialState.fieldsInfo.form.partner_id.Widget;
var relField = new relational_fields.FieldMany2One(wid,
'partner_id',
var relField = new relational_fields.FieldMany2One(
wid,
"partner_id",
self.initialState,
{
mode: 'edit',
viewType: 'form',
});
mode: "edit",
viewType: "form",
}
);
relField.getParent = function() {
// necessary for correct _trigger_up implementation in mixins.js
// Necessary for correct _trigger_up implementation in mixins.js
return self;
};
var wizard_popup = relField._searchCreatePopup("form", false, {
'message_name_from': field_data.message_name_from && field_data.message_name_from.split('@')[0],
'message_email_from': field_data.message_email_from,
'message_id': field_data.res_id,
'mail_move_message': 1,
message_name_from:
field_data.message_name_from &&
field_data.message_name_from.split("@")[0],
message_email_from: field_data.message_email_from,
message_id: field_data.res_id,
mail_move_message: 1,
});
} else {
this._super.apply(this, arguments);

11
mail_move_message/static/src/xml/mail_move_message_main.xml

@ -1,15 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<!--# Copyright 2016 Ildar Nasyrov <https://it-projects.info/team/iledarn>
# Copyright 2016 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2016 Pavel Romanchenko
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).-->
<template>
<t t-extend="mail.ChatThread.Message">
<t t-jquery='p.o_mail_info span:last-child i:first-child' t-operation="before">
<i t-if="!message.is_system_notification" t-att-class="'fa fa-exchange oe_move' + (message.is_moved ? ' oe_moved' : '')"
t-att-data-message-id="message.id" title="Move to thread"/>
<i
t-if="!message.is_system_notification"
t-att-class="'fa fa-exchange oe_move' + (message.is_moved ? ' oe_moved' : '')"
t-att-data-message-id="message.id"
title="Move to thread"
/>
</t>
</t>
</template>

1
mail_move_message/tests/__init__.py

@ -1,3 +1,2 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import test_mail_move

13
mail_move_message/tests/test_mail_move.py

@ -8,19 +8,22 @@ from odoo.api import Environment
@odoo.tests.common.at_install(True)
@odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase):
def test_create_new_partner_and_move_message(self):
env = Environment(self.registry.test_cr, self.uid, {})
# 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.
env['ir.module.module'].search([('name', '=', 'mail_move_message')], limit=1).state = 'installed'
env["ir.module.module"].search(
[("name", "=", "mail_move_message")], limit=1
).state = "installed"
self.registry.cursor().release()
# updating models, to be able relocate messages to a partner at_install
config_parameters = self.env["ir.config_parameter"].sudo()
config_parameters.set_param("mail_relocation_models", "crm.lead,project.task,res.partner")
config_parameters.set_param(
"mail_relocation_models", "crm.lead,project.task,res.partner"
)
code = """
var delayed_button_click = function(delay, button){
@ -49,4 +52,6 @@ class TestUi(odoo.tests.HttpCase):
console.log('ok')
"""
self.phantom_js('/web', code, login="admin", ready="$('.o_thread_icons').length")
self.phantom_js(
"/web", code, login="admin", ready="$('.o_thread_icons').length"
)

18
mail_multi_website/__init__.py

@ -13,10 +13,10 @@ def post_init_hook(cr, registry):
env.cr.execute("ALTER TABLE res_users ADD COLUMN email_multi_website VARCHAR")
# fill new email column with values from partner
for user in env['res.users'].with_context(active_test=False).search([]):
for user in env["res.users"].with_context(active_test=False).search([]):
email = user.partner_id.email
if email:
user._force_default('email_multi_website', email)
user._force_default("email_multi_website", email)
def uninstall_hook(cr, registry):
@ -26,15 +26,15 @@ def uninstall_hook(cr, registry):
# 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.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()
env["ir.property"].search([("fields_id", "in", field_ids)]).unlink()
# copy emails from partner to user
cr.execute("SELECT partner_id,email_multi_website FROM res_users")
for partner_id, default_email in cr.fetchall():
env['res.partner'].browse(partner_id).email = default_email
env["res.partner"].browse(partner_id).email = default_email

15
mail_multi_website/__manifest__.py

@ -9,14 +9,12 @@
"images": ["images/main.jpg"],
"version": "12.0.1.0.1",
"application": False,
"author": "IT-Projects LLC, Ivan Yelizariev",
"support": "apps@it-projects.info",
"website": "https://it-projects.info/team/yelizariev",
"license": "LGPL-3",
"price": 230.00,
"currency": "EUR",
"depends": [
"ir_config_parameter_multi_company",
"web_website",
@ -24,22 +22,15 @@
"test_mail",
],
"external_dependencies": {"python": [], "bin": []},
"data": [
"views/website_views.xml",
],
"demo": [
],
"qweb": [
],
"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": [
# ],

12
mail_multi_website/models/ir_property.py

@ -1,20 +1,20 @@
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo import models, api
from odoo import api, models
class IrProperty(models.Model):
_inherit = 'ir.property'
_inherit = "ir.property"
@api.multi
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'),
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)

8
mail_multi_website/models/mail_message.py

@ -1,16 +1,16 @@
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo import models, fields
from odoo import fields, models
class Message(models.Model):
_inherit = 'mail.message'
_inherit = "mail.message"
def _default_mail_server_id(self):
website = self.env.context.get('website_id')
website = self.env.context.get("website_id")
if not website:
return
website = self.env['website'].sudo().browse(website)
website = self.env["website"].sudo().browse(website)
return website.mail_server_id.id
mail_server_id = fields.Many2one(default=_default_mail_server_id)

112
mail_multi_website/models/mail_template.py

@ -1,28 +1,41 @@
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import logging
from odoo import models, fields, api, tools, _
from odoo import _, api, fields, models, tools
from odoo.exceptions import UserError
from odoo.tools import pycompat
from odoo.addons.mail.models.mail_template import format_date, format_tz, format_amount
from odoo.addons.mail.models.mail_template import format_amount, format_date, format_tz
_logger = logging.getLogger(__name__)
FIELDS = ['body_html', 'mail_server_id', 'report_template']
FIELDS = ["body_html", "mail_server_id", "report_template"]
try:
from odoo.addons.mail.models.mail_template import mako_safe_template_env, mako_template_env
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'
_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)
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,
)
@api.multi
def generate_email(self, res_ids, fields=None):
@ -37,8 +50,8 @@ class MailTemplate(models.Model):
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']
if "mail_server_id" in data and not data.get("mail_server_id"):
del data["mail_server_id"]
return res
@ -54,67 +67,93 @@ class MailTemplate(models.Model):
# try to load the template
try:
mako_env = mako_safe_template_env if self.env.context.get('safe') else mako_template_env
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]
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, format=False, context=self._context: format_date(self.env, date, format),
'format_tz': lambda dt, tz=False, format=False, context=self._context: format_tz(self.env, dt, tz, format),
'format_amount': lambda amount, currency, context=self._context: format_amount(self.env, amount, currency),
'user': self.env.user,
'ctx': self._context, # context kw would clash with mako internals
"format_date": lambda date, format=False, context=self._context: format_date(
self.env, date, format
),
"format_tz": lambda dt, tz=False, format=False, context=self._context: format_tz(
self.env, dt, tz, format
),
"format_amount": lambda amount, currency, context=self._context: format_amount(
self.env, amount, currency
),
"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 = self.env["res.company"] # empty value
company_id = self.env.context.get('force_company')
company_id = self.env.context.get("force_company")
if company_id:
company = self.env['res.company'].sudo().browse(company_id)
company = self.env["res.company"].sudo().browse(company_id)
if self.env.context.get('website_id'):
website = self.env['website'].browse(self.env.context.get('website_id'))
if self.env.context.get("website_id"):
website = self.env["website"].browse(self.env.context.get("website_id"))
else:
website = self.env.user.backend_website_id
for res_id, record in res_to_rec.items():
record_company = company
if not record_company:
if hasattr(record, 'company_id') and record.company_id:
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:
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:
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_website = self.env["website"] # empty value
record_context = dict(force_company=record_company.id, website_id=record_website.id)
variables['object'] = record.with_context(**record_context)
variables['website'] = record_website
record_context = dict(
force_company=record_company.id, website_id=record_website.id
)
variables["object"] = record.with_context(**record_context)
variables["website"] = record_website
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))
_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""
if post_process:
render_result = self.with_context(**record_context).render_post_process(render_result)
render_result = self.with_context(**record_context).render_post_process(
render_result
)
results[res_id] = render_result
@ -133,13 +172,10 @@ class MailTemplate(models.Model):
res = super(MailTemplate, self).write(vals)
# TODO: will it work with OCA's partner_firstname module?
if 'name' in vals:
if "name" in vals:
fields_to_update = FIELDS
else:
fields_to_update = [
f for f in FIELDS
if f in vals
]
fields_to_update = [f for f in FIELDS if f in vals]
for f in fields_to_update:
self._update_properties_label(f)

29
mail_multi_website/models/mail_thread.py

@ -1,23 +1,28 @@
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo import models, api, tools
from odoo import api, models, tools
class MailThread(models.AbstractModel):
_inherit = 'mail.thread'
_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)
])
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)])
)
if website:
self = self.with_context(website_id=website[0].id)

18
mail_multi_website/models/res_users.py

@ -1,31 +1,33 @@
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import logging
from odoo import models, fields, api
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
FIELD_NAME = 'email_multi_website'
FIELDS = ['signature']
FIELD_NAME = "email_multi_website"
FIELDS = ["signature"]
ALL_FIELDS = [FIELD_NAME] + FIELDS
class User(models.Model):
_inherit = ['res.users', 'website_dependent.mixin']
_name = 'res.users'
_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', related='email_multi_website', inherited=False)
email = fields.Char(
string="Multi Website Email", related="email_multi_website", inherited=False
)
email_multi_website = fields.Char(company_dependent=True, website_dependent=True)
@api.model
def create(self, vals):
res = super(User, self).create(vals)
# make value company independent
res._force_default(FIELD_NAME, vals.get('email'))
res._force_default(FIELD_NAME, vals.get("email"))
for f in FIELDS:
res._force_default(f, vals.get(f))
return res
@ -34,7 +36,7 @@ class User(models.Model):
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', 'email'] + FIELDS):
if any(k in vals for k in ["name", "email"] + FIELDS):
for f in ALL_FIELDS:
self._update_properties_label(f)
return res

6
mail_multi_website/models/website.py

@ -1,10 +1,12 @@
# Copyright 2017 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import models, fields
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')
mail_server_id = fields.Many2one(
"ir.mail_server", "Outgoing Mails", help="Default outgoing mail server"
)

53
mail_multi_website/tests/test_fetch.py

@ -1,9 +1,10 @@
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo.addons.test_mail.tests.test_mail_mail import TestMail
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
class TestFetch(TestMail):
@ -12,30 +13,44 @@ class TestFetch(TestMail):
def setUp(self):
super(TestFetch, self).setUp()
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 = 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')
mail_test_model = self.env["ir.model"]._get("mail.test.simple")
# groups@.. will cause the creation of new mail.test
self.alias = self.env['mail.alias'].create({
'alias_name': 'groups',
'alias_user_id': False,
'alias_model_id': mail_test_model.id,
'alias_contact': 'everyone'})
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')
@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, subject='My Frogs', to='groups@example.com, other@gmail.com')
new_groups = self.format_and_process(
MAIL_TEMPLATE, subject="My Frogs", to="groups@example.com, other@gmail.com"
)
# 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')
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",
)

16
mail_multi_website/tests/test_mail_model.py

@ -1,10 +1,18 @@
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo import models, fields
from odoo import fields, models
class MailTest(models.Model):
_inherit = 'mail.test.simple'
_inherit = "mail.test.simple"
company_id = fields.Many2one('res.company', default=lambda self: self.env['res.company']._company_default_get())
website_id = fields.Many2one('website', default=lambda self: self.env['website'].browse(self.env.context.get('website_id')))
company_id = fields.Many2one(
"res.company",
default=lambda self: self.env["res.company"]._company_default_get(),
)
website_id = fields.Many2one(
"website",
default=lambda self: self.env["website"].browse(
self.env.context.get("website_id")
),
)

147
mail_multi_website/tests/test_render.py

@ -15,100 +15,115 @@ class TestRender(TestMail):
self.original_email = self.env.user.email
self.original_company = self.env.user.company_id
self.email = 'superadmin@second-website.example'
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 = 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.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')
user_admin = self.env.ref("base.user_admin")
# copy-paste from mail.tests.test_mail_template
self._attachments = [{
'name': '_Test_First',
'datas_fname':
'first.txt',
'datas': base64.b64encode(b'My first attachment'),
'res_model': 'res.partner',
'res_id': user_admin.partner_id.id
}, {
'name': '_Test_Second',
'datas_fname': '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._attachments = [
{
"name": "_Test_First",
"datas_fname": "first.txt",
"datas": base64.b64encode(b"My first attachment"),
"res_model": "res.partner",
"res_id": user_admin.partner_id.id,
},
{
"name": "_Test_Second",
"datas_fname": "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': '%s, %s' % (self.email_1, self.email_2),
'email_cc': '%s' % self.email_3})
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)]
))
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',
})
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, '')
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, website_id=self.website.id).send_mail(self.test_pigs.id)
mail = self.env['mail.mail'].browse(mail_id)
mail_id = self.email_template.with_context(
wdb=True, website_id=self.website.id
).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)
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)
mail = self.env["mail.mail"].browse(mail_id)
self.assertEqual(mail.subject, self.website.name)
def _test_message_post_with_template(self):
@ -124,6 +139,8 @@ class TestRender(TestMail):
self.env.user.invalidate_cache()
self.assertEqual(self.env.user.email, self.original_email)
self.test_pigs.with_context(website_id=self.website.id).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)
self.test_pigs.with_context(
website_id=self.website.id
).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)

49
mail_multi_website/tests/test_send.py

@ -9,13 +9,11 @@ class TestSendMail(TransactionCase):
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.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'
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)
@ -23,12 +21,14 @@ class TestSendMail(TransactionCase):
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)]
))
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"""
@ -37,21 +37,28 @@ class TestSendMail(TransactionCase):
self.env.user.email = self.email
# Check that writing works
self.env.user.invalidate_cache()
self.assertEqual(self.env.user.email, self.email, 'Write methods doesn\'t work (Field is not in registry?)')
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')
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,
})
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
@ -59,4 +66,6 @@ class TestSendMail(TransactionCase):
# 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')
self.assertEqual(
partner.email, new_email, "Partner's email must not be Multi-website"
)

6
mail_multi_website/views/website_views.xml

@ -1,13 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). -->
<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="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"/>
<field name="mail_server_id" />
</xpath>
</field>
</record>

12
mail_multi_website/wizard/mail_compose_message.py

@ -1,20 +1,22 @@
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo import models, api
from odoo import api, models
from odoo.http import request
class MailComposer(models.TransientModel):
_inherit = 'mail.compose.message'
_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 "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()
website = self.env["website"].get_current_website()
if website:
self = self.with_context(website_id=website.id)
return super(MailComposer, self).create(vals)

23
mail_private/__manifest__.py

@ -6,39 +6,26 @@
"summary": """Send private messages to specified recipients, regardless of who are in followers list.""",
"category": "Discuss",
# "live_test_url": "http://apps.it-projects.info/shop/product/DEMO-URL?version=12.0",
"images": ['images/mail_private_image.png'],
"images": ["images/mail_private_image.png"],
"version": "12.0.1.1.2",
"application": False,
"author": "IT-Projects LLC, Pavel Romanchenko",
"support": "apps@it-projects.info",
"website": "https://it-projects.info/",
"license": "LGPL-3",
"price": 50.00,
"currency": "EUR",
"depends": [
"mail"
],
"depends": ["mail"],
"external_dependencies": {"python": [], "bin": []},
"data": [
'template.xml',
'full_composer_wizard.xml',
],
"demo": [
],
"qweb": [
'static/src/xml/mail_private.xml',
],
"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,
# "demo_title": "{MODULE_NAME}",
# "demo_addons": [
# ],

15
mail_private/full_composer_wizard.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?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 LGPL-3.0 (https://www.gnu.org/licenses/lgpl.html).-->
@ -7,8 +7,8 @@
<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="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>
@ -16,8 +16,13 @@
<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)]}
<xpath
expr="//div[@groups='base.group_user']/span[2]"
position="attributes"
>
<attribute
name="attrs"
>{'invisible': [('is_private', '=', True)]}
</attribute>
</xpath>

79
mail_private/models.py

@ -3,19 +3,19 @@
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/>
# License LGPL-3.0 (https://www.gnu.org/licenses/lgpl.html).
from odoo import models, fields, api
from odoo import api, fields, models
class MailComposeMessage(models.TransientModel):
_inherit = 'mail.compose.message'
_inherit = "mail.compose.message"
is_private = fields.Boolean(string='Send Internal Message')
is_private = fields.Boolean(string="Send Internal Message")
class MailMessage(models.Model):
_inherit = 'mail.message'
_inherit = "mail.message"
is_private = fields.Boolean(string='Send Internal Message')
is_private = fields.Boolean(string="Send Internal Message")
def send_recepients_for_internal_message(self, model, domain):
result = []
@ -26,14 +26,17 @@ class MailMessage(models.Model):
# channel_ids = [c.channel_id for c in follower_ids if c.channel_id]
for recipient in recipient_ids:
result.append({
'checked': recipient.user_ids.id and not any(recipient.user_ids.mapped('share')),
'partner_id': recipient.id,
'full_name': recipient.name,
'name': recipient.name,
'email_address': recipient.email,
'reason': 'Recipient'
})
result.append(
{
"checked": recipient.user_ids.id
and not any(recipient.user_ids.mapped("share")),
"partner_id": recipient.id,
"full_name": recipient.name,
"name": recipient.name,
"email_address": recipient.email,
"reason": "Recipient",
}
)
# for channel in channel_ids:
# result.append({
@ -46,20 +49,52 @@ class MailMessage(models.Model):
return result
@api.multi
def _notify(self, record, msg_vals, force_send=False, send_after_commit=True, model_description=False, mail_auto_delete=True):
def _notify(
self,
record,
msg_vals,
force_send=False,
send_after_commit=True,
model_description=False,
mail_auto_delete=True,
):
self_sudo = self.sudo()
msg_vals = msg_vals if msg_vals else {}
if 'is_private' not in self_sudo._context or not self_sudo._context['is_private']:
return super(MailMessage, self)._notify(record, msg_vals, force_send, send_after_commit, model_description, mail_auto_delete)
if (
"is_private" not in self_sudo._context
or not self_sudo._context["is_private"]
):
return super(MailMessage, self)._notify(
record,
msg_vals,
force_send,
send_after_commit,
model_description,
mail_auto_delete,
)
else:
rdata = self._notify_compute_internal_recipients(record, msg_vals)
return self._notify_recipients(rdata, record, msg_vals,
force_send=force_send, send_after_commit=send_after_commit,
model_description=model_description, mail_auto_delete=mail_auto_delete)
return self._notify_recipients(
rdata,
record,
msg_vals,
force_send=force_send,
send_after_commit=send_after_commit,
model_description=model_description,
mail_auto_delete=mail_auto_delete,
)
@api.multi
def _notify_compute_internal_recipients(self, record, msg_vals):
recipient_data = super(MailMessage, self)._notify_compute_recipients(record, msg_vals)
pids = [x[1] 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]
recipient_data = super(MailMessage, self)._notify_compute_recipients(
record, msg_vals
)
pids = (
[x[1] 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

485
mail_private/static/src/js/mail_private.js

@ -4,240 +4,289 @@
Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/>
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). */
odoo.define('mail_private', function (require) {
'use strict';
var core = require('web.core');
var Chatter = require('mail.Chatter');
var ChatterComposer = require('mail.composer.Chatter');
var session = require('web.session');
var rpc = require('web.rpc');
var config = require('web.config');
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({
odoo.define("mail_private", function(require) {
"use strict";
var core = require("web.core");
var Chatter = require("mail.Chatter");
var ChatterComposer = require("mail.composer.Chatter");
var session = require("web.session");
var rpc = require("web.rpc");
var config = require("web.config");
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
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;
});
},
_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,
}
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});
}
}).fail(function () {
self._enableComposer();
);
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,
});
}
})
.fail(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;
});
});
}
});
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)
);
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),
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
);
});
},
// Subtype
if (self.options.isLog) {
message.subtype = 'mail.mt_note';
}
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;
});
});
},
});
if (self.options.is_private) {
message.context.is_private = true;
message.channel_ids = self.get_checked_channel_ids();
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;
}
},
// Partner_ids
if (self.options.isLog) {
def.resolve(message);
} else {
var check_suggested_partners = self._getCheckedSuggestedPartners();
self._checkSuggestedPartners(check_suggested_partners).done(function (partnerIDs) {
message.partner_ids = (message.partner_ids || []).concat(partnerIDs);
// update context
message.context = _.defaults({}, message.context, {
mail_post_autofollow: true,
});
def.resolve(message);
_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),
});
}
});
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 () {
// Subtype
if (self.options.isLog) {
recipientDoneDef.resolve([]);
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 checkedSuggestedPartners = self._getCheckedSuggestedPartners();
self._checkSuggestedPartners(checkedSuggestedPartners)
.then(recipientDoneDef.resolve.bind(recipientDoneDef));
var check_suggested_partners = self._getCheckedSuggestedPartners();
self._checkSuggestedPartners(check_suggested_partners).done(
function(partnerIDs) {
message.partner_ids = (message.partner_ids || []).concat(
partnerIDs
);
// Update context
message.context = _.defaults({}, message.context, {
mail_post_autofollow: true,
});
def.resolve(message);
}
);
}
},
});
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;
}
});
return def;
},
on_uncheck_recipients: function() {
this.$(".o_composer_suggested_partners input:checked").each(function() {
$(this).prop("checked", false);
});
},
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;
_onOpenFullComposer: function() {
if (!this._doCheckAttachmentUpload()) {
return false;
}
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');
}
});
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");
},
});
});

67
mail_private/static/src/js/test_private.js

@ -1,61 +1,72 @@
/* Copyright 2018-2019 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/>
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).*/
odoo.define('mail_private.tour', function(require) {
"use strict";
odoo.define("mail_private.tour", function(require) {
"use strict";
var core = require('web.core');
var tour = require('web_tour.tour');
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',
}, {
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',
}, {
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"
}, {
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
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);
$("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);
tour.register("mail_private_tour", {test: true, url: "/web"}, steps);
});

15
mail_private/static/src/xml/mail_private.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<?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 LGPL-3.0 (https://www.gnu.org/licenses/lgpl.html).-->
@ -6,7 +6,11 @@
<t t-extend="mail.chatter.Buttons">
<t t-jquery="button[title='Send a message']" t-operation="after">
<button t-if="newMessageButton" class="btn btn-link oe_compose_post_private" title="Send a message to specified recipients only">Send internal message</button>
<button
t-if="newMessageButton"
class="btn btn-link oe_compose_post_private"
title="Send a message to specified recipients only"
>Send internal message</button>
</t>
</t>
@ -15,7 +19,7 @@
<small class="o_chatter_composer_info" t-if="!widget.options.is_private">
To: Followers of
<t t-if="widget.options.record_name">
&quot;<t t-esc="widget.options.record_name"/>&quot;
&quot;<t t-esc="widget.options.record_name" />&quot;
</t>
<t t-if="!widget.options.record_name">
this document
@ -23,7 +27,10 @@
</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>
<button
class="btn btn-link oe_composer_uncheck"
t-if="widget.options.is_private"
>Uncheck all</button>
</t>
</t>

19
mail_private/template.xml

@ -1,19 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<?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 LGPL-3.0 (https://www.gnu.org/licenses/lgpl.html).-->
<odoo>
<template
id="assets_backend"
name="mail_private_assets_backend"
inherit_id="web.assets_backend">
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/mail_private.js"
/>
<script
type="text/javascript"
src="/mail_private/static/src/js/test_private.js"></script>
type="text/javascript"
src="/mail_private/static/src/js/test_private.js"
/>
</xpath>
</template>
</odoo>

16
mail_private/tests/test_js.py

@ -8,17 +8,21 @@ 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'
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)
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,
)

22
mail_recovery/__manifest__.py

@ -1,18 +1,16 @@
{
'name': "Mail Recovery",
'summary': """Backup and recover unsent message""",
'author': "IT-Projects LLC, Ildar Nasyrov",
'license': 'LGPL-3',
"name": "Mail Recovery",
"summary": """Backup and recover unsent message""",
"author": "IT-Projects LLC, Ildar Nasyrov",
"license": "LGPL-3",
"price": 190.00,
"currency": "EUR",
"support": "apps@it-projects.info",
'website': "https://twitter.com/nasyrov_ildar",
'category': 'Discuss',
'images': ['images/mail_recovery.png'],
'version': '1.0.0',
'depends': ['mail'],
'data': [
'data.xml',
],
"website": "https://twitter.com/nasyrov_ildar",
"category": "Discuss",
"images": ["images/mail_recovery.png"],
"vesion": "12.0.1.0.0",
"depends": ["mail"],
"data": ["data.xml"],
"installable": False,
}

13
mail_recovery/data.xml

@ -1,9 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<openerp>
<data>
<template id="assets_backend" name="message storage" inherit_id="web.assets_backend">
<template
id="assets_backend"
name="message storage"
inherit_id="web.assets_backend"
>
<xpath expr="." position="inside">
<script type="text/javascript" src="/mail_recovery/static/src/js/mail_recovery.js"></script>
<script
type="text/javascript"
src="/mail_recovery/static/src/js/mail_recovery.js"
/>
</xpath>
</template>
</data>

15
mail_recovery/static/src/js/mail_recovery.js

@ -1,11 +1,12 @@
odoo.define('mail_recovery', function (require) {
var composer = require('mail.composer');
odoo.define("mail_recovery", function(require) {
"use strict";
var composer = require("mail.composer");
composer.BasicComposer.include({
init: function(){
init: function() {
this._super.apply(this, arguments);
this.events['focus .o_composer_input textarea'] = 'on_focus_textarea';
this.events['keyup .o_composer_input textarea'] = 'on_keyup_textarea';
this.events["focus .o_composer_input textarea"] = "on_focus_textarea";
this.events["keyup .o_composer_input textarea"] = "on_keyup_textarea";
},
on_focus_textarea: function(event) {
var $input = $(event.target);
@ -16,8 +17,8 @@ odoo.define('mail_recovery', function (require) {
on_keyup_textarea: function(event) {
window.localStorage.message_storage = $(event.target).val();
},
send_message: function (event) {
window.localStorage.message_storage = '';
send_message: function(event) {
window.localStorage.message_storage = "";
return this._super(event);
},
});

16
mail_reply/__manifest__.py

@ -3,25 +3,17 @@
"summary": """Got a message out of a Record? Now you can reply to it too!""",
"category": "Discuss",
"images": [],
"version": "1.0.0",
"vesion": "12.0.1.0.0",
"author": "IT-Projects LLC, Pavel Romanchenko",
"support": "apps@it-projects.info",
"website": "https://it-projects.info",
"license": "LGPL-3",
"price": 40.00,
"currency": "EUR",
"depends": [
"mail_base",
],
"depends": ["mail_base"],
"external_dependencies": {"python": [], "bin": []},
"data": [
'templates.xml'
],
"qweb": [
"static/src/xml/reply_button.xml",
],
"data": ["templates.xml"],
"qweb": ["static/src/xml/reply_button.xml"],
"demo": [],
"installable": False,
"auto_install": False,

76
mail_reply/static/src/js/mail_reply.js

@ -1,51 +1,51 @@
odoo.define('mail_reply.reply', function (require) {
"use strict";
odoo.define("mail_reply.reply", function(require) {
"use strict";
var core = require('web.core');
var chat_manager = require('mail_base.base').chat_manager;
var core = require("web.core");
var chat_manager = require("mail_base.base").chat_manager;
var ChatAction = core.action_registry.get('mail.chat.instant_messaging');
var ChatAction = core.action_registry.get("mail.chat.instant_messaging");
ChatAction.include({
_selectMessage: function (message_id) {
this._super.apply(this, arguments);
var message = chat_manager.get_message(message_id);
var subject = '';
if (message.record_name){
subject = "Re: " + message.record_name;
} else if (message.subject){
subject = "Re: " + message.subject;
}
this.extended_composer.set_subject(subject);
},
ChatAction.include({
_selectMessage: function(message_id) {
this._super.apply(this, arguments);
var message = chat_manager.get_message(message_id);
var subject = "";
if (message.record_name) {
subject = "Re: " + message.record_name;
} else if (message.subject) {
subject = "Re: " + message.subject;
}
this.extended_composer.set_subject(subject);
},
_onPostMessage: function (message) {
var self = this;
var options = this.selected_message
? {}
: {channel_id: this.channel.id};
if (this.selected_message) {
message.subtype = 'mail.mt_comment';
message.subtype_id = false;
message.message_type = 'comment';
message.content_subtype = 'html';
_onPostMessage: function(message) {
var self = this;
var options = this.selected_message ? {} : {channel_id: this.channel.id};
if (this.selected_message) {
message.subtype = "mail.mt_comment";
message.subtype_id = false;
message.message_type = "comment";
message.content_subtype = "html";
options.model = this.selected_message.model;
options.res_id = this.selected_message.res_id;
options.parent_id = this.selected_message.id;
}
chat_manager.post_message(message, options).
then(function () {
options.model = this.selected_message.model;
options.res_id = this.selected_message.res_id;
options.parent_id = this.selected_message.id;
}
chat_manager.post_message(message, options).then(function() {
if (self.selected_message) {
self._renderSnackbar('mail.chat.MessageSentSnackbar', {record_name: self.selected_message.record_name}, 5000);
self._renderSnackbar(
"mail.chat.MessageSentSnackbar",
{record_name: self.selected_message.record_name},
5000
);
self._unselectMessage();
} else {
self.thread.scroll_to();
}
});
}
});
return chat_manager;
},
});
return chat_manager;
});

14
mail_reply/static/src/xml/reply_button.xml

@ -1,10 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<template>
<t t-extend="mail.ChatThread.Message">
<t t-jquery='i[class="fa fa-reply o_thread_icon o_thread_message_reply"]' t-operation="replace">
<i t-if="message.author_id != 'ODOOBOT' &amp;&amp; message.model != 'mail.channel' &amp;&amp; options.display_reply_icon"
<t
t-jquery='i[class="fa fa-reply o_thread_icon o_thread_message_reply"]'
t-operation="replace"
>
<i
t-if="message.author_id != 'ODOOBOT' &amp;&amp; message.model != 'mail.channel' &amp;&amp; options.display_reply_icon"
class="fa fa-reply o_thread_icon o_thread_message_reply"
t-att-data-message-id="message.id" title="Reply"/>
t-att-data-message-id="message.id"
title="Reply"
/>
</t>
</t>
</template>

15
mail_reply/templates.xml

@ -1,11 +1,16 @@
<?xml version="1.0"?>
<?xml version="1.0" ?>
<openerp>
<data>
<template id="mail_reply_assets_backend"
name="mail_reply_assets_backend"
inherit_id="web.assets_backend">
<template
id="mail_reply_assets_backend"
name="mail_reply_assets_backend"
inherit_id="web.assets_backend"
>
<xpath expr="." position="inside">
<script src="/mail_reply/static/src/js/mail_reply.js" type="text/javascript"></script>
<script
src="/mail_reply/static/src/js/mail_reply.js"
type="text/javascript"
/>
</xpath>
</template>
</data>

1
mail_reply/tests/__init__.py

@ -1,2 +1 @@
from . import test_default

7
mail_reply/tests/test_default.py

@ -4,7 +4,6 @@ import odoo.tests
@odoo.tests.common.at_install(False)
@odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase):
def test_01_mail_all(self):
# wait till page loaded and then click and wait again
code = """
@ -35,5 +34,7 @@ class TestUi(odoo.tests.HttpCase):
}, 1000);
"""
link = '/web#action=%s' % self.ref('mail.mail_channel_action_client_chat')
self.phantom_js(link, code, "odoo.__DEBUG__.services['mail_reply.reply']", login="admin")
link = "/web#action=%s" % self.ref("mail.mail_channel_action_client_chat")
self.phantom_js(
link, code, "odoo.__DEBUG__.services['mail_reply.reply']", login="admin"
)

23
mail_sent/__manifest__.py

@ -5,25 +5,16 @@
"name": "Sentbox",
"summary": """Quick way to find sent messages""",
"category": "Discuss",
"images": ['images/menu.png'],
"images": ["images/menu.png"],
"version": "12.0.1.2.0",
"author": "IT-Projects LLC, Ivan Yelizariev, Pavel Romanchenko",
"support": "apps@it-projects.info",
"website": "https://it-projects.info",
"license": "LGPL-3",
'price': 40.00,
'currency': 'EUR',
"depends": [
"mail",
],
"data": [
"views/templates.xml",
],
"qweb": [
"static/src/xml/menu.xml",
],
'installable': True,
"price": 40.00,
"currency": "EUR",
"depends": ["mail"],
"data": ["views/templates.xml"],
"qweb": ["static/src/xml/menu.xml"],
"installable": True,
}

38
mail_sent/models.py

@ -1,46 +1,44 @@
from odoo import api, models, fields
from odoo import api, fields, models
class MailMessage(models.Model):
_inherit = 'mail.message'
_inherit = "mail.message"
sent = fields.Boolean('Sent', compute="_compute_sent", help='Was message sent to someone', store=True)
sent = fields.Boolean(
"Sent", compute="_compute_sent", help="Was message sent to someone", store=True
)
@api.depends('author_id', 'partner_ids', 'channel_ids')
@api.depends("author_id", "partner_ids", "channel_ids")
def _compute_sent(self):
for r in self:
r_sudo = r.sudo()
recipient_ids = r_sudo.partner_ids
author_id = r_sudo.author_id
res_id = r_sudo.model and r_sudo.res_id and r_sudo.env[r_sudo.model].browse(r_sudo.res_id)
res_id = (
r_sudo.model
and r_sudo.res_id
and r_sudo.env[r_sudo.model].browse(r_sudo.res_id)
)
sent = author_id and (
len(recipient_ids) > 1
or (
len(recipient_ids) == 1
and recipient_ids[0].id != author_id.id
)
or (
len(r_sudo.channel_ids)
)
or (
res_id
and len(res_id.message_partner_ids - author_id) > 0
)
or (len(recipient_ids) == 1 and recipient_ids[0].id != author_id.id)
or (len(r_sudo.channel_ids))
or (res_id and len(res_id.message_partner_ids - author_id) > 0)
)
r.sent = sent
@api.multi
def message_format(self):
message_values = super(MailMessage, self).message_format()
message_index = {message['id']: message for message in message_values}
message_index = {message["id"]: message for message in message_values}
for item in self:
msg = message_index.get(item.id)
if msg:
msg['sent'] = item.sent
msg["sent"] = item.sent
return message_values
class MailComposeMessage(models.TransientModel):
_inherit = 'mail.compose.message'
sent = fields.Boolean('Sent', help='dummy field to fix inherit error')
_inherit = "mail.compose.message"
sent = fields.Boolean("Sent", help="dummy field to fix inherit error")

185
mail_sent/static/src/js/sent.js

@ -1,106 +1,111 @@
/* # Copyright 2018 Artyom Losev <https://it-projects.info/team/ArtyomLosev>
# Copyright 2019 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). */
odoo.define('mail_sent.sent', function (require) {
"use strict";
odoo.define("mail_sent.sent", function(require) {
"use strict";
var core = require('web.core');
var session = require('web.session');
var Manager = require('mail.Manager');
var Mailbox = require('mail.model.Mailbox');
var SearchableThread = require('mail.model.SearchableThread');
var core = require("web.core");
var session = require("web.session");
var Manager = require("mail.Manager");
var Mailbox = require("mail.model.Mailbox");
var SearchableThread = require("mail.model.SearchableThread");
var _t = core._t;
var _t = core._t;
Manager.include({
_updateMailboxesFromServer: function (data) {
var self = this;
this._super(data);
if (!_.find(this.getThreads(), function(th){
return th.getID() === 'mailbox_channel_sent';
})) {
this._addMailbox({
id: 'channel_sent',
name: _t("Sent Messages"),
mailboxCounter: 0,
});
}
},
Manager.include({
_updateMailboxesFromServer: function(data) {
var self = this;
this._super(data);
if (
!_.find(this.getThreads(), function(th) {
return th.getID() === "mailbox_channel_sent";
})
) {
this._addMailbox({
id: "channel_sent",
name: _t("Sent Messages"),
mailboxCounter: 0,
});
}
},
addMessage: function (data, options) {
var message = this.getMessage(data.id);
if (message) {
var current_threads = message._threadIDs;
var new_channels = data.channel_ids;
if (_.without(new_channels, ...current_threads).length) {
message._threadIDs = _.union(new_channels, current_threads);
addMessage: function(data, options) {
var message = this.getMessage(data.id);
if (message) {
var current_threads = message._threadIDs;
var new_channels = data.channel_ids;
if (_.without(new_channels, ...current_threads).length) {
message._threadIDs = _.union(new_channels, current_threads);
}
} else if (
data.author_id &&
data.author_id[0] &&
odoo.session_info.partner_id &&
data.author_id[0] === odoo.session_info.partner_id
) {
data.channel_ids.push("mailbox_channel_sent");
}
} else if (data.author_id && data.author_id[0] && odoo.session_info.partner_id &&
data.author_id[0] === odoo.session_info.partner_id) {
data.channel_ids.push('mailbox_channel_sent');
}
return this._super(data, options);
},
});
return this._super(data, options);
},
});
SearchableThread.include({
_fetchMessages: function (pDomain, loadMore) {
var self = this;
if (this._id !== 'mailbox_channel_sent') {
return this._super(pDomain, loadMore);
}
SearchableThread.include({
_fetchMessages: function(pDomain, loadMore) {
var self = this;
if (this._id !== "mailbox_channel_sent") {
return this._super(pDomain, loadMore);
}
// this is a copy-paste from super method
var domain = this._getThreadDomain();
var cache = this._getCache(pDomain);
if (pDomain) {
domain = domain.concat(pDomain || []);
}
if (loadMore) {
var minMessageID = cache.messages[0].getID();
domain = [['id', '<', minMessageID]].concat(domain);
}
return this._rpc({
model: 'mail.message',
method: 'message_fetch',
args: [domain],
kwargs: this._getFetchMessagesKwargs(),
}).then(function (messages) {
// except this function. It adds the required thread to downloaded messages
_.each(messages, function(m){
m.channel_ids.push('mailbox_channel_sent');
});
if (!cache.allHistoryLoaded) {
cache.allHistoryLoaded = messages.length < self._FETCH_LIMIT;
// This is a copy-paste from super method
var domain = this._getThreadDomain();
var cache = this._getCache(pDomain);
if (pDomain) {
domain = domain.concat(pDomain || []);
}
cache.loaded = true;
_.each(messages, function (message) {
self.call('mail_service', 'addMessage', message, {
silent: true,
domain: pDomain,
if (loadMore) {
var minMessageID = cache.messages[0].getID();
domain = [["id", "<", minMessageID]].concat(domain);
}
return this._rpc({
model: "mail.message",
method: "message_fetch",
args: [domain],
kwargs: this._getFetchMessagesKwargs(),
}).then(function(messages) {
// Except this function. It adds the required thread to downloaded messages
_.each(messages, function(m) {
m.channel_ids.push("mailbox_channel_sent");
});
if (!cache.allHistoryLoaded) {
cache.allHistoryLoaded = messages.length < self._FETCH_LIMIT;
}
cache.loaded = true;
_.each(messages, function(message) {
self.call("mail_service", "addMessage", message, {
silent: true,
domain: pDomain,
});
});
cache = self._getCache(pDomain || []);
return cache.messages;
});
cache = self._getCache(pDomain || []);
return cache.messages;
});
},
});
},
});
Mailbox.include({
_getThreadDomain: function () {
if (this._id === 'mailbox_channel_sent') {
return [
['sent', '=', true],
['author_id.user_ids', 'in', [session.uid]]
];
}
return this._super();
},
});
return {
'Manager': Manager,
'Mailbox': Mailbox,
};
Mailbox.include({
_getThreadDomain: function() {
if (this._id === "mailbox_channel_sent") {
return [
["sent", "=", true],
["author_id.user_ids", "in", [session.uid]],
];
}
return this._super();
},
});
return {
Manager: Manager,
Mailbox: Mailbox,
};
});

15
mail_sent/static/src/xml/menu.xml

@ -1,11 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<template>
<!--Inherit Sidebar and add Sent menu item after Starred -->
<t t-extend="mail.discuss.Sidebar">
<t t-jquery="div[data-thread-id=mailbox_starred]" t-operation="after">
<div t-attf-class="o_mail_discuss_title_main o_mail_mailbox_title_sent o_mail_discuss_item #{(activeThreadID == 'channel_sent') ? 'o_active': ''}"
data-thread-id="mailbox_channel_sent">
<span class="o_channel_name mail_sent"> <i class="fa fa-send-o"/> Sent </span>
<div
t-attf-class="o_mail_discuss_title_main o_mail_mailbox_title_sent o_mail_discuss_item #{(activeThreadID == 'channel_sent') ? 'o_active': ''}"
data-thread-id="mailbox_channel_sent"
>
<span class="o_channel_name mail_sent"> <i
class="fa fa-send-o"
/> Sent </span>
</div>
</t>
</t>
@ -14,7 +18,8 @@
<t t-jquery="t:last-child" t-operation="after">
<t t-if="thread.getID() === 'mailbox_channel_sent'">
<div class="o_thread_title">No sent messages</div>
<div>You can send messages and then these messages will appear here.</div>
<div
>You can send messages and then these messages will appear here.</div>
</t>
</t>
</t>

1
mail_sent/tests/__init__.py

@ -1,2 +1 @@
from . import test_js

17
mail_sent/tests/test_js.py

@ -1,11 +1,11 @@
import odoo.tests
from werkzeug import url_encode
import odoo.tests
@odoo.tests.common.at_install(True)
@odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase):
def test_01_mail_sent(self):
# wait till page loaded and then click and wait again
@ -13,7 +13,9 @@ class TestUi(odoo.tests.HttpCase):
# 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.
self.env['ir.module.module'].search([('name', '=', 'mail_sent')], limit=1).state = 'installed'
self.env["ir.module.module"].search(
[("name", "=", "mail_sent")], limit=1
).state = "installed"
code = """
setTimeout(function () {
@ -21,5 +23,10 @@ class TestUi(odoo.tests.HttpCase):
setTimeout(function () {console.log('ok');}, 3000);
}, 1000);
"""
link = '/web#%s' % url_encode({'action': 'mail.action_discuss'})
self.phantom_js(link, code, "odoo.__DEBUG__.services['web_tour.tour'].tours.mail_tour.ready", login="demo")
link = "/web#%s" % url_encode({"action": "mail.action_discuss"})
self.phantom_js(
link,
code,
"odoo.__DEBUG__.services['web_tour.tour'].tours.mail_tour.ready",
login="demo",
)

14
mail_sent/views/templates.xml

@ -1,12 +1,14 @@
<?xml version="1.0"?>
<?xml version="1.0" ?>
<openerp>
<data>
<template id="mail_sent_assets_backend"
name="mail_sent_assets_backend"
inherit_id="web.assets_backend">
<template
id="mail_sent_assets_backend"
name="mail_sent_assets_backend"
inherit_id="web.assets_backend"
>
<xpath expr="." position="inside">
<link rel="stylesheet" href="/mail_sent/static/src/css/sent.css"/>
<script src="/mail_sent/static/src/js/sent.js" type="text/javascript"></script>
<link rel="stylesheet" href="/mail_sent/static/src/css/sent.css" />
<script src="/mail_sent/static/src/js/sent.js" type="text/javascript" />
</xpath>
</template>
</data>

16
mail_to/__manifest__.py

@ -2,26 +2,18 @@
"name": """Show message recipients""",
"summary": """Allows you be sure, that all discussion participants were notified""",
"category": "Discuss",
"images": ['images/1.png'],
"images": ["images/1.png"],
"version": "12.0.1.0.1",
"author": "IT-Projects LLC, Pavel Romanchenko",
"support": "apps@it-projects.info",
"website": "https://it-projects.info",
"license": "LGPL-3",
"price": 40.00,
"currency": "EUR",
"depends": [
'mail',
],
"depends": ["mail"],
"external_dependencies": {"python": [], "bin": []},
"data": [
'templates.xml',
],
"qweb": [
'static/src/xml/recipient.xml',
],
"data": ["templates.xml"],
"qweb": ["static/src/xml/recipient.xml"],
"demo": [],
"installable": True,
"auto_install": False,

15
mail_to/static/src/xml/recipient.xml

@ -1,23 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<template>
<t t-extend="mail.widget.Thread.Message">
<t t-jquery="p.o_mail_info" t-operation="append">
<span class="recipients_info">
<t t-set="partner_ids" t-value="message.getCustomerEmailData()"/>
<t t-set="partner_ids" t-value="message.getCustomerEmailData()" />
<t t-if="partner_ids">
<t t-if="partner_ids.length > 0">To: </t>
<t t-foreach="partner_ids.length" t-as="i">
<t t-if="i &lt; 4">
<a t-att-href="_.str.sprintf('/web?#id=%s&amp;view_type=form&amp;model=res.partner', partner_ids[i][0])" class="recipient_link">
<i t-esc="partner_ids[i][1]"/><t t-if="i &lt; partner_ids.length - 1">; </t>
<a
t-att-href="_.str.sprintf('/web?#id=%s&amp;view_type=form&amp;model=res.partner', partner_ids[i][0])"
class="recipient_link"
>
<i t-esc="partner_ids[i][1]" /><t
t-if="i &lt; partner_ids.length - 1"
>; </t>
</a>
</t>
</t>
<t t-if="partner_ids.length &gt; 4">
<span t-att-title="more_recipients">
and <t t-esc="partner_ids.length - 4"/> more
and <t t-esc="partner_ids.length - 4" /> more
</span>
</t>

12
mail_to/templates.xml

@ -1,11 +1,13 @@
<?xml version="1.0"?>
<?xml version="1.0" ?>
<openerp>
<data>
<template id="mail_to_assets_backend"
name="mail_to_assets_backend"
inherit_id="web.assets_backend">
<template
id="mail_to_assets_backend"
name="mail_to_assets_backend"
inherit_id="web.assets_backend"
>
<xpath expr="." position="inside">
<link rel="stylesheet" href="/mail_to/static/src/css/mail_to.css"/>
<link rel="stylesheet" href="/mail_to/static/src/css/mail_to.css" />
</xpath>
</template>
</data>

47
mail_to/tests/test_default.py

@ -1,11 +1,11 @@
import odoo.tests
from werkzeug import url_encode
import odoo.tests
@odoo.tests.common.at_install(True)
@odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase):
def test_01_mail_to(self):
# checks the presence of an element with a link to the recipient
@ -14,33 +14,40 @@ class TestUi(odoo.tests.HttpCase):
# 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.
env['ir.module.module'].search([('name', '=', 'mail_to')], limit=1).state = 'installed'
env["ir.module.module"].search(
[("name", "=", "mail_to")], limit=1
).state = "installed"
# Handle messages in Odoo
res_users_ids = env['res.users'].search([])
res_users_ids.write({'notification_type': 'inbox'})
res_users_ids = env["res.users"].search([])
res_users_ids.write({"notification_type": "inbox"})
# demo messages
partner_ids = env['res.partner'].search([])
msg = env['mail.message'].create({
'subject': '_Test',
'body': self._testMethodName,
'subtype_id': self.ref('mail.mt_comment'),
'model': 'mail.channel',
'partner_ids': [(6, 0, [i.id for i in partner_ids])],
})
partner_ids = env["res.partner"].search([])
msg = env["mail.message"].create(
{
"subject": "_Test",
"body": self._testMethodName,
"subtype_id": self.ref("mail.mt_comment"),
"model": "mail.channel",
"partner_ids": [(6, 0, [i.id for i in partner_ids])],
}
)
# notifications for everyone
for p in partner_ids.ids:
env['mail.notification'].create({
'res_partner_id': p,
'mail_message_id': msg.id,
'is_read': False,
})
env["mail.notification"].create(
{"res_partner_id": p, "mail_message_id": msg.id, "is_read": False}
)
code = """
setTimeout(function () {
console.log($('a.recipient_link').length && 'ok' || 'error');
}, 3000);
"""
link = '/web#%s' % url_encode({'action': 'mail.action_discuss'})
self.phantom_js(link, code, "odoo.__DEBUG__.services['web_tour.tour'].tours.mail_tour.ready", login="admin")
link = "/web#%s" % url_encode({"action": "mail.action_discuss"})
self.phantom_js(
link,
code,
"odoo.__DEBUG__.services['web_tour.tour'].tours.mail_tour.ready",
login="admin",
)

23
mailgun/__manifest__.py

@ -8,40 +8,27 @@
"images": ["images/mailgun_main.png"],
"version": "11.0.1.1.0",
"application": False,
"author": "IT-Projects LLC, Ildar Nasyrov",
"support": "apps@it-projects.info",
"website": "https://it-projects.info/team/iledarn",
"license": "LGPL-3",
"price": 389.00,
"currency": "EUR",
"depends": [
"mail",
],
"depends": ["mail"],
"external_dependencies": {"python": [], "bin": []},
"data": [
'data/ir_cron_data.xml',
],
"demo": [
],
"qweb": [
],
"data": ["data/ir_cron_data.xml"],
"demo": [],
"qweb": [],
"post_load": None,
"pre_init_hook": None,
"post_init_hook": None,
"uninstall_hook": None,
"auto_install": False,
"installable": False,
"demo_title": "Mailgun",
"demo_addons": [],
"demo_addons_hidden": [],
"demo_url": "mailgun",
"demo_summary": "Easy to send outgoing and fetch incoming messages by using Mailgun",
"demo_images": [
"images/mailgun_main.png",
]
"demo_images": ["images/mailgun_main.png"],
}

16
mailgun/controllers/main.py

@ -1,16 +1,16 @@
import re
from odoo import http
from odoo.http import request
import re
class MailMailgun(http.Controller):
@http.route('/mailgun/notify', auth='public', type='http', csrf=False)
@http.route("/mailgun/notify", auth="public", type="http", csrf=False)
def mailgun_notify(self, **kw):
# mailgun notification in json format
message_url = kw.get('message-url')
if not re.match('^https://[^/]*api.mailgun.net/', message_url):
message_url = kw.get("message-url")
if not re.match("^https://[^/]*api.mailgun.net/", message_url):
# simple security check failed
raise Exception('wrong message-url')
request.env['mail.thread'].sudo().mailgun_fetch_message(message_url)
return 'ok'
raise Exception("wrong message-url")
request.env["mail.thread"].sudo().mailgun_fetch_message(message_url)
return "ok"

6
mailgun/data/ir_cron_data.xml

@ -1,15 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<openerp>
<data noupdate="1">
<record id="mailgun_domain_verification" model="ir.cron">
<field name="name">Mailgun - domain verification request</field>
<field name="model_id" ref="model_ir_config_parameter"/>
<field name="model_id" ref="model_ir_config_parameter" />
<field name="interval_number">10</field>
<field name="interval_type">minutes</field>
<field name="numbercall">10</field>
<field name="state">code</field>
<field name="code">model.mailgun_verify()</field>
<field name="doall" eval="True"/>
<field name="doall" eval="True" />
</record>
</data>
</openerp>

20
mailgun/models/ir_config_parameter.py

@ -1,24 +1,28 @@
import logging
import requests
import simplejson
from openerp import models, api
from odoo import api, models
import logging
_logger = logging.getLogger(__name__)
class IrConfigParameter(models.Model):
_inherit = ['ir.config_parameter']
_inherit = ["ir.config_parameter"]
@api.model
def mailgun_verify(self):
verified = self.sudo().get_param('mailgun.verified')
verified = self.sudo().get_param("mailgun.verified")
if verified:
return
api_key = self.sudo().get_param('mailgun.apikey')
mail_domain = self.sudo().get_param('mail.catchall.domain')
api_key = self.sudo().get_param("mailgun.apikey")
mail_domain = self.sudo().get_param("mail.catchall.domain")
if api_key and mail_domain:
url = "https://api.mailgun.net/v3/domains/%s/verify" % mail_domain
res = requests.put(url, auth=("api", api_key))
if res.status_code == 200 and simplejson.loads(res.text)["domain"]["state"] == "active":
self.sudo().set_param('mailgun.verified', '1')
if (
res.status_code == 200
and simplejson.loads(res.text)["domain"]["state"] == "active"
):
self.sudo().set_param("mailgun.verified", "1")

19
mailgun/models/mail_thread.py

@ -1,17 +1,22 @@
import requests
import logging
from odoo import models, api
import requests
from odoo import api, models
import logging
_logger = logging.getLogger(__name__)
class MailThread(models.AbstractModel):
_inherit = 'mail.thread'
_inherit = "mail.thread"
@api.model
def mailgun_fetch_message(self, message_url):
api_key = self.env['ir.config_parameter'].sudo().get_param('mailgun.apikey')
res = requests.get(message_url, headers={'Accept': 'message/rfc2822'}, auth=('api', api_key), verify=False)
self.message_process(False, res.json().get('body-mime'))
api_key = self.env["ir.config_parameter"].sudo().get_param("mailgun.apikey")
res = requests.get(
message_url,
headers={"Accept": "message/rfc2822"},
auth=("api", api_key),
verify=False,
)
self.message_process(False, res.json().get("body-mime"))

22
res_partner_company_messages/__manifest__.py

@ -1,17 +1,15 @@
{
'name': "Aggregate messages from company's contacts",
'version': '1.0.0',
'author': 'IT-Projects LLC, Ivan Yelizariev',
'license': 'LGPL-3',
"name": "Aggregate messages from company's contacts",
"vesion": "12.0.1.0.0",
"author": "IT-Projects LLC, Ivan Yelizariev",
"license": "LGPL-3",
"price": 70.00,
"currency": "EUR",
'category': 'Discuss',
"category": "Discuss",
"support": "apps@it-projects.info",
'website': 'https://twitter.com/yelizariev',
'images': ['images/child.png', 'images/parent.png'],
'depends': ['mail'],
'data': [
'views.xml',
],
'installable': False
"website": "https://twitter.com/yelizariev",
"images": ["images/child.png", "images/parent.png"],
"depends": ["mail"],
"data": ["views.xml"],
"installable": False,
}

18
res_partner_company_messages/models.py

@ -1,18 +1,20 @@
from openerp import api
from openerp import models
from odoo import api, models
class Partner(models.Model):
_inherit = 'res.partner'
_inherit = "res.partner"
@api.multi
def read(self, fields=None, load='_classic_read'):
def read(self, fields=None, load="_classic_read"):
res = super(Partner, self).read(fields=fields, load=load)
if fields and 'message_ids' in fields:
if fields and "message_ids" in fields:
for vals in res:
partner = self.browse(vals['id'])
partner = self.browse(vals["id"])
if not partner.is_company:
continue
domain = [('model', '=', 'res.partner'), ('res_id', 'in', [partner.id] + partner.child_ids.ids)]
vals['message_ids'] = self.env['mail.message'].search(domain).ids
domain = [
("model", "=", "res.partner"),
("res_id", "in", [partner.id] + partner.child_ids.ids),
]
vals["message_ids"] = self.env["mail.message"].search(domain).ids
return res

2
res_partner_company_messages/views.xml

@ -1,3 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<openerp><data>
</data></openerp>

16
res_partner_mails_count/__manifest__.py

@ -2,27 +2,21 @@
"name": """Partner mails count""",
"summary": """Displays amount of incoming and outgoing partner mails.""",
"category": "Discuss",
"images": ['images/1.png'],
"version": "1.0.0",
"images": ["images/1.png"],
"vesion": "12.0.1.0.0",
"author": "IT-Projects LLC, Pavel Romanchenko",
"support": "apps@it-projects.info",
"website": "https://it-projects.info",
"license": "LGPL-3",
"price": 30.00,
"currency": "EUR",
"depends": [
'mail_all',
"mail_all",
# 'web_tour_extra',
],
"external_dependencies": {"python": [], "bin": []},
"data": [
'views/res_partner_mails_count.xml',
'templates.xml',
],
"demo": [
],
"data": ["views/res_partner_mails_count.xml", "templates.xml"],
"demo": [],
"installable": False,
"auto_install": False,
}

15
res_partner_mails_count/models.py

@ -1,18 +1,23 @@
from openerp import models, fields, api
from odoo import api, fields, models
class ResPartner(models.Model):
_inherit = 'res.partner'
_inherit = "res.partner"
mails_to = fields.Integer(compute="_compute_mails_to")
mails_from = fields.Integer(compute="_compute_mails_from")
@api.multi
def _compute_mails_to(self):
for r in self:
r.mails_to = self.env['mail.message'].sudo().search_count([('partner_ids', 'in', r.id)])
r.mails_to = (
self.env["mail.message"]
.sudo()
.search_count([("partner_ids", "in", r.id)])
)
@api.multi
def _compute_mails_from(self):
for r in self:
r.mails_from = self.env['mail.message'].sudo().search_count([('author_id', '=', r.id)])
r.mails_from = (
self.env["mail.message"].sudo().search_count([("author_id", "=", r.id)])
)

44
res_partner_mails_count/static/src/js/res_partner_mails_count_tour.js

@ -1,36 +1,38 @@
odoo.define('res_partner_mails_count.res_partner_mails_count_tour', function (require) {
'use strict';
var Core = require('web.core');
var Tour = require('web.Tour');
odoo.define("res_partner_mails_count.res_partner_mails_count_tour", function(require) {
"use strict";
var Core = require("web.core");
var Tour = require("web.Tour");
var _t = Core._t;
Tour.register({
id: 'mails_count_tour',
id: "mails_count_tour",
name: _t("Mails count Tour"),
mode: 'test',
path: '/web?res_partner_mails_count=tutorial#id=3&view_type=form&model=res.partner',
mode: "test",
path:
"/web?res_partner_mails_count=tutorial#id=3&view_type=form&model=res.partner",
steps: [
{
title: _t("Mails count tutorial"),
content: _t("Let's see how mails count work."),
popover: { next: _t("Start Tutorial"), end: _t("Skip") },
title: _t("Mails count tutorial"),
content: _t("Let's see how mails count work."),
popover: {next: _t("Start Tutorial"), end: _t("Skip")},
},
{
title: _t("New fields"),
content: _t("Here is new fields with mails counters. Press one of it."),
element: '.mails_to',
waitFor: '.mails_to:visible',
title: _t("New fields"),
content: _t("Here is new fields with mails counters. Press one of it."),
element: ".mails_to",
waitFor: ".mails_to:visible",
},
{
title: _t("Done"),
placement: 'top',
waitNot: '.mails_to:visible',
waitFor: '.o_mail_thread',
element: '.o_mail_thread',
content: _t("Message are found. <br/>Enjoy your day! <br/> <br/><a href='https://www.it-projects.info/apps' target='_blank'>IT-Projects LLC</a> team "),
placement: "top",
waitNot: ".mails_to:visible",
waitFor: ".o_mail_thread",
element: ".o_mail_thread",
content: _t(
"Message are found. <br/>Enjoy your day! <br/> <br/><a href='https://www.it-projects.info/apps' target='_blank'>IT-Projects LLC</a> team "
),
popover: {next: _t("Close Tutorial")},
},
]
],
});
});

35
res_partner_mails_count/templates.xml

@ -1,4 +1,4 @@
<?xml version="1.0"?>
<?xml version="1.0" ?>
<openerp>
<data>
<!--
@ -13,34 +13,47 @@
<record id="view_res_partner_mails_count_info_form" model="ir.ui.view">
<field name="name">res.partner.mails.count</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="priority" eval="50"/>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="priority" eval="50" />
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button class="oe_stat_button mails_to" type="action"
<button
class="oe_stat_button mails_to"
type="action"
name="%(action_mails)d"
context="{'search_default_partner_ids': [active_id], 'default_model': 'res.partner', 'default_res_id': active_id}"
icon="fa-envelope">
<field string="Mails to" name="mails_to" widget="statinfo"/>
icon="fa-envelope"
>
<field string="Mails to" name="mails_to" widget="statinfo" />
</button>
<button class="oe_stat_button mails_from" type="action"
<button
class="oe_stat_button mails_from"
type="action"
name="%(action_mails)d"
context="{'search_default_author_id': active_id, 'default_model': 'res.partner', 'default_res_id': active_id}"
icon="fa-envelope-o">
<field string="Mails from" name="mails_from" widget="statinfo"/>
icon="fa-envelope-o"
>
<field
string="Mails from"
name="mails_from"
widget="statinfo"
/>
</button>
</div>
</field>
</record>
<record id="res_partner_mails_count_tutorial" model="ir.actions.act_url">
<field name="name">res_partner_mails_count Tutorial</field>
<field name="url" eval="'/web?res_partner_mails_count=tutorial#id='+str(ref('base.partner_root'))+'&amp;view_type=form&amp;model=res.partner&amp;/#tutorial_extra.mails_count_tour=true'"/>
<field
name="url"
eval="'/web?res_partner_mails_count=tutorial#id='+str(ref('base.partner_root'))+'&amp;view_type=form&amp;model=res.partner&amp;/#tutorial_extra.mails_count_tour=true'"
/>
<field name="target">self</field>
</record>
</data>
<data noupdate="1">
<record id="base.open_menu" model="ir.actions.todo">
<field name="action_id" ref="res_partner_mails_count_tutorial"/>
<field name="action_id" ref="res_partner_mails_count_tutorial" />
<field name="state">open</field>
<field name="sequence">500</field>
<field name="type">automatic</field>

2
res_partner_mails_count/tests/__init__.py

@ -1,3 +1,3 @@
from . import test_mail
# from . import test_phantom

67
res_partner_mails_count/tests/test_mail.py

@ -1,33 +1,62 @@
from openerp.tests.common import TransactionCase
from odoo.tests.common import TransactionCase
class TestMessageCount(TransactionCase):
post_install = True
def test_count(self):
new_partner1 = self.env['res.partner'].sudo().create({'name': 'rpmc Test Partner one', 'email': 'tt@tt', 'notify_email': 'always'})
new_partner2 = self.env['res.partner'].sudo().create({'name': 'rpmc Test Partner two', 'email': 'rr@rr', 'notify_email': 'always'})
self.assertEqual(new_partner1.mails_to, 0, 'rpmc: new partner have mails_to != 0')
mail_compose = self.env['mail.compose.message']
new_partner1 = (
self.env["res.partner"]
.sudo()
.create(
{
"name": "rpmc Test Partner one",
"email": "tt@tt",
"notify_email": "always",
}
)
)
new_partner2 = (
self.env["res.partner"]
.sudo()
.create(
{
"name": "rpmc Test Partner two",
"email": "rr@rr",
"notify_email": "always",
}
)
)
self.assertEqual(
new_partner1.mails_to, 0, "rpmc: new partner have mails_to != 0"
)
mail_compose = self.env["mail.compose.message"]
compose = mail_compose.with_context(
{"default_composition_mode": "comment"}
).create(
{
'default_composition_mode': 'comment',
}).create(
{
'subject': 'test subj',
'body': 'test body',
'partner_ids': [(4, new_partner2.id)],
'email_from': 'tt@tt',
'author_id': new_partner1.id
})
"subject": "test subj",
"body": "test body",
"partner_ids": [(4, new_partner2.id)],
"email_from": "tt@tt",
"author_id": new_partner1.id,
}
)
compose.send_mail()
self.assertEqual(new_partner1.mails_to, 0)
self.assertEqual(new_partner1.mails_from, 1, 'rpmc: one message but mails_from != 1')
self.assertEqual(new_partner2.mails_to, 1, 'rpmc: one message but mails_to != 1')
self.assertEqual(
new_partner1.mails_from, 1, "rpmc: one message but mails_from != 1"
)
self.assertEqual(
new_partner2.mails_to, 1, "rpmc: one message but mails_to != 1"
)
self.assertEqual(new_partner2.mails_from, 0)
compose.send_mail()
self.assertEqual(new_partner1.mails_to, 0)
self.assertEqual(new_partner1.mails_from, 2, 'rpmc: one message but mails_from != 2')
self.assertEqual(new_partner2.mails_to, 2, 'rpmc: one message but mails_to != 2')
self.assertEqual(
new_partner1.mails_from, 2, "rpmc: one message but mails_from != 2"
)
self.assertEqual(
new_partner2.mails_to, 2, "rpmc: one message but mails_to != 2"
)
self.assertEqual(new_partner2.mails_from, 0)

17
res_partner_mails_count/tests/test_phantom.py

@ -4,10 +4,14 @@ import odoo.tests
@odoo.tests.common.at_install(False)
@odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase):
def test_01_res_partner_mails_to_count(self):
# self.phantom_js('/', "openerp.Tour.run('mails_count_tour', 'test')", "openerp.Tour.tours.mails_count_tour", login="admin")
self.phantom_js("/", "odoo.__DEBUG__.services['web.Tour'].run('mails_count_tour', 'test')", "odoo.__DEBUG__.services['web.Tour'].tours.mails_count_tour", login="admin")
self.phantom_js(
"/",
"odoo.__DEBUG__.services['web.Tour'].run('mails_count_tour', 'test')",
"odoo.__DEBUG__.services['web.Tour'].tours.mails_count_tour",
login="admin",
)
def test_02_res_partner_mails_from_count(self):
# wait till page loaded and then click and wait again
@ -17,5 +21,10 @@ class TestUi(odoo.tests.HttpCase):
setTimeout(function () {console.log('ok');}, 3000);
}, 3000);
"""
link = '/web#id=3&view_type=form&model=res.partner'
self.phantom_js(link, code, "odoo.__DEBUG__.services['web.Tour'].tours.mails_count_tour", login="admin")
link = "/web#id=3&view_type=form&model=res.partner"
self.phantom_js(
link,
code,
"odoo.__DEBUG__.services['web.Tour'].tours.mails_count_tour",
login="admin",
)

2
res_partner_mails_count/views/res_partner_mails_count.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<openerp>
<data>
<record id="action_mails" model="ir.actions.client">

Loading…
Cancel
Save