Browse Source

💚 pre-commit: auto cleanups

pull/260/head
Ivan Yelizariev 5 years ago
parent
commit
6297d42801
No known key found for this signature in database GPG Key ID: 93F14FB6A8B57057
  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. 4
      mail_base/controllers/main.py
  21. 19
      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. 49
      mail_check_immediately/models.py
  27. 52
      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. 148
      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. 522
      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. 14
      mail_recovery/static/src/js/mail_recovery.js
  68. 14
      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. 21
      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. 14
      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. 65
      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 # see https://github.com/Tecnativa/doodba#optodoocustomsrcaddonsyaml
--- ---
ENV: ENV:
DEFAULT_REPO_PATTERN: https://github.com/it-projects-llc/{}.git DEFAULT_REPO_PATTERN: https://github.com/it-projects-llc/{}.git

13
.github/workflows/main.yml

@ -6,11 +6,14 @@ on:
jobs: jobs:
notify: notify:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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=odoo
known_odoo_addons=odoo.addons known_odoo_addons=odoo.addons
sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER
known_third_party=
known_third_party=openerp,requests,simplejson,werkzeug

19
.travis.yml

@ -12,23 +12,24 @@ addons:
postgresql: "9.5" postgresql: "9.5"
apt: apt:
packages: 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: env:
global: 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: 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: install:
- pip install anybox.testing.openerp - 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} - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH}
- travis_install_nightly - travis_install_nightly

24
mail_all/__manifest__.py

@ -7,34 +7,24 @@
"summary": """Checkout all messages where you have access""", "summary": """Checkout all messages where you have access""",
"category": "Discuss", "category": "Discuss",
# "live_test_url": "", # "live_test_url": "",
"images": ['images/1.jpg'],
"images": ["images/1.jpg"],
"version": "12.0.1.0.1", "version": "12.0.1.0.1",
"application": False, "application": False,
"author": "IT-Projects LLC, Pavel Romanchenko", "author": "IT-Projects LLC, Pavel Romanchenko",
"support": "apps@it-projects.info", "support": "apps@it-projects.info",
"website": "https://it-projects.info", "website": "https://it-projects.info",
"license": "LGPL-3", "license": "LGPL-3",
'price': 40.00,
'currency': 'EUR',
"depends": [
"mail"
],
"price": 40.00,
"currency": "EUR",
"depends": ["mail"],
"external_dependencies": {"python": [], "bin": []}, "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": [], "demo": [],
"post_load": None, "post_load": None,
"pre_init_hook": None, "pre_init_hook": None,
"post_init_hook": None, "post_init_hook": None,
"uninstall_hook": None, "uninstall_hook": None,
'installable': True,
"installable": True,
"auto_install": False, "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 2017-2018 Artyom Losev <https://it-projects.info/team/ArtyomLosev>
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> # Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). */ # 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> /* # Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). */ # 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"; "use strict";
var tour = require("web_tour.tour"); var tour = require("web_tour.tour");
var core = require('web.core');
var core = require("web.core");
var _t = core._t; var _t = core._t;
var steps = [{
var steps = [
{
trigger: 'a.full[href="#"]', trigger: 'a.full[href="#"]',
content: _t("Click to open app list"), content: _t("Click to open app list"),
position: 'bottom',
}, {
position: "bottom",
},
{
trigger: 'a.dropdown-item.o_app:contains("Discuss")', trigger: 'a.dropdown-item.o_app:contains("Discuss")',
content: _t("Click to enter menu discuss"), content: _t("Click to enter menu discuss"),
position: 'bottom',
}, {
position: "bottom",
},
{
content: _t("Open All Messages"), 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"), 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 Artyom Losev <https://it-projects.info/team/ArtyomLosev>
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> # Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). --> # 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 --> <!--Inherit Sidebar and add All messages menu item after Starred -->
<t t-extend="mail.discuss.Sidebar"> <t t-extend="mail.discuss.Sidebar">
<t t-jquery="div[data-thread-id=mailbox_starred]" t-operation="after"> <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> </div>
</t> </t>
</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.at_install(True)
@odoo.tests.common.post_install(True) @odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase): class TestUi(odoo.tests.HttpCase):
def test_01_mail_all(self): def test_01_mail_all(self):
# needed because tests are run before the module is marked as # needed because tests are run before the module is marked as
# installed. In js web will only load qweb coming from modules # installed. In js web will only load qweb coming from modules
# that are returned by the backend in module_boot. Without # that are returned by the backend in module_boot. Without
# this you end up with js, css but no qweb. # 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 2016 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2018 Artyom Losev <https://it-projects.info/team/ArtyomLosev> # Copyright 2018 Artyom Losev <https://it-projects.info/team/ArtyomLosev>
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> # Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). --> # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). -->
<odoo> <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"> <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> </xpath>
</template> </template>
</odoo> </odoo>

23
mail_archives/__manifest__.py

@ -2,25 +2,16 @@
"name": "Mail archives", "name": "Mail archives",
"summary": """Adds menu to find old messages""", "summary": """Adds menu to find old messages""",
"category": "Discuss", "category": "Discuss",
"images": ['images/1.jpg'],
"images": ["images/1.jpg"],
"version": "12.0.1.0.1", "version": "12.0.1.0.1",
"author": "IT-Projects LLC, Pavel Romanchenko", "author": "IT-Projects LLC, Pavel Romanchenko",
"support": "apps@it-projects.info", "support": "apps@it-projects.info",
"website": "https://it-projects.info", "website": "https://it-projects.info",
"license": "LGPL-3", "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> <template>
<!--Inherit Sidebar and add Archive menu item after Starred --> <!--Inherit Sidebar and add Archive menu item after Starred -->
<t t-extend="mail.discuss.Sidebar"> <t t-extend="mail.discuss.Sidebar">
<t t-jquery="div[data-thread-id=mailbox_starred]" t-operation="after"> <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> </div>
</t> </t>
</t> </t>

1
mail_archives/tests/__init__.py

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

17
mail_archives/tests/test_js.py

@ -1,18 +1,20 @@
import odoo.tests
from werkzeug import url_encode from werkzeug import url_encode
import odoo.tests
@odoo.tests.common.at_install(True) @odoo.tests.common.at_install(True)
@odoo.tests.common.post_install(True) @odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase): class TestUi(odoo.tests.HttpCase):
def test_01_mail_archives(self): def test_01_mail_archives(self):
# needed because tests are run before the module is marked as # needed because tests are run before the module is marked as
# installed. In js web will only load qweb coming from modules # installed. In js web will only load qweb coming from modules
# that are returned by the backend in module_boot. Without # that are returned by the backend in module_boot. Without
# this you end up with js, css but no qweb. # 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 # wait till page loaded and then click and wait again
code = """ code = """
@ -20,5 +22,10 @@ class TestUi(odoo.tests.HttpCase):
console.log($(".mail_archives").length && 'ok' || 'error'); console.log($(".mail_archives").length && 'ok' || 'error');
}, 3000); }, 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> <openerp>
<data> <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"> <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> </xpath>
</template> </template>
</data> </data>

1
mail_base/__init__.py

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

18
mail_base/__manifest__.py

@ -4,21 +4,13 @@
"category": "Discuss", "category": "Discuss",
"images": [], "images": [],
"version": "11.0.1.0.2", "version": "11.0.1.0.2",
"author": "IT-Projects LLC, Pavel Romanchenko", "author": "IT-Projects LLC, Pavel Romanchenko",
"support": "apps@it-projects.info", "support": "apps@it-projects.info",
"website": "https://it-projects.info", "website": "https://it-projects.info",
"license": "LGPL-3", "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,
} }

4
mail_base/controllers/main.py

@ -1,5 +1,5 @@
from openerp.http import request
from openerp.addons.bus.controllers.main import BusController from openerp.addons.bus.controllers.main import BusController
from openerp.http import request
class MailChatController(BusController): class MailChatController(BusController):
@ -9,6 +9,6 @@ class MailChatController(BusController):
def _poll(self, dbname, channels, last, options): def _poll(self, dbname, channels, last, options):
if request.session.uid: 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) return super(MailChatController, self)._poll(dbname, channels, last, options)

19
mail_base/models.py

@ -1,30 +1,31 @@
from openerp import api, models from openerp import api, models
class MailMessage(models.Model): class MailMessage(models.Model):
_inherit = 'mail.message'
_inherit = "mail.message"
@api.multi @api.multi
def write(self, values): 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: if triplet[0] == 6:
for id in triplet[2]: 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) return super(MailMessage, self).write(values)
class MailComposer(models.TransientModel): class MailComposer(models.TransientModel):
_inherit = 'mail.compose.message'
_inherit = "mail.compose.message"
@api.multi @api.multi
def send_mail(self, auto_commit=False): def send_mail(self, auto_commit=False):
res = super(MailComposer, self).send_mail(auto_commit=auto_commit) res = super(MailComposer, self).send_mail(auto_commit=auto_commit)
notification = {} 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 return res

1
mail_base/tests/__init__.py

@ -1,2 +1 @@
from . import test_default 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.at_install(False)
@odoo.tests.common.post_install(True) @odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase): class TestUi(odoo.tests.HttpCase):
def test_01_mail_base(self): def test_01_mail_base(self):
# wait till page loaded # wait till page loaded
code = """ code = """
@ -12,5 +11,7 @@ class TestUi(odoo.tests.HttpCase):
console.log('ok'); console.log('ok');
}, 1000); }, 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> <openerp>
<data> <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"> <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> </xpath>
</template> </template>
</data> </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",
"version": "1.0.1",
"author": "IT-Projects LLC, Ivan Yelizariev",
"license": "LGPL-3",
"category": "Discuss", "category": "Discuss",
"support": "apps@it-projects.info", "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,
} }

49
mail_check_immediately/models.py

@ -1,15 +1,12 @@
import datetime import datetime
from openerp import api, exceptions, fields, models, tools
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp import tools
from openerp import exceptions
from openerp import models, fields, api
class FetchMailServer(models.Model): class FetchMailServer(models.Model):
_inherit = 'fetchmail.server'
_name = 'fetchmail.server'
_inherit = "fetchmail.server"
_name = "fetchmail.server"
_last_updated = None _last_updated = None
@ -19,36 +16,54 @@ class FetchMailServer(models.Model):
if not self._last_updated: if not self._last_updated:
self._last_updated = tools.datetime.now() 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 src_format = tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
dst_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 return _now
@api.model @api.model
def _fetch_mails(self): 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 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() 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: if res:
res[0].run_time = self._run_time() res[0].run_time = self._run_time()
class FetchMailImmediately(models.AbstractModel): class FetchMailImmediately(models.AbstractModel):
_name = 'fetch_mail.imm'
_name = "fetch_mail.imm"
@api.model @api.model
def get_last_update_time(self): 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] array = [r.run_time for r in res]
if array: if array:
return array[0] return array[0]
@ -58,8 +73,8 @@ class FetchMailImmediately(models.AbstractModel):
@api.model @api.model
def run_fetchmail_manually(self): 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_task._try_lock()
fetchmail_model.with_context(run_fetchmail_manually=True)._fetch_mails() fetchmail_model.with_context(run_fetchmail_manually=True)._fetch_mails()

52
mail_check_immediately/static/src/js/main.js

@ -1,14 +1,12 @@
openerp.mail_check_immediately = function(instance, local) { openerp.mail_check_immediately = function(instance, local) {
instance.mail.Wall.include({ instance.mail.Wall.include({
init: function(){
init: function() {
this._super.apply(this, arguments); this._super.apply(this, arguments);
var _this = this; 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(); _this.run_fetchmail_manually();
}; };
}, },
@ -16,40 +14,46 @@ openerp.mail_check_immediately = function(instance, local) {
start: function() { start: function() {
var _this = this; var _this = this;
this._super(); this._super();
this.get_last_fetched_time(); this.get_last_fetched_time();
this.get_time_loop = setInterval(function(){
this.get_time_loop = setInterval(function() {
_this.get_last_fetched_time(); _this.get_last_fetched_time();
}, 30000); }, 30000);
}, },
run_fetchmail_manually: function(){
run_fetchmail_manually: function() {
var _this = this; 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; 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;
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); 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> <templates>
<t t-name="fetch_mail_immediately.header"> <t t-name="fetch_mail_immediately.header">
<tr class="oe_header_row"> <tr class="oe_header_row">
@ -6,19 +6,23 @@
<div class="oe_view_manager_fetch_mail_imm"> <div class="oe_view_manager_fetch_mail_imm">
<em> <em>
<span>Mails fetched:</span> <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> </a>
</em> </em>
</div> </div>
</td> </td>
<td></td>
<td />
</tr> </tr>
</t> </t>
<t t-extend="mail.wall"> <t t-extend="mail.wall">
<t t-jquery="tr.oe_header_row_top" t-operation="after"> <t t-jquery="tr.oe_header_row_top" t-operation="after">
<t t-call="fetch_mail_immediately.header"> <t t-call="fetch_mail_immediately.header">
<t t-set="colspan" t-value="2"/>
<t t-set="colspan" t-value="2" />
</t> </t>
</t> </t>
</t> </t>

11
mail_check_immediately/views.xml

@ -1,8 +1,15 @@
<openerp> <openerp>
<data> <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"> <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> </xpath>
</template> </template>
</data> </data>

4
mail_fix_553/__manifest__.py

@ -2,11 +2,11 @@
"name": "Fix mail error 553", "name": "Fix mail error 553",
"version": "0.3", "version": "0.3",
"author": "IT-Projects LLC, Ivan Yelizariev", "author": "IT-Projects LLC, Ivan Yelizariev",
'license': 'LGPL-3',
"license": "LGPL-3",
"category": "Discuss", "category": "Discuss",
"support": "apps@it-projects.info", "support": "apps@it-projects.info",
"website": "https://yelizariev.github.io", "website": "https://yelizariev.github.io",
"depends": ["base", "mail"], "depends": ["base", "mail"],
"data": ["data.xml"], "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> <openerp>
<data noupdate="1"> <data noupdate="1">
<!-- Catchall Email Alias --> <!-- Catchall Email Alias -->

148
mail_fix_553/mail_fix_553.py

@ -1,11 +1,9 @@
import base64 import base64
import logging import logging
import re import re
from email.utils import formataddr from email.utils import formataddr
from openerp import tools
from openerp import SUPERUSER_ID
from openerp import SUPERUSER_ID, tools
from openerp.addons.base.ir.ir_mail_server import MailDeliveryException from openerp.addons.base.ir.ir_mail_server import MailDeliveryException
from openerp.osv import osv from openerp.osv import osv
from openerp.tools.safe_eval import safe_eval as eval from openerp.tools.safe_eval import safe_eval as eval
@ -17,7 +15,9 @@ _logger = logging.getLogger(__name__)
class MailMail(osv.Model): class MailMail(osv.Model):
_inherit = "mail.mail" _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 # copy-paste from addons/mail/mail_mail.py
""" Sends the selected emails immediately, ignoring their current """ Sends the selected emails immediately, ignoring their current
state (mails that have already been sent should not be passed state (mails that have already been sent should not be passed
@ -35,50 +35,83 @@ class MailMail(osv.Model):
""" """
# NEW STUFF # 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 {}) 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): for mail in self.browse(cr, SUPERUSER_ID, ids, context=context):
try: 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 # 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: 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: else:
model = None model = None
if model: if model:
context['model_name'] = model.name
context["model_name"] = model.name
# load attachment binary data with a separate read(), as prefetching all # load attachment binary data with a separate read(), as prefetching all
# `datas` (binary field) could bloat the browse cache, triggerring # `datas` (binary field) could bloat the browse cache, triggerring
# soft/hard mem limits with temporary data. # soft/hard mem limits with temporary data.
attachment_ids = [a.id for a in mail.attachment_ids] 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 # specific behavior to customize the send email for notified partners
email_list = [] email_list = []
if mail.email_to: 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: 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
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 bounce_alias and catchall_domain:
if mail.model and mail.res_id: 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: 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: if mail.headers:
try: try:
headers.update(eval(mail.headers)) headers.update(eval(mail.headers))
@ -88,7 +121,7 @@ class MailMail(osv.Model):
# Writing on the mail object may fail (e.g. lock on user) which # Writing on the mail object may fail (e.g. lock on user) which
# would trigger a rollback *after* actually sending the email. # would trigger a rollback *after* actually sending the email.
# To avoid sending twice the same email, provoke the failure earlier # To avoid sending twice the same email, provoke the failure earlier
mail.write({'state': 'exception'})
mail.write({"state": "exception"})
mail_sent = False mail_sent = False
# build an RFC2822 email.message.Message object and send it without queuing # build an RFC2822 email.message.Message object and send it without queuing
@ -104,58 +137,77 @@ class MailMail(osv.Model):
msg = ir_mail_server.build_email( msg = ir_mail_server.build_email(
email_from=email_from, # NEW STUFF 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), email_cc=tools.email_split(mail.email_cc),
reply_to=mail.reply_to, reply_to=mail.reply_to,
attachments=attachments, attachments=attachments,
message_id=mail.message_id, message_id=mail.message_id,
references=mail.references, 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: 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: except AssertionError as error:
if error.message == ir_mail_server.NO_VALID_RECIPIENT: if error.message == ir_mail_server.NO_VALID_RECIPIENT:
# No valid recipient found for this particular # No valid recipient found for this particular
# mail item -> ignore error to avoid blocking # mail item -> ignore error to avoid blocking
# delivery to next recipients, if any. If this is # delivery to next recipients, if any. If this is
# the only recipient, the mail will show as failed. # 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: else:
raise raise
if res: if res:
mail.write({'state': 'sent', 'message_id': res})
mail.write({"state": "sent", "message_id": res})
mail_sent = True mail_sent = True
# /!\ can't use mail.state here, as mail.refresh() will cause an error # /!\ can't use mail.state here, as mail.refresh() will cause an error
# see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1 # see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
if mail_sent: 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: except MemoryError:
# prevent catching transient MemoryErrors, bubble up to notify user or abort cron job # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job
# instead of marking the mail as failed # 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 raise
except Exception as e: 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 raise_exception:
if isinstance(e, AssertionError): if isinstance(e, AssertionError):
# get the args of the original error, wrap into a value and throw a MailDeliveryException # 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 # 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 MailDeliveryException(_("Mail Delivery Failed"), value)
raise raise

33
mail_move_message/__manifest__.py

@ -6,25 +6,18 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). # 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", "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). # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo.http import request from odoo.http import request
from odoo.addons.bus.controllers.main import BusController from odoo.addons.bus.controllers.main import BusController
@ -14,6 +15,6 @@ class MailChatController(BusController):
def _poll(self, dbname, channels, last, options): def _poll(self, dbname, channels, last, options):
if request.session.uid: 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) 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 2016 Ildar Nasyrov <https://it-projects.info/team/iledarn>
# Copyright 2017 Ivan Yelizariev <https://it-projects.info/team/yelizariev> # Copyright 2017 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).--> # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).-->
<odoo> <odoo>
<record id="mail_relocation_models" model="ir.config_parameter"> <record id="mail_relocation_models" model="ir.config_parameter">
<field name="key">mail_relocation_models</field> <field name="key">mail_relocation_models</field>

522
mail_move_message/mail_move_message_models.py

@ -5,32 +5,34 @@
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> # Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). # 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 import email_split
from odoo.tools.translate import _ from odoo.tools.translate import _
from odoo import exceptions
class Wizard(models.TransientModel): 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 @api.model
def _model_selection(self): def _model_selection(self):
selection = [] 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: if message.model and message.model not in model_names:
model_names.append(message.model) model_names.append(message.model)
if message.moved_from_model and message.moved_from_model not in model_names: if message.moved_from_model and message.moved_from_model not in model_names:
model_names.append(message.moved_from_model) model_names.append(message.moved_from_model)
if model_names: 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 return selection
@api.model @api.model
@ -40,70 +42,107 @@ class Wizard(models.TransientModel):
available_models = self._model_selection() available_models = self._model_selection()
if len(available_models): if len(available_models):
record = self.env[available_models[0][0]].search([], limit=1) 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 email_from = message.email_from
parts = email_split(email_from.replace(' ', ','))
parts = email_split(email_from.replace(" ", ","))
if parts: if parts:
email = parts[0] 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: else:
name, email = email_from 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: 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 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_email_from = fields.Char()
message_name_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 # 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() uid = fields.Integer()
move_followers = fields.Boolean( move_followers = fields.Boolean(
'Move Followers',
"Move Followers",
help="Add followers of current record to a new record.\n" 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.multi
@api.depends('model_record')
@api.depends("model_record")
def _compute_model_res_id(self): def _compute_model_res_id(self):
for rec in self: for rec in self:
rec.model = rec.model_record and rec.model_record._name or False 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 rec.res_id = rec.model_record and rec.model_record.id or False
@api.depends('message_id')
@api.depends("message_id")
@api.multi @api.multi
def _compute_get_can_move(self): def _compute_get_can_move(self):
for r in self: for r in self:
@ -111,32 +150,43 @@ class Wizard(models.TransientModel):
@api.multi @api.multi
def _compute_is_read(self): 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] self.message_to_read = True in [m.needaction for m in messages]
@api.multi @api.multi
def get_can_move_one(self): def get_can_move_one(self):
self.ensure_one() self.ensure_one()
# message was not moved before OR message is a top message of previous move # 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): def on_change_move_back(self):
if not self.move_back: if not self.move_back:
return return
self.parent_id = self.message_id.moved_from_parent_id self.parent_id = self.message_id.moved_from_parent_id
message = self.message_id message = self.message_id
if message.is_moved: 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): def update_move_back(self):
model = self.message_id.moved_from_model 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)) 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): def on_change_parent_id(self):
if self.parent_id and self.parent_id.model: if self.parent_id and self.parent_id.model:
self.model = self.parent_id.model self.model = self.parent_id.model
@ -145,24 +195,26 @@ class Wizard(models.TransientModel):
self.model = None self.model = None
self.res_id = 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): 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: if self.model and self.filter_by_partner and self.partner_id:
fields = self.env[self.model].fields_get(False) fields = self.env[self.model].fields_get(False)
contact_field = False contact_field = False
for n, f in fields.items(): 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 contact_field = n
break break
if contact_field: 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: 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 self.res_id = res_id and res_id[0].id
else: else:
self.res_id = None self.res_id = None
return {'domain': domain}
return {"domain": domain}
@api.multi @api.multi
def check_access(self): def check_access(self):
@ -172,16 +224,18 @@ class Wizard(models.TransientModel):
@api.multi @api.multi
def check_access_one(self): def check_access_one(self):
self.ensure_one() self.ensure_one()
operation = 'write'
operation = "write"
if not (self.model and self.res_id): if not (self.model and self.res_id):
return True return True
model_obj = self.env[self.model] model_obj = self.env[self.model]
mids = model_obj.browse(self.res_id).exists() 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) model_obj.check_mail_message_access(mids.ids, operation)
else: 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 @api.multi
def open_moved_by_message_id(self): def open_moved_by_message_id(self):
@ -189,44 +243,60 @@ class Wizard(models.TransientModel):
for r in self: for r in self:
message_id = r.message_moved_by_message_id.id message_id = r.message_moved_by_message_id.id
return { 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 @api.multi
def move(self): def move(self):
for r in self: for r in self:
if not r.model: 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: for r in self:
r.check_access() 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 # 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.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 { 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 { 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 @api.multi
@ -240,8 +310,10 @@ class Wizard(models.TransientModel):
msg_id = self.message_id.id msg_id = self.message_id.id
# Send notification # 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() self.message_id.unlink()
return {} return {}
@ -256,20 +328,34 @@ class Wizard(models.TransientModel):
self.ensure_one() self.ensure_one()
self.message_id.set_message_done() self.message_id.set_message_done()
self.message_id.child_ids.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): 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 @api.multi
def _compute_get_all_childs(self, include_myself=True): def _compute_get_all_childs(self, include_myself=True):
@ -283,30 +369,59 @@ class MailMessage(models.Model):
if include_myself: if include_myself:
ids.append(self.id) ids.append(self.id)
while True: 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: if new_ids:
ids = ids + new_ids ids = ids + new_ids
continue continue
break 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 self.all_child_ids = ids + moved_childs
@api.multi @api.multi
def move_followers(self, model, ids): def move_followers(self, model, ids):
fol_obj = self.env['mail.followers']
fol_obj = self.env["mail.followers"]
for message in self: 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: 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 @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: 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 @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() self.ensure_one()
if parent_id == self.id: if parent_id == self.id:
# if for any reason method is called to move message with parent # if for any reason method is called to move message with parent
@ -314,116 +429,131 @@ class MailMessage(models.Model):
# building message tree # building message tree
return return
if not self.author_id: if not self.author_id:
self.write({
'author_id': author.id,
})
self.write(
{"author_id": author.id,}
)
vals = {} vals = {}
if move_back: if move_back:
# clear variables if we move everything 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: 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 # 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 # unread message remains unread after moving back to origin
if self.moved_as_unread and move_back: if self.moved_as_unread and move_back:
notification = { 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: for r in self.all_child_ids:
r_vals = vals.copy() r_vals = vals.copy()
if not r.is_moved: if not r.is_moved:
# moved_from_* variables contain not last, but original # moved_from_* variables contain not last, but original
# reference # 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: 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: 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.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 # Send notification
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 @api.multi
def name_get(self): def name_get(self):
context = self.env.context context = self.env.context
if not (context or {}).get('extended_name'):
if not (context or {}).get("extended_name"):
return super(MailMessage, self).name_get() return super(MailMessage, self).name_get()
reads = self.read(['record_name', 'model', 'res_id'])
reads = self.read(["record_name", "model", "res_id"])
res = [] res = []
for record in reads: 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 return res
@api.multi @api.multi
def message_format(self): def message_format(self):
message_values = super(MailMessage, self).message_format() 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: for item in self:
msg = message_index.get(item.id) msg = message_index.get(item.id)
if msg: if msg:
msg['is_moved'] = item.is_moved
msg["is_moved"] = item.is_moved
return message_values return message_values
class MailMoveMessageConfiguration(models.TransientModel): 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 @api.model
def get_values(self): def get_values(self):
res = super(MailMoveMessageConfiguration, self).get_values() res = super(MailMoveMessageConfiguration, self).get_values()
config_parameters = self.env["ir.config_parameter"].sudo() 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( res.update(
model_ids=[m.id for m in model_ids], 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 return res
@ -432,38 +562,50 @@ class MailMoveMessageConfiguration(models.TransientModel):
super(MailMoveMessageConfiguration, self).set_values() super(MailMoveMessageConfiguration, self).set_values()
config_parameters = self.env["ir.config_parameter"].sudo() config_parameters = self.env["ir.config_parameter"].sudo()
for record in self: 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): class ResPartner(models.Model):
_inherit = 'res.partner'
_inherit = "res.partner"
@api.model @api.model
def create(self, vals): def create(self, vals):
res = super(ResPartner, self).create(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 # 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 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: if messages:
messages.sudo().write({'author_id': res.id})
messages.sudo().write({"author_id": res.id})
return res return res
@api.model @api.model
def default_get(self, default_fields): def default_get(self, default_fields):
contextual_self = self 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( 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) 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 Ildar Nasyrov <https://it-projects.info/team/iledarn>
# Copyright 2016 Ivan Yelizariev <https://it-projects.info/team/yelizariev> # Copyright 2016 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> # Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).--> # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).-->
<odoo> <odoo>
<template id="assets_backend" name="custom bar assets" inherit_id="web.assets_backend"> <template id="assets_backend" name="custom bar assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside"> <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> </xpath>
</template> </template>
@ -19,51 +24,76 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Move Message"> <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)]}"> <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>
<group attrs="{'invisible':[('can_move', '=', False)]}" colspan="2"> <group attrs="{'invisible':[('can_move', '=', False)]}" colspan="2">
<label for="model_record"/>
<label for="model_record" />
<div> <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> </div>
<label for="filter_by_partner"/>
<label for="filter_by_partner" />
<div> <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" 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> </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)]}"> <div attrs="{'invisible':[('message_is_moved','=',False)]}">
<field name="move_back"/>
<field name="move_back" />
</div> </div>
<label for="move_followers"/>
<label for="move_followers" />
<div> <div>
<field name="move_followers"/>
<field name="move_followers" />
</div> </div>
</group> </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" /> <button string="Close" class="" special="cancel" />
<separator string="Message"/>
<separator string="Message" />
<group> <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> </group>
<div class="openerp mail_move_message"> <div class="openerp mail_move_message">
<div class="oe_mail"> <div class="oe_mail">
@ -71,18 +101,30 @@
<div class="oe_msg_content"> <div class="oe_msg_content">
<div class="oe_msg_body"> <div class="oe_msg_body">
<!-- use built-in css for messages --> <!-- use built-in css for messages -->
<field name="message_body"/>
<field name="message_body" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<footer> <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> </footer>
</form> </form>
</field> </field>
@ -92,30 +134,35 @@
<record id="view_mail_move_message_config_settings" model="ir.ui.view"> <record id="view_mail_move_message_config_settings" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit</field> <field name="name">res.config.settings.view.form.inherit</field>
<field name="model">res.config.settings</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"> <field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside"> <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> <h2>Mail Relocation</h2>
<div class="row mt16 o_settings_container"> <div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-6 o_setting_box" id="crm_lead"> <div class="col-xs-12 col-md-6 o_setting_box" id="crm_lead">
<div class="o_setting_left_pane"> <div class="o_setting_left_pane">
</div> </div>
<div class="o_setting_right_pane"> <div class="o_setting_right_pane">
<label for="model_ids"/>
<label for="model_ids" />
<div class="text-muted"> <div class="text-muted">
Add models to be used for message relocation Add models to be used for message relocation
</div> </div>
<field name="model_ids" widget="many2many_tags"/>
<field name="model_ids" widget="many2many_tags" />
</div> </div>
</div> </div>
<div class="col-xs-12 col-md-6 o_setting_box" id="crm_lead"> <div class="col-xs-12 col-md-6 o_setting_box" id="crm_lead">
<div class="o_setting_left_pane"> <div class="o_setting_left_pane">
<field name="move_followers"/>
<field name="move_followers" />
</div> </div>
<div class="o_setting_right_pane"> <div class="o_setting_right_pane">
<label for="move_followers"/>
<label for="move_followers" />
</div> </div>
</div> </div>
</div> </div>
@ -128,7 +175,7 @@
<field name="name">Mail Relocation</field> <field name="name">Mail Relocation</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
<field name="res_model">res.config.settings</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="view_mode">form</field>
<field name="target">inline</field> <field name="target">inline</field>
<field name="context">{'module' : 'mail_move_message'}</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 { 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 { .mail_move_message {
width: 864px; 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 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2016 Pavel Romanchenko # Copyright 2016 Pavel Romanchenko
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> # Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). */ # 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"; "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; var _t = core._t;
thread.include({ thread.include({
init: function(){
init: function() {
this._super.apply(this, arguments); this._super.apply(this, arguments);
// Add click reaction in the events of the thread object // 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); this.trigger("move_message", message_id);
}; };
}, },
on_move_message: function(message_id){
on_move_message: function(message_id) {
var action = { 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, { this.do_action(action, {
'on_close': function(){}
on_close: function() {},
}); });
}
},
}); });
chatter.include({ chatter.include({
@ -61,39 +61,39 @@ odoo.define('mail_move_message.relocate', function (require) {
// For show wizard in the form // For show wizard in the form
if (this.fields.thread && this.fields.thread.thread) { if (this.fields.thread && this.fields.thread.thread) {
var 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() {}); 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({ ChatAction.include({
start: function() { start: function() {
var result = this._super.apply(this, arguments); var result = this._super.apply(this, arguments);
// For show wizard in the channels // 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() {}); 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; 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); var msg = chat_manager_super_make_message(data);
// Mark msg as moved after reload // Mark msg as moved after reload
msg.is_moved = data.is_moved || false; msg.is_moved = data.is_moved || false;
return msg; return msg;
}; };
var chat_manager_super_on_notification = chat_manager.on_notification; 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); chat_manager_super_on_notification(notifications);
var self = this; var self = this;
_.each(notifications, function (notification) {
_.each(notifications, function(notification) {
var model = notification[0][1]; var model = notification[0][1];
var message_id = notification[1].id; var message_id = notification[1].id;
var message = chat_manager.get_message(message_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.res_id = notification[1].res_id;
message.model = notification[1].model; message.model = notification[1].model;
message.record_name = notification[1].record_name; 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 // Update cache and accordingly message in the thread
self.add_to_cache(message, []); self.add_to_cache(message, []);
// Call thread.on_update_message(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); self.remove_message_from_channel(ch, message);
})
chat_manager.bus.trigger('update_message', message);
});
chat_manager.bus.trigger("update_message", message);
} }
}); });
}; };
Basicmodel.include({ 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({ 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 self = this;
var field_data = event.data.record.data; var field_data = event.data.record.data;
this.on_saved = function(record, bool) { 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 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, self.initialState,
{ {
mode: 'edit',
viewType: 'form',
});
mode: "edit",
viewType: "form",
}
);
relField.getParent = function() { relField.getParent = function() {
// necessary for correct _trigger_up implementation in mixins.js
// Necessary for correct _trigger_up implementation in mixins.js
return self; return self;
}; };
var wizard_popup = relField._searchCreatePopup("form", false, { 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 { } else {
this._super.apply(this, arguments); 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 Ildar Nasyrov <https://it-projects.info/team/iledarn>
# Copyright 2016 Ivan Yelizariev <https://it-projects.info/team/yelizariev> # Copyright 2016 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2016 Pavel Romanchenko # Copyright 2016 Pavel Romanchenko
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> # Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).--> # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).-->
<template> <template>
<t t-extend="mail.ChatThread.Message"> <t t-extend="mail.ChatThread.Message">
<t t-jquery='p.o_mail_info span:last-child i:first-child' t-operation="before"> <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>
</t> </t>
</template> </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). # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import test_mail_move 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.at_install(True)
@odoo.tests.common.post_install(True) @odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase): class TestUi(odoo.tests.HttpCase):
def test_create_new_partner_and_move_message(self): def test_create_new_partner_and_move_message(self):
env = Environment(self.registry.test_cr, self.uid, {}) env = Environment(self.registry.test_cr, self.uid, {})
# needed because tests are run before the module is marked as # needed because tests are run before the module is marked as
# installed. In js web will only load qweb coming from modules # installed. In js web will only load qweb coming from modules
# that are returned by the backend in module_boot. Without # that are returned by the backend in module_boot. Without
# this you end up with js, css but no qweb. # 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() self.registry.cursor().release()
# updating models, to be able relocate messages to a partner at_install # updating models, to be able relocate messages to a partner at_install
config_parameters = self.env["ir.config_parameter"].sudo() 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 = """ code = """
var delayed_button_click = function(delay, button){ var delayed_button_click = function(delay, button){
@ -49,4 +52,6 @@ class TestUi(odoo.tests.HttpCase):
console.log('ok') 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") env.cr.execute("ALTER TABLE res_users ADD COLUMN email_multi_website VARCHAR")
# fill new email column with values from partner # 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 email = user.partner_id.email
if email: if email:
user._force_default('email_multi_website', email)
user._force_default("email_multi_website", email)
def uninstall_hook(cr, registry): def uninstall_hook(cr, registry):
@ -26,15 +26,15 @@ def uninstall_hook(cr, registry):
# remove properties # remove properties
field_ids = [ 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 # copy emails from partner to user
cr.execute("SELECT partner_id,email_multi_website FROM res_users") cr.execute("SELECT partner_id,email_multi_website FROM res_users")
for partner_id, default_email in cr.fetchall(): 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"], "images": ["images/main.jpg"],
"version": "12.0.1.0.1", "version": "12.0.1.0.1",
"application": False, "application": False,
"author": "IT-Projects LLC, Ivan Yelizariev", "author": "IT-Projects LLC, Ivan Yelizariev",
"support": "apps@it-projects.info", "support": "apps@it-projects.info",
"website": "https://it-projects.info/team/yelizariev", "website": "https://it-projects.info/team/yelizariev",
"license": "LGPL-3", "license": "LGPL-3",
"price": 230.00, "price": 230.00,
"currency": "EUR", "currency": "EUR",
"depends": [ "depends": [
"ir_config_parameter_multi_company", "ir_config_parameter_multi_company",
"web_website", "web_website",
@ -24,22 +22,15 @@
"test_mail", "test_mail",
], ],
"external_dependencies": {"python": [], "bin": []}, "external_dependencies": {"python": [], "bin": []},
"data": [
"views/website_views.xml",
],
"demo": [
],
"qweb": [
],
"data": ["views/website_views.xml",],
"demo": [],
"qweb": [],
"post_load": None, "post_load": None,
"pre_init_hook": None, "pre_init_hook": None,
"post_init_hook": "post_init_hook", "post_init_hook": "post_init_hook",
"uninstall_hook": "uninstall_hook", "uninstall_hook": "uninstall_hook",
"auto_install": False, "auto_install": False,
"installable": True, "installable": True,
# "demo_title": "Email Addresses per Website", # "demo_title": "Email Addresses per Website",
# "demo_addons": [ # "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 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> # Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). # 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): class IrProperty(models.Model):
_inherit = 'ir.property'
_inherit = "ir.property"
@api.multi @api.multi
def write(self, vals): def write(self, vals):
res = super(IrProperty, self).write(vals) res = super(IrProperty, self).write(vals)
field_object_list = [ 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: for fobj in field_object_list:
self._update_db_value_website_dependent(fobj) 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> # Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). # 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): class Message(models.Model):
_inherit = 'mail.message'
_inherit = "mail.message"
def _default_mail_server_id(self): def _default_mail_server_id(self):
website = self.env.context.get('website_id')
website = self.env.context.get("website_id")
if not website: if not website:
return return
website = self.env['website'].sudo().browse(website)
website = self.env["website"].sudo().browse(website)
return website.mail_server_id.id return website.mail_server_id.id
mail_server_id = fields.Many2one(default=_default_mail_server_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> # Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import logging import logging
from odoo import models, fields, api, tools, _
from odoo import _, api, fields, models, tools
from odoo.exceptions import UserError from odoo.exceptions import UserError
from odoo.tools import pycompat 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__) _logger = logging.getLogger(__name__)
FIELDS = ['body_html', 'mail_server_id', 'report_template']
FIELDS = ["body_html", "mail_server_id", "report_template"]
try: 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: except ImportError:
_logger.warning("jinja2 not available, templating features will not work!") _logger.warning("jinja2 not available, templating features will not work!")
class MailTemplate(models.Model): 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) 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 @api.multi
def generate_email(self, res_ids, fields=None): def generate_email(self, res_ids, fields=None):
@ -37,8 +50,8 @@ class MailTemplate(models.Model):
list_of_dict = res list_of_dict = res
for _unused, data in list_of_dict.items(): 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 return res
@ -54,67 +67,93 @@ class MailTemplate(models.Model):
# try to load the template # try to load the template
try: 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)) template = mako_env.from_string(tools.ustr(template_txt))
except Exception: except Exception:
_logger.info("Failed to load template %r", template_txt, exc_info=True) _logger.info("Failed to load template %r", template_txt, exc_info=True)
return multi_mode and results or results[res_ids[0]] return multi_mode and results or results[res_ids[0]]
# prepare template variables # 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) res_to_rec = dict.fromkeys(res_ids, None)
for record in records: for record in records:
res_to_rec[record.id] = record res_to_rec[record.id] = record
variables = { 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 # [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: 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: else:
website = self.env.user.backend_website_id website = self.env.user.backend_website_id
for res_id, record in res_to_rec.items(): for res_id, record in res_to_rec.items():
record_company = company record_company = company
if not record_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_company = record.company_id
record_website = website 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 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 # 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: try:
render_result = template.render(variables) render_result = template.render(variables)
except Exception: 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": if render_result == u"False":
render_result = u"" render_result = u""
if post_process: 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 results[res_id] = render_result
@ -133,13 +172,10 @@ class MailTemplate(models.Model):
res = super(MailTemplate, self).write(vals) res = super(MailTemplate, self).write(vals)
# TODO: will it work with OCA's partner_firstname module? # TODO: will it work with OCA's partner_firstname module?
if 'name' in vals:
if "name" in vals:
fields_to_update = FIELDS fields_to_update = FIELDS
else: 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: for f in fields_to_update:
self._update_properties_label(f) 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> # Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). # 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): class MailThread(models.AbstractModel):
_inherit = 'mail.thread'
_inherit = "mail.thread"
@api.model @api.model
def message_route_process(self, message, message_dict, routes): 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: if website:
self = self.with_context(website_id=website[0].id) 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> # Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import logging import logging
from odoo import models, fields, api
from odoo import api, fields, models
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
FIELD_NAME = 'email_multi_website'
FIELDS = ['signature']
FIELD_NAME = "email_multi_website"
FIELDS = ["signature"]
ALL_FIELDS = [FIELD_NAME] + FIELDS ALL_FIELDS = [FIELD_NAME] + FIELDS
class User(models.Model): 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) signature = fields.Html(company_dependent=True, website_dependent=True)
# extra field to detach email field from res.partner # 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) email_multi_website = fields.Char(company_dependent=True, website_dependent=True)
@api.model @api.model
def create(self, vals): def create(self, vals):
res = super(User, self).create(vals) res = super(User, self).create(vals)
# make value company independent # make value company independent
res._force_default(FIELD_NAME, vals.get('email'))
res._force_default(FIELD_NAME, vals.get("email"))
for f in FIELDS: for f in FIELDS:
res._force_default(f, vals.get(f)) res._force_default(f, vals.get(f))
return res return res
@ -34,7 +36,7 @@ class User(models.Model):
def write(self, vals): def write(self, vals):
res = super(User, self).write(vals) res = super(User, self).write(vals)
# TODO: will it work with OCA's partner_firstname module? # 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: for f in ALL_FIELDS:
self._update_properties_label(f) self._update_properties_label(f)
return res return res

6
mail_multi_website/models/website.py

@ -1,10 +1,12 @@
# Copyright 2017 Ivan Yelizariev <https://it-projects.info/team/yelizariev> # Copyright 2017 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). # 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): class Website(models.Model):
_inherit = "website" _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 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> # Copyright 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). # 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.tools import mute_logger
from odoo.addons.test_mail.data.test_mail_data import MAIL_TEMPLATE 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): class TestFetch(TestMail):
@ -12,30 +13,44 @@ class TestFetch(TestMail):
def setUp(self): def setUp(self):
super(TestFetch, self).setUp() 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 self.website.company_id = self.company
# copy-paste from mail.tests.test_mail_gateway # 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 # 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): def test_fetch_multi_website(self):
""" Incoming email on an alias creating a new record + message_new + message details """ """ 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 # 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> # Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). # 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): 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_email = self.env.user.email
self.original_company = self.env.user.company_id 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.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.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 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 # 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_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): def switch_user_website(self):
# add website to allowed # 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): def test_website_in_render_variables(self):
"""Mail values are per website""" """Mail values are per website"""
self.env.user.backend_website_id = None 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 # sending without website
mail_id = self.email_template.send_mail(self.test_pigs.id) 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) self.assertFalse(mail.mail_server_id)
# sending from frontend # sending from frontend
self.test_pigs.company_id = None 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.subject, self.website.name)
self.assertEqual(mail.mail_server_id, self.mail_server_id) self.assertEqual(mail.mail_server_id, self.mail_server_id)
# copy-pasted tests # copy-pasted tests
self.assertEqual(mail.email_to, self.email_template.email_to) self.assertEqual(mail.email_to, self.email_template.email_to)
# for some reason self.email_template.email_cc might return u'False' # 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 # sending from frontend
self.switch_user_website() self.switch_user_website()
mail_id = self.email_template.send_mail(self.test_pigs.id) 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) self.assertEqual(mail.subject, self.website.name)
def _test_message_post_with_template(self): def _test_message_post_with_template(self):
@ -124,6 +139,8 @@ class TestRender(TestMail):
self.env.user.invalidate_cache() self.env.user.invalidate_cache()
self.assertEqual(self.env.user.email, self.original_email) 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): def setUp(self):
super(TestSendMail, self).setUp() 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_email = self.env.user.email
self.original_company = self.env.user.company_id 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 # Check that current email is set and differs
self.assertTrue(self.email) self.assertTrue(self.email)
self.assertNotEqual(self.original_email, self.email) self.assertNotEqual(self.original_email, self.email)
@ -23,12 +21,14 @@ class TestSendMail(TransactionCase):
def switch_user_website(self): def switch_user_website(self):
# add website to allowed # 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): def test_multi_email(self):
"""User has email addresses per website""" """User has email addresses per website"""
@ -37,21 +37,28 @@ class TestSendMail(TransactionCase):
self.env.user.email = self.email self.env.user.email = self.email
# Check that writing works # Check that writing works
self.env.user.invalidate_cache() 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 # changing company will automatically update website value to empty value
self.env.user.company_id = self.original_company self.env.user.company_id = self.original_company
self.env.user.invalidate_cache() 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): def test_multi_email_partner(self):
"""Partner doesn't have email addresses per website""" """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() self.switch_user_website()
# update partner's email # update partner's email
partner.email = new_email partner.email = new_email
@ -59,4 +66,6 @@ class TestSendMail(TransactionCase):
# changing company will automatically update website value to empty value # changing company will automatically update website value to empty value
self.env.user.company_id = self.original_company self.env.user.company_id = self.original_company
self.env.user.invalidate_cache() 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> <!-- Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). --> License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). -->
<odoo> <odoo>
<record id="view_website_multi_mail_form" model="ir.ui.view"> <record id="view_website_multi_mail_form" model="ir.ui.view">
<field name="model">website</field> <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"> <field name="arch" type="xml">
<xpath expr="//field[@name='default_lang_id']" position="after"> <xpath expr="//field[@name='default_lang_id']" position="after">
<field name="mail_server_id"/>
<field name="mail_server_id" />
</xpath> </xpath>
</field> </field>
</record> </record>

12
mail_multi_website/wizard/mail_compose_message.py

@ -1,20 +1,22 @@
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev> # Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). # 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 from odoo.http import request
class MailComposer(models.TransientModel): class MailComposer(models.TransientModel):
_inherit = 'mail.compose.message'
_inherit = "mail.compose.message"
@api.model @api.model
def create(self, vals): def create(self, vals):
"""Workaround for https://github.com/odoo/odoo/pull/26589""" """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: if not website:
website = self.env['website'].get_current_website()
website = self.env["website"].get_current_website()
if website: if website:
self = self.with_context(website_id=website.id) self = self.with_context(website_id=website.id)
return super(MailComposer, self).create(vals) 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.""", "summary": """Send private messages to specified recipients, regardless of who are in followers list.""",
"category": "Discuss", "category": "Discuss",
# "live_test_url": "http://apps.it-projects.info/shop/product/DEMO-URL?version=12.0", # "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", "version": "12.0.1.1.2",
"application": False, "application": False,
"author": "IT-Projects LLC, Pavel Romanchenko", "author": "IT-Projects LLC, Pavel Romanchenko",
"support": "apps@it-projects.info", "support": "apps@it-projects.info",
"website": "https://it-projects.info/", "website": "https://it-projects.info/",
"license": "LGPL-3", "license": "LGPL-3",
"price": 50.00, "price": 50.00,
"currency": "EUR", "currency": "EUR",
"depends": [
"mail"
],
"depends": ["mail"],
"external_dependencies": {"python": [], "bin": []}, "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, "post_load": None,
"pre_init_hook": None, "pre_init_hook": None,
"post_init_hook": None, "post_init_hook": None,
"uninstall_hook": None, "uninstall_hook": None,
"auto_install": False, "auto_install": False,
"installable": True, "installable": True,
# "demo_title": "{MODULE_NAME}", # "demo_title": "{MODULE_NAME}",
# "demo_addons": [ # "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 2017 Artyom Losev <https://github.com/ArtyomLosev>
Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/> Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/>
License LGPL-3.0 (https://www.gnu.org/licenses/lgpl.html).--> 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"> <record model="ir.ui.view" id="email_compose_message_wizard_form_private">
<field name="name">mail.compose.message.form.private</field> <field name="name">mail.compose.message.form.private</field>
<field name="model">mail.compose.message</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"> <field name="arch" type="xml">
<data> <data>
@ -16,8 +16,13 @@
<field name="is_private" invisible="1" /> <field name="is_private" invisible="1" />
</xpath> </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> </attribute>
</xpath> </xpath>

79
mail_private/models.py

@ -3,19 +3,19 @@
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev> # Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/> # Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/>
# License LGPL-3.0 (https://www.gnu.org/licenses/lgpl.html). # 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): 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): 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): def send_recepients_for_internal_message(self, model, domain):
result = [] result = []
@ -26,14 +26,17 @@ class MailMessage(models.Model):
# channel_ids = [c.channel_id for c in follower_ids if c.channel_id] # channel_ids = [c.channel_id for c in follower_ids if c.channel_id]
for recipient in recipient_ids: 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: # for channel in channel_ids:
# result.append({ # result.append({
@ -46,20 +49,52 @@ class MailMessage(models.Model):
return result return result
@api.multi @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() self_sudo = self.sudo()
msg_vals = msg_vals if msg_vals else {} 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: else:
rdata = self._notify_compute_internal_recipients(record, msg_vals) 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 @api.multi
def _notify_compute_internal_recipients(self, record, msg_vals): 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 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 2018 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/> Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/>
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). */ 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, 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) { 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 { } 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 2018-2019 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/> Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/>
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).*/ 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 _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")', 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")', trigger: '.o_data_cell:contains("YourCompany, Marc Demo")',
content: _t("Select a user"), content: _t("Select a user"),
position: "bottom", position: "bottom",
}, {
},
{
trigger: '.o_form_uri.o_field_widget:contains("YourCompany, Marc Demo")', trigger: '.o_form_uri.o_field_widget:contains("YourCompany, Marc Demo")',
content: _t("Go to user page"), content: _t("Go to user page"),
position: "bottom"
}, {
position: "bottom",
},
{
trigger: "button.oe_compose_post_private", trigger: "button.oe_compose_post_private",
content: _t("Click on Private mail creating button"), 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", trigger: ".o_control_panel.o_breadcrumb_full li.active",
content: _t("Dummy action"), content: _t("Dummy action"),
}, {
},
{
trigger: "button.oe_composer_uncheck", trigger: "button.oe_composer_uncheck",
extra_trigger: "button.oe_composer_uncheck", extra_trigger: "button.oe_composer_uncheck",
content: _t("Uncheck all Followers"), content: _t("Uncheck all Followers"),
timeout: 10000, timeout: 10000,
}, {
},
{
trigger: "div.o_composer_suggested_partners", trigger: "div.o_composer_suggested_partners",
content: _t("Check the first one"), content: _t("Check the first one"),
}, {
},
{
trigger: "textarea.o_composer_text_field:first", trigger: "textarea.o_composer_text_field:first",
content: _t("Write some email"), content: _t("Write some email"),
run: function() { 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", trigger: ".o_composer_send .o_composer_button_send",
content: _t("Send email"), content: _t("Send email"),
}, {
},
{
trigger: ".o_mail_thread .o_thread_message:contains(" + email + ")", trigger: ".o_mail_thread .o_thread_message:contains(" + email + ")",
content: _t("Send 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 2016-2017 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/> Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/>
License LGPL-3.0 (https://www.gnu.org/licenses/lgpl.html).--> License LGPL-3.0 (https://www.gnu.org/licenses/lgpl.html).-->
@ -6,7 +6,11 @@
<t t-extend="mail.chatter.Buttons"> <t t-extend="mail.chatter.Buttons">
<t t-jquery="button[title='Send a message']" t-operation="after"> <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>
</t> </t>
@ -15,7 +19,7 @@
<small class="o_chatter_composer_info" t-if="!widget.options.is_private"> <small class="o_chatter_composer_info" t-if="!widget.options.is_private">
To: Followers of To: Followers of
<t t-if="widget.options.record_name"> <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 t-if="!widget.options.record_name"> <t t-if="!widget.options.record_name">
this document this document
@ -23,7 +27,10 @@
</small> </small>
</t> </t>
<t t-jquery="div[class='o_composer_suggested_partners']" t-operation="after"> <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>
</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 2016 x620 <https://github.com/x620>
Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/> Copyright 2019 Artem Rafailov <https://it-projects.info/team/Ommo73/>
License LGPL-3.0 (https://www.gnu.org/licenses/lgpl.html).--> License LGPL-3.0 (https://www.gnu.org/licenses/lgpl.html).-->
<odoo> <odoo>
<template <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"> <xpath expr="." position="inside">
<script <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 <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> </xpath>
</template> </template>
</odoo> </odoo>

16
mail_private/tests/test_js.py

@ -8,17 +8,21 @@ import odoo.tests
@odoo.tests.common.at_install(True) @odoo.tests.common.at_install(True)
@odoo.tests.common.post_install(True) @odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase): class TestUi(odoo.tests.HttpCase):
def test_01_mail_private(self): def test_01_mail_private(self):
# needed because tests are run before the module is marked as # needed because tests are run before the module is marked as
# installed. In js web will only load qweb coming from modules # installed. In js web will only load qweb coming from modules
# that are returned by the backend in module_boot. Without # that are returned by the backend in module_boot. Without
# this you end up with js, css but no qweb. # this you end up with js, css but no qweb.
cr = self.registry.cursor() 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() 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, "price": 190.00,
"currency": "EUR", "currency": "EUR",
"support": "apps@it-projects.info", "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"],
"version": "1.0.0",
"depends": ["mail"],
"data": ["data.xml",],
"installable": False, "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> <openerp>
<data> <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"> <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> </xpath>
</template> </template>
</data> </data>

14
mail_recovery/static/src/js/mail_recovery.js

@ -1,11 +1,11 @@
odoo.define('mail_recovery', function (require) {
var composer = require('mail.composer');
odoo.define("mail_recovery", function(require) {
var composer = require("mail.composer");
composer.BasicComposer.include({ composer.BasicComposer.include({
init: function(){
init: function() {
this._super.apply(this, arguments); 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) { on_focus_textarea: function(event) {
var $input = $(event.target); var $input = $(event.target);
@ -16,8 +16,8 @@ odoo.define('mail_recovery', function (require) {
on_keyup_textarea: function(event) { on_keyup_textarea: function(event) {
window.localStorage.message_storage = $(event.target).val(); 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); return this._super(event);
}, },
}); });

14
mail_reply/__manifest__.py

@ -4,24 +4,16 @@
"category": "Discuss", "category": "Discuss",
"images": [], "images": [],
"version": "1.0.0", "version": "1.0.0",
"author": "IT-Projects LLC, Pavel Romanchenko", "author": "IT-Projects LLC, Pavel Romanchenko",
"support": "apps@it-projects.info", "support": "apps@it-projects.info",
"website": "https://it-projects.info", "website": "https://it-projects.info",
"license": "LGPL-3", "license": "LGPL-3",
"price": 40.00, "price": 40.00,
"currency": "EUR", "currency": "EUR",
"depends": [
"mail_base",
],
"depends": ["mail_base",],
"external_dependencies": {"python": [], "bin": []}, "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": [], "demo": [],
"installable": False, "installable": False,
"auto_install": 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) { 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(); self._unselectMessage();
} else { } else {
self.thread.scroll_to(); 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> <template>
<t t-extend="mail.ChatThread.Message"> <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" 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>
</t> </t>
</template> </template>

15
mail_reply/templates.xml

@ -1,11 +1,16 @@
<?xml version="1.0"?>
<?xml version="1.0" ?>
<openerp> <openerp>
<data> <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"> <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> </xpath>
</template> </template>
</data> </data>

1
mail_reply/tests/__init__.py

@ -1,2 +1 @@
from . import test_default 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.at_install(False)
@odoo.tests.common.post_install(True) @odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase): class TestUi(odoo.tests.HttpCase):
def test_01_mail_all(self): def test_01_mail_all(self):
# wait till page loaded and then click and wait again # wait till page loaded and then click and wait again
code = """ code = """
@ -35,5 +34,7 @@ class TestUi(odoo.tests.HttpCase):
}, 1000); }, 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", "name": "Sentbox",
"summary": """Quick way to find sent messages""", "summary": """Quick way to find sent messages""",
"category": "Discuss", "category": "Discuss",
"images": ['images/menu.png'],
"images": ["images/menu.png"],
"version": "12.0.1.2.0", "version": "12.0.1.2.0",
"author": "IT-Projects LLC, Ivan Yelizariev, Pavel Romanchenko", "author": "IT-Projects LLC, Ivan Yelizariev, Pavel Romanchenko",
"support": "apps@it-projects.info", "support": "apps@it-projects.info",
"website": "https://it-projects.info", "website": "https://it-projects.info",
"license": "LGPL-3", "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): 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): def _compute_sent(self):
for r in self: for r in self:
r_sudo = r.sudo() r_sudo = r.sudo()
recipient_ids = r_sudo.partner_ids recipient_ids = r_sudo.partner_ids
author_id = r_sudo.author_id 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 ( sent = author_id and (
len(recipient_ids) > 1 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 r.sent = sent
@api.multi @api.multi
def message_format(self): def message_format(self):
message_values = super(MailMessage, self).message_format() 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: for item in self:
msg = message_index.get(item.id) msg = message_index.get(item.id)
if msg: if msg:
msg['sent'] = item.sent
msg["sent"] = item.sent
return message_values return message_values
class MailComposeMessage(models.TransientModel): 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 2018 Artyom Losev <https://it-projects.info/team/ArtyomLosev>
# Copyright 2019 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr> # Copyright 2019 Kolushov Alexandr <https://it-projects.info/team/KolushovAlexandr>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). */ # 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> <template>
<!--Inherit Sidebar and add Sent menu item after Starred --> <!--Inherit Sidebar and add Sent menu item after Starred -->
<t t-extend="mail.discuss.Sidebar"> <t t-extend="mail.discuss.Sidebar">
<t t-jquery="div[data-thread-id=mailbox_starred]" t-operation="after"> <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> </div>
</t> </t>
</t> </t>
@ -14,7 +18,8 @@
<t t-jquery="t:last-child" t-operation="after"> <t t-jquery="t:last-child" t-operation="after">
<t t-if="thread.getID() === 'mailbox_channel_sent'"> <t t-if="thread.getID() === 'mailbox_channel_sent'">
<div class="o_thread_title">No sent messages</div> <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> </t>
</t> </t>

1
mail_sent/tests/__init__.py

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

17
mail_sent/tests/test_js.py

@ -1,11 +1,11 @@
import odoo.tests
from werkzeug import url_encode from werkzeug import url_encode
import odoo.tests
@odoo.tests.common.at_install(True) @odoo.tests.common.at_install(True)
@odoo.tests.common.post_install(True) @odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase): class TestUi(odoo.tests.HttpCase):
def test_01_mail_sent(self): def test_01_mail_sent(self):
# wait till page loaded and then click and wait again # 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 # installed. In js web will only load qweb coming from modules
# that are returned by the backend in module_boot. Without # that are returned by the backend in module_boot. Without
# this you end up with js, css but no qweb. # 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 = """ code = """
setTimeout(function () { setTimeout(function () {
@ -21,5 +23,10 @@ class TestUi(odoo.tests.HttpCase):
setTimeout(function () {console.log('ok');}, 3000); setTimeout(function () {console.log('ok');}, 3000);
}, 1000); }, 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> <openerp>
<data> <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"> <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> </xpath>
</template> </template>
</data> </data>

16
mail_to/__manifest__.py

@ -2,26 +2,18 @@
"name": """Show message recipients""", "name": """Show message recipients""",
"summary": """Allows you be sure, that all discussion participants were notified""", "summary": """Allows you be sure, that all discussion participants were notified""",
"category": "Discuss", "category": "Discuss",
"images": ['images/1.png'],
"images": ["images/1.png"],
"version": "12.0.1.0.1", "version": "12.0.1.0.1",
"author": "IT-Projects LLC, Pavel Romanchenko", "author": "IT-Projects LLC, Pavel Romanchenko",
"support": "apps@it-projects.info", "support": "apps@it-projects.info",
"website": "https://it-projects.info", "website": "https://it-projects.info",
"license": "LGPL-3", "license": "LGPL-3",
"price": 40.00, "price": 40.00,
"currency": "EUR", "currency": "EUR",
"depends": [
'mail',
],
"depends": ["mail",],
"external_dependencies": {"python": [], "bin": []}, "external_dependencies": {"python": [], "bin": []},
"data": [
'templates.xml',
],
"qweb": [
'static/src/xml/recipient.xml',
],
"data": ["templates.xml",],
"qweb": ["static/src/xml/recipient.xml",],
"demo": [], "demo": [],
"installable": True, "installable": True,
"auto_install": False, "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> <template>
<t t-extend="mail.widget.Thread.Message"> <t t-extend="mail.widget.Thread.Message">
<t t-jquery="p.o_mail_info" t-operation="append"> <t t-jquery="p.o_mail_info" t-operation="append">
<span class="recipients_info"> <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">
<t t-if="partner_ids.length > 0">To: </t> <t t-if="partner_ids.length > 0">To: </t>
<t t-foreach="partner_ids.length" t-as="i"> <t t-foreach="partner_ids.length" t-as="i">
<t t-if="i &lt; 4"> <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> </a>
</t> </t>
</t> </t>
<t t-if="partner_ids.length &gt; 4"> <t t-if="partner_ids.length &gt; 4">
<span t-att-title="more_recipients"> <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> </span>
</t> </t>

12
mail_to/templates.xml

@ -1,11 +1,13 @@
<?xml version="1.0"?>
<?xml version="1.0" ?>
<openerp> <openerp>
<data> <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"> <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> </xpath>
</template> </template>
</data> </data>

47
mail_to/tests/test_default.py

@ -1,11 +1,11 @@
import odoo.tests
from werkzeug import url_encode from werkzeug import url_encode
import odoo.tests
@odoo.tests.common.at_install(True) @odoo.tests.common.at_install(True)
@odoo.tests.common.post_install(True) @odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase): class TestUi(odoo.tests.HttpCase):
def test_01_mail_to(self): def test_01_mail_to(self):
# checks the presence of an element with a link to the recipient # 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 # installed. In js web will only load qweb coming from modules
# that are returned by the backend in module_boot. Without # that are returned by the backend in module_boot. Without
# this you end up with js, css but no qweb. # 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 # 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 # 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 # notifications for everyone
for p in partner_ids.ids: 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 = """ code = """
setTimeout(function () { setTimeout(function () {
console.log($('a.recipient_link').length && 'ok' || 'error'); console.log($('a.recipient_link').length && 'ok' || 'error');
}, 3000); }, 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"], "images": ["images/mailgun_main.png"],
"version": "11.0.1.1.0", "version": "11.0.1.1.0",
"application": False, "application": False,
"author": "IT-Projects LLC, Ildar Nasyrov", "author": "IT-Projects LLC, Ildar Nasyrov",
"support": "apps@it-projects.info", "support": "apps@it-projects.info",
"website": "https://it-projects.info/team/iledarn", "website": "https://it-projects.info/team/iledarn",
"license": "LGPL-3", "license": "LGPL-3",
"price": 389.00, "price": 389.00,
"currency": "EUR", "currency": "EUR",
"depends": [
"mail",
],
"depends": ["mail",],
"external_dependencies": {"python": [], "bin": []}, "external_dependencies": {"python": [], "bin": []},
"data": [
'data/ir_cron_data.xml',
],
"demo": [
],
"qweb": [
],
"data": ["data/ir_cron_data.xml",],
"demo": [],
"qweb": [],
"post_load": None, "post_load": None,
"pre_init_hook": None, "pre_init_hook": None,
"post_init_hook": None, "post_init_hook": None,
"uninstall_hook": None, "uninstall_hook": None,
"auto_install": False, "auto_install": False,
"installable": False, "installable": False,
"demo_title": "Mailgun", "demo_title": "Mailgun",
"demo_addons": [], "demo_addons": [],
"demo_addons_hidden": [], "demo_addons_hidden": [],
"demo_url": "mailgun", "demo_url": "mailgun",
"demo_summary": "Easy to send outgoing and fetch incoming messages by using 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 import http
from odoo.http import request from odoo.http import request
import re
class MailMailgun(http.Controller): 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): def mailgun_notify(self, **kw):
# mailgun notification in json format # 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 # 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> <openerp>
<data noupdate="1"> <data noupdate="1">
<record id="mailgun_domain_verification" model="ir.cron"> <record id="mailgun_domain_verification" model="ir.cron">
<field name="name">Mailgun - domain verification request</field> <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_number">10</field>
<field name="interval_type">minutes</field> <field name="interval_type">minutes</field>
<field name="numbercall">10</field> <field name="numbercall">10</field>
<field name="state">code</field> <field name="state">code</field>
<field name="code">model.mailgun_verify()</field> <field name="code">model.mailgun_verify()</field>
<field name="doall" eval="True"/>
<field name="doall" eval="True" />
</record> </record>
</data> </data>
</openerp> </openerp>

21
mailgun/models/ir_config_parameter.py

@ -1,24 +1,27 @@
import logging
import requests import requests
import simplejson import simplejson
from openerp import api, models
from openerp import models, api
import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class IrConfigParameter(models.Model): class IrConfigParameter(models.Model):
_inherit = ['ir.config_parameter']
_inherit = ["ir.config_parameter"]
@api.model @api.model
def mailgun_verify(self): def mailgun_verify(self):
verified = self.sudo().get_param('mailgun.verified')
verified = self.sudo().get_param("mailgun.verified")
if verified: if verified:
return 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: if api_key and mail_domain:
url = "https://api.mailgun.net/v3/domains/%s/verify" % mail_domain url = "https://api.mailgun.net/v3/domains/%s/verify" % mail_domain
res = requests.put(url, auth=("api", api_key)) 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__) _logger = logging.getLogger(__name__)
class MailThread(models.AbstractModel): class MailThread(models.AbstractModel):
_inherit = 'mail.thread'
_inherit = "mail.thread"
@api.model @api.model
def mailgun_fetch_message(self, message_url): 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",
"version": "1.0.0",
"author": "IT-Projects LLC, Ivan Yelizariev",
"license": "LGPL-3",
"price": 70.00, "price": 70.00,
"currency": "EUR", "currency": "EUR",
'category': 'Discuss',
"category": "Discuss",
"support": "apps@it-projects.info", "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 openerp import api, models
class Partner(models.Model): class Partner(models.Model):
_inherit = 'res.partner'
_inherit = "res.partner"
@api.multi @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) 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: for vals in res:
partner = self.browse(vals['id'])
partner = self.browse(vals["id"])
if not partner.is_company: if not partner.is_company:
continue 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 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> <openerp><data>
</data></openerp> </data></openerp>

14
res_partner_mails_count/__manifest__.py

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

15
res_partner_mails_count/models.py

@ -1,18 +1,23 @@
from openerp import models, fields, api
from openerp import api, fields, models
class ResPartner(models.Model): class ResPartner(models.Model):
_inherit = 'res.partner'
_inherit = "res.partner"
mails_to = fields.Integer(compute="_compute_mails_to") mails_to = fields.Integer(compute="_compute_mails_to")
mails_from = fields.Integer(compute="_compute_mails_from") mails_from = fields.Integer(compute="_compute_mails_from")
@api.multi @api.multi
def _compute_mails_to(self): def _compute_mails_to(self):
for r in 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 @api.multi
def _compute_mails_from(self): def _compute_mails_from(self):
for r in 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; var _t = Core._t;
Tour.register({ Tour.register({
id: 'mails_count_tour',
id: "mails_count_tour",
name: _t("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: [ 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"), 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")}, 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> <openerp>
<data> <data>
<!-- <!--
@ -13,34 +13,47 @@
<record id="view_res_partner_mails_count_info_form" model="ir.ui.view"> <record id="view_res_partner_mails_count_info_form" model="ir.ui.view">
<field name="name">res.partner.mails.count</field> <field name="name">res.partner.mails.count</field>
<field name="model">res.partner</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"> <field name="arch" type="xml">
<div name="button_box" position="inside"> <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" name="%(action_mails)d"
context="{'search_default_partner_ids': [active_id], 'default_model': 'res.partner', 'default_res_id': active_id}" 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>
<button class="oe_stat_button mails_from" type="action"
<button
class="oe_stat_button mails_from"
type="action"
name="%(action_mails)d" name="%(action_mails)d"
context="{'search_default_author_id': active_id, 'default_model': 'res.partner', 'default_res_id': active_id}" 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> </button>
</div> </div>
</field> </field>
</record> </record>
<record id="res_partner_mails_count_tutorial" model="ir.actions.act_url"> <record id="res_partner_mails_count_tutorial" model="ir.actions.act_url">
<field name="name">res_partner_mails_count Tutorial</field> <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> <field name="target">self</field>
</record> </record>
</data> </data>
<data noupdate="1"> <data noupdate="1">
<record id="base.open_menu" model="ir.actions.todo"> <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="state">open</field>
<field name="sequence">500</field> <field name="sequence">500</field>
<field name="type">automatic</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_mail
# from . import test_phantom # from . import test_phantom

65
res_partner_mails_count/tests/test_mail.py

@ -1,4 +1,3 @@
from openerp.tests.common import TransactionCase from openerp.tests.common import TransactionCase
@ -6,28 +5,58 @@ class TestMessageCount(TransactionCase):
post_install = True post_install = True
def test_count(self): 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( 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() compose.send_mail()
self.assertEqual(new_partner1.mails_to, 0) 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) self.assertEqual(new_partner2.mails_from, 0)
compose.send_mail() compose.send_mail()
self.assertEqual(new_partner1.mails_to, 0) 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) 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.at_install(False)
@odoo.tests.common.post_install(True) @odoo.tests.common.post_install(True)
class TestUi(odoo.tests.HttpCase): class TestUi(odoo.tests.HttpCase):
def test_01_res_partner_mails_to_count(self): 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('/', "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): def test_02_res_partner_mails_from_count(self):
# wait till page loaded and then click and wait again # 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); setTimeout(function () {console.log('ok');}, 3000);
}, 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> <openerp>
<data> <data>
<record id="action_mails" model="ir.actions.client"> <record id="action_mails" model="ir.actions.client">

Loading…
Cancel
Save