mirror of
https://github.com/bringout/oca-ocb-test.git
synced 2026-04-21 20:41:59 +02:00
19.0 vanilla
This commit is contained in:
parent
38c6088dcc
commit
d9452d2060
243 changed files with 30797 additions and 10815 deletions
|
|
@ -11,7 +11,7 @@ present in a separate module as it contains models used only to perform
|
|||
tests independently to functional aspects of other models. """,
|
||||
'depends': [
|
||||
'mail',
|
||||
'test_performance',
|
||||
'test_orm',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
|
|
@ -21,16 +21,14 @@ tests independently to functional aspects of other models. """,
|
|||
'data/subtype_data.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.qunit_suite_tests': [
|
||||
'test_mail/static/tests/*',
|
||||
'web.assets_unit_tests': [
|
||||
'test_mail/static/tests/**/*',
|
||||
],
|
||||
'web.qunit_mobile_suite_tests': [
|
||||
'test_mail/static/tests/mobile/activity_tests.js',
|
||||
],
|
||||
'web.tests_assets': [
|
||||
'test_mail/static/tests/helpers/*',
|
||||
'web.assets_tests': [
|
||||
'test_mail/static/tests/tours/*',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'author': 'Odoo S.A.',
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,5 +36,18 @@
|
|||
<field name="chaining_type">trigger</field>
|
||||
<field name="triggered_next_type_id" ref="test_mail.mail_act_test_chained_2"/>
|
||||
</record>
|
||||
<record id="mail_act_test_upload_document" model="mail.activity.type">
|
||||
<field name="name">Document</field>
|
||||
<field name="summary">Document</field>
|
||||
<field name="delay_count">5</field>
|
||||
<field name="category">upload_file</field>
|
||||
<field name="res_model">mail.test.activity</field>
|
||||
</record>
|
||||
|
||||
<record id="mail_act_test_todo_generic" model="mail.activity.type">
|
||||
<field name="name">Do Stuff</field>
|
||||
<field name="summary">Hey Zoidberg! Get in here!</field>
|
||||
<field name="category">default</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<field name="name">Mail Test Full: Tracking Template</field>
|
||||
<field name="subject">Test Template</field>
|
||||
<field name="partner_to">{{ object.customer_id.id }}</field>
|
||||
<field name="use_default_to" eval="False"/>
|
||||
<field name="body_html" type="html"><p>Hello <t t-out="object.name or ''"></t></p></field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_ticket"/>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
|
|
@ -13,6 +14,7 @@
|
|||
<field name="name">Mail Test: Template</field>
|
||||
<field name="subject">Post on {{ object.name }}</field>
|
||||
<field name="partner_to">{{ object.customer_id.id }}</field>
|
||||
<field name="use_default_to" eval="False"/>
|
||||
<field name="body_html" type="html"><p>Adding stuff on <t t-out="object.name or ''"></t></p></field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_container"/>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
|
|
@ -33,6 +35,30 @@
|
|||
</t>
|
||||
</template>
|
||||
|
||||
<template id="mail_test_ticket_test_template_2">
|
||||
<t t-call="web.html_container">
|
||||
<t t-set="o" t-value="res_company"/>
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<p>This is another sample of an external report.</p>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="mail_test_ticket_test_variable_template">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="ticket">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<p>This is a sample of an external report for a ticket for
|
||||
<span t-out="ticket.count"></span> people.</p>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="mail_template_simple_test">
|
||||
<p>Hello <t t-out="partner.name"/>, this comes from <t t-out="object.name"/>.</p>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -52,4 +52,13 @@
|
|||
<field name="internal" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- mail.test.ticket.partner -->
|
||||
<record id="st_mail_test_ticket_partner_new" model="mail.message.subtype">
|
||||
<field name="name">New ticket</field>
|
||||
<field name="description">New Ticket</field>
|
||||
<field name="res_model">mail.test.ticket.partner</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="internal" eval="False"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Subject: {subject}
|
|||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="----=_Part_4200734_24778174.1344608186754"
|
||||
Date: Fri, 10 Aug 2012 14:16:26 +0000
|
||||
Date: {date}
|
||||
Message-ID: {msg_id}
|
||||
{extra}
|
||||
------=_Part_4200734_24778174.1344608186754
|
||||
|
|
@ -135,6 +135,38 @@ Message-ID: {msg_id}
|
|||
</html>
|
||||
"""
|
||||
|
||||
MAIL_TEMPLATE_SHORT = """Return-Path: {return_path}
|
||||
To: {to}
|
||||
cc: {cc}
|
||||
Received: by mail1.openerp.com (Postfix, from userid 10002)
|
||||
id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
|
||||
From: {email_from}
|
||||
Subject: {subject}
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="----=_Part_4200734_24778174.1344608186754"
|
||||
Date: Fri, 10 Aug 2012 14:16:26 +0000
|
||||
Message-ID: {msg_id}
|
||||
{extra}
|
||||
------=_Part_4200734_24778174.1344608186754
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Eli alla à l'eau
|
||||
|
||||
--
|
||||
Signature
|
||||
------=_Part_4200734_24778174.1344608186754
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
<div>Eli alla à l'eau<br/>
|
||||
--<br/>
|
||||
Sylvie
|
||||
</div>
|
||||
------=_Part_4200734_24778174.1344608186754--
|
||||
"""
|
||||
|
||||
|
||||
MAIL_MULTIPART_MIXED = """Return-Path: <ignasse.carambar@gmail.com>
|
||||
X-Original-To: raoul@grosbedon.fr
|
||||
Delivered-To: raoul@grosbedon.fr
|
||||
|
|
@ -249,7 +281,7 @@ Date: Sun, 26 Mar 2023 05:23:22 +0200
|
|||
Message-ID: {msg_id}
|
||||
Subject: {subject}
|
||||
From: "Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>
|
||||
To: groups@test.com
|
||||
To: groups@test.mycompany.com
|
||||
Content-Type: multipart/mixed; boundary="000000000000b951de05f7c47a9e"
|
||||
|
||||
--000000000000b951de05f7c47a9e
|
||||
|
|
@ -648,11 +680,11 @@ AAAAACwAAAAAAgACAAAEA3DJFQA7
|
|||
--001a11416b9e9b229a05272b7052--
|
||||
"""
|
||||
|
||||
MAIL_EML_ATTACHMENT = """Subject: Re: test attac
|
||||
MAIL_EML_ATTACHMENT = """Subject: {subject}
|
||||
From: {email_from}
|
||||
To: {to}
|
||||
References: <f3b9f8f8-28fa-2543-cab2-7aa68f679ebb@odoo.com>
|
||||
Message-ID: <cb7eaf62-58dc-2017-148c-305d0c78892f@odoo.com>
|
||||
References: {references}
|
||||
Message-ID: {msg_id}
|
||||
Date: Wed, 14 Mar 2018 14:26:58 +0100
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101
|
||||
Thunderbird/52.6.0
|
||||
|
|
@ -1395,6 +1427,7 @@ Date: Fri, 10 Aug 2012 14:16:26 +0000
|
|||
|
||||
------=_Part_4200734_24778174.1344608186754
|
||||
Content-Type: {pdf_mime}; name="scan_soraya.lernout_1691652648.pdf"
|
||||
Content-Disposition: attachment; filename="scan_soraya.lernout_1691652648.pdf"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
JVBERi0xLjEKJcKlwrHDqwoKMSAwIG9iagogIDw8IC9UeXBlIC9DYXRhbG9nCiAgICAgL1BhZ2VzIDIgMCBSCiAgPj4KZW5kb2JqCgoyIDAgb2JqCiAgPDwgL1R5cGUgL1BhZ2VzCiAgICAgL0tpZHMgWzMgMCBSXQogICAgIC9Db3VudCAxCiAgICAgL01lZGlhQm94IFswIDAgMzAwIDE0NF0KICA+PgplbmRvYmoKCjMgMCBvYmoKICA8PCAgL1R5cGUgL1BhZ2UKICAgICAgL1BhcmVudCAyIDAgUgogICAgICAvUmVzb3VyY2VzCiAgICAgICA8PCAvRm9udAogICAgICAgICAgIDw8IC9GMQogICAgICAgICAgICAgICA8PCAvVHlwZSAvRm9udAogICAgICAgICAgICAgICAgICAvU3VidHlwZSAvVHlwZTEKICAgICAgICAgICAgICAgICAgL0Jhc2VGb250IC9UaW1lcy1Sb21hbgogICAgICAgICAgICAgICA+PgogICAgICAgICAgID4+CiAgICAgICA+PgogICAgICAvQ29udGVudHMgNCAwIFIKICA+PgplbmRvYmoKCjQgMCBvYmoKICA8PCAvTGVuZ3RoIDU1ID4+CnN0cmVhbQogIEJUCiAgICAvRjEgMTggVGYKICAgIDAgMCBUZAogICAgKEhlbGxvIFdvcmxkKSBUagogIEVUCmVuZHN0cmVhbQplbmRvYmoKCnhyZWYKMCA1CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMDAxOCAwMDAwMCBuIAowMDAwMDAwMDc3IDAwMDAwIG4gCjAwMDAwMDAxNzggMDAwMDAgbiAKMDAwMDAwMDQ1NyAwMDAwMCBuIAp0cmFpbGVyCiAgPDwgIC9Sb290IDEgMCBSCiAgICAgIC9TaXplIDUKICA+PgpzdGFydHhyZWYKNTY1CiUlRU9GCg==
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mail_test_access
|
||||
from . import mail_test_lead
|
||||
from . import mail_test_ticket
|
||||
from . import test_mail_corner_case_models
|
||||
from . import test_mail_feature_models
|
||||
from . import test_mail_models
|
||||
from . import test_mail_thread_models
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
from odoo import exceptions, fields, models
|
||||
from odoo import fields, models, tools
|
||||
|
||||
|
||||
class MailTestAccess(models.Model):
|
||||
""" Test access on mail models without depending on real models like channel
|
||||
or partner which have their own set of ACLs. """
|
||||
or partner which have their own set of ACLs. Public, portal and internal
|
||||
have access to this model depending on 'access' field, allowing to check
|
||||
ir.rule usage. """
|
||||
_description = 'Mail Access Test'
|
||||
_name = 'mail.test.access'
|
||||
_inherit = ['mail.thread.blacklist']
|
||||
|
|
@ -27,7 +29,7 @@ class MailTestAccess(models.Model):
|
|||
],
|
||||
name='Access', default='public')
|
||||
|
||||
def _mail_get_partner_fields(self):
|
||||
def _mail_get_partner_fields(self, introspect_fields=False):
|
||||
return ['customer_id']
|
||||
|
||||
|
||||
|
|
@ -36,7 +38,7 @@ class MailTestAccessCusto(models.Model):
|
|||
or partner which have their own set of ACLs. """
|
||||
_description = 'Mail Access Test with Custo'
|
||||
_name = 'mail.test.access.custo'
|
||||
_inherit = ['mail.thread.blacklist']
|
||||
_inherit = ['mail.thread.blacklist', 'mail.activity.mixin']
|
||||
_mail_post_access = 'write' # default value but ease mock
|
||||
_order = 'id DESC'
|
||||
_primary_email = 'email_from'
|
||||
|
|
@ -46,15 +48,47 @@ class MailTestAccessCusto(models.Model):
|
|||
phone = fields.Char()
|
||||
customer_id = fields.Many2one('res.partner', 'Customer')
|
||||
is_locked = fields.Boolean()
|
||||
is_readonly = fields.Boolean()
|
||||
|
||||
def _mail_get_partner_fields(self):
|
||||
def _mail_get_partner_fields(self, introspect_fields=False):
|
||||
return ['customer_id']
|
||||
|
||||
def _get_mail_message_access(self, res_ids, operation, model_name=None):
|
||||
# customize message creation
|
||||
if operation == "create":
|
||||
if any(record.is_locked for record in self.browse(res_ids)):
|
||||
raise exceptions.AccessError('Cannot post on locked records')
|
||||
else:
|
||||
return "read"
|
||||
return super()._get_mail_message_access(res_ids, operation, model_name=model_name)
|
||||
def _mail_get_operation_for_mail_message_operation(self, message_operation):
|
||||
# customize message creation: only unlocked, except admins
|
||||
if message_operation == "create" and not self.env.user._is_admin():
|
||||
return dict.fromkeys(self.filtered(lambda r: not r.is_locked), 'read')
|
||||
# customize read: read access on unlocked, write access on locked
|
||||
elif message_operation == "read":
|
||||
return {
|
||||
record: 'write' if record.is_locked else 'read'
|
||||
for record in self
|
||||
}
|
||||
return super()._mail_get_operation_for_mail_message_operation(message_operation)
|
||||
|
||||
|
||||
class MailTestAccessPublic(models.Model):
|
||||
"""A model inheriting from mail.thread with public read and write access
|
||||
to test some public and guest interactions."""
|
||||
_description = "Access Test Public"
|
||||
_name = "mail.test.access.public"
|
||||
_inherit = ["mail.thread"]
|
||||
|
||||
name = fields.Char("Name")
|
||||
customer_id = fields.Many2one('res.partner', 'Customer')
|
||||
email = fields.Char('Email')
|
||||
mobile = fields.Char('Mobile')
|
||||
is_locked = fields.Boolean()
|
||||
|
||||
def _mail_get_partner_fields(self, introspect_fields=False):
|
||||
return ['customer_id']
|
||||
|
||||
def _get_customer_information(self):
|
||||
email_key_to_values = super()._get_customer_information()
|
||||
for record in self.filtered('email'):
|
||||
# do not fill Falsy with random data, unless monorecord (= always correct)
|
||||
if not tools.email_normalize(record.email) and len(self) > 1:
|
||||
continue
|
||||
values = email_key_to_values.setdefault(record.email, {})
|
||||
if not values.get('phone'):
|
||||
values['phone'] = record.mobile
|
||||
return email_key_to_values
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
from odoo import fields, models, _
|
||||
from odoo.tools.mail import parse_contact_from_email
|
||||
|
||||
|
||||
class MailTestTLead(models.Model):
|
||||
""" Lead-like model for business flows testing """
|
||||
_name = "mail.test.lead"
|
||||
_description = 'Lead-like model'
|
||||
_inherit = [
|
||||
'mail.thread.blacklist',
|
||||
'mail.thread.cc',
|
||||
'mail.activity.mixin',
|
||||
]
|
||||
_mail_defaults_to_email = True
|
||||
_primary_email = 'email_from'
|
||||
|
||||
name = fields.Char()
|
||||
company_id = fields.Many2one('res.company')
|
||||
user_id = fields.Many2one('res.users', tracking=1)
|
||||
email_from = fields.Char()
|
||||
customer_name = fields.Char()
|
||||
partner_id = fields.Many2one('res.partner', tracking=2)
|
||||
lang_code = fields.Char()
|
||||
phone = fields.Char()
|
||||
|
||||
def _creation_message(self):
|
||||
self.ensure_one()
|
||||
return _('A new lead has been created and is assigned to %(user_name)s.', user_name=self.user_id.name or _('nobody'))
|
||||
|
||||
def _get_customer_information(self):
|
||||
email_normalized_to_values = super()._get_customer_information()
|
||||
|
||||
for lead in self:
|
||||
email_key = lead.email_normalized or lead.email_from
|
||||
values = email_normalized_to_values.setdefault(email_key, {})
|
||||
values['lang'] = values.get('lang') or lead.lang_code
|
||||
values['name'] = values.get('name') or lead.customer_name or parse_contact_from_email(lead.email_from)[0] or lead.email_from
|
||||
values['phone'] = values.get('phone') or lead.phone
|
||||
return email_normalized_to_values
|
||||
|
||||
def _message_post_after_hook(self, message, msg_vals):
|
||||
if self.email_from and not self.partner_id:
|
||||
# we consider that posting a message with a specified recipient (not a follower, a specific one)
|
||||
# on a document without customer means that it was created through the chatter using
|
||||
# suggested recipients. This heuristic allows to avoid ugly hacks in JS.
|
||||
new_partner = message.partner_ids.filtered(
|
||||
lambda partner: partner.email == self.email_from or (self.email_normalized and partner.email_normalized == self.email_normalized)
|
||||
)
|
||||
if new_partner:
|
||||
if new_partner[0].email_normalized:
|
||||
email_domain = ('email_normalized', '=', new_partner[0].email_normalized)
|
||||
else:
|
||||
email_domain = ('email_from', '=', new_partner[0].email)
|
||||
self.search([('partner_id', '=', False), email_domain]).write({'partner_id': new_partner[0].id})
|
||||
return super()._message_post_after_hook(message, msg_vals)
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
import ast
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools import email_normalize
|
||||
|
||||
|
||||
class MailTestTicket(models.Model):
|
||||
""" This model can be used in tests when complex chatter features are
|
||||
required like modeling tasks or tickets. """
|
||||
_description = 'Ticket-like model'
|
||||
_name = "mail.test.ticket"
|
||||
_inherit = ['mail.thread']
|
||||
_primary_email = 'email_from'
|
||||
|
||||
name = fields.Char()
|
||||
email_from = fields.Char(tracking=True)
|
||||
phone_number = fields.Char()
|
||||
count = fields.Integer(default=1)
|
||||
datetime = fields.Datetime(default=fields.Datetime.now)
|
||||
mail_template = fields.Many2one('mail.template', 'Template')
|
||||
customer_id = fields.Many2one('res.partner', 'Customer', tracking=2)
|
||||
user_id = fields.Many2one('res.users', 'Responsible', tracking=1)
|
||||
container_id = fields.Many2one('mail.test.container', tracking=True)
|
||||
|
||||
def _mail_get_partner_fields(self, introspect_fields=False):
|
||||
return ['customer_id']
|
||||
|
||||
def _message_compute_subject(self):
|
||||
self.ensure_one()
|
||||
return f"Ticket for {self.name} on {self.datetime.strftime('%m/%d/%Y, %H:%M:%S')}"
|
||||
|
||||
def _notify_get_recipients_groups(self, message, model_description, msg_vals=False):
|
||||
# Activate more groups to test query counters notably (and be backward compatible for tests)
|
||||
groups = super()._notify_get_recipients_groups(
|
||||
message, model_description, msg_vals=msg_vals
|
||||
)
|
||||
for group_name, _group_method, group_data in groups:
|
||||
if group_name == 'portal':
|
||||
group_data['active'] = True
|
||||
elif group_name == 'customer':
|
||||
group_data['active'] = True
|
||||
group_data['has_button_access'] = True
|
||||
|
||||
return groups
|
||||
|
||||
def _track_template(self, changes):
|
||||
res = super()._track_template(changes)
|
||||
record = self[0]
|
||||
if 'customer_id' in changes and record.mail_template:
|
||||
res['customer_id'] = (
|
||||
record.mail_template,
|
||||
{
|
||||
'composition_mode': 'mass_mail',
|
||||
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
}
|
||||
)
|
||||
elif 'datetime' in changes:
|
||||
res['datetime'] = (
|
||||
'test_mail.mail_test_ticket_tracking_view',
|
||||
{
|
||||
'composition_mode': 'mass_mail',
|
||||
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
}
|
||||
)
|
||||
return res
|
||||
|
||||
def _creation_subtype(self):
|
||||
if self.container_id:
|
||||
return self.env.ref('test_mail.st_mail_test_ticket_container_upd')
|
||||
return super(MailTestTicket, self)._creation_subtype()
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
self.ensure_one()
|
||||
if 'container_id' in init_values and self.container_id:
|
||||
return self.env.ref('test_mail.st_mail_test_ticket_container_upd')
|
||||
return super(MailTestTicket, self)._track_subtype(init_values)
|
||||
|
||||
def _get_customer_information(self):
|
||||
email_keys_to_values = super()._get_customer_information()
|
||||
|
||||
for ticket in self:
|
||||
email_key = email_normalize(ticket.email_from) or ticket.email_from
|
||||
# do not fill Falsy with random data, unless monorecord (= always correct)
|
||||
if not email_key and len(self) > 1:
|
||||
continue
|
||||
values = email_keys_to_values.setdefault(email_key, {})
|
||||
if not values.get('phone'):
|
||||
values['phone'] = ticket.phone_number
|
||||
return email_keys_to_values
|
||||
|
||||
|
||||
class MailTestTicketEl(models.Model):
|
||||
""" Just mail.test.ticket, but exclusion-list enabled. Kept as different
|
||||
model to avoid messing with existing tests, notably performance, and ease
|
||||
backward comparison. """
|
||||
_description = 'Ticket-like model with exclusion list'
|
||||
_name = "mail.test.ticket.el"
|
||||
_inherit = [
|
||||
'mail.test.ticket',
|
||||
'mail.thread.blacklist',
|
||||
]
|
||||
_primary_email = 'email_from'
|
||||
|
||||
email_from = fields.Char(
|
||||
'Email',
|
||||
compute='_compute_email_from', readonly=False, store=True)
|
||||
|
||||
@api.depends('customer_id')
|
||||
def _compute_email_from(self):
|
||||
for ticket in self.filtered(lambda r: r.customer_id and not r.email_from):
|
||||
ticket.email_from = ticket.customer_id.email_formatted
|
||||
|
||||
|
||||
class MailTestTicketMc(models.Model):
|
||||
""" Just mail.test.ticket, but multi company. Kept as different model to
|
||||
avoid messing with existing tests, notably performance, and ease backward
|
||||
comparison. """
|
||||
_description = 'Ticket-like model'
|
||||
_name = "mail.test.ticket.mc"
|
||||
_inherit = ['mail.test.ticket']
|
||||
_primary_email = 'email_from'
|
||||
|
||||
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
|
||||
container_id = fields.Many2one('mail.test.container.mc', tracking=True)
|
||||
|
||||
def _get_customer_information(self):
|
||||
email_keys_to_values = super()._get_customer_information()
|
||||
|
||||
for ticket in self:
|
||||
email_key = email_normalize(ticket.email_from) or ticket.email_from
|
||||
# do not fill Falsy with random data, unless monorecord (= always correct)
|
||||
if not email_key and len(self) > 1:
|
||||
continue
|
||||
values = email_keys_to_values.setdefault(email_key, {})
|
||||
if not values.get('company_id'):
|
||||
values['company_id'] = ticket.company_id.id
|
||||
return email_keys_to_values
|
||||
|
||||
def _notify_get_reply_to(self, default=None, author_id=False):
|
||||
# Override to use alias of the parent container
|
||||
aliases = self.sudo().mapped('container_id')._notify_get_reply_to(default=default, author_id=author_id)
|
||||
res = {ticket.id: aliases.get(ticket.container_id.id) for ticket in self}
|
||||
leftover = self.filtered(lambda rec: not rec.container_id)
|
||||
if leftover:
|
||||
res.update(super()._notify_get_reply_to(default=default, author_id=author_id))
|
||||
return res
|
||||
|
||||
def _creation_subtype(self):
|
||||
if self.container_id:
|
||||
return self.env.ref('test_mail.st_mail_test_ticket_container_mc_upd')
|
||||
return super()._creation_subtype()
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
self.ensure_one()
|
||||
if 'container_id' in init_values and self.container_id:
|
||||
return self.env.ref('test_mail.st_mail_test_ticket_container_mc_upd')
|
||||
return super()._track_subtype(init_values)
|
||||
|
||||
|
||||
class MailTestTicketPartner(models.Model):
|
||||
""" Mail.test.ticket.mc, with complete partner support. More functional
|
||||
and therefore done in a separate model to avoid breaking other tests. """
|
||||
_description = 'MC ticket-like model with partner support'
|
||||
_name = "mail.test.ticket.partner"
|
||||
_inherit = [
|
||||
'mail.test.ticket.mc',
|
||||
'mail.thread.blacklist',
|
||||
]
|
||||
_primary_email = 'email_from'
|
||||
|
||||
# fields to mimic stage-based tracing
|
||||
state = fields.Selection(
|
||||
[('new', 'New'), ('open', 'Open'), ('close', 'Close'),],
|
||||
default='open', tracking=10)
|
||||
state_template_id = fields.Many2one('mail.template')
|
||||
|
||||
def _message_post_after_hook(self, message, msg_vals):
|
||||
if self.email_from and not self.customer_id:
|
||||
# we consider that posting a message with a specified recipient (not a follower, a specific one)
|
||||
# on a document without customer means that it was created through the chatter using
|
||||
# suggested recipients. This heuristic allows to avoid ugly hacks in JS.
|
||||
new_partner = message.partner_ids.filtered(
|
||||
lambda partner: partner.email == self.email_from or (self.email_normalized and partner.email_normalized == self.email_normalized)
|
||||
)
|
||||
if new_partner:
|
||||
if new_partner[0].email_normalized:
|
||||
email_domain = ('email_normalized', '=', new_partner[0].email_normalized)
|
||||
else:
|
||||
email_domain = ('email_from', '=', new_partner[0].email)
|
||||
self.search([
|
||||
('customer_id', '=', False), email_domain,
|
||||
]).write({'customer_id': new_partner[0].id})
|
||||
return super()._message_post_after_hook(message, msg_vals)
|
||||
|
||||
def _creation_subtype(self):
|
||||
if self.state == 'new':
|
||||
return self.env.ref('test_mail.st_mail_test_ticket_partner_new')
|
||||
return super(MailTestTicket, self)._creation_subtype()
|
||||
|
||||
def _track_template(self, changes):
|
||||
res = super()._track_template(changes)
|
||||
record = self[0]
|
||||
# acknowledgement-like email, like in project/helpdesk
|
||||
if 'state' in changes and record.state == 'new' and record.state_template_id:
|
||||
res['state'] = (
|
||||
record.state_template_id,
|
||||
{
|
||||
'auto_delete_keep_log': False,
|
||||
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
'email_layout_xmlid': 'mail.mail_notification_light'
|
||||
},
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
class MailTestContainer(models.Model):
|
||||
""" This model can be used in tests when container records like projects
|
||||
or teams are required. """
|
||||
_description = 'Project-like model with alias'
|
||||
_name = "mail.test.container"
|
||||
_mail_post_access = 'read'
|
||||
_inherit = ['mail.thread', 'mail.alias.mixin']
|
||||
|
||||
name = fields.Char()
|
||||
description = fields.Text()
|
||||
customer_id = fields.Many2one('res.partner', 'Customer')
|
||||
|
||||
def _mail_get_partner_fields(self, introspect_fields=False):
|
||||
return ['customer_id']
|
||||
|
||||
def _notify_get_recipients_groups(self, message, model_description, msg_vals=False):
|
||||
# Activate more groups to test query counters notably (and be backward compatible for tests)
|
||||
groups = super()._notify_get_recipients_groups(
|
||||
message, model_description, msg_vals=msg_vals
|
||||
)
|
||||
for group_name, _group_method, group_data in groups:
|
||||
if group_name == 'portal':
|
||||
group_data['active'] = True
|
||||
|
||||
return groups
|
||||
|
||||
def _alias_get_creation_values(self):
|
||||
values = super()._alias_get_creation_values()
|
||||
values['alias_model_id'] = self.env['ir.model']._get('mail.test.ticket').id
|
||||
values['alias_force_thread_id'] = False
|
||||
if self.id:
|
||||
values['alias_defaults'] = defaults = ast.literal_eval(self.alias_defaults or "{}")
|
||||
defaults['container_id'] = self.id
|
||||
return values
|
||||
|
||||
|
||||
class MailTestContainerMc(models.Model):
|
||||
""" Just mail.test.container, but multi company. Kept as different model to
|
||||
avoid messing with existing tests, notably performance, and ease backward
|
||||
comparison. """
|
||||
_description = 'Project-like model with alias (MC)'
|
||||
_name = "mail.test.container.mc"
|
||||
_mail_post_access = 'read'
|
||||
_inherit = ['mail.test.container']
|
||||
|
||||
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
|
||||
|
||||
def _alias_get_creation_values(self):
|
||||
values = super()._alias_get_creation_values()
|
||||
values['alias_model_id'] = self.env['ir.model']._get('mail.test.ticket.mc').id
|
||||
return values
|
||||
|
|
@ -21,6 +21,19 @@ class MailPerformanceThread(models.Model):
|
|||
record.value_pc = float(record.value) / 100
|
||||
|
||||
|
||||
class MailPerformanceThreadRecipients(models.Model):
|
||||
_name = 'mail.performance.thread.recipients'
|
||||
_description = 'Performance: mail.thread, for recipients'
|
||||
_inherit = ['mail.thread']
|
||||
_primary_email = 'email_from'
|
||||
|
||||
name = fields.Char()
|
||||
value = fields.Integer()
|
||||
email_from = fields.Char('Email From')
|
||||
partner_id = fields.Many2one('res.partner', string='Customer')
|
||||
user_id = fields.Many2one('res.users', 'Responsible', tracking=1)
|
||||
|
||||
|
||||
class MailPerformanceTracking(models.Model):
|
||||
_name = 'mail.performance.tracking'
|
||||
_description = 'Performance: multi tracking'
|
||||
|
|
@ -36,7 +49,7 @@ class MailTestFieldType(models.Model):
|
|||
""" Test default values, notably type, messing through models during gateway
|
||||
processing (i.e. lead.type versus attachment.type). """
|
||||
_description = 'Test Field Type'
|
||||
_name = 'mail.test.field.type'
|
||||
_name = "mail.test.field.type"
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char()
|
||||
|
|
@ -49,11 +62,11 @@ class MailTestFieldType(models.Model):
|
|||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
# Emulate an addon that alters the creation context, such as `crm`
|
||||
if not self._context.get('default_type'):
|
||||
if not self.env.context.get('default_type'):
|
||||
self = self.with_context(default_type='first')
|
||||
return super(MailTestFieldType, self).create(vals_list)
|
||||
|
||||
def _mail_get_partner_fields(self):
|
||||
def _mail_get_partner_fields(self, introspect_fields=False):
|
||||
return ['customer_id']
|
||||
|
||||
|
||||
|
|
@ -61,7 +74,7 @@ class MailTestLang(models.Model):
|
|||
""" A simple chatter model with lang-based capabilities, allowing to
|
||||
test translations. """
|
||||
_description = 'Lang Chatter Model'
|
||||
_name = 'mail.test.lang'
|
||||
_name = "mail.test.lang"
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char()
|
||||
|
|
@ -69,30 +82,85 @@ class MailTestLang(models.Model):
|
|||
customer_id = fields.Many2one('res.partner')
|
||||
lang = fields.Char('Lang')
|
||||
|
||||
def _mail_get_partner_fields(self):
|
||||
def _mail_get_partner_fields(self, introspect_fields=False):
|
||||
return ['customer_id']
|
||||
|
||||
def _notify_get_recipients_groups(self, msg_vals=None):
|
||||
groups = super(MailTestLang, self)._notify_get_recipients_groups(msg_vals=msg_vals)
|
||||
|
||||
local_msg_vals = dict(msg_vals or {})
|
||||
|
||||
def _notify_get_recipients_groups(self, message, model_description, msg_vals=False):
|
||||
groups = super()._notify_get_recipients_groups(
|
||||
message, model_description, msg_vals=msg_vals
|
||||
)
|
||||
for group in [g for g in groups if g[0] in('follower', 'customer')]:
|
||||
group_options = group[2]
|
||||
group_options['has_button_access'] = True
|
||||
group_options['actions'] = [
|
||||
{'url': self._notify_get_action_link('controller', controller='/test_mail/do_stuff', **local_msg_vals),
|
||||
'title': _('NotificationButtonTitle')}
|
||||
]
|
||||
return groups
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# TRACKING MODELS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
|
||||
class MailTestTrackAllM2m(models.Model):
|
||||
_description = 'Sub-model: pseudo tags for tracking'
|
||||
_name = "mail.test.track.all.m2m"
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char('Name')
|
||||
|
||||
|
||||
class MailTestTrackAllO2m(models.Model):
|
||||
_description = 'Sub-model: pseudo tags for tracking'
|
||||
_name = "mail.test.track.all.o2m"
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char('Name')
|
||||
mail_track_all_id = fields.Many2one('mail.test.track.all')
|
||||
|
||||
|
||||
class MailTestTrackAllPropertiesParent(models.Model):
|
||||
_description = 'Properties Parent'
|
||||
_name = "mail.test.track.all.properties.parent"
|
||||
|
||||
definition_properties = fields.PropertiesDefinition()
|
||||
|
||||
|
||||
class MailTestTrackAll(models.Model):
|
||||
_description = 'Test tracking on all field types'
|
||||
_name = "mail.test.track.all"
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
boolean_field = fields.Boolean('Boolean', tracking=1)
|
||||
char_field = fields.Char('Char', tracking=2)
|
||||
company_id = fields.Many2one('res.company')
|
||||
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
|
||||
date_field = fields.Date('Date', tracking=3)
|
||||
datetime_field = fields.Datetime('Datetime', tracking=4)
|
||||
float_field = fields.Float('Float', tracking=5)
|
||||
float_field_with_digits = fields.Float('Precise Float', digits=(10, 8), tracking=5)
|
||||
html_field = fields.Html('Html', tracking=False)
|
||||
integer_field = fields.Integer('Integer', tracking=7)
|
||||
many2many_field = fields.Many2many(
|
||||
'mail.test.track.all.m2m', string='Many2Many',
|
||||
tracking=8)
|
||||
many2one_field_id = fields.Many2one('res.partner', string='Many2one', tracking=9)
|
||||
monetary_field = fields.Monetary('Monetary', tracking=10)
|
||||
one2many_field = fields.One2many(
|
||||
'mail.test.track.all.o2m', 'mail_track_all_id',
|
||||
string='One2Many',
|
||||
tracking=11)
|
||||
properties_parent_id = fields.Many2one('mail.test.track.all.properties.parent', tracking=True)
|
||||
properties = fields.Properties('Properties', definition='properties_parent_id.definition_properties')
|
||||
selection_field = fields.Selection(
|
||||
string='Selection',
|
||||
selection=[('first', 'FIRST'), ('second', 'SECOND')],
|
||||
tracking=12)
|
||||
text_field = fields.Text('Text', tracking=13)
|
||||
|
||||
name = fields.Char('Name')
|
||||
|
||||
|
||||
class MailTestTrackCompute(models.Model):
|
||||
_name = 'mail.test.track.compute'
|
||||
_description = "Test tracking with computed fields"
|
||||
_name = "mail.test.track.compute"
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
partner_id = fields.Many2one('res.partner', tracking=True)
|
||||
|
|
@ -101,63 +169,60 @@ class MailTestTrackCompute(models.Model):
|
|||
partner_phone = fields.Char(related='partner_id.phone', tracking=True)
|
||||
|
||||
|
||||
class MailTestTrackDurationMixin(models.Model):
|
||||
_description = 'Fake model to test the mixin mail.tracking.duration.mixin'
|
||||
_name = "mail.test.track.duration.mixin"
|
||||
_track_duration_field = 'customer_id'
|
||||
_inherit = ['mail.tracking.duration.mixin']
|
||||
|
||||
name = fields.Char()
|
||||
customer_id = fields.Many2one('res.partner', 'Customer', tracking=True)
|
||||
|
||||
def _mail_get_partner_fields(self, introspect_fields=False):
|
||||
return ['customer_id']
|
||||
|
||||
|
||||
class MailTestTrackGroups(models.Model):
|
||||
_description = "Test tracking with groups"
|
||||
_name = "mail.test.track.groups"
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char(tracking=1)
|
||||
partner_id = fields.Many2one('res.partner', tracking=2, groups="base.group_user")
|
||||
secret = fields.Char(tracking=3, groups="base.group_user")
|
||||
|
||||
|
||||
class MailTestTrackMonetary(models.Model):
|
||||
_name = 'mail.test.track.monetary'
|
||||
_description = 'Test tracking monetary field'
|
||||
_name = "mail.test.track.monetary"
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
company_id = fields.Many2one('res.company')
|
||||
company_currency = fields.Many2one("res.currency", string='Currency', related='company_id.currency_id', readonly=True, tracking=True)
|
||||
revenue = fields.Monetary('Revenue', currency_field='company_currency', tracking=True)
|
||||
|
||||
class MailTestMultiCompanyWithActivity(models.Model):
|
||||
""" This model can be used in multi company tests with activity"""
|
||||
_name = "mail.test.multi.company.with.activity"
|
||||
_description = "Test Multi Company Mail With Activity"
|
||||
_inherit = ["mail.thread", "mail.activity.mixin"]
|
||||
|
||||
name = fields.Char()
|
||||
company_id = fields.Many2one("res.company")
|
||||
|
||||
|
||||
class MailTestSelectionTracking(models.Model):
|
||||
class MailTestTrackSelection(models.Model):
|
||||
""" Test tracking for selection fields """
|
||||
_description = 'Test Selection Tracking'
|
||||
_name = 'mail.test.track.selection'
|
||||
_name = "mail.test.track.selection"
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char()
|
||||
selection_type = fields.Selection([('first', 'First'), ('second', 'Second')], tracking=True)
|
||||
|
||||
|
||||
class MailTestTrackAll(models.Model):
|
||||
_name = 'mail.test.track.all'
|
||||
_description = 'Test tracking on all field types'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
boolean_field = fields.Boolean('Boolean', tracking=True)
|
||||
char_field = fields.Char('Char', tracking=True)
|
||||
company_id = fields.Many2one('res.company')
|
||||
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
|
||||
date_field = fields.Date('Date', tracking=True)
|
||||
datetime_field = fields.Datetime('Datetime', tracking=True)
|
||||
float_field = fields.Float('Float', tracking=True)
|
||||
html_field = fields.Html('Html', tracking=True)
|
||||
integer_field = fields.Integer('Integer', tracking=True)
|
||||
many2one_field_id = fields.Many2one('res.partner', string='Many2one', tracking=True)
|
||||
monetary_field = fields.Monetary('Monetary', tracking=True)
|
||||
selection_field = fields.Selection(string='Selection', selection=[['first', 'FIRST']], tracking=True)
|
||||
text_field = fields.Text('Text', tracking=True)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# OTHER
|
||||
# ------------------------------------------------------------
|
||||
|
||||
|
||||
class MailTestMultiCompany(models.Model):
|
||||
""" This model can be used in multi company tests"""
|
||||
_name = 'mail.test.multi.company'
|
||||
""" This model can be used in multi company tests, with attachments support
|
||||
for checking record update in MC """
|
||||
_description = "Test Multi Company Mail"
|
||||
_inherit = 'mail.thread'
|
||||
_name = "mail.test.multi.company"
|
||||
_inherit = ['mail.thread.main.attachment']
|
||||
|
||||
name = fields.Char()
|
||||
company_id = fields.Many2one('res.company')
|
||||
|
|
@ -168,16 +233,29 @@ class MailTestMultiCompanyRead(models.Model):
|
|||
even if the user has no write access. """
|
||||
_description = 'Simple Chatter Model '
|
||||
_name = 'mail.test.multi.company.read'
|
||||
_inherit = ['mail.test.multi.company']
|
||||
_inherit = ['mail.test.multi.company', 'mail.activity.mixin']
|
||||
_mail_post_access = 'read'
|
||||
|
||||
|
||||
class MailTestNotMailThread(models.Model):
|
||||
class MailTestMultiCompanyWithActivity(models.Model):
|
||||
""" This model can be used in multi company tests with activity"""
|
||||
_description = "Test Multi Company Mail With Activity"
|
||||
_name = "mail.test.multi.company.with.activity"
|
||||
_inherit = ["mail.thread", "mail.activity.mixin"]
|
||||
|
||||
name = fields.Char()
|
||||
company_id = fields.Many2one("res.company")
|
||||
|
||||
|
||||
class MailTestNothread(models.Model):
|
||||
""" Models not inheriting from mail.thread but using some cross models
|
||||
capabilities of mail. """
|
||||
_name = 'mail.test.nothread'
|
||||
_description = "NoThread Model"
|
||||
_name = "mail.test.nothread"
|
||||
|
||||
name = fields.Char()
|
||||
company_id = fields.Many2one('res.company')
|
||||
customer_id = fields.Many2one('res.partner')
|
||||
|
||||
def _mail_get_partner_fields(self, introspect_fields=False):
|
||||
return ['customer_id']
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
from odoo import api, fields, models
|
||||
from odoo.fields import Domain
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# RECIPIENTS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
|
||||
class MailTestRecipients(models.Model):
|
||||
_name = 'mail.test.recipients'
|
||||
_description = "Test Recipients Computation"
|
||||
_inherit = ['mail.thread.cc']
|
||||
_primary_email = 'customer_email'
|
||||
|
||||
company_id = fields.Many2one('res.company')
|
||||
contact_ids = fields.Many2many('res.partner')
|
||||
customer_id = fields.Many2one('res.partner')
|
||||
customer_email = fields.Char('Customer Email', compute='_compute_customer_email', readonly=False, store=True)
|
||||
customer_phone = fields.Char('Customer Phone', compute='_compute_customer_phone', readonly=False, store=True)
|
||||
name = fields.Char()
|
||||
|
||||
@api.depends('customer_id')
|
||||
def _compute_customer_email(self):
|
||||
for source in self.filtered(lambda r: r.customer_id and not r.customer_email):
|
||||
source.customer_email = source.customer_id.email_formatted
|
||||
|
||||
@api.depends('customer_id')
|
||||
def _compute_customer_phone(self):
|
||||
for source in self.filtered(lambda r: r.customer_id and not r.customer_phone):
|
||||
source.customer_phone = source.customer_id.phone
|
||||
|
||||
def _mail_get_partner_fields(self, introspect_fields=False):
|
||||
return ['customer_id', 'contact_ids']
|
||||
|
||||
|
||||
class MailTestThreadCustomer(models.Model):
|
||||
_name = 'mail.test.thread.customer'
|
||||
_description = "Test Customer Thread Model"
|
||||
_inherit = ['mail.test.recipients']
|
||||
_mail_thread_customer = True
|
||||
_primary_email = 'customer_email'
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# PROPERTIES
|
||||
# ------------------------------------------------------------
|
||||
|
||||
|
||||
class MailTestProperties(models.Model):
|
||||
_name = 'mail.test.properties'
|
||||
_description = 'Mail Test Properties'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char('Name')
|
||||
parent_id = fields.Many2one('mail.test.properties', string='Parent')
|
||||
properties = fields.Properties('Properties', definition='parent_id.definition_properties')
|
||||
definition_properties = fields.PropertiesDefinition('Definitions')
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ROTTING RESOURCES
|
||||
# ------------------------------------------------------------
|
||||
|
||||
|
||||
class MailTestStageField(models.Model):
|
||||
_description = 'Fake model to be a stage to help test rotting implementation'
|
||||
_name = 'mail.test.rotting.stage'
|
||||
|
||||
name = fields.Char()
|
||||
rotting_threshold_days = fields.Integer(default=3)
|
||||
no_rot = fields.Boolean(default=False)
|
||||
|
||||
|
||||
class MailTestRottingMixin(models.Model):
|
||||
_description = 'Fake model to test the rotting part of the mixin mail.thread.tracking.duration.mixin'
|
||||
_name = 'mail.test.rotting.resource'
|
||||
_track_duration_field = 'stage_id'
|
||||
_inherit = ['mail.tracking.duration.mixin']
|
||||
|
||||
name = fields.Char()
|
||||
date_last_stage_update = fields.Datetime(
|
||||
'Last Stage Update', compute='_compute_date_last_stage_update', index=True, readonly=True, store=True)
|
||||
stage_id = fields.Many2one('mail.test.rotting.stage', 'Stage')
|
||||
done = fields.Boolean(default=False)
|
||||
|
||||
def _get_rotting_depends_fields(self):
|
||||
return super()._get_rotting_depends_fields() + ['done', 'stage_id.no_rot']
|
||||
|
||||
def _get_rotting_domain(self):
|
||||
return super()._get_rotting_domain() & Domain([
|
||||
('done', '=', False),
|
||||
('stage_id.no_rot', '=', False),
|
||||
])
|
||||
|
||||
@api.depends('stage_id')
|
||||
def _compute_date_last_stage_update(self):
|
||||
self.date_last_stage_update = fields.Datetime.now()
|
||||
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
|
|
@ -8,29 +5,93 @@ class MailTestSimple(models.Model):
|
|||
""" A very simple model only inheriting from mail.thread when only
|
||||
communication history is necessary. """
|
||||
_description = 'Simple Chatter Model'
|
||||
_name = 'mail.test.simple'
|
||||
_name = "mail.test.simple"
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char()
|
||||
email_from = fields.Char()
|
||||
|
||||
def _message_compute_subject(self):
|
||||
""" To ease mocks """
|
||||
_a = super()._message_compute_subject()
|
||||
return _a
|
||||
|
||||
def _notify_by_email_get_final_mail_values(self, *args, **kwargs):
|
||||
""" To ease mocks """
|
||||
_a = super()._notify_by_email_get_final_mail_values(*args, **kwargs)
|
||||
return _a
|
||||
|
||||
def _notify_by_email_get_headers(self, headers=None):
|
||||
headers = super()._notify_by_email_get_headers(headers=headers)
|
||||
headers['X-Custom'] = 'Done'
|
||||
return headers
|
||||
|
||||
class MailTestSimpleUnnamed(models.Model):
|
||||
""" A very simple model only inheriting from mail.thread when only
|
||||
communication history is necessary, and has no 'name' field """
|
||||
_description = 'Simple Chatter Model without "name" field'
|
||||
_name = 'mail.test.simple.unnamed'
|
||||
_inherit = ['mail.thread']
|
||||
_rec_name = "description"
|
||||
|
||||
description = fields.Char()
|
||||
|
||||
class MailTestSimpleMainAttachment(models.Model):
|
||||
_description = 'Simple Chatter Model With Main Attachment Management'
|
||||
_name = "mail.test.simple.main.attachment"
|
||||
_inherit = ['mail.test.simple', 'mail.thread.main.attachment']
|
||||
|
||||
|
||||
class MailTestSimpleUnfollow(models.Model):
|
||||
""" A very simple model only inheriting from mail.thread when only
|
||||
communication history is necessary with unfollow link enabled in
|
||||
notification emails even for non-internal user. """
|
||||
_description = 'Simple Chatter Model'
|
||||
_name = "mail.test.simple.unfollow"
|
||||
_inherit = ['mail.thread']
|
||||
_partner_unfollow_enabled = True
|
||||
|
||||
name = fields.Char()
|
||||
company_id = fields.Many2one('res.company')
|
||||
email_from = fields.Char()
|
||||
|
||||
|
||||
class MailTestAliasOptional(models.Model):
|
||||
""" A chatter model inheriting from the alias mixin using optional alias_id
|
||||
field, hence no inherits. """
|
||||
_description = 'Chatter Model using Optional Alias Mixin'
|
||||
_name = "mail.test.alias.optional"
|
||||
_inherit = ['mail.alias.mixin.optional']
|
||||
|
||||
name = fields.Char()
|
||||
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
|
||||
email_from = fields.Char()
|
||||
|
||||
def _alias_get_creation_values(self):
|
||||
""" Updates itself """
|
||||
values = super()._alias_get_creation_values()
|
||||
values['alias_model_id'] = self.env['ir.model']._get_id('mail.test.alias.optional')
|
||||
if self.id:
|
||||
values['alias_force_thread_id'] = self.id
|
||||
values['alias_defaults'] = {'company_id': self.company_id.id}
|
||||
return values
|
||||
|
||||
|
||||
class MailTestGateway(models.Model):
|
||||
""" A very simple model only inheriting from mail.thread to test pure mass
|
||||
mailing features and base performances. """
|
||||
_description = 'Simple Chatter Model for Mail Gateway'
|
||||
_name = 'mail.test.gateway'
|
||||
_name = "mail.test.gateway"
|
||||
_inherit = ['mail.thread.blacklist']
|
||||
_primary_email = 'email_from'
|
||||
|
||||
name = fields.Char()
|
||||
email_from = fields.Char()
|
||||
custom_field = fields.Char()
|
||||
user_id = fields.Many2one('res.users', 'Responsible')
|
||||
|
||||
@api.model
|
||||
def message_new(self, msg_dict, custom_values=None):
|
||||
""" Check override of 'message_new' allowing to update record values
|
||||
base on incoming email. """
|
||||
defaults = {
|
||||
'email_from': msg_dict.get('from'),
|
||||
}
|
||||
|
|
@ -38,11 +99,32 @@ class MailTestGateway(models.Model):
|
|||
return super().message_new(msg_dict, custom_values=defaults)
|
||||
|
||||
|
||||
class MailTestGatewayCompany(models.Model):
|
||||
""" A very simple model only inheriting from mail.thread to test pure mass
|
||||
mailing features and base performances, with a company field. """
|
||||
_description = 'Simple Chatter Model for Mail Gateway with company'
|
||||
_name = "mail.test.gateway.company"
|
||||
_inherit = ['mail.test.gateway']
|
||||
|
||||
company_id = fields.Many2one('res.company', 'Company')
|
||||
|
||||
|
||||
class MailTestGatewayMainAttachment(models.Model):
|
||||
""" A very simple model only inheriting from mail.thread to test pure mass
|
||||
mailing features and base performances, with a company field and main
|
||||
attachment management. """
|
||||
_description = 'Simple Chatter Model for Mail Gateway with company'
|
||||
_name = "mail.test.gateway.main.attachment"
|
||||
_inherit = ['mail.test.gateway', 'mail.thread.main.attachment']
|
||||
|
||||
company_id = fields.Many2one('res.company', 'Company')
|
||||
|
||||
|
||||
class MailTestGatewayGroups(models.Model):
|
||||
""" A model looking like discussion channels / groups (flat thread and
|
||||
alias). Used notably for advanced gatewxay tests. """
|
||||
_description = 'Channel/Group-like Chatter Model for Mail Gateway'
|
||||
_name = 'mail.test.gateway.groups'
|
||||
_name = "mail.test.gateway.groups"
|
||||
_inherit = ['mail.thread.blacklist', 'mail.alias.mixin']
|
||||
_mail_flat_thread = False
|
||||
_primary_email = 'email_from'
|
||||
|
|
@ -60,25 +142,15 @@ class MailTestGatewayGroups(models.Model):
|
|||
values['alias_parent_thread_id'] = self.id
|
||||
return values
|
||||
|
||||
def _mail_get_partner_fields(self):
|
||||
def _mail_get_partner_fields(self, introspect_fields=False):
|
||||
return ['customer_id']
|
||||
|
||||
def _message_get_default_recipients(self):
|
||||
return dict(
|
||||
(record.id, {
|
||||
'email_cc': False,
|
||||
'email_to': record.email_from if not record.customer_id.ids else False,
|
||||
'partner_ids': record.customer_id.ids,
|
||||
})
|
||||
for record in self
|
||||
)
|
||||
|
||||
|
||||
class MailTestStandard(models.Model):
|
||||
class MailTestTrack(models.Model):
|
||||
""" This model can be used in tests when automatic subscription and simple
|
||||
tracking is necessary. Most features are present in a simple way. """
|
||||
_description = 'Standard Chatter Model'
|
||||
_name = 'mail.test.track'
|
||||
_name = "mail.test.track"
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char()
|
||||
|
|
@ -86,24 +158,40 @@ class MailTestStandard(models.Model):
|
|||
user_id = fields.Many2one('res.users', 'Responsible', tracking=True)
|
||||
container_id = fields.Many2one('mail.test.container', tracking=True)
|
||||
company_id = fields.Many2one('res.company')
|
||||
track_fields_tofilter = fields.Char() # comma-separated list of field names
|
||||
track_enable_default_log = fields.Boolean(default=False)
|
||||
parent_id = fields.Many2one('mail.test.track', string='Parent')
|
||||
|
||||
def _track_filter_for_display(self, tracking_values):
|
||||
values = super()._track_filter_for_display(tracking_values)
|
||||
filtered_fields = set(self.track_fields_tofilter.split(',') if self.track_fields_tofilter else '')
|
||||
return values.filtered(lambda val: val.field_id.name not in filtered_fields)
|
||||
|
||||
def _track_get_default_log_message(self, changes):
|
||||
filtered_fields = set(self.track_fields_tofilter.split(',') if self.track_fields_tofilter else '')
|
||||
if self.track_enable_default_log and not all(change in filtered_fields for change in changes):
|
||||
return f'There was a change on {self.name} for fields "{",".join(changes)}"'
|
||||
return super()._track_get_default_log_message(changes)
|
||||
|
||||
|
||||
class MailTestActivity(models.Model):
|
||||
""" This model can be used to test activities in addition to simple chatter
|
||||
features. """
|
||||
_description = 'Activity Model'
|
||||
_name = 'mail.test.activity'
|
||||
_name = "mail.test.activity"
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
name = fields.Char()
|
||||
date = fields.Date()
|
||||
email_from = fields.Char()
|
||||
active = fields.Boolean(default=True)
|
||||
company_id = fields.Many2one('res.company')
|
||||
|
||||
def action_start(self, action_summary):
|
||||
return self.activity_schedule(
|
||||
'test_mail.mail_act_test_todo',
|
||||
summary=action_summary
|
||||
summary=action_summary,
|
||||
user_id=self.env.uid,
|
||||
)
|
||||
|
||||
def action_close(self, action_feedback, attachment_ids=None):
|
||||
|
|
@ -112,196 +200,18 @@ class MailTestActivity(models.Model):
|
|||
attachment_ids=attachment_ids)
|
||||
|
||||
|
||||
class MailTestTicket(models.Model):
|
||||
""" This model can be used in tests when complex chatter features are
|
||||
required like modeling tasks or tickets. """
|
||||
_description = 'Ticket-like model'
|
||||
_name = 'mail.test.ticket'
|
||||
_inherit = ['mail.thread']
|
||||
_primary_email = 'email_from'
|
||||
|
||||
name = fields.Char()
|
||||
email_from = fields.Char(tracking=True)
|
||||
count = fields.Integer(default=1)
|
||||
datetime = fields.Datetime(default=fields.Datetime.now)
|
||||
mail_template = fields.Many2one('mail.template', 'Template')
|
||||
customer_id = fields.Many2one('res.partner', 'Customer', tracking=2)
|
||||
user_id = fields.Many2one('res.users', 'Responsible', tracking=1)
|
||||
container_id = fields.Many2one('mail.test.container', tracking=True)
|
||||
|
||||
def _mail_get_partner_fields(self):
|
||||
return ['customer_id']
|
||||
|
||||
def _message_get_default_recipients(self):
|
||||
return dict(
|
||||
(record.id, {
|
||||
'email_cc': False,
|
||||
'email_to': record.email_from if not record.customer_id.ids else False,
|
||||
'partner_ids': record.customer_id.ids,
|
||||
})
|
||||
for record in self
|
||||
)
|
||||
|
||||
def _notify_get_recipients_groups(self, msg_vals=None):
|
||||
""" Activate more groups to test query counters notably (and be backward
|
||||
compatible for tests). """
|
||||
local_msg_vals = dict(msg_vals or {})
|
||||
groups = super()._notify_get_recipients_groups(msg_vals=msg_vals)
|
||||
for group_name, _group_method, group_data in groups:
|
||||
if group_name == 'portal':
|
||||
group_data['active'] = True
|
||||
elif group_name == 'customer':
|
||||
group_data['active'] = True
|
||||
group_data['has_button_access'] = True
|
||||
group_data['actions'] = [{
|
||||
'url': self._notify_get_action_link(
|
||||
'controller',
|
||||
controller='/test_mail/do_stuff',
|
||||
**local_msg_vals
|
||||
),
|
||||
'title': _('NotificationButtonTitle')
|
||||
}]
|
||||
|
||||
return groups
|
||||
|
||||
def _track_template(self, changes):
|
||||
res = super(MailTestTicket, self)._track_template(changes)
|
||||
record = self[0]
|
||||
if 'customer_id' in changes and record.mail_template:
|
||||
res['customer_id'] = (record.mail_template, {'composition_mode': 'mass_mail'})
|
||||
elif 'datetime' in changes:
|
||||
res['datetime'] = ('test_mail.mail_test_ticket_tracking_view', {'composition_mode': 'mass_mail'})
|
||||
return res
|
||||
|
||||
def _creation_subtype(self):
|
||||
if self.container_id:
|
||||
return self.env.ref('test_mail.st_mail_test_ticket_container_upd')
|
||||
return super(MailTestTicket, self)._creation_subtype()
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
self.ensure_one()
|
||||
if 'container_id' in init_values and self.container_id:
|
||||
return self.env.ref('test_mail.st_mail_test_ticket_container_upd')
|
||||
return super(MailTestTicket, self)._track_subtype(init_values)
|
||||
|
||||
|
||||
|
||||
class MailTestTicketEL(models.Model):
|
||||
""" Just mail.test.ticket, but exclusion-list enabled. Kept as different
|
||||
model to avoid messing with existing tests, notably performance, and ease
|
||||
backward comparison. """
|
||||
_description = 'Ticket-like model with exclusion list'
|
||||
_name = 'mail.test.ticket.el'
|
||||
_inherit = [
|
||||
'mail.test.ticket',
|
||||
'mail.thread.blacklist',
|
||||
]
|
||||
_primary_email = 'email_from'
|
||||
|
||||
email_from = fields.Char(
|
||||
'Email',
|
||||
compute='_compute_email_from', readonly=False, store=True)
|
||||
|
||||
@api.depends('customer_id')
|
||||
def _compute_email_from(self):
|
||||
for ticket in self.filtered(lambda r: r.customer_id and not r.email_from):
|
||||
ticket.email_from = ticket.customer_id.email_formatted
|
||||
|
||||
|
||||
class MailTestTicketMC(models.Model):
|
||||
""" Just mail.test.ticket, but multi company. Kept as different model to
|
||||
avoid messing with existing tests, notably performance, and ease backward
|
||||
comparison. """
|
||||
_description = 'Ticket-like model'
|
||||
_name = 'mail.test.ticket.mc'
|
||||
_inherit = ['mail.test.ticket']
|
||||
_primary_email = 'email_from'
|
||||
|
||||
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
|
||||
container_id = fields.Many2one('mail.test.container.mc', tracking=True)
|
||||
|
||||
def _creation_subtype(self):
|
||||
if self.container_id:
|
||||
return self.env.ref('test_mail.st_mail_test_ticket_container_mc_upd')
|
||||
return super()._creation_subtype()
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
self.ensure_one()
|
||||
if 'container_id' in init_values and self.container_id:
|
||||
return self.env.ref('test_mail.st_mail_test_ticket_container_mc_upd')
|
||||
return super()._track_subtype(init_values)
|
||||
|
||||
|
||||
class MailTestContainer(models.Model):
|
||||
""" This model can be used in tests when container records like projects
|
||||
or teams are required. """
|
||||
_description = 'Project-like model with alias'
|
||||
_name = 'mail.test.container'
|
||||
_mail_post_access = 'read'
|
||||
_inherit = ['mail.thread', 'mail.alias.mixin']
|
||||
|
||||
name = fields.Char()
|
||||
description = fields.Text()
|
||||
customer_id = fields.Many2one('res.partner', 'Customer')
|
||||
alias_id = fields.Many2one(
|
||||
'mail.alias', 'Alias',
|
||||
delegate=True)
|
||||
|
||||
def _mail_get_partner_fields(self):
|
||||
return ['customer_id']
|
||||
|
||||
def _message_get_default_recipients(self):
|
||||
return dict(
|
||||
(record.id, {
|
||||
'email_cc': False,
|
||||
'email_to': False,
|
||||
'partner_ids': record.customer_id.ids,
|
||||
})
|
||||
for record in self
|
||||
)
|
||||
|
||||
def _notify_get_recipients_groups(self, msg_vals=None):
|
||||
""" Activate more groups to test query counters notably (and be backward
|
||||
compatible for tests). """
|
||||
groups = super(MailTestContainer, self)._notify_get_recipients_groups(msg_vals=msg_vals)
|
||||
for group_name, _group_method, group_data in groups:
|
||||
if group_name == 'portal':
|
||||
group_data['active'] = True
|
||||
|
||||
return groups
|
||||
|
||||
def _alias_get_creation_values(self):
|
||||
values = super(MailTestContainer, self)._alias_get_creation_values()
|
||||
values['alias_model_id'] = self.env['ir.model']._get('mail.test.container').id
|
||||
if self.id:
|
||||
values['alias_force_thread_id'] = self.id
|
||||
values['alias_parent_thread_id'] = self.id
|
||||
return values
|
||||
|
||||
class MailTestContainerMC(models.Model):
|
||||
""" Just mail.test.container, but multi company. Kept as different model to
|
||||
avoid messing with existing tests, notably performance, and ease backward
|
||||
comparison. """
|
||||
_description = 'Project-like model with alias (MC)'
|
||||
_name = 'mail.test.container.mc'
|
||||
_mail_post_access = 'read'
|
||||
_inherit = ['mail.test.container']
|
||||
|
||||
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
|
||||
|
||||
|
||||
class MailTestComposerMixin(models.Model):
|
||||
""" A simple invite-like wizard using the composer mixin, rendering on
|
||||
composer source test model. Purpose is to have a minimal composer which
|
||||
runs on other records and check notably dynamic template support and
|
||||
translations. """
|
||||
_description = 'Invite-like Wizard'
|
||||
_name = 'mail.test.composer.mixin'
|
||||
_name = "mail.test.composer.mixin"
|
||||
_inherit = ['mail.composer.mixin']
|
||||
|
||||
name = fields.Char('Name')
|
||||
author_id = fields.Many2one('res.partner')
|
||||
description = fields.Html('Description', render_engine="qweb", render_options={"post_process": True}, sanitize=False)
|
||||
description = fields.Html('Description', render_engine="qweb", render_options={"post_process": True}, sanitize='email_outgoing')
|
||||
source_ids = fields.Many2many('mail.test.composer.source', string='Invite source')
|
||||
|
||||
def _compute_render_model(self):
|
||||
|
|
@ -310,8 +220,8 @@ class MailTestComposerMixin(models.Model):
|
|||
|
||||
class MailTestComposerSource(models.Model):
|
||||
""" A simple model on which invites are sent. """
|
||||
_description = 'Invite-like Wizard'
|
||||
_name = 'mail.test.composer.source'
|
||||
_description = 'Invite-like Source'
|
||||
_name = "mail.test.composer.source"
|
||||
_inherit = ['mail.thread.blacklist']
|
||||
_primary_email = 'email_from'
|
||||
|
||||
|
|
@ -326,5 +236,5 @@ class MailTestComposerSource(models.Model):
|
|||
for source in self.filtered(lambda r: r.customer_id and not r.email_from):
|
||||
source.email_from = source.customer_id.email_formatted
|
||||
|
||||
def _mail_get_partner_fields(self):
|
||||
def _mail_get_partner_fields(self, introspect_fields=False):
|
||||
return ['customer_id']
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class MailTestCC(models.Model):
|
||||
class MailTestCc(models.Model):
|
||||
_name = 'mail.test.cc'
|
||||
_description = "Test Email CC Thread"
|
||||
_inherit = ['mail.thread.cc']
|
||||
|
|
|
|||
|
|
@ -1,15 +1,29 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_mail_performance_thread,access_mail_performance_thread,model_mail_performance_thread,,1,1,1,1
|
||||
access_mail_performance_thread,access_mail_performance_thread,model_mail_performance_thread,base.group_user,1,1,1,1
|
||||
access_mail_performance_thread_recipients,access_mail_performance_thread_recipients,model_mail_performance_thread_recipients,base.group_user,1,1,1,1
|
||||
access_mail_performance_tracking_user,mail.performance.tracking,model_mail_performance_tracking,base.group_user,1,1,1,1
|
||||
access_mail_test_access_portal,mail.access.portal.portal,model_mail_test_access,base.group_portal,1,1,0,0
|
||||
access_mail_test_access_public,mail.access.portal.public,model_mail_test_access,base.group_public,1,0,0,0
|
||||
access_mail_test_access_user,mail.access.portal.user,model_mail_test_access,base.group_user,1,1,1,1
|
||||
access_mail_test_access_custo_portal,mail.access.portal.portal,model_mail_test_access_custo,base.group_portal,1,0,0,0
|
||||
access_mail_test_access_custo_user,mail.access.portal.user,model_mail_test_access_custo,base.group_user,1,1,1,1
|
||||
access_mail_test_access_public_public,mail.test.access.public.public,model_mail_test_access_public,base.group_public,1,1,0,0
|
||||
access_mail_test_access_public_portal,mail.test.access.public.portal,model_mail_test_access_public,base.group_portal,1,1,0,0
|
||||
access_mail_test_access_public_user,mail.test.access.public.user,model_mail_test_access_public,base.group_user,1,1,1,1
|
||||
access_mail_test_alias_optional_portal,mail.test.alias.optional.portal,model_mail_test_alias_optional,base.group_portal,1,0,0,0
|
||||
access_mail_test_alias_optional_user,mail.test.alias.optional.user,model_mail_test_alias_optional,base.group_user,1,1,1,1
|
||||
access_mail_test_simple_portal,mail.test.simple.portal,model_mail_test_simple,base.group_portal,1,0,0,0
|
||||
access_mail_test_simple_user,mail.test.simple.user,model_mail_test_simple,base.group_user,1,1,1,1
|
||||
access_mail_test_simple_unnamed_portal,mail.test.simple.unnamed.portal,model_mail_test_simple_unnamed,base.group_portal,1,0,0,0
|
||||
access_mail_test_simple_unnamed_user,mail.test.simple.unnamed.user,model_mail_test_simple_unnamed,base.group_user,1,1,1,1
|
||||
access_mail_test_simple_unfollow_portal,mail.test.simple.unfollow.portal,model_mail_test_simple_unfollow,base.group_portal,0,0,0,0
|
||||
access_mail_test_simple_unfollow_user,mail.test.simple.unfollow.user,model_mail_test_simple_unfollow,base.group_user,1,1,1,1
|
||||
access_mail_test_simple_main_attachment_portal,mail.test.simple.main.attachment.portal,model_mail_test_simple_main_attachment,base.group_portal,1,0,0,0
|
||||
access_mail_test_simple_main_attachment_user,mail.test.simple.main.attachment.user,model_mail_test_simple_main_attachment,base.group_user,1,1,1,1
|
||||
access_mail_test_gateway_portal,mail.test.gateway.portal,model_mail_test_gateway,base.group_portal,1,0,0,0
|
||||
access_mail_test_gateway_user,mail.test.gateway.user,model_mail_test_gateway,base.group_user,1,1,1,1
|
||||
access_mail_test_gateway_company_user,mail.test.gateway.company.user,model_mail_test_gateway_company,base.group_user,1,1,1,1
|
||||
access_mail_test_gateway_main_attachment_user,mail.test.gateway.main.attachment.user,model_mail_test_gateway_main_attachment,base.group_user,1,1,1,1
|
||||
access_mail_test_gateway_groups_portal,mail.test.gateway.groups.portal,model_mail_test_gateway_groups,base.group_portal,1,0,0,0
|
||||
access_mail_test_gateway_groups_user,mail.test.gateway.groups.user,model_mail_test_gateway_groups,base.group_user,1,1,1,1
|
||||
access_mail_test_track_portal,mail.test.track.portal,model_mail_test_track,base.group_portal,0,0,0,0
|
||||
|
|
@ -18,15 +32,18 @@ access_mail_test_activity_portal,mail.test.activity.portal,model_mail_test_activ
|
|||
access_mail_test_activity_user,mail.test.activity.user,model_mail_test_activity,base.group_user,1,1,1,1
|
||||
access_mail_test_field_type_portal,mail.test.field.type.portal,model_mail_test_field_type,base.group_portal,0,0,0,0
|
||||
access_mail_test_field_type_user,mail.test.field.type.user,model_mail_test_field_type,base.group_user,1,1,1,1
|
||||
access_mail_test_lead_user,mail.test.lead.user,model_mail_test_lead,base.group_user,1,1,1,1
|
||||
access_mail_test_ticket_portal,mail.test.ticket.portal,model_mail_test_ticket,base.group_portal,1,0,0,0
|
||||
access_mail_test_ticket_user,mail.test.ticket.user,model_mail_test_ticket,base.group_user,1,1,1,1
|
||||
access_mail_test_ticket_el_portal,mail.test.ticket.el.portal,model_mail_test_ticket_el,base.group_portal,1,0,0,0
|
||||
access_mail_test_ticket_el_user,mail.test.ticket.el.user,model_mail_test_ticket_el,base.group_user,1,1,1,1
|
||||
access_mail_test_ticket_mc_portal,mail.test.ticket.mc.portal,model_mail_test_ticket_mc,base.group_portal,1,0,0,0
|
||||
access_mail_test_ticket_mc_user,mail.test.ticket.mc.user,model_mail_test_ticket_mc,base.group_user,1,1,1,1
|
||||
access_mail_test_ticket_partner_portal,mail.test.ticket.partner.portal,model_mail_test_ticket_partner,base.group_portal,1,0,0,0
|
||||
access_mail_test_ticket_partner_user,mail.test.ticket.partner.user,model_mail_test_ticket_partner,base.group_user,1,1,1,1
|
||||
access_mail_test_composer_mixin_all,mail.test.composer.mixin.all,model_mail_test_composer_mixin,,0,0,0,0
|
||||
access_mail_test_composer_mixin_user,mail.test.composer.mixin.user,model_mail_test_composer_mixin,base.group_user,1,1,1,1
|
||||
access_mail_test_composer_source_all,mail.test.composer.source.all,model_mail_test_composer_source,,1,0,0,0
|
||||
access_mail_test_composer_source_all,mail.test.composer.source.all,model_mail_test_composer_source,base.group_user,1,0,0,0
|
||||
access_mail_test_composer_source_user,mail.test.composer.source.user,model_mail_test_composer_source,base.group_user,1,1,1,1
|
||||
access_mail_test_container_portal,mail.test.container_portal,model_mail_test_container,base.group_portal,1,0,0,0
|
||||
access_mail_test_container_user,mail.test.container.user,model_mail_test_container,base.group_user,1,1,1,1
|
||||
|
|
@ -44,8 +61,18 @@ access_mail_test_multi_company_with_activity_user,mail.test.multi.company.with.a
|
|||
access_mail_test_multi_company_with_activity_portal,mail.test.multi.company.with.activity.portal,model_mail_test_multi_company_with_activity,base.group_portal,1,0,0,0
|
||||
access_mail_test_nothread_user,mail.test.nothread.user,model_mail_test_nothread,base.group_user,1,1,1,1
|
||||
access_mail_test_nothread_portal,mail.test.nothread.portal,model_mail_test_nothread,base.group_portal,1,0,0,0
|
||||
access_mail_test_recipients_user,mail.test.recipients.user,model_mail_test_recipients,base.group_user,1,1,1,1
|
||||
access_mail_test_rotting_resource,mail.test.rotting.resource,model_mail_test_rotting_resource,base.group_user,1,1,1,1
|
||||
access_mail_test_rotting_stage,mail.test.rotting.stage,model_mail_test_rotting_stage,base.group_user,1,1,1,1
|
||||
access_mail_test_thread_customer_user,mail.test.thread.customer.user,model_mail_test_thread_customer,base.group_user,1,1,1,1
|
||||
access_mail_test_track_all,mail.test.track.all,model_mail_test_track_all,base.group_user,1,1,1,1
|
||||
access_mail_test_track_all_properties_parent,access_mail_test_track_all_properties_parent,model_mail_test_track_all_properties_parent,base.group_user,1,0,0,0
|
||||
access_mail_test_track_all_m2m,mail.test.track.all.m2m,model_mail_test_track_all_m2m,base.group_user,1,1,1,1
|
||||
access_mail_test_track_all_o2m,mail.test.track.all.o2m,model_mail_test_track_all_o2m,base.group_user,1,1,1,1
|
||||
access_mail_test_track_compute,mail.test.track.compute,model_mail_test_track_compute,base.group_user,1,1,1,1
|
||||
access_mail_test_track_groups,mail.test.track.groups,model_mail_test_track_groups,base.group_user,1,1,1,1
|
||||
access_mail_test_track_monetary,mail.test.track.monetary,model_mail_test_track_monetary,base.group_user,1,1,1,1
|
||||
access_mail_test_track_selection_portal,mail.test.track.selection.portal,model_mail_test_track_selection,base.group_portal,0,0,0,0
|
||||
access_mail_test_track_selection_user,mail.test.track.selection.user,model_mail_test_track_selection,base.group_user,1,1,1,1
|
||||
access_mail_test_properties_user,mail.test.properties.user,model_mail_test_properties,base.group_user,1,1,1,1
|
||||
access_mail_test_track_duration_mixin,mail.test.track.duration.mixin,model_mail_test_track_duration_mixin,base.group_user,1,1,1,1
|
||||
|
|
|
|||
|
|
|
@ -1,6 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<!-- Having rules triggers call to check_access_rules and allow to spot crashes
|
||||
notably when records are unlinked. Without rule, method is not called and
|
||||
some crashes are not trigerred in tests. -->
|
||||
<record id="ir_rule_mail_test_simple_dummy" model="ir.rule">
|
||||
<field name="name">Dummy rule, just to enable rule evaluation, shows some specific errors</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_simple"/>
|
||||
<field name="domain_force">[('email_from', '!=', 'donotsetmewiththisvalue')]</field>
|
||||
</record>
|
||||
|
||||
<!-- MAIL.TEST.ACCESS -->
|
||||
<record id="ir_rule_mail_test_access_public" model="ir.rule">
|
||||
<field name="name">Public: public only</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_access"/>
|
||||
|
|
@ -55,21 +65,45 @@
|
|||
<field name="groups" eval="[(4, ref('base.group_system'))]"/>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="mail_test_multi_company_rule" model="ir.rule">
|
||||
<field name="name">Mail Test Multi Company</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_multi_company"/>
|
||||
<field eval="True" name="global"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
<!-- MAIL.TEST.ACCESS.CUSTO -->
|
||||
<record id="ir_rule_mail_test_access_custo_portal_read" model="ir.rule">
|
||||
<field name="name">Portal: read unlocked</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_access_custo"/>
|
||||
<field name="domain_force">[('is_locked', '=', False)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
<record id="ir_rule_mail_test_access_custo_update" model="ir.rule">
|
||||
<field name="name">Internal: create/write/unlink unlocked and not readonly</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_access_custo"/>
|
||||
<field name="domain_force">[('is_readonly', '=', False), ('is_locked', '=', False)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('base.group_portal'))]"/>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
<record id="ir_rule_mail_test_access_custo_update_admin" model="ir.rule">
|
||||
<field name="name">Admin: all</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_access_custo"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_system'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- MAIL.TEST.MULTI.COMPANY(.*) -->
|
||||
<record id="mail_test_multi_company_rule" model="ir.rule">
|
||||
<field name="name">Mail Test Multi Company</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_multi_company"/>
|
||||
<field eval="True" name="global"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
|
||||
<record id="mail_test_multi_company_read_rule" model="ir.rule">
|
||||
<field name="name">MC Readonly Rule</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_multi_company_read"/>
|
||||
|
|
@ -77,7 +111,6 @@
|
|||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
<field name="global" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="mail_test_multi_company_with_activity_rule" model="ir.rule">
|
||||
<field name="name">Mail Test Multi Company With Activity</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_multi_company_with_activity"/>
|
||||
|
|
@ -85,15 +118,13 @@
|
|||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
|
||||
<!-- TICKET-LIKE -->
|
||||
<!-- MAIL.TEST.TICKET(.*) (TICKET-LIKE) -->
|
||||
<record id="mail_test_ticket_rule_portal" model="ir.rule">
|
||||
<field name="name">Portal Mail Test Ticket</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_ticket"/>
|
||||
<field name="domain_force">[('message_partner_ids', 'in', [user.partner_id.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- MULTI COMPANY TICKET LIKE -->
|
||||
<record id="mail_test_ticket_mc_rule" model="ir.rule">
|
||||
<field name="name">Mail Test Ticket Multi Company</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_ticket_mc"/>
|
||||
|
|
@ -106,16 +137,26 @@
|
|||
<field name="domain_force">[('message_partner_ids', 'in', [user.partner_id.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
<record id="mail_test_ticket_partner_rule" model="ir.rule">
|
||||
<field name="name">Mail Test Ticket Multi Company Partner</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_ticket_partner"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
<record id="mail_test_ticket_partner_rule_portal" model="ir.rule">
|
||||
<field name="name">Portal Mail Test Ticket Multi Company Partner</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_ticket_partner"/>
|
||||
<field name="domain_force">[('message_partner_ids', 'in', [user.partner_id.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- PROJECT-LIKE -->
|
||||
<!-- MAIL.TEST.CONTAINER(.*) (PROJECT-LIKE) -->
|
||||
<record id="mail_test_container_rule_portal" model="ir.rule">
|
||||
<field name="name">Portal Mail Test Container</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_container"/>
|
||||
<field name="domain_force">[('message_partner_ids', 'in', [user.partner_id.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- MULTI COMPANY PROJECT LIKE -->
|
||||
<record id="mail_test_container_mc_rule" model="ir.rule">
|
||||
<field name="name">Mail Test Container Multi Company</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_container_mc"/>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,35 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
|
||||
import { openView, start, startServer } from "@mail/../tests/mail_test_helpers";
|
||||
|
||||
describe.current.tags("mobile");
|
||||
defineTestMailModels();
|
||||
|
||||
test("horizontal scroll applies only to the content, not to the whole controller", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["mail.activity.type"].create([
|
||||
{ name: "Email" },
|
||||
{ name: "Call" },
|
||||
{ name: "Upload document" },
|
||||
]);
|
||||
await start();
|
||||
await openView({
|
||||
res_model: "mail.test.activity",
|
||||
views: [[false, "activity"]],
|
||||
});
|
||||
const o_view_controller = document.querySelector(".o_view_controller");
|
||||
const o_content = o_view_controller.querySelector(".o_content");
|
||||
const o_cp_item = document.querySelector(".o_breadcrumb .active");
|
||||
const initialXCpItem = o_cp_item.getBoundingClientRect().x;
|
||||
const o_header_cell = o_content.querySelector(".o_activity_type_cell");
|
||||
const initialXHeaderCell = o_header_cell.getBoundingClientRect().x;
|
||||
expect(o_view_controller).toHaveClass("o_action_delegate_scroll");
|
||||
expect(o_view_controller).toHaveStyle({ overflow: "hidden" });
|
||||
expect(o_content).toHaveStyle({ overflow: "auto" });
|
||||
expect(o_content.scrollLeft).toBe(0);
|
||||
|
||||
o_content.scrollLeft = 100;
|
||||
expect(o_content.scrollLeft).toBe(100);
|
||||
expect(o_header_cell.getBoundingClientRect().x).toBeLessThan(initialXHeaderCell);
|
||||
expect(o_cp_item).toHaveRect({ x: initialXCpItem });
|
||||
});
|
||||
|
|
@ -1,676 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import ActivityRenderer from '@mail/js/views/activity/activity_renderer';
|
||||
import { start, startServer } from '@mail/../tests/helpers/test_utils';
|
||||
|
||||
import testUtils from 'web.test_utils';
|
||||
import { click, insertText } from "@web/../tests/utils";
|
||||
import { legacyExtraNextTick, patchWithCleanup} from "@web/../tests/helpers/utils";
|
||||
import { doAction } from "@web/../tests/webclient/helpers";
|
||||
import { session } from '@web/session';
|
||||
|
||||
let serverData;
|
||||
let pyEnv;
|
||||
|
||||
QUnit.module('test_mail', {}, function () {
|
||||
QUnit.module('activity view', {
|
||||
async beforeEach() {
|
||||
pyEnv = await startServer();
|
||||
const mailTemplateIds = pyEnv['mail.template'].create([{ name: "Template1" }, { name: "Template2" }]);
|
||||
// reset incompatible setup
|
||||
pyEnv['mail.activity.type'].unlink(pyEnv['mail.activity.type'].search([]));
|
||||
const mailActivityTypeIds = pyEnv['mail.activity.type'].create([
|
||||
{ name: "Email", mail_template_ids: mailTemplateIds },
|
||||
{ name: "Call" },
|
||||
{ name: "Call for Demo" },
|
||||
{ name: "To Do" },
|
||||
]);
|
||||
const resUsersId1 = pyEnv['res.users'].create({ display_name: 'first user' });
|
||||
const mailActivityIds = pyEnv['mail.activity'].create([
|
||||
{
|
||||
display_name: "An activity",
|
||||
date_deadline: moment().add(3, "days").format("YYYY-MM-DD"), // now
|
||||
can_write: true,
|
||||
state: "planned",
|
||||
activity_type_id: mailActivityTypeIds[0],
|
||||
mail_template_ids: mailTemplateIds,
|
||||
user_id: resUsersId1,
|
||||
},
|
||||
{
|
||||
display_name: "An activity",
|
||||
date_deadline: moment().format("YYYY-MM-DD"), // now
|
||||
can_write: true,
|
||||
state: "today",
|
||||
activity_type_id: mailActivityTypeIds[0],
|
||||
mail_template_ids: mailTemplateIds,
|
||||
user_id: resUsersId1,
|
||||
},
|
||||
{
|
||||
res_model: 'mail.test.activity',
|
||||
display_name: "An activity",
|
||||
date_deadline: moment().subtract(2, "days").format("YYYY-MM-DD"), // now
|
||||
can_write: true,
|
||||
state: "overdue",
|
||||
activity_type_id: mailActivityTypeIds[1],
|
||||
user_id: resUsersId1,
|
||||
},
|
||||
]);
|
||||
pyEnv['mail.test.activity'].create([
|
||||
{ name: 'Meeting Room Furnitures', activity_ids: [mailActivityIds[0]] },
|
||||
{ name: 'Office planning', activity_ids: [mailActivityIds[1], mailActivityIds[2]] },
|
||||
]);
|
||||
serverData = {
|
||||
views: {
|
||||
'mail.test.activity,false,activity':
|
||||
'<activity string="MailTestActivity">' +
|
||||
'<templates>' +
|
||||
'<div t-name="activity-box">' +
|
||||
'<field name="name"/>' +
|
||||
'</div>' +
|
||||
'</templates>' +
|
||||
'</activity>',
|
||||
'mail.test.activity,false,form':
|
||||
'<form string="MailTestActivity">' +
|
||||
'<field name="name"/>' +
|
||||
'</form>',
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
var activityDateFormat = function (date) {
|
||||
return date.toLocaleDateString(moment().locale(), { day: 'numeric', month: 'short' });
|
||||
};
|
||||
|
||||
QUnit.test('activity view: simple activity rendering', async function (assert) {
|
||||
assert.expect(15);
|
||||
const mailTestActivityIds = pyEnv['mail.test.activity'].search([]);
|
||||
const mailActivityTypeIds = pyEnv['mail.activity.type'].search([]);
|
||||
|
||||
const { click , env, openView } = await start({
|
||||
serverData,
|
||||
});
|
||||
await openView({
|
||||
res_model: "mail.test.activity",
|
||||
views: [[false, "activity"], [false, "form"]],
|
||||
});
|
||||
patchWithCleanup(env.services.action, {
|
||||
doAction(action, options) {
|
||||
assert.deepEqual(action, {
|
||||
context: {
|
||||
default_res_id: mailTestActivityIds[1],
|
||||
default_res_model: "mail.test.activity",
|
||||
default_activity_type_id: mailActivityTypeIds[2],
|
||||
},
|
||||
res_id: false,
|
||||
res_model: "mail.activity",
|
||||
target: "new",
|
||||
type: "ir.actions.act_window",
|
||||
view_mode: "form",
|
||||
view_type: "form",
|
||||
views: [[false, "form"]]
|
||||
},
|
||||
"should do a do_action with correct parameters");
|
||||
options.onClose();
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
const $activity = $(document.querySelector('.o_activity_view'));
|
||||
assert.containsOnce($activity, 'table',
|
||||
'should have a table');
|
||||
var $th1 = $activity.find('table thead tr:first th:nth-child(2)');
|
||||
assert.containsOnce($th1, 'span:first:contains(Email)', 'should contain "Email" in header of first column');
|
||||
assert.containsOnce($th1, '.o_legacy_kanban_counter', 'should contain a progressbar in header of first column');
|
||||
assert.hasAttrValue($th1.find('.o_kanban_counter_progress .progress-bar:first'), 'data-bs-original-title', '1 Planned',
|
||||
'the counter progressbars should be correctly displayed');
|
||||
assert.hasAttrValue($th1.find('.o_kanban_counter_progress .progress-bar:nth-child(2)'), 'data-bs-original-title', '1 Today',
|
||||
'the counter progressbars should be correctly displayed');
|
||||
var $th2 = $activity.find('table thead tr:first th:nth-child(3)');
|
||||
assert.containsOnce($th2, 'span:first:contains(Call)', 'should contain "Call" in header of second column');
|
||||
assert.hasAttrValue($th2.find('.o_kanban_counter_progress .progress-bar:nth-child(3)'), 'data-bs-original-title', '1 Overdue',
|
||||
'the counter progressbars should be correctly displayed');
|
||||
assert.containsNone($activity, 'table thead tr:first th:nth-child(4) .o_kanban_counter',
|
||||
'should not contain a progressbar in header of 3rd column');
|
||||
assert.ok($activity.find('table tbody tr:first td:first:contains(Office planning)').length,
|
||||
'should contain "Office planning" in first colum of first row');
|
||||
assert.ok($activity.find('table tbody tr:nth-child(2) td:first:contains(Meeting Room Furnitures)').length,
|
||||
'should contain "Meeting Room Furnitures" in first colum of second row');
|
||||
|
||||
var today = activityDateFormat(new Date());
|
||||
|
||||
assert.ok($activity.find('table tbody tr:first td:nth-child(2).today .o_closest_deadline:contains(' + today + ')').length,
|
||||
'should contain an activity for today in second cell of first line ' + today);
|
||||
var td = 'table tbody tr:nth-child(1) td.o_activity_empty_cell';
|
||||
assert.containsN($activity, td, 2, 'should contain an empty cell as no activity scheduled yet.');
|
||||
|
||||
// schedule an activity (this triggers a do_action)
|
||||
await testUtils.fields.editAndTrigger($activity.find(td + ':first'), null, ['mouseenter', 'click']);
|
||||
assert.containsOnce($activity, 'table tfoot tr .o_record_selector',
|
||||
'should contain search more selector to choose the record to schedule an activity for it');
|
||||
|
||||
// Ensure that the form view is opened in edit mode
|
||||
await click(document.querySelector(".o_activity_record"));
|
||||
const $form = $(document.querySelector('.o_form_view'));
|
||||
assert.containsOnce($form, '.o_form_editable',
|
||||
'Form view should be opened in edit mode');
|
||||
});
|
||||
|
||||
QUnit.test('activity view: no content rendering', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const { openView, pyEnv } = await start({
|
||||
serverData,
|
||||
});
|
||||
// reset incompatible setup
|
||||
pyEnv['mail.activity.type'].unlink(pyEnv['mail.activity.type'].search([]));
|
||||
await openView({
|
||||
res_model: "mail.test.activity",
|
||||
views: [[false, "activity"]],
|
||||
});
|
||||
const $activity = $(document);
|
||||
|
||||
assert.containsOnce($activity, '.o_view_nocontent',
|
||||
"should display the no content helper");
|
||||
assert.strictEqual($activity.find('.o_view_nocontent .o_view_nocontent_empty_folder').text().trim(),
|
||||
"No data to display",
|
||||
"should display the no content helper text");
|
||||
});
|
||||
|
||||
QUnit.test('activity view: batch send mail on activity', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const mailTestActivityIds = pyEnv['mail.test.activity'].search([]);
|
||||
const mailTemplateIds = pyEnv['mail.template'].search([]);
|
||||
const { openView } = await start({
|
||||
serverData,
|
||||
mockRPC: function(route, args) {
|
||||
if (args.method === 'activity_send_mail') {
|
||||
assert.step(JSON.stringify(args.args));
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
await openView({
|
||||
res_model: "mail.test.activity",
|
||||
views: [[false, "activity"]],
|
||||
});
|
||||
const $activity = $(document);
|
||||
assert.notOk($activity.find('table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show').length,
|
||||
'dropdown shouldn\'t be displayed');
|
||||
|
||||
testUtils.dom.click($activity.find('table thead tr:first th:nth-child(2) span:nth-child(2) i.fa-ellipsis-v'));
|
||||
assert.ok($activity.find('table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show').length,
|
||||
'dropdown should have appeared');
|
||||
|
||||
testUtils.dom.click($activity.find('table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show .o_send_mail_template:contains(Template2)'));
|
||||
assert.notOk($activity.find('table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show').length,
|
||||
'dropdown shouldn\'t be displayed');
|
||||
|
||||
testUtils.dom.click($activity.find('table thead tr:first th:nth-child(2) span:nth-child(2) i.fa-ellipsis-v'));
|
||||
testUtils.dom.click($activity.find('table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show .o_send_mail_template:contains(Template1)'));
|
||||
assert.verifySteps([
|
||||
`[[${mailTestActivityIds[0]},${mailTestActivityIds[1]}],${mailTemplateIds[1]}]`, // send mail template 1 on mail.test.activity 1 and 2
|
||||
`[[${mailTestActivityIds[0]},${mailTestActivityIds[1]}],${mailTemplateIds[0]}]`, // send mail template 2 on mail.test.activity 1 and 2
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('activity view: activity widget', async function (assert) {
|
||||
assert.expect(16);
|
||||
|
||||
const mailActivityTypeIds = pyEnv['mail.activity.type'].search([]);
|
||||
const [mailTestActivityId2] = pyEnv['mail.test.activity'].search([['name', '=', 'Office planning']]);
|
||||
const [mailTemplateId1] = pyEnv['mail.template'].search([['name', '=', 'Template1']]);
|
||||
const { env, openView } = await start({
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'activity_send_mail') {
|
||||
assert.deepEqual([[mailTestActivityId2], mailTemplateId1], args.args, "Should send template related to mailTestActivity2");
|
||||
assert.step('activity_send_mail');
|
||||
// random value returned in order for the mock server to know that this route is implemented.
|
||||
return true;
|
||||
}
|
||||
if (args.method === 'action_feedback_schedule_next') {
|
||||
assert.deepEqual(
|
||||
[pyEnv['mail.activity'].search([['state', '=', 'overdue']])],
|
||||
args.args,
|
||||
"Should execute action_feedback_schedule_next only on the overude activity"
|
||||
);
|
||||
assert.equal(args.kwargs.feedback, "feedback2");
|
||||
assert.step('action_feedback_schedule_next');
|
||||
return Promise.resolve({ serverGeneratedAction: true });
|
||||
}
|
||||
},
|
||||
serverData,
|
||||
});
|
||||
await openView({
|
||||
res_model: 'mail.test.activity',
|
||||
views: [[false, 'activity']],
|
||||
});
|
||||
patchWithCleanup(env.services.action, {
|
||||
doAction(action) {
|
||||
if (action.serverGeneratedAction) {
|
||||
assert.step('serverGeneratedAction');
|
||||
} else if (action.res_model === 'mail.compose.message') {
|
||||
assert.deepEqual({
|
||||
default_model: 'mail.test.activity',
|
||||
default_res_id: mailTestActivityId2,
|
||||
default_template_id: mailTemplateId1,
|
||||
default_use_template: true,
|
||||
force_email: true
|
||||
}, action.context);
|
||||
assert.step("do_action_compose");
|
||||
} else if (action.res_model === 'mail.activity') {
|
||||
assert.deepEqual({
|
||||
"default_activity_type_id": mailActivityTypeIds[1],
|
||||
"default_res_id": mailTestActivityId2,
|
||||
"default_res_model": 'mail.test.activity',
|
||||
}, action.context);
|
||||
assert.step("do_action_activity");
|
||||
} else {
|
||||
assert.step("Unexpected action");
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.dom.click(document.querySelector('.today .o_closest_deadline'));
|
||||
assert.hasClass(document.querySelector('.today .dropdown-menu.o_activity'), 'show', "dropdown should be displayed");
|
||||
assert.ok(document.querySelector('.o_activity_color_today').textContent.includes('Today'), "Title should be today");
|
||||
assert.ok([...document.querySelectorAll('.today .o_activity_title_entry')].filter(el => el.textContent.includes('Template1')).length,
|
||||
"Template1 should be available");
|
||||
assert.ok([...document.querySelectorAll('.today .o_activity_title_entry')].filter(el => el.textContent.includes('Template2')).length,
|
||||
"Template2 should be available");
|
||||
|
||||
await testUtils.dom.click(document.querySelector('.o_activity_title_entry[data-activity-id="2"] .o_activity_template_preview'));
|
||||
await testUtils.dom.click(document.querySelector('.o_activity_title_entry[data-activity-id="2"] .o_activity_template_send'));
|
||||
await testUtils.dom.click(document.querySelector('.overdue .o_closest_deadline'));
|
||||
assert.notOk(document.querySelector('.overdue .o_activity_template_preview'),
|
||||
"No template should be available");
|
||||
|
||||
await testUtils.dom.click(document.querySelector('.overdue .o_schedule_activity'));
|
||||
await testUtils.dom.click(document.querySelector('.overdue .o_closest_deadline'));
|
||||
await testUtils.dom.click(document.querySelector('.overdue .o_mark_as_done'));
|
||||
document.querySelector('.overdue #activity_feedback').value = "feedback2";
|
||||
|
||||
await testUtils.dom.click(document.querySelector('.overdue .o_activity_popover_done_next'));
|
||||
assert.verifySteps([
|
||||
"do_action_compose",
|
||||
"activity_send_mail",
|
||||
"do_action_activity",
|
||||
"action_feedback_schedule_next",
|
||||
"serverGeneratedAction"
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
QUnit.test("activity view: no group_by_menu and no comparison_menu", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
serverData.actions = {
|
||||
1: {
|
||||
id: 1,
|
||||
name: "MailTestActivity Action",
|
||||
res_model: "mail.test.activity",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "activity"]],
|
||||
},
|
||||
};
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (args.method === "get_activity_data") {
|
||||
assert.strictEqual(
|
||||
args.kwargs.context.lang,
|
||||
"zz_ZZ",
|
||||
"The context should have been passed"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
patchWithCleanup(session.user_context, { lang: "zz_ZZ" });
|
||||
|
||||
const { webClient } = await start({ serverData, mockRPC });
|
||||
|
||||
await doAction(webClient, 1);
|
||||
|
||||
assert.containsN(
|
||||
document.body,
|
||||
".o_search_options .dropdown button:visible",
|
||||
2,
|
||||
"only two elements should be available in view search"
|
||||
);
|
||||
assert.isVisible(
|
||||
document.querySelector(".o_search_options .dropdown.o_filter_menu > button"),
|
||||
"filter should be available in view search"
|
||||
);
|
||||
assert.isVisible(
|
||||
document.querySelector(".o_search_options .dropdown.o_favorite_menu > button"),
|
||||
"favorites should be available in view search"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('activity view: search more to schedule an activity for a record of a respecting model', async function (assert) {
|
||||
assert.expect(5);
|
||||
const mailTestActivityId1 = pyEnv['mail.test.activity'].create({ name: 'MailTestActivity 3' });
|
||||
Object.assign(serverData.views, {
|
||||
'mail.test.activity,false,list': '<tree string="MailTestActivity"><field name="name"/></tree>',
|
||||
});
|
||||
const { env, openView } = await start({
|
||||
mockRPC(route, args) {
|
||||
if (args.method === 'name_search') {
|
||||
args.kwargs.name = "MailTestActivity";
|
||||
}
|
||||
},
|
||||
serverData,
|
||||
});
|
||||
await openView({
|
||||
res_model: 'mail.test.activity',
|
||||
views: [[false, 'activity']],
|
||||
});
|
||||
patchWithCleanup(env.services.action, {
|
||||
doAction(action, options) {
|
||||
assert.step('doAction');
|
||||
var expectedAction = {
|
||||
context: {
|
||||
default_res_id: mailTestActivityId1,
|
||||
default_res_model: "mail.test.activity",
|
||||
},
|
||||
name: "Schedule Activity",
|
||||
res_id: false,
|
||||
res_model: "mail.activity",
|
||||
target: "new",
|
||||
type: "ir.actions.act_window",
|
||||
view_mode: "form",
|
||||
views: [[false, "form"]],
|
||||
};
|
||||
assert.deepEqual(action, expectedAction,
|
||||
"should execute an action with correct params");
|
||||
options.onClose();
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
const activity = $(document);
|
||||
assert.containsOnce(activity, 'table tfoot tr .o_record_selector',
|
||||
'should contain search more selector to choose the record to schedule an activity for it');
|
||||
await testUtils.dom.click(activity.find('table tfoot tr .o_record_selector'));
|
||||
// search create dialog
|
||||
var $modal = $('.modal-lg');
|
||||
assert.strictEqual($modal.find('.o_data_row').length, 3, "all mail.test.activity should be available to select");
|
||||
// select a record to schedule an activity for it (this triggers a do_action)
|
||||
await testUtils.dom.click($modal.find('.o_data_row:last .o_data_cell'));
|
||||
assert.verifySteps(['doAction']);
|
||||
});
|
||||
|
||||
QUnit.test("Activity view: discard an activity creation dialog", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
serverData.actions = {
|
||||
1: {
|
||||
id: 1,
|
||||
name: "MailTestActivity Action",
|
||||
res_model: "mail.test.activity",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "activity"]],
|
||||
},
|
||||
};
|
||||
|
||||
Object.assign(serverData.views, {
|
||||
'mail.activity,false,form':
|
||||
`<form>
|
||||
<field name="display_name"/>
|
||||
<footer>
|
||||
<button string="Discard" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const { webClient } = await start({ serverData, mockRPC });
|
||||
await doAction(webClient, 1);
|
||||
|
||||
await testUtils.dom.click(
|
||||
document.querySelector(".o_activity_view .o_data_row .o_activity_empty_cell")
|
||||
);
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce($, ".modal.o_technical_modal", "Activity Modal should be opened");
|
||||
|
||||
await testUtils.dom.click($('.modal.o_technical_modal button[special="cancel"]'));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsNone($, ".modal.o_technical_modal", "Activity Modal should be closed");
|
||||
});
|
||||
|
||||
QUnit.test('Activity view: many2one_avatar_user widget in activity view', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const [mailTestActivityId1] = pyEnv['mail.test.activity'].search([['name', '=', 'Meeting Room Furnitures']]);
|
||||
const resUsersId1 = pyEnv['res.users'].create({
|
||||
display_name: "first user",
|
||||
avatar_128: "Atmaram Bhide",
|
||||
});
|
||||
pyEnv['mail.test.activity'].write([mailTestActivityId1], { activity_user_id: resUsersId1 });
|
||||
Object.assign(serverData.views, {
|
||||
'mail.test.activity,false,activity':
|
||||
`<activity string="MailTestActivity">
|
||||
<templates>
|
||||
<div t-name="activity-box">
|
||||
<field name="activity_user_id" widget="many2one_avatar_user"/>
|
||||
<field name="name"/>
|
||||
</div>
|
||||
</templates>
|
||||
</activity>`,
|
||||
});
|
||||
serverData.actions = {
|
||||
1: {
|
||||
id: 1,
|
||||
name: 'MailTestActivity Action',
|
||||
res_model: 'mail.test.activity',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'activity']],
|
||||
}
|
||||
};
|
||||
|
||||
const { webClient } = await start({ serverData });
|
||||
await doAction(webClient, 1);
|
||||
|
||||
await legacyExtraNextTick();
|
||||
assert.containsN(document.body, '.o_m2o_avatar', 2);
|
||||
assert.containsOnce(document.body, `tr[data-res-id=${mailTestActivityId1}] .o_m2o_avatar > img[data-src="/web/image/res.users/${resUsersId1}/avatar_128"]`,
|
||||
"should have m2o avatar image");
|
||||
assert.containsNone(document.body, '.o_m2o_avatar > span',
|
||||
"should not have text on many2one_avatar_user if onlyImage node option is passed");
|
||||
});
|
||||
|
||||
QUnit.test("Activity view: on_destroy_callback doesn't crash", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
patchWithCleanup(ActivityRenderer.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
owl.onMounted(() => {
|
||||
assert.step('mounted');
|
||||
});
|
||||
owl.onWillUnmount(() => {
|
||||
assert.step('willUnmount');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const { openView } = await start({
|
||||
serverData,
|
||||
});
|
||||
await openView({
|
||||
res_model: 'mail.test.activity',
|
||||
views: [[false, 'activity']],
|
||||
});
|
||||
// force the unmounting of the activity view by opening another one
|
||||
await openView({
|
||||
res_model: 'mail.test.activity',
|
||||
views: [[false, 'form']],
|
||||
});
|
||||
|
||||
assert.verifySteps([
|
||||
'mounted',
|
||||
'willUnmount'
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("Schedule activity dialog uses the same search view as activity view", async function (assert) {
|
||||
assert.expect(8);
|
||||
pyEnv['mail.test.activity'].unlink(pyEnv['mail.test.activity'].search([]));
|
||||
Object.assign(serverData.views, {
|
||||
"mail.test.activity,false,list": `<list><field name="name"/></list>`,
|
||||
"mail.test.activity,false,search": `<search/>`,
|
||||
'mail.test.activity,1,search': `<search/>`,
|
||||
});
|
||||
|
||||
function mockRPC(route, args) {
|
||||
if (args.method === "get_views") {
|
||||
assert.step(JSON.stringify(args.kwargs.views));
|
||||
}
|
||||
}
|
||||
|
||||
const { webClient , click } = await start({ serverData, mockRPC });
|
||||
|
||||
// open an activity view (with default search arch)
|
||||
await doAction(webClient, {
|
||||
name: 'Dashboard',
|
||||
res_model: 'mail.test.activity',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'activity']],
|
||||
});
|
||||
|
||||
assert.verifySteps([
|
||||
'[[false,"activity"],[false,"search"]]',
|
||||
])
|
||||
|
||||
// click on "Schedule activity"
|
||||
await click(document.querySelector(".o_activity_view .o_record_selector"));
|
||||
|
||||
assert.verifySteps([
|
||||
'[[false,"list"],[false,"search"]]',
|
||||
])
|
||||
|
||||
// open an activity view (with search arch 1)
|
||||
await doAction(webClient, {
|
||||
name: 'Dashboard',
|
||||
res_model: 'mail.test.activity',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'activity']],
|
||||
search_view_id: [1,"search"],
|
||||
});
|
||||
|
||||
assert.verifySteps([
|
||||
'[[false,"activity"],[1,"search"]]',
|
||||
])
|
||||
|
||||
// click on "Schedule activity"
|
||||
await click(document.querySelector(".o_activity_view .o_record_selector"));
|
||||
|
||||
assert.verifySteps([
|
||||
'[[false,"list"],[1,"search"]]',
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('Activity view: apply progressbar filter', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
serverData.actions = {
|
||||
1: {
|
||||
id: 1,
|
||||
name: 'MailTestActivity Action',
|
||||
res_model: 'mail.test.activity',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'activity']],
|
||||
}
|
||||
};
|
||||
|
||||
const { webClient } = await start({ serverData });
|
||||
|
||||
await doAction(webClient, 1);
|
||||
|
||||
assert.containsNone(document.querySelector('.o_activity_view thead'),
|
||||
'.o_activity_filter_planned,.o_activity_filter_today,.o_activity_filter_overdue,.o_activity_filter___false',
|
||||
"should not have active filter");
|
||||
assert.containsNone(document.querySelector('.o_activity_view tbody'),
|
||||
'.o_activity_filter_planned,.o_activity_filter_today,.o_activity_filter_overdue,.o_activity_filter___false',
|
||||
"should not have active filter");
|
||||
assert.strictEqual(document.querySelector('.o_activity_view tbody .o_activity_record').textContent,
|
||||
'Office planning', "'Office planning' should be first record");
|
||||
assert.containsOnce(document.querySelector('.o_activity_view tbody'), '.planned',
|
||||
"other records should be available");
|
||||
|
||||
await testUtils.dom.click(document.querySelector('.o_kanban_counter_progress .progress-bar[data-filter="planned"]'));
|
||||
assert.containsOnce(document.querySelector('.o_activity_view thead'), '.o_activity_filter_planned',
|
||||
"planned should be active filter");
|
||||
assert.containsN(document.querySelector('.o_activity_view tbody'), '.o_activity_filter_planned', 5,
|
||||
"planned should be active filter");
|
||||
assert.strictEqual(document.querySelector('.o_activity_view tbody .o_activity_record').textContent,
|
||||
'Meeting Room Furnitures', "'Office planning' should be first record");
|
||||
const tr = document.querySelectorAll('.o_activity_view tbody tr')[1];
|
||||
assert.hasClass(tr.querySelectorAll('td')[1], 'o_activity_empty_cell',
|
||||
"other records should be hidden");
|
||||
assert.containsNone(document.querySelector('.o_activity_view tbody'), 'planned',
|
||||
"other records should be hidden");
|
||||
});
|
||||
|
||||
QUnit.test("Activity view: luxon in renderingContext", async function (assert) {
|
||||
Object.assign(serverData.views, {
|
||||
"mail.test.activity,false,activity": `
|
||||
<activity string="MailTestActivity">
|
||||
<templates>
|
||||
<div t-name="activity-box">
|
||||
<t t-if="luxon">
|
||||
<span class="luxon">luxon</span>
|
||||
</t>
|
||||
</div>
|
||||
</templates>
|
||||
</activity>`,
|
||||
});
|
||||
const { openView } = await start({
|
||||
serverData,
|
||||
});
|
||||
await openView({
|
||||
res_model: "mail.test.activity",
|
||||
views: [[false, "activity"]],
|
||||
});
|
||||
assert.containsN(document.body, ".luxon", 2);
|
||||
});
|
||||
|
||||
QUnit.test('update activity view after creating multiple activities', async function (assert) {
|
||||
assert.expect(9);
|
||||
pyEnv['mail.test.activity'].create({ name: 'MailTestActivity 3' });
|
||||
Object.assign(serverData.views, {
|
||||
'mail.test.activity,false,list': '<tree string="MailTestActivity"><field name="name"/><field name="activity_ids" widget="list_activity"/></tree>',
|
||||
'mail.activity,false,form': '<form><field name="activity_type_id"/></form>'
|
||||
});
|
||||
|
||||
const { openView } = await start({
|
||||
mockRPC(route, args) {
|
||||
if (args.method === 'name_search') {
|
||||
args.kwargs.name = "MailTestActivity";
|
||||
}
|
||||
},
|
||||
serverData,
|
||||
});
|
||||
await openView({
|
||||
res_model: 'mail.test.activity',
|
||||
views: [[false, 'activity']],
|
||||
});
|
||||
|
||||
await click("table tfoot tr .o_record_selector");
|
||||
await click(".o_list_renderer table tbody tr:nth-child(2) td:nth-child(2) .o_ActivityButtonView")
|
||||
await click(".o-main-components-container .o_PopoverManager .o_ActivityListView .o_ActivityListView_addActivityButton");
|
||||
await insertText('.o_field_many2one_selection .o_input_dropdown .dropdown input[id=activity_type_id]', "test1");
|
||||
await click(".o_field_many2one_selection .o_input_dropdown .dropdown input[id=activity_type_id]");
|
||||
await click('.o-autocomplete--dropdown-menu li:nth-child(1) .dropdown-item');
|
||||
await click(".modal-footer .o_cp_buttons .o_form_buttons_edit .btn-primary");
|
||||
await click(".modal-footer .o_form_button_cancel");
|
||||
await click("table tbody tr:nth-child(1) td:nth-child(6) .o_mail_activity .o_activity_btn .o_closest_deadline");
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
|
||||
import { beforeEach, describe, test, expect } from "@odoo/hoot";
|
||||
import { queryOne, waitUntil } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
click,
|
||||
contains,
|
||||
openFormView,
|
||||
registerArchs,
|
||||
start,
|
||||
startServer,
|
||||
patchUiSize,
|
||||
SIZES,
|
||||
dragenterFiles,
|
||||
dropFiles,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineTestMailModels();
|
||||
|
||||
let popoutIframe, popoutWindow;
|
||||
|
||||
beforeEach(() => {
|
||||
popoutIframe = document.createElement("iframe");
|
||||
popoutWindow = {
|
||||
closed: false,
|
||||
get document() {
|
||||
const doc = popoutIframe.contentDocument;
|
||||
if (!doc) {
|
||||
return undefined;
|
||||
}
|
||||
const originalWrite = doc.write;
|
||||
doc.write = (content) => {
|
||||
// This avoids duplicating the test script in the popoutWindow
|
||||
const sanitizedContent = content.replace(
|
||||
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
|
||||
""
|
||||
);
|
||||
originalWrite.call(doc, sanitizedContent);
|
||||
};
|
||||
return doc;
|
||||
},
|
||||
close: () => {
|
||||
popoutWindow.closed = true;
|
||||
popoutIframe.remove(popoutAttachmentViewBody());
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
open: () => {
|
||||
popoutWindow.closed = false;
|
||||
queryOne(".o_popout_holder").append(popoutIframe);
|
||||
return popoutWindow;
|
||||
},
|
||||
});
|
||||
|
||||
function popoutAttachmentViewBody() {
|
||||
return popoutWindow.document.querySelector(".o-mail-PopoutAttachmentView");
|
||||
}
|
||||
async function popoutIsEmpty() {
|
||||
await animationFrame();
|
||||
expect(popoutAttachmentViewBody()).toBe(null);
|
||||
}
|
||||
async function popoutContains(selector) {
|
||||
await animationFrame();
|
||||
await waitUntil(() => popoutAttachmentViewBody());
|
||||
const target = popoutAttachmentViewBody().querySelector(selector);
|
||||
expect(target).toBeDisplayed();
|
||||
return target;
|
||||
}
|
||||
async function popoutClick(selector) {
|
||||
const target = await popoutContains(selector);
|
||||
click(target);
|
||||
}
|
||||
|
||||
test("Attachment view popout controls test", async () => {
|
||||
/*
|
||||
* This test makes sure that the attachment view controls are working in the following cases:
|
||||
* - Inside the popout window
|
||||
* - After closing the popout window
|
||||
*/
|
||||
const pyEnv = await startServer();
|
||||
const recordId = pyEnv["mail.test.simple.main.attachment"].create({
|
||||
display_name: "first partner",
|
||||
message_attachment_count: 2,
|
||||
});
|
||||
pyEnv["ir.attachment"].create([
|
||||
{
|
||||
mimetype: "image/jpeg",
|
||||
res_id: recordId,
|
||||
res_model: "mail.test.simple.main.attachment",
|
||||
},
|
||||
{
|
||||
mimetype: "application/pdf",
|
||||
res_id: recordId,
|
||||
res_model: "mail.test.simple.main.attachment",
|
||||
},
|
||||
]);
|
||||
registerArchs({
|
||||
"mail.test.simple.main.attachment,false,form": `
|
||||
<form string="Test document">
|
||||
<div class="o_popout_holder"/>
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<div class="o_attachment_preview"/>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
patchUiSize({ size: SIZES.XXL });
|
||||
await start();
|
||||
await openFormView("mail.test.simple.main.attachment", recordId);
|
||||
await click(".o_attachment_preview .o_attachment_control");
|
||||
await animationFrame();
|
||||
expect(".o_attachment_preview").not.toBeVisible();
|
||||
await popoutClick(".o_move_next");
|
||||
await popoutContains("img");
|
||||
await popoutClick(".o_move_previous");
|
||||
await popoutContains("iframe");
|
||||
popoutWindow.close();
|
||||
await contains(".o_attachment_preview:not(.d-none)");
|
||||
expect(".o_attachment_preview").toBeVisible();
|
||||
await click(".o_attachment_preview .o_move_next");
|
||||
await contains(".o_attachment_preview img");
|
||||
await click(".o_attachment_preview .o_move_previous");
|
||||
await contains(".o_attachment_preview iframe");
|
||||
await click(".o_attachment_preview .o_attachment_control");
|
||||
await animationFrame();
|
||||
expect(".o_attachment_preview").not.toBeVisible();
|
||||
});
|
||||
|
||||
test("Chatter main attachment: can change from non-viewable to viewable", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const recordId = pyEnv['mail.test.simple.main.attachment'].create({});
|
||||
const irAttachmentId = pyEnv['ir.attachment'].create({
|
||||
mimetype: 'text/plain',
|
||||
name: "Blah.txt",
|
||||
res_id: recordId,
|
||||
res_model: 'mail.test.simple.main.attachment',
|
||||
});
|
||||
pyEnv['mail.message'].create({
|
||||
attachment_ids: [irAttachmentId],
|
||||
model: 'mail.test.simple.main.attachment',
|
||||
res_id: recordId,
|
||||
});
|
||||
pyEnv['mail.test.simple.main.attachment'].write([recordId], {message_main_attachment_id : irAttachmentId});
|
||||
|
||||
registerArchs({
|
||||
"mail.test.simple.main.attachment,false,form": `
|
||||
<form string="Test document">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<div class="o_attachment_preview"/>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
});
|
||||
patchUiSize({ size: SIZES.XXL });
|
||||
await start();
|
||||
await openFormView("mail.test.simple.main.attachment", recordId);
|
||||
|
||||
// Add a PDF file
|
||||
const pdfFile = new File([new Uint8Array(1)], "text.pdf", { type: "application/pdf" });
|
||||
await dragenterFiles(".o-mail-Chatter", [pdfFile]);
|
||||
await dropFiles(".o-Dropzone", [pdfFile]);
|
||||
await contains(".o_attachment_preview");
|
||||
await contains(".o-mail-Attachment > iframe", { count: 0 }); // The viewer tries to display the text file not the PDF
|
||||
|
||||
// Switch to the PDF file in the viewer
|
||||
await click(".o_move_next");
|
||||
await contains(".o-mail-Attachment > iframe"); // There should be iframe for PDF viewer
|
||||
});
|
||||
|
||||
test.skip("Attachment view / chatter popout across multiple records test", async () => {
|
||||
// skip because test has race conditions: https://runbot.odoo.com/odoo/runbot.build.error/109795
|
||||
const pyEnv = await startServer();
|
||||
const recordIds = pyEnv["mail.test.simple.main.attachment"].create([
|
||||
{
|
||||
display_name: "first partner",
|
||||
message_attachment_count: 1,
|
||||
},
|
||||
{
|
||||
display_name: "second partner",
|
||||
message_attachment_count: 0,
|
||||
},
|
||||
{
|
||||
display_name: "third partner",
|
||||
message_attachment_count: 1,
|
||||
},
|
||||
]);
|
||||
pyEnv["ir.attachment"].create([
|
||||
{
|
||||
mimetype: "image/jpeg",
|
||||
res_id: recordIds[0],
|
||||
res_model: "mail.test.simple.main.attachment",
|
||||
},
|
||||
{
|
||||
mimetype: "application/pdf",
|
||||
res_id: recordIds[2],
|
||||
res_model: "mail.test.simple.main.attachment",
|
||||
},
|
||||
]);
|
||||
registerArchs({
|
||||
"mail.test.simple.main.attachment,false,form": `
|
||||
<form string="Test document">
|
||||
<div class="o_popout_holder"/>
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<div class="o_attachment_preview"/>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
async function navigateRecords() {
|
||||
/**
|
||||
* It should be called on the first record of recordIds
|
||||
* The popout window should be open
|
||||
* It navigates recordIds as 0 -> 1 -> 2 -> 0 -> 2
|
||||
*/
|
||||
await animationFrame();
|
||||
expect(".o_attachment_preview").not.toBeVisible();
|
||||
await popoutContains("img");
|
||||
await click(".o_pager_next");
|
||||
await popoutIsEmpty();
|
||||
await click(".o_pager_next");
|
||||
await popoutContains("iframe");
|
||||
await click(".o_pager_next");
|
||||
await popoutContains("img");
|
||||
await click(".o_pager_previous");
|
||||
await popoutContains("iframe");
|
||||
popoutWindow.close();
|
||||
await contains(".o_attachment_preview:not(.d-none)");
|
||||
}
|
||||
|
||||
patchUiSize({ size: SIZES.XXL });
|
||||
await start();
|
||||
await openFormView("mail.test.simple.main.attachment", recordIds[0], {
|
||||
resIds: recordIds,
|
||||
});
|
||||
await click(".o_attachment_preview .o_attachment_control");
|
||||
await navigateRecords();
|
||||
await openFormView("mail.test.simple.main.attachment", recordIds[0], {
|
||||
resIds: recordIds,
|
||||
});
|
||||
await click("button i[title='Pop out Attachments']");
|
||||
await navigateRecords();
|
||||
});
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
inputFiles,
|
||||
insertText,
|
||||
listenStoreFetch,
|
||||
openFormView,
|
||||
patchUiSize,
|
||||
registerArchs,
|
||||
SIZES,
|
||||
start,
|
||||
startServer,
|
||||
triggerHotkey,
|
||||
waitStoreFetch,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
|
||||
import { MockServer, onRpc } from "@web/../tests/web_test_helpers";
|
||||
import { mail_data } from "@mail/../tests/mock_server/mail_mock_server";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineTestMailModels();
|
||||
|
||||
test("Send message button activation (access rights dependent)", async () => {
|
||||
const pyEnv = await startServer();
|
||||
registerArchs({
|
||||
"mail.test.multi.company,false,form": `
|
||||
<form string="Simple">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
`,
|
||||
"mail.test.multi.company.read,false,form": `
|
||||
<form string="Simple">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
let userAccess = {};
|
||||
listenStoreFetch("mail.thread", {
|
||||
async onRpc(request) {
|
||||
const { params } = await request.json();
|
||||
if (params.fetch_params.some((fetchParam) => fetchParam[0] === "mail.thread")) {
|
||||
const res = await mail_data.bind(MockServer.current)(request);
|
||||
res["mail.thread"][0].hasWriteAccess = userAccess.hasWriteAccess;
|
||||
res["mail.thread"][0].hasReadAccess = userAccess.hasReadAccess;
|
||||
return res;
|
||||
}
|
||||
},
|
||||
});
|
||||
await start();
|
||||
const simpleId = pyEnv["mail.test.multi.company"].create({ name: "Test MC Simple" });
|
||||
const simpleMcId = pyEnv["mail.test.multi.company.read"].create({
|
||||
name: "Test MC Readonly with Activities",
|
||||
});
|
||||
async function assertSendButton(
|
||||
enabled,
|
||||
activities,
|
||||
msg,
|
||||
model = null,
|
||||
resId = null,
|
||||
hasReadAccess = false,
|
||||
hasWriteAccess = false
|
||||
) {
|
||||
userAccess = { hasReadAccess, hasWriteAccess };
|
||||
await openFormView(model, resId);
|
||||
if (resId) {
|
||||
await waitStoreFetch("mail.thread");
|
||||
}
|
||||
if (enabled) {
|
||||
await contains(".o-mail-Chatter-topbar button:enabled", { text: "Send message" });
|
||||
await contains(".o-mail-Chatter-topbar button:enabled", { text: "Log note" });
|
||||
if (activities) {
|
||||
await contains(".o-mail-Chatter-topbar button:enabled", { text: "Activity" });
|
||||
|
||||
}
|
||||
} else {
|
||||
await contains(".o-mail-Chatter-topbar button:disabled", { text: "Send message" });
|
||||
await contains(".o-mail-Chatter-topbar button:disabled", { text: "Log note" });
|
||||
if (activities) {
|
||||
await contains(".o-mail-Chatter-topbar button:disabled", { text: "Activity" });
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
await assertSendButton(
|
||||
true,
|
||||
false,
|
||||
"Record, all rights",
|
||||
"mail.test.multi.company",
|
||||
simpleId,
|
||||
true,
|
||||
true
|
||||
);
|
||||
await assertSendButton(
|
||||
true,
|
||||
true,
|
||||
"Record, all rights",
|
||||
"mail.test.multi.company.read",
|
||||
simpleId,
|
||||
true,
|
||||
true
|
||||
);
|
||||
await assertSendButton(
|
||||
false,
|
||||
false,
|
||||
"Record, no write access",
|
||||
"mail.test.multi.company",
|
||||
simpleId,
|
||||
true
|
||||
);
|
||||
await assertSendButton(
|
||||
true,
|
||||
true,
|
||||
"Record, read access but model accept post with read only access",
|
||||
"mail.test.multi.company.read",
|
||||
simpleMcId,
|
||||
true
|
||||
);
|
||||
await assertSendButton(false, false, "Record, no rights", "mail.test.multi.company", simpleId);
|
||||
await assertSendButton(false, true, "Record, no rights", "mail.test.multi.company.read", simpleMcId);
|
||||
// Note that rights have no impact on send button for draft record (chatter.isTemporary=true)
|
||||
await assertSendButton(true, false, "Draft record", "mail.test.multi.company");
|
||||
await assertSendButton(true, true, "Draft record", "mail.test.multi.company.read");
|
||||
});
|
||||
|
||||
test("basic chatter rendering with a model without activities", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const recordId = pyEnv["mail.test.simple"].create({ name: "new record" });
|
||||
registerArchs({
|
||||
"mail.test.simple,false,form": `
|
||||
<form string="Records">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
await start();
|
||||
await openFormView("mail.test.simple", recordId);
|
||||
await contains(".o-mail-Chatter");
|
||||
await contains(".o-mail-Chatter-topbar");
|
||||
await contains("button[aria-label='Attach files']");
|
||||
await contains("button", { count: 0, text: "Activities" });
|
||||
await contains(".o-mail-Followers");
|
||||
await contains(".o-mail-Thread");
|
||||
});
|
||||
|
||||
test("opened attachment box should remain open after adding a new attachment", async (assert) => {
|
||||
const pyEnv = await startServer();
|
||||
const recordId = pyEnv["mail.test.simple.main.attachment"].create({});
|
||||
const attachmentId = pyEnv["ir.attachment"].create({
|
||||
mimetype: "image/jpeg",
|
||||
res_id: recordId,
|
||||
res_model: "mail.test.simple.main.attachment",
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
attachment_ids: [attachmentId],
|
||||
model: "mail.test.simple.main.attachment",
|
||||
res_id: recordId,
|
||||
});
|
||||
onRpc("/mail/thread/data", async (request) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1)); // need extra time for useEffect
|
||||
});
|
||||
patchUiSize({ size: SIZES.XXL });
|
||||
await start();
|
||||
await openFormView("mail.test.simple.main.attachment", recordId, {
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<div class="o_attachment_preview" />
|
||||
<chatter reload_on_post="True" reload_on_attachment="True"/>
|
||||
</form>`,
|
||||
});
|
||||
await contains(".o_attachment_preview");
|
||||
await click(".o-mail-Chatter-attachFiles");
|
||||
await contains(".o-mail-AttachmentBox");
|
||||
await click("button", { text: "Send message" });
|
||||
await inputFiles(".o-mail-Composer .o_input_file", [
|
||||
new File(["image"], "testing.jpeg", { type: "image/jpeg" }),
|
||||
]);
|
||||
await click(".o-mail-Composer-send:enabled");
|
||||
await contains(".o_move_next");
|
||||
await click("button", { text: "Send message" });
|
||||
await insertText(".o-mail-Composer-input", "test");
|
||||
triggerHotkey("control+Enter");
|
||||
await contains(".o-mail-Message-body", { text: "test" });
|
||||
await contains(".o-mail-AttachmentBox .o-mail-AttachmentImage", { count: 2 });
|
||||
});
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
start,
|
||||
startServer,
|
||||
} from '@mail/../tests/helpers/test_utils';
|
||||
|
||||
|
||||
QUnit.module('mail', {}, function () {
|
||||
QUnit.module('Chatter');
|
||||
|
||||
QUnit.test('Send message button activation (access rights dependent)', async function (assert) {
|
||||
const pyEnv = await startServer();
|
||||
const view = `<form string="Simple">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>`;
|
||||
|
||||
let userAccess = {};
|
||||
const { openView } = await start({
|
||||
serverData: {
|
||||
views: {
|
||||
'mail.test.multi.company,false,form': view,
|
||||
'mail.test.multi.company.read,false,form': view,
|
||||
}
|
||||
},
|
||||
async mockRPC(route, args, performRPC) {
|
||||
const res = await performRPC(route, args);
|
||||
if (route === '/mail/thread/data') {
|
||||
// mimic user with custom access defined in userAccess variable
|
||||
const { thread_model } = args;
|
||||
Object.assign(res, userAccess);
|
||||
res['canPostOnReadonly'] = thread_model === 'mail.test.multi.company.read';
|
||||
}
|
||||
return res;
|
||||
},
|
||||
});
|
||||
const resSimpleId1 = pyEnv['mail.test.multi.company'].create({ name: 'Test MC Simple' });
|
||||
const resSimpleMCId1 = pyEnv['mail.test.multi.company.read'].create({ name: 'Test MC Readonly' });
|
||||
async function assertSendButton(enabled, msg,
|
||||
model = null, resId = null,
|
||||
hasReadAccess = false, hasWriteAccess = false) {
|
||||
userAccess = { hasReadAccess, hasWriteAccess };
|
||||
await openView({
|
||||
res_id: resId,
|
||||
res_model: model,
|
||||
views: [[false, 'form']],
|
||||
});
|
||||
const details = `hasReadAccess: ${hasReadAccess}, hasWriteAccess: ${hasWriteAccess}, model: ${model}, resId: ${resId}`;
|
||||
if (enabled) {
|
||||
assert.containsNone(document.body, '.o_ChatterTopbar_buttonSendMessage:disabled',
|
||||
`${msg}: send message button must not be disabled (${details}`);
|
||||
} else {
|
||||
assert.containsOnce(document.body, '.o_ChatterTopbar_buttonSendMessage:disabled',
|
||||
`${msg}: send message button must be disabled (${details})`);
|
||||
}
|
||||
}
|
||||
const enabled = true, disabled = false;
|
||||
|
||||
await assertSendButton(enabled, 'Record, all rights', 'mail.test.multi.company', resSimpleId1, true, true);
|
||||
await assertSendButton(enabled, 'Record, all rights', 'mail.test.multi.company.read', resSimpleId1, true, true);
|
||||
await assertSendButton(disabled, 'Record, no write access', 'mail.test.multi.company', resSimpleId1, true);
|
||||
await assertSendButton(enabled, 'Record, read access but model accept post with read only access',
|
||||
'mail.test.multi.company.read', resSimpleMCId1, true);
|
||||
await assertSendButton(disabled, 'Record, no rights', 'mail.test.multi.company', resSimpleId1);
|
||||
await assertSendButton(disabled, 'Record, no rights', 'mail.test.multi.company.read', resSimpleMCId1);
|
||||
|
||||
// Note that rights have no impact on send button for draft record (chatter.isTemporary=true)
|
||||
await assertSendButton(enabled, 'Draft record', 'mail.test.multi.company');
|
||||
await assertSendButton(enabled, 'Draft record', 'mail.test.multi.company.read');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
|
||||
|
||||
addModelNamesToFetch(['mail.test.track.all', 'mail.test.activity', 'mail.test.multi.company', 'mail.test.multi.company.read']);
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { start } from "@mail/../tests/helpers/test_utils";
|
||||
import { prepareTarget } from "web.test_utils";
|
||||
|
||||
QUnit.module("test_mail", () => {
|
||||
QUnit.module("activity view mobile");
|
||||
|
||||
QUnit.test('horizontal scroll applies only to the content, not to the whole controller', async (assert) => {
|
||||
const viewPort = prepareTarget();
|
||||
viewPort.style.position = "initial";
|
||||
viewPort.style.width = "initial";
|
||||
|
||||
const { openView } = await start();
|
||||
await openView({
|
||||
res_model: "mail.test.activity",
|
||||
views: [[false, "activity"]],
|
||||
});
|
||||
const o_view_controller = document.querySelector(".o_view_controller");
|
||||
const o_content = o_view_controller.querySelector(".o_content");
|
||||
|
||||
const o_cp_buttons = o_view_controller.querySelector(".o_control_panel .o_cp_buttons");
|
||||
const initialXCpBtn = o_cp_buttons.getBoundingClientRect().x;
|
||||
|
||||
const o_header_cell = o_content.querySelector(".o_activity_type_cell");
|
||||
const initialXHeaderCell = o_header_cell.getBoundingClientRect().x;
|
||||
|
||||
assert.hasClass(o_view_controller, "o_action_delegate_scroll",
|
||||
"the 'o_view_controller' should be have the 'o_action_delegate_scroll'.");
|
||||
assert.strictEqual(window.getComputedStyle(o_view_controller).overflow,"hidden",
|
||||
"the view controller should have overflow hidden");
|
||||
assert.strictEqual(window.getComputedStyle(o_content).overflow,"auto",
|
||||
"the view content should have the overflow auto");
|
||||
assert.strictEqual(o_content.scrollLeft, 0, "the o_content should not have scroll value");
|
||||
|
||||
// Horizontal scroll
|
||||
o_content.scrollLeft = 100;
|
||||
|
||||
assert.strictEqual(o_content.scrollLeft, 100, "the o_content should be 100 due to the overflow auto");
|
||||
assert.ok(o_header_cell.getBoundingClientRect().x < initialXHeaderCell,
|
||||
"the gantt header cell x position value should be lower after the scroll");
|
||||
assert.strictEqual(o_cp_buttons.getBoundingClientRect().x, initialXCpBtn,
|
||||
"the btn x position of the control panel button should be the same after the scroll");
|
||||
viewPort.style.position = "";
|
||||
viewPort.style.width = "";
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailTestActivity extends models.ServerModel {
|
||||
_name = "mail.test.activity";
|
||||
_inherit = ["mail.thread"];
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailTestMultiCompany extends models.ServerModel {
|
||||
_name = "mail.test.multi.company";
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailTestMultiCompanyRead extends models.ServerModel {
|
||||
_name = "mail.test.multi.company.read";
|
||||
_mail_post_access = "read";
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailTestProperties extends models.ServerModel {
|
||||
_name = "mail.test.properties";
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailTestSimple extends models.ServerModel {
|
||||
_name = "mail.test.simple";
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailTestSimpleMainAttachment extends models.ServerModel {
|
||||
_name = "mail.test.simple.main.attachment";
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { fields, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailTestTrackAll extends models.ServerModel {
|
||||
_name = "mail.test.track.all";
|
||||
_inherit = ["mail.thread"];
|
||||
|
||||
float_field_with_digits = fields.Float({
|
||||
digits: [10, 8],
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ResCurrency extends models.ServerModel {
|
||||
_name = "res.currency";
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
openFormView,
|
||||
registerArchs,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
|
||||
import { asyncStep, onRpc, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
|
||||
/**
|
||||
* Open a chat window when clicking on an avatar many2one / many2many properties.
|
||||
*/
|
||||
async function testPropertyFieldAvatarOpenChat(propertyType) {
|
||||
const pyEnv = await startServer();
|
||||
registerArchs({
|
||||
"mail.test.properties,false,form": `
|
||||
<form string="Form With Avatar Users">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
<field name="parent_id"/>
|
||||
<field name="properties"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
onRpc("mail.test.properties", "has_access", () => true);
|
||||
onRpc("res.users", "read", () => {
|
||||
asyncStep("read res.users");
|
||||
return [{ id: userId, partner_id: [partnerId, "Partner Test"] }];
|
||||
});
|
||||
onRpc("res.users", "search_read", () => [{ id: userId, name: "User Test" }]);
|
||||
await start();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Partner Test" });
|
||||
const userId = pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
const propertyDefinition = {
|
||||
type: propertyType,
|
||||
comodel: "res.users",
|
||||
name: "user",
|
||||
string: "user",
|
||||
};
|
||||
const parentId = pyEnv["mail.test.properties"].create({
|
||||
name: "Parent",
|
||||
definition_properties: [propertyDefinition],
|
||||
});
|
||||
const childId = pyEnv["mail.test.properties"].create({
|
||||
name: "Test",
|
||||
parent_id: parentId,
|
||||
properties: [{ ...propertyDefinition, value: [userId] }],
|
||||
});
|
||||
|
||||
await openFormView("mail.test.properties", childId);
|
||||
await waitForSteps([]);
|
||||
await click(
|
||||
propertyType === "many2one" ? ".o_field_property_many2one_value img" : ".o_m2m_avatar"
|
||||
);
|
||||
await waitForSteps(["read res.users"]);
|
||||
await contains(".o-mail-ChatWindow", { text: "Partner Test" });
|
||||
}
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineTestMailModels();
|
||||
|
||||
test("Properties fields: many2one avatar open chat on click", async () => {
|
||||
await testPropertyFieldAvatarOpenChat("many2one");
|
||||
});
|
||||
|
||||
test("Properties fields: m2m avatar list open chat on click", async () => {
|
||||
await testPropertyFieldAvatarOpenChat("many2many");
|
||||
});
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
import { start, startServer } from "@mail/../tests/mail_test_helpers";
|
||||
import { click, contains } from "@mail/../tests/mail_test_helpers_contains";
|
||||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { mockDate } from "@odoo/hoot-mock";
|
||||
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
|
||||
import { asyncStep, mockService, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
import { serializeDate, today } from "@web/core/l10n/dates";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineTestMailModels();
|
||||
// Avoid problem around midnight (Ex.: tomorrow activities become today activities when reaching midnight)
|
||||
beforeEach(() => mockDate("2023-04-08 10:00:00", 0));
|
||||
|
||||
test("menu with no records", async () => {
|
||||
await start();
|
||||
await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Activities'])");
|
||||
await contains(".o-mail-ActivityMenu", {
|
||||
text: "Congratulations, you're done with your activities.",
|
||||
});
|
||||
});
|
||||
|
||||
test("do not show empty text when at least some future activities", async () => {
|
||||
const tomorrow = today().plus({ days: 1 });
|
||||
const pyEnv = await startServer();
|
||||
const activityId = pyEnv["mail.test.activity"].create({});
|
||||
pyEnv["mail.activity"].create([
|
||||
{
|
||||
date_deadline: serializeDate(tomorrow),
|
||||
res_id: activityId,
|
||||
res_model: "mail.test.activity",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Activities'])");
|
||||
await contains(".o-mail-ActivityMenu", {
|
||||
count: 0,
|
||||
text: "Congratulations, you're done with your activities.",
|
||||
});
|
||||
});
|
||||
|
||||
test("activity menu widget: activity menu with 2 models", async () => {
|
||||
const tomorrow = today().plus({ days: 1 });
|
||||
const yesterday = today().plus({ days: -1 });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const activityIds = pyEnv["mail.test.activity"].create([{}, {}, {}, {}]);
|
||||
pyEnv["mail.activity"].create([
|
||||
{ res_id: partnerId, res_model: "res.partner", date_deadline: serializeDate(today()) },
|
||||
{
|
||||
res_id: activityIds[0],
|
||||
res_model: "mail.test.activity",
|
||||
date_deadline: serializeDate(today()),
|
||||
},
|
||||
{
|
||||
date_deadline: serializeDate(tomorrow),
|
||||
res_id: activityIds[1],
|
||||
res_model: "mail.test.activity",
|
||||
},
|
||||
{
|
||||
date_deadline: serializeDate(tomorrow),
|
||||
res_id: activityIds[2],
|
||||
res_model: "mail.test.activity",
|
||||
},
|
||||
{
|
||||
date_deadline: serializeDate(yesterday),
|
||||
res_id: activityIds[3],
|
||||
res_model: "mail.test.activity",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await contains(".o_menu_systray i[aria-label='Activities']");
|
||||
await contains(".o-mail-ActivityMenu-counter");
|
||||
await contains(".o-mail-ActivityMenu-counter", { text: "5" });
|
||||
const actionChecks = {
|
||||
context: {
|
||||
force_search_count: 1,
|
||||
search_default_filter_activities_my: 1,
|
||||
search_default_activities_overdue: 1,
|
||||
search_default_activities_today: 1,
|
||||
},
|
||||
domain: [["active", "in", [true, false]]],
|
||||
};
|
||||
mockService("action", {
|
||||
doAction(action) {
|
||||
Object.entries(actionChecks).forEach(([key, value]) => {
|
||||
if (Array.isArray(value) || typeof value === "object") {
|
||||
expect(action[key]).toEqual(value);
|
||||
} else {
|
||||
expect(action[key]).toBe(value);
|
||||
}
|
||||
});
|
||||
asyncStep("do_action:" + action.name);
|
||||
},
|
||||
});
|
||||
await click(".o_menu_systray i[aria-label='Activities']");
|
||||
await contains(".o-mail-ActivityMenu");
|
||||
await contains(".o-mail-ActivityMenu .o-mail-ActivityGroup", { count: 2 });
|
||||
await contains(".o-mail-ActivityMenu .o-mail-ActivityGroup", {
|
||||
contains: [
|
||||
["div[name='activityTitle']", { text: "res.partner" }],
|
||||
["span", { text: "0 Late" }],
|
||||
["span", { text: "1 Today" }],
|
||||
["span", { text: "0 Future" }],
|
||||
],
|
||||
});
|
||||
await contains(".o-mail-ActivityMenu .o-mail-ActivityGroup", {
|
||||
contains: [
|
||||
["div[name='activityTitle']", { text: "mail.test.activity" }],
|
||||
["span", { text: "1 Late" }],
|
||||
["span", { text: "1 Today" }],
|
||||
["span", { text: "2 Future" }],
|
||||
],
|
||||
});
|
||||
actionChecks.res_model = "res.partner";
|
||||
await click(".o-mail-ActivityMenu .o-mail-ActivityGroup", { text: "res.partner" });
|
||||
await contains(".o-mail-ActivityMenu", { count: 0 });
|
||||
await click(".o_menu_systray i[aria-label='Activities']");
|
||||
actionChecks.res_model = "mail.test.activity";
|
||||
await click(".o-mail-ActivityMenu .o-mail-ActivityGroup", { text: "mail.test.activity" });
|
||||
await waitForSteps(["do_action:res.partner", "do_action:mail.test.activity"]);
|
||||
});
|
||||
|
||||
test("activity menu widget: close on messaging menu click", async () => {
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Activities']");
|
||||
await contains(".o-mail-ActivityMenu");
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await contains(".o-mail-ActivityMenu", { count: 0 });
|
||||
});
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
start,
|
||||
startServer,
|
||||
} from '@mail/../tests/helpers/test_utils';
|
||||
|
||||
import session from 'web.session';
|
||||
import { date_to_str } from 'web.time';
|
||||
import { patchWithCleanup } from '@web/../tests/helpers/utils';
|
||||
|
||||
|
||||
QUnit.module('test_mail', {}, function () {
|
||||
QUnit.module('systray_activity_menu_tests.js', {
|
||||
async beforeEach() {
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const pyEnv = await startServer();
|
||||
const resPartnerId1 = pyEnv['res.partner'].create({});
|
||||
const mailTestActivityIds = pyEnv['mail.test.activity'].create([{}, {}, {}, {}]);
|
||||
pyEnv['mail.activity'].create([
|
||||
{ res_id: resPartnerId1, res_model: 'res.partner' },
|
||||
{ res_id: mailTestActivityIds[0], res_model: 'mail.test.activity' },
|
||||
{ date_deadline: date_to_str(tomorrow), res_id: mailTestActivityIds[1], res_model: 'mail.test.activity' },
|
||||
{ date_deadline: date_to_str(tomorrow), res_id: mailTestActivityIds[2], res_model: 'mail.test.activity' },
|
||||
{ date_deadline: date_to_str(yesterday), res_id: mailTestActivityIds[3], res_model: 'mail.test.activity' },
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test('activity menu widget: menu with no records', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const { click } = await start({
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'systray_get_activities') {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
},
|
||||
});
|
||||
await click('.o_ActivityMenuView_dropdownToggle');
|
||||
assert.containsOnce(document.body, '.o_ActivityMenuView_noActivity');
|
||||
});
|
||||
|
||||
QUnit.test('activity menu widget: activity menu with 2 models', async function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
const { click, env } = await start();
|
||||
|
||||
await click('.o_ActivityMenuView_dropdownToggle');
|
||||
assert.containsOnce(document.body, '.o_ActivityMenuView', 'should contain an instance of widget');
|
||||
assert.ok(document.querySelectorAll('.o_ActivityMenuView_activityGroup').length);
|
||||
assert.containsOnce(document.body, '.o_ActivityMenuView_counter', "widget should have notification counter");
|
||||
assert.strictEqual(parseInt(document.querySelector('.o_ActivityMenuView_counter').innerText), 5, "widget should have 5 notification counter");
|
||||
|
||||
var context = {};
|
||||
patchWithCleanup(env.services.action, {
|
||||
doAction(action) {
|
||||
assert.deepEqual(action.context, context, "wrong context value");
|
||||
},
|
||||
});
|
||||
|
||||
// case 1: click on "late"
|
||||
context = {
|
||||
force_search_count: 1,
|
||||
search_default_activities_overdue: 1,
|
||||
};
|
||||
assert.containsOnce(document.body, '.o_ActivityMenuView_dropdownMenu.show', 'ActivityMenu should be open');
|
||||
await click('.o_ActivityMenuView_activityGroupFilterButton[data-model_name="mail.test.activity"][data-filter="overdue"]');
|
||||
assert.containsNone(document.body, '.show', 'ActivityMenu should be closed');
|
||||
// case 2: click on "today"
|
||||
context = {
|
||||
force_search_count: 1,
|
||||
search_default_activities_today: 1,
|
||||
};
|
||||
await click('.dropdown-toggle[title="Activities"]');
|
||||
await click('.o_ActivityMenuView_activityGroupFilterButton[data-model_name="mail.test.activity"][data-filter="today"]');
|
||||
// case 3: click on "future"
|
||||
context = {
|
||||
force_search_count: 1,
|
||||
search_default_activities_upcoming_all: 1,
|
||||
};
|
||||
await click('.dropdown-toggle[title="Activities"]');
|
||||
await click('.o_ActivityMenuView_activityGroupFilterButton[data-model_name="mail.test.activity"][data-filter="upcoming_all"]');
|
||||
// case 4: click anywere else
|
||||
context = {
|
||||
force_search_count: 1,
|
||||
search_default_activities_overdue: 1,
|
||||
search_default_activities_today: 1,
|
||||
};
|
||||
await click('.dropdown-toggle[title="Activities"]');
|
||||
await click('.o_ActivityMenuView_activityGroups > div[data-model_name="mail.test.activity"]');
|
||||
});
|
||||
|
||||
QUnit.test('activity menu widget: activity view icon', async function (assert) {
|
||||
assert.expect(14);
|
||||
|
||||
patchWithCleanup(session, { uid: 10 });
|
||||
const { click, env } = await start();
|
||||
|
||||
await click('.o_ActivityMenuView_dropdownToggle');
|
||||
assert.containsN(document.body, '.o_ActivityMenuView_activityGroupActionButton', 2,
|
||||
"widget should have 2 activity view icons");
|
||||
|
||||
var first = document.querySelector('.o_ActivityMenuView_activityGroupActionButton[data-model_name="res.partner"]');
|
||||
var second = document.querySelector('.o_ActivityMenuView_activityGroupActionButton[data-model_name="mail.test.activity"]');
|
||||
assert.ok(first, "should have activity action linked to 'res.partner'");
|
||||
assert.hasClass(first, 'fa-clock-o', "should display the activity action icon");
|
||||
|
||||
assert.ok(second, "should have activity action linked to 'mail.test.activity'");
|
||||
assert.hasClass(second, 'fa-clock-o', "should display the activity action icon");
|
||||
|
||||
patchWithCleanup(env.services.action, {
|
||||
doAction(action) {
|
||||
if (action.name) {
|
||||
assert.ok(action.domain, "should define a domain on the action");
|
||||
assert.deepEqual(action.domain, [["activity_ids.user_id", "=", 10]],
|
||||
"should set domain to user's activity only");
|
||||
assert.step('do_action:' + action.name);
|
||||
} else {
|
||||
assert.step('do_action:' + action);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(document.querySelector('.o-dropdown-menu'), 'show',
|
||||
"dropdown should be expanded");
|
||||
|
||||
await click('.o_ActivityMenuView_activityGroupActionButton[data-model_name="mail.test.activity"]');
|
||||
assert.containsNone(document.body, '.o-dropdown-menu',
|
||||
"dropdown should be collapsed");
|
||||
|
||||
// click on the "res.partner" activity icon
|
||||
await click('.dropdown-toggle[title="Activities"]');
|
||||
await click('.o_ActivityMenuView_activityGroupActionButton[data-model_name="res.partner"]');
|
||||
|
||||
assert.verifySteps([
|
||||
'do_action:mail.test.activity',
|
||||
'do_action:res.partner'
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('activity menu widget: close on messaging menu click', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const { click } = await start();
|
||||
|
||||
await click('.dropdown-toggle[title="Activities"]');
|
||||
assert.hasClass(
|
||||
document.querySelector('.o_ActivityMenuView_dropdownMenu'),
|
||||
'show',
|
||||
"activity menu should be shown after click on itself"
|
||||
);
|
||||
|
||||
await click(`.o_MessagingMenu_toggler`);
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_ActivityMenuView_dropdownMenu',
|
||||
"activity menu should be hidden after click on messaging menu"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { contains, mailModels } from "@mail/../tests/mail_test_helpers";
|
||||
import { MailTestActivity } from "@test_mail/../tests/mock_server/models/mail_test_activity";
|
||||
import { MailTestMultiCompany } from "@test_mail/../tests/mock_server/models/mail_test_multi_company";
|
||||
import { MailTestMultiCompanyRead } from "@test_mail/../tests/mock_server/models/mail_test_multi_company_read";
|
||||
import { MailTestProperties } from "@test_mail/../tests/mock_server/models/mail_test_properties";
|
||||
import { MailTestSimpleMainAttachment } from "./mock_server/models/mail_test_simple_main_attachment";
|
||||
import { MailTestSimple } from "@test_mail/../tests/mock_server/models/mail_test_simple";
|
||||
import { MailTestTrackAll } from "@test_mail/../tests/mock_server/models/mail_test_track_all";
|
||||
import { defineModels, defineParams } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export const testMailModels = {
|
||||
...mailModels,
|
||||
MailTestActivity,
|
||||
MailTestMultiCompany,
|
||||
MailTestMultiCompanyRead,
|
||||
MailTestProperties,
|
||||
MailTestSimpleMainAttachment,
|
||||
MailTestSimple,
|
||||
MailTestTrackAll,
|
||||
};
|
||||
|
||||
export function defineTestMailModels() {
|
||||
defineParams({ suite: "test_mail" }, "replace");
|
||||
defineModels(testMailModels);
|
||||
}
|
||||
|
||||
export async function editSelect(selector, value) {
|
||||
await contains(selector);
|
||||
const el = document.querySelector(selector);
|
||||
el.value = value;
|
||||
el.dispatchEvent(new Event("change"));
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
|
||||
const setPager = value => [
|
||||
{
|
||||
content: "Click Pager",
|
||||
trigger: ".o_pager_value:first()",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Change pager to display lines " + value,
|
||||
trigger: "input.o_pager_value",
|
||||
run: `edit ${value} && click body`,
|
||||
},
|
||||
{
|
||||
trigger: `.o_pager_value:contains('${value}')`,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
const checkRows = values => {
|
||||
return {
|
||||
trigger: '.o_activity_view',
|
||||
run: () => {
|
||||
const dataRow = document.querySelectorAll('.o_activity_view tbody .o_data_row .o_activity_record');
|
||||
if (dataRow.length !== values.length) {
|
||||
throw Error(`There should be ${values.length} activities`);
|
||||
}
|
||||
values.forEach((value, index) => {
|
||||
if (dataRow[index].textContent !== value) {
|
||||
throw Error(`Record does not match ${value} != ${dataRow[index]}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("web_tour.tours").add("mail_activity_view", {
|
||||
steps: () => [
|
||||
{
|
||||
content: "Open the debug menu",
|
||||
trigger: ".o_debug_manager button",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Click the Set Defaults menu",
|
||||
trigger: ".o-dropdown-item:contains(Open View)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_searchview_input",
|
||||
run: "edit Test Activity View"
|
||||
},
|
||||
{
|
||||
trigger: ".o_searchview_autocomplete .o-dropdown-item.focus",
|
||||
content: "Validate search",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Select Test Activity View",
|
||||
trigger: `.o_data_row td:contains("Test Activity View")`,
|
||||
run: "click",
|
||||
},
|
||||
checkRows(["Task 1", "Task 2", "Task 3"]),
|
||||
...setPager("1-2"),
|
||||
checkRows(["Task 2", "Task 3"]),
|
||||
...setPager("3"),
|
||||
checkRows(["Task 1"]),
|
||||
],
|
||||
})
|
||||
|
|
@ -0,0 +1,377 @@
|
|||
import {
|
||||
contains,
|
||||
click,
|
||||
insertText,
|
||||
openFormView,
|
||||
registerArchs,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { mockDate, mockTimeZone } from "@odoo/hoot-mock";
|
||||
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
|
||||
import { editSelectMenu, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { currencies } from "@web/core/currency";
|
||||
|
||||
const archs = {
|
||||
"mail.test.track.all,false,form": `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="boolean_field"/>
|
||||
<field name="char_field"/>
|
||||
<field name="date_field"/>
|
||||
<field name="datetime_field"/>
|
||||
<field name="float_field"/>
|
||||
<field name="float_field_with_digits"/>
|
||||
<field name="integer_field"/>
|
||||
<field name="monetary_field"/>
|
||||
<field name="many2one_field_id"/>
|
||||
<field name="selection_field"/>
|
||||
<field name="text_field"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineTestMailModels();
|
||||
beforeEach(() => mockTimeZone(0));
|
||||
|
||||
test("basic rendering of tracking value (float type)", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({ float_field: 12.3 });
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await insertText("div[name=float_field] input", "45.67", { replace: true });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking");
|
||||
await contains(".o-mail-Message-trackingField");
|
||||
await contains(".o-mail-Message-trackingField", { text: "(Float)" });
|
||||
await contains(".o-mail-Message-trackingOld");
|
||||
await contains(".o-mail-Message-trackingOld", { text: "12.30" });
|
||||
await contains(".o-mail-Message-trackingSeparator");
|
||||
await contains(".o-mail-Message-trackingNew");
|
||||
await contains(".o-mail-Message-trackingNew", { text: "45.67" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type float: from non-0 to 0", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
float_field: 1,
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await insertText("div[name=float_field] input", "0", { replace: true });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "1.000.00(Float)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type float: from 0 to non-0", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
float_field: 0,
|
||||
float_field_with_digits: 0,
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await insertText("div[name=float_field] input", "1.01", { replace: true });
|
||||
await insertText("div[name=float_field_with_digits] input", "1.0001", { replace: true });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { count: 2 });
|
||||
const [increasedPrecisionLine, defaultPrecisionLine] =
|
||||
document.getElementsByClassName("o-mail-Message-tracking");
|
||||
const expectedText = [
|
||||
[defaultPrecisionLine, ["0.00", "1.01", "(Float)"]],
|
||||
[increasedPrecisionLine, ["0.00000000", "1.00010000", "(Float)"]],
|
||||
];
|
||||
for (const [targetLine, [oldText, newText, fieldName]] of expectedText) {
|
||||
await contains(".o-mail-Message-trackingOld", { target: targetLine, text: oldText });
|
||||
await contains(".o-mail-Message-trackingNew", { target: targetLine, text: newText });
|
||||
await contains(".o-mail-Message-trackingField", { target: targetLine, text: fieldName });
|
||||
}
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type integer: from non-0 to 0", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
integer_field: 1,
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await insertText("div[name=integer_field] input", "0", { replace: true });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "10(Integer)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type integer: from 0 to non-0", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
integer_field: 0,
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await insertText("div[name=integer_field] input", "1", { replace: true });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "01(Integer)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type monetary: from non-0 to 0", async () => {
|
||||
const pyEnv = await startServer();
|
||||
|
||||
const testCurrencyId = pyEnv["res.currency"].create({ name: "ECU", symbol: "§" });
|
||||
// need to patch currencies as they're passed via cookies, not through the orm
|
||||
patchWithCleanup(currencies, {
|
||||
[testCurrencyId]: { digits: [69, 2], position: "after", symbol: "§" },
|
||||
});
|
||||
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
currency_id: testCurrencyId,
|
||||
monetary_field: 1,
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await insertText("div[name=monetary_field] input", "0", { replace: true });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "1.00 §0.00 §(Monetary)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type monetary: from 0 to non-0", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
monetary_field: 0,
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await insertText("div[name=monetary_field] input", "1", { replace: true });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "0.001.00(Monetary)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type boolean: from true to false", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
boolean_field: true,
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await click(".o_field_boolean input");
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "YesNo(Boolean)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type boolean: from false to true", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await click(".o_field_boolean input");
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "NoYes(Boolean)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type char: from a string to empty string", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
char_field: "Marc",
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await insertText("div[name=char_field] input", "", { replace: true });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "MarcNone(Char)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type char: from empty string to a string", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
char_field: "",
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await insertText("div[name=char_field] input", "Marc", { replace: true });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "NoneMarc(Char)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type date: from no date to a set date", async () => {
|
||||
mockDate("2018-12-01");
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
date_field: false,
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await click("div[name=date_field] input");
|
||||
await click(".o_datetime_button", { text: "14" });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "None12/14/2018(Date)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type date: from a set date to no date", async () => {
|
||||
mockDate("2018-12-01");
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
date_field: "2018-12-14",
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await click("div[name=date_field] button");
|
||||
await insertText("div[name=date_field] input", "", { replace: true });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "12/14/2018None(Date)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type datetime: from no date and time to a set date and time", async function () {
|
||||
mockDate("2018-12-01", 3);
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
datetime_field: false,
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await click("div[name=datetime_field] input");
|
||||
await click(".o_datetime_button", { text: "14" });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "None12/14/2018 12:00:00(Datetime)" });
|
||||
const [savedRecord] = pyEnv["mail.test.track.all"].search_read([
|
||||
["id", "=", mailTestTrackAllId1],
|
||||
]);
|
||||
expect(savedRecord.datetime_field).toBe("2018-12-14 09:00:00");
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type datetime: from a set date and time to no date and time", async () => {
|
||||
mockTimeZone(3);
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
datetime_field: "2018-12-14 13:42:28 ",
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await click("div[name=datetime_field] button");
|
||||
await insertText("div[name=datetime_field] input", "", { replace: true });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "12/14/2018 16:42:28None(Datetime)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type text: from some text to empty", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
text_field: "Marc",
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await insertText("div[name=text_field] textarea", "", { replace: true });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "MarcNone(Text)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type text: from empty to some text", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
text_field: "",
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await insertText("div[name=text_field] textarea", "Marc", { replace: true });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "NoneMarc(Text)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type selection: from a selection to no selection", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
selection_field: "first",
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await editSelectMenu("div[name=selection_field] input", { value: "" });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "firstNone(Selection)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type selection: from no selection to a selection", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await editSelectMenu("div[name=selection_field] input", { value: "First" });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "Nonefirst(Selection)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type many2one: from having a related record to no related record", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const resPartnerId1 = pyEnv["res.partner"].create({ name: "Marc" });
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
|
||||
many2one_field_id: resPartnerId1,
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await insertText(".o_field_many2one_selection input", "", { replace: true });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "MarcNone(Many2one)" });
|
||||
});
|
||||
|
||||
test("rendering of tracked field of type many2one: from no related record to having a related record", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.partner"].create({ name: "Marc" });
|
||||
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId1);
|
||||
await click("[name=many2one_field_id] input");
|
||||
await click("[name=many2one_field_id] .o-autocomplete--dropdown-item", { text: "Marc" });
|
||||
await click(".o_form_button_save");
|
||||
await contains(".o-mail-Message-tracking", { text: "NoneMarc(Many2one)" });
|
||||
});
|
||||
|
||||
test("Search message with filter in chatter", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId = pyEnv["mail.test.track.all"].create({});
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Hermit",
|
||||
model: "mail.test.track.all",
|
||||
res_id: mailTestTrackAllId,
|
||||
});
|
||||
await start();
|
||||
registerArchs(archs);
|
||||
await openFormView("mail.test.track.all", mailTestTrackAllId);
|
||||
await click("[name=many2one_field_id] input");
|
||||
await click("[name=many2one_field_id] .o-autocomplete--dropdown-item", { text: "Hermit" });
|
||||
await click(".o_form_button_save");
|
||||
// Search message with filter
|
||||
await click("[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "Hermit");
|
||||
await click("button[title='Filter Messages']");
|
||||
await click("span", { text: "Conversations" });
|
||||
await contains(".o-mail-SearchMessageResult .o-mail-Message", { text: "Hermit" });
|
||||
|
||||
await click("button[title='Filter Messages']");
|
||||
await click("span", { text: "Tracked Changes" });
|
||||
await contains(".o-mail-SearchMessageResult .o-mail-Message", { text: "Hermit" });
|
||||
|
||||
await click("button[title='Filter Messages']");
|
||||
await click("span", { text: "All" });
|
||||
await contains(".o-mail-SearchMessageResult .o-mail-Message", { count: 2 });
|
||||
});
|
||||
|
|
@ -1,438 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
start,
|
||||
startServer,
|
||||
} from '@mail/../tests/helpers/test_utils';
|
||||
|
||||
import { editInput, editSelect, selectDropdownItem, patchWithCleanup, patchTimeZone } from "@web/../tests/helpers/utils";
|
||||
|
||||
import session from 'web.session';
|
||||
import testUtils from 'web.test_utils';
|
||||
|
||||
QUnit.module('test_mail', {}, function () {
|
||||
QUnit.module('tracking_value_tests.js', {
|
||||
beforeEach() {
|
||||
const views = {
|
||||
'mail.test.track.all,false,form':
|
||||
`<form>
|
||||
<sheet>
|
||||
<field name="boolean_field"/>
|
||||
<field name="char_field"/>
|
||||
<field name="date_field"/>
|
||||
<field name="datetime_field"/>
|
||||
<field name="float_field"/>
|
||||
<field name="integer_field"/>
|
||||
<field name="monetary_field"/>
|
||||
<field name="many2one_field_id"/>
|
||||
<field name="selection_field"/>
|
||||
<field name="text_field"/>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>`,
|
||||
};
|
||||
this.start = async ({ res_id }) => {
|
||||
const { openFormView, ...remainder } = await start({
|
||||
serverData: { views },
|
||||
});
|
||||
await openFormView(
|
||||
{
|
||||
res_model: 'mail.test.track.all',
|
||||
res_id,
|
||||
},
|
||||
{
|
||||
props: { mode: 'edit' },
|
||||
},
|
||||
);
|
||||
return remainder;
|
||||
};
|
||||
|
||||
patchWithCleanup(session, {
|
||||
getTZOffset() {
|
||||
return 0;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test('basic rendering of tracking value (float type)', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ float_field: 12.30 });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await editInput(document.body, 'div[name=float_field] input', 45.67);
|
||||
await click('.o_form_button_save');
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_TrackingValue',
|
||||
"should display a tracking value"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_TrackingValue_fieldName',
|
||||
"should display the name of the tracked field"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue_fieldName').textContent,
|
||||
"(Float)",
|
||||
"should display the correct tracked field name (Float)",
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_TrackingValue_oldValue',
|
||||
"should display the old value"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue_oldValue').textContent,
|
||||
"12.30",
|
||||
"should display the correct old value (12.30)",
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_TrackingValue_separator',
|
||||
"should display the separator"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_TrackingValue_newValue',
|
||||
"should display the new value"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue_newValue').textContent,
|
||||
"45.67",
|
||||
"should display the correct new value (45.67)",
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type float: from non-0 to 0', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ float_field: 1 });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await editInput(document.body, 'div[name=float_field] input', 0);
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"1.000.00(Float)",
|
||||
"should display the correct content of tracked field of type float: from non-0 to 0 (1.00 -> 0.00 (Float))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type float: from 0 to non-0', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ float_field: 0 });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await editInput(document.body, 'div[name=float_field] input', 1);
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"0.001.00(Float)",
|
||||
"should display the correct content of tracked field of type float: from 0 to non-0 (0.00 -> 1.00 (Float))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type integer: from non-0 to 0', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ integer_field: 1 });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await editInput(document.body, 'div[name=integer_field] input', 0);
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"10(Integer)",
|
||||
"should display the correct content of tracked field of type integer: from non-0 to 0 (1 -> 0 (Integer))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type integer: from 0 to non-0', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ integer_field: 0 });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await editInput(document.body, 'div[name=integer_field] input', 1);
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"01(Integer)",
|
||||
"should display the correct content of tracked field of type integer: from 0 to non-0 (0 -> 1 (Integer))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type monetary: from non-0 to 0', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ monetary_field: 1 });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await editInput(document.body, 'div[name=monetary_field] input', 0);
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"1.000.00(Monetary)",
|
||||
"should display the correct content of tracked field of type monetary: from non-0 to 0 (1.00 -> 0.00 (Monetary))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type monetary: from 0 to non-0', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ monetary_field: 0 });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await editInput(document.body, 'div[name=monetary_field] input', 1);
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"0.001.00(Monetary)",
|
||||
"should display the correct content of tracked field of type monetary: from 0 to non-0 (0.00 -> 1.00 (Monetary))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type boolean: from true to false', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ boolean_field: true });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
document.querySelector('.o_field_boolean input').click();
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"YesNo(Boolean)",
|
||||
"should display the correct content of tracked field of type boolean: from true to false (True -> False (Boolean))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type boolean: from false to true', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({});
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
document.querySelector('.o_field_boolean input').click();
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"NoYes(Boolean)",
|
||||
"should display the correct content of tracked field of type boolean: from false to true (False -> True (Boolean))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type char: from a string to empty string', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ char_field: 'Marc' });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await editInput(document.body, 'div[name=char_field] input', '');
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"MarcNone(Char)",
|
||||
"should display the correct content of tracked field of type char: from a string to empty string (Marc -> None (Char))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type char: from empty string to a string', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ char_field: '' });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await editInput(document.body, 'div[name=char_field] input', 'Marc');
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"NoneMarc(Char)",
|
||||
"should display the correct content of tracked field of type char: from empty string to a string (None -> Marc (Char))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type date: from no date to a set date', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ date_field: false });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await testUtils.fields.editAndTrigger(document.querySelector('div[name=date_field] .o_datepicker .o_datepicker_input'), '12/14/2018', ['change']);
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"None12/14/2018(Date)",
|
||||
"should display the correct content of tracked field of type date: from no date to a set date (None -> 12/14/2018 (Date))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type date: from a set date to no date', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ date_field: '2018-12-14' });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await testUtils.fields.editAndTrigger(document.querySelector('div[name=date_field] .o_datepicker .o_datepicker_input'), '', ['change']);
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"12/14/2018None(Date)",
|
||||
"should display the correct content of tracked field of type date: from a set date to no date (12/14/2018 -> None (Date))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type datetime: from no date and time to a set date and time', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
patchTimeZone(180);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ datetime_field: false });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await testUtils.fields.editAndTrigger(document.querySelector('div[name=datetime_field] .o_datepicker .o_datepicker_input'), '12/14/2018 13:42:28', ['change']);
|
||||
await click('.o_form_button_save');
|
||||
const savedRecord = pyEnv.getData()["mail.test.track.all"].records.find(({id}) => id === mailTestTrackAllId1);
|
||||
assert.strictEqual(savedRecord.datetime_field, '2018-12-14 10:42:28');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"None12/14/2018 13:42:28(Datetime)",
|
||||
"should display the correct content of tracked field of type datetime: from no date and time to a set date and time (None -> 12/14/2018 13:42:28 (Datetime))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type datetime: from a set date and time to no date and time', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
patchTimeZone(180)
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ datetime_field: '2018-12-14 13:42:28 ' });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await testUtils.fields.editAndTrigger(document.querySelector('div[name=datetime_field] .o_datepicker .o_datepicker_input'), '', ['change']);
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"12/14/2018 16:42:28None(Datetime)",
|
||||
"should display the correct content of tracked field of type datetime: from a set date and time to no date and time (12/14/2018 13:42:28 -> None (Datetime))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type text: from some text to empty', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ text_field: 'Marc' });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await editInput(document.body, 'div[name=text_field] textarea', '');
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"MarcNone(Text)",
|
||||
"should display the correct content of tracked field of type text: from some text to empty (Marc -> None (Text))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type text: from empty to some text', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ text_field: '' });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await editInput(document.body, 'div[name=text_field] textarea', 'Marc');
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"NoneMarc(Text)",
|
||||
"should display the correct content of tracked field of type text: from empty to some text (None -> Marc (Text))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type selection: from a selection to no selection', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ selection_field: 'first' });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await editSelect(document.body, 'div[name=selection_field] select', false);
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"firstNone(Selection)",
|
||||
"should display the correct content of tracked field of type selection: from a selection to no selection (first -> None (Selection))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type selection: from no selection to a selection', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({});
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await editSelect(document.body, 'div[name=selection_field] select', '"first"');
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"Nonefirst(Selection)",
|
||||
"should display the correct content of tracked field of type selection: from no selection to a selection (None -> first (Selection))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type many2one: from having a related record to no related record', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const resPartnerId1 = pyEnv['res.partner'].create({ display_name: 'Marc' });
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ many2one_field_id: resPartnerId1 });
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await editInput(document.body, ".o_field_many2one_selection input", '')
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"MarcNone(Many2one)",
|
||||
"should display the correct content of tracked field of type many2one: from having a related record to no related record (Marc -> None (Many2one))"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of tracked field of type many2one: from no related record to having a related record', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
pyEnv['res.partner'].create({ display_name: 'Marc' });
|
||||
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({});
|
||||
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
|
||||
|
||||
await selectDropdownItem(document.body, "many2one_field_id", "Marc")
|
||||
await click('.o_form_button_save');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_TrackingValue').textContent,
|
||||
"NoneMarc(Many2one)",
|
||||
"should display the correct content of tracked field of type many2one: from no related record to having a related record (None -> Marc (Many2one))"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,24 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_controller_attachment
|
||||
from . import test_controller_binary
|
||||
from . import test_controller_thread
|
||||
from . import test_invite
|
||||
from . import test_ir_actions
|
||||
from . import test_ir_attachment
|
||||
from . import test_mail_activity
|
||||
from . import test_mail_activity_mixin
|
||||
from . import test_mail_activity_plan
|
||||
from . import test_mail_alias
|
||||
from . import test_mail_composer
|
||||
from . import test_mail_composer_mixin
|
||||
from . import test_mail_followers
|
||||
from . import test_mail_gateway
|
||||
from . import test_mail_flow
|
||||
from . import test_mail_mail
|
||||
from . import test_mail_management
|
||||
from . import test_mail_message
|
||||
from . import test_mail_message_security
|
||||
from . import test_mail_mail
|
||||
from . import test_mail_gateway
|
||||
from . import test_mail_multicompany
|
||||
from . import test_mail_push
|
||||
from . import test_mail_scheduled_message
|
||||
from . import test_mail_security
|
||||
from . import test_mail_thread_internals
|
||||
from . import test_mail_thread_mixins
|
||||
from . import test_mail_template
|
||||
from . import test_mail_template_preview
|
||||
from . import test_message_management
|
||||
from . import test_message_post
|
||||
from . import test_message_track
|
||||
from . import test_performance
|
||||
from . import test_ui
|
||||
from . import test_mail_management
|
||||
from . import test_mail_security
|
||||
|
|
|
|||
|
|
@ -1,15 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestMailCommon(MailCommon):
|
||||
""" Main entry point for functional tests. Kept to ease backward
|
||||
compatibility. """
|
||||
|
||||
|
||||
class TestRecipients(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
|
|
@ -25,13 +19,11 @@ class TestRecipients(TransactionCase):
|
|||
'name': 'Valid Lelitre',
|
||||
'email': 'valid.lelitre@agrolait.com',
|
||||
'country_id': cls.env.ref('base.be').id,
|
||||
'mobile': '0456001122',
|
||||
'phone': False,
|
||||
'phone': '0456001122',
|
||||
})
|
||||
cls.partner_2 = Partner.create({
|
||||
'name': 'Valid Poilvache',
|
||||
'email': 'valid.other@gmail.com',
|
||||
'country_id': cls.env.ref('base.be').id,
|
||||
'mobile': '+32 456 22 11 00',
|
||||
'phone': False,
|
||||
'phone': '+32 456 22 11 00',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo
|
||||
from odoo.addons.mail.tests.common_controllers import MailControllerAttachmentCommon
|
||||
|
||||
|
||||
@odoo.tests.tagged("-at_install", "post_install", "mail_controller")
|
||||
class TestAttachmentController(MailControllerAttachmentCommon):
|
||||
def test_independent_attachment_delete(self):
|
||||
"""Test access to delete an attachment whether or not limited `ownership_token` is sent"""
|
||||
self._execute_subtests_delete(self.all_users, token=True, allowed=True)
|
||||
self._execute_subtests_delete(self.user_admin, token=False, allowed=True)
|
||||
self._execute_subtests_delete(
|
||||
(self.guest, self.user_employee, self.user_portal, self.user_public),
|
||||
token=False,
|
||||
allowed=False,
|
||||
)
|
||||
|
||||
def test_attachment_delete_linked_to_thread(self):
|
||||
"""Test access to delete an attachment associated with a thread
|
||||
whether or not limited `ownership_token` is sent"""
|
||||
thread = self.env["mail.test.simple"].create({"name": "Test"})
|
||||
self._execute_subtests_delete(self.all_users, token=True, allowed=True, thread=thread)
|
||||
self._execute_subtests_delete(
|
||||
(self.user_admin, self.user_employee),
|
||||
token=False,
|
||||
allowed=True,
|
||||
thread=thread,
|
||||
)
|
||||
self._execute_subtests_delete(
|
||||
(self.guest, self.user_portal, self.user_public),
|
||||
token=False,
|
||||
allowed=False,
|
||||
thread=thread,
|
||||
)
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
from odoo.addons.mail.tests.common_controllers import MailControllerBinaryCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install", "mail_controller")
|
||||
class TestPublicBinaryController(MailControllerBinaryCommon):
|
||||
|
||||
def test_avatar_no_public(self):
|
||||
"""Test access to open a guest / partner avatar who hasn't sent a message on a
|
||||
public record."""
|
||||
for source in (self.guest_2, self.user_employee_nopartner.partner_id):
|
||||
self._execute_subtests(
|
||||
source, (
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
)
|
||||
)
|
||||
|
||||
def test_avatar_private(self):
|
||||
"""Test access to open a partner avatar who has sent a message on a private record."""
|
||||
document = self.env["mail.test.simple.unfollow"].create({"name": "Test"})
|
||||
self._post_message(document, self.user_employee_nopartner)
|
||||
self._execute_subtests(
|
||||
self.user_employee_nopartner.partner_id, (
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
)
|
||||
)
|
||||
|
||||
def test_avatar_public(self):
|
||||
"""Test access to open a guest avatar who has sent a message on a public record."""
|
||||
document = self.env["mail.test.access.public"].create({"name": "Test"})
|
||||
for author, source in ((self.guest_2, self.guest_2), (self.user_employee_nopartner, self.user_employee_nopartner.partner_id)):
|
||||
self._post_message(document, author)
|
||||
self._execute_subtests(
|
||||
source,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
import json
|
||||
|
||||
from odoo import http
|
||||
from odoo.addons.mail.tests.common_controllers import MailControllerThreadCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install", "mail_controller")
|
||||
class TestMessageController(MailControllerThreadCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.test_public_record = cls.env["mail.test.access.public"].create({"name": "Public Channel", "email": "john@test.be", "mobile": "+32455001122"})
|
||||
|
||||
@mute_logger("odoo.http")
|
||||
def test_thread_attachment_hijack(self):
|
||||
att = self.env["ir.attachment"].create({
|
||||
"name": "arguments_for_firing_marc_demo",
|
||||
"res_id": 0,
|
||||
"res_model": "mail.compose.message",
|
||||
})
|
||||
self.authenticate(self.user_employee.login, self.user_employee.login)
|
||||
record = self.env["mail.test.access.public"].create({"name": "Public Channel"})
|
||||
record.with_user(self.user_employee).write({'name': 'updated'}) # can access, update, ...
|
||||
# if this test breaks, it might be due to a change in /web/content, or the default rules for accessing an attachment. This is not an issue but it makes this test irrelevant.
|
||||
self.assertFalse(self.url_open(f"/web/content/{att.id}").ok)
|
||||
response = self.url_open(
|
||||
url="/mail/message/post",
|
||||
headers={"Content-Type": "application/json"}, # route called as demo
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"post_data": {
|
||||
"attachment_ids": [att.id], # demo does not have access to this attachment id
|
||||
"body": "",
|
||||
"message_type": "comment",
|
||||
"partner_ids": [],
|
||||
"subtype_xmlid": "mail.mt_comment",
|
||||
},
|
||||
"thread_id": record.id,
|
||||
"thread_model": record._name,
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
self.assertNotIn(
|
||||
"arguments_for_firing_marc_demo", response.text
|
||||
) # demo should not be able to see the name of the document
|
||||
|
||||
def test_thread_partner_from_email_authenticated(self):
|
||||
self.authenticate(self.user_employee.login, self.user_employee.login)
|
||||
res3 = self.url_open(
|
||||
url="/mail/partner/from_email",
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"thread_model": self.test_public_record._name,
|
||||
"thread_id": self.test_public_record.id,
|
||||
"emails": ["john@test.be"],
|
||||
},
|
||||
}
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
self.assertEqual(res3.status_code, 200)
|
||||
self.assertEqual(
|
||||
1,
|
||||
self.env["res.partner"].search_count([('email', '=', "john@test.be"), ('phone', '=', "+32455001122")]),
|
||||
"authenticated users can create a partner from an email",
|
||||
)
|
||||
# should not create another partner with same email
|
||||
res4 = self.url_open(
|
||||
url="/mail/partner/from_email",
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"thread_model": self.test_public_record._name,
|
||||
"thread_id": self.test_public_record.id,
|
||||
"emails": ["john@test.be"],
|
||||
},
|
||||
}
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
self.assertEqual(res4.status_code, 200)
|
||||
self.assertEqual(
|
||||
1,
|
||||
self.env["res.partner"].search_count([('email', '=', "john@test.be")]),
|
||||
"'mail/partner/from_email' does not create another user if there's already a user with matching email",
|
||||
)
|
||||
|
||||
self.test_public_record.write({'email': 'john2@test.be'})
|
||||
res5 = self.url_open(
|
||||
url="/mail/message/post",
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"thread_model": self.test_public_record._name,
|
||||
"thread_id": self.test_public_record.id,
|
||||
"post_data": {
|
||||
"body": "test",
|
||||
"partner_emails": ["john2@test.be"],
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
self.assertEqual(res5.status_code, 200)
|
||||
self.assertEqual(
|
||||
1,
|
||||
self.env["res.partner"].search_count([('email', '=', "john2@test.be"), ('phone', '=', "+32455001122")]),
|
||||
"authenticated users can create a partner from an email from message_post",
|
||||
)
|
||||
# should not create another partner with same email
|
||||
res6 = self.url_open(
|
||||
url="/mail/message/post",
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"thread_model": self.test_public_record._name,
|
||||
"thread_id": self.test_public_record.id,
|
||||
"post_data": {
|
||||
"body": "test",
|
||||
"partner_emails": ["john2@test.be"],
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
self.assertEqual(res6.status_code, 200)
|
||||
self.assertEqual(
|
||||
1,
|
||||
self.env["res.partner"].search_count([('email', '=', "john2@test.be")]),
|
||||
"'mail/message/post' does not create another user if there's already a user with matching email",
|
||||
)
|
||||
|
||||
def test_thread_post_archived_record(self):
|
||||
self.authenticate(self.user_employee.login, self.user_employee.login)
|
||||
archived_partner = self.env["res.partner"].create({"name": "partner", "active": False})
|
||||
|
||||
# 1. posting a message
|
||||
data = self.make_jsonrpc_request("/mail/message/post", {
|
||||
"thread_model": "res.partner",
|
||||
"thread_id": archived_partner.id,
|
||||
"post_data": {
|
||||
"body": "A great message",
|
||||
}
|
||||
})
|
||||
message = next(filter(lambda m: m["id"] == data["message_id"], data["store_data"]["mail.message"]))
|
||||
self.assertEqual(["markup", "<p>A great message</p>"], message["body"])
|
||||
|
||||
# 2. attach a file
|
||||
response = self.url_open(
|
||||
"/mail/attachment/upload",
|
||||
{
|
||||
"csrf_token": http.Request.csrf_token(self),
|
||||
"thread_id": archived_partner.id,
|
||||
"thread_model": "res.partner",
|
||||
},
|
||||
files={"ufile": b""},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('mail_followers')
|
||||
class TestInvite(TestMailCommon):
|
||||
class TestInvite(MailCommon):
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_invite_email(self):
|
||||
|
|
@ -16,18 +16,38 @@ class TestInvite(TestMailCommon):
|
|||
'name': 'Valid Lelitre',
|
||||
'email': 'valid.lelitre@agrolait.com'})
|
||||
|
||||
mail_invite = self.env['mail.wizard.invite'].with_context({
|
||||
mail_invite = self.env['mail.followers.edit'].with_context({
|
||||
'default_res_model': 'mail.test.simple',
|
||||
'default_res_id': test_record.id
|
||||
}).with_user(self.user_employee).create({
|
||||
'partner_ids': [(4, test_partner.id), (4, self.user_admin.partner_id.id)],
|
||||
'send_mail': True})
|
||||
with self.mock_mail_gateway():
|
||||
mail_invite.add_followers()
|
||||
'default_res_ids': [test_record.id],
|
||||
}).with_user(self.user_employee).create({'partner_ids': [(4, test_partner.id), (4, self.user_admin.partner_id.id)],
|
||||
'notify': True})
|
||||
with self.mock_mail_app(), self.mock_mail_gateway():
|
||||
mail_invite.edit_followers()
|
||||
|
||||
# check added followers and that emails were sent
|
||||
# Check added followers and that notifications are sent.
|
||||
# Admin notification preference is inbox so the notification must be of inbox type
|
||||
# while partner_employee must receive it by email.
|
||||
self.assertEqual(test_record.message_partner_ids,
|
||||
test_partner | self.user_admin.partner_id)
|
||||
self.assertEqual(len(self._new_msgs), 1)
|
||||
self.assertEqual(len(self._mails), 1)
|
||||
self.assertSentEmail(self.partner_employee, [test_partner])
|
||||
self.assertSentEmail(self.partner_employee, [self.partner_admin])
|
||||
self.assertEqual(len(self._mails), 2)
|
||||
self.assertNotSentEmail([self.partner_admin])
|
||||
self.assertNotified(
|
||||
self._new_msgs[0],
|
||||
[{'partner': self.partner_admin, 'type': 'inbox', 'is_read': False}]
|
||||
)
|
||||
|
||||
# Remove followers
|
||||
mail_remove = self.env['mail.followers.edit'].with_context({
|
||||
'default_res_model': 'mail.test.simple',
|
||||
'default_res_ids': [test_record.id],
|
||||
}).with_user(self.user_employee).create({
|
||||
"operation": "remove",
|
||||
'partner_ids': [(4, test_partner.id), (4, self.user_admin.partner_id.id)]})
|
||||
|
||||
with self.mock_mail_app(), self.mock_mail_gateway():
|
||||
mail_remove.edit_followers()
|
||||
|
||||
# Check removed followers and that notifications are sent.
|
||||
self.assertEqual(test_record.message_partner_ids, self.env["res.partner"])
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.base.tests.test_ir_actions import TestServerActionsBase
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('ir_actions')
|
||||
class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
|
||||
class TestServerActionsEmail(MailCommon, TestServerActionsBase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestServerActionsEmail, self).setUp()
|
||||
|
|
@ -61,6 +61,17 @@ class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
|
|||
self.action.with_context(self.context).run()
|
||||
self.assertEqual(self.test_partner.message_partner_ids, self.env.ref('base.partner_admin') | random_partner)
|
||||
|
||||
def test_action_followers_warning(self):
|
||||
self.test_partner.message_unsubscribe(self.test_partner.message_partner_ids.ids)
|
||||
self.action.write({
|
||||
'state': 'followers',
|
||||
"followers_type": "generic",
|
||||
"followers_partner_field_name": "user_id.name"
|
||||
})
|
||||
self.assertEqual(self.action.warning, "The field 'Salesperson > Name' is not a partner field.")
|
||||
self.action.write({"followers_partner_field_name": "parent_id.child_ids"})
|
||||
self.assertEqual(self.action.warning, False)
|
||||
|
||||
def test_action_message_post(self):
|
||||
# initial state
|
||||
self.assertEqual(len(self.test_partner.message_ids), 1,
|
||||
|
|
@ -78,7 +89,10 @@ class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
|
|||
with self.assertSinglePostNotifications(
|
||||
[{'partner': self.test_partner, 'type': 'email', 'status': 'ready'}],
|
||||
message_info={'content': 'Hello %s' % self.test_partner.name,
|
||||
'message_type': 'notification',
|
||||
'mail_mail_values': {
|
||||
'author_id': self.env.user.partner_id,
|
||||
},
|
||||
'message_type': 'auto_comment',
|
||||
'subtype': 'mail.mt_comment',
|
||||
}
|
||||
):
|
||||
|
|
@ -95,7 +109,7 @@ class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
|
|||
with self.assertSinglePostNotifications(
|
||||
[{'partner': self.test_partner, 'type': 'email', 'status': 'ready'}],
|
||||
message_info={'content': 'Hello %s' % self.test_partner.name,
|
||||
'message_type': 'notification',
|
||||
'message_type': 'auto_comment',
|
||||
'subtype': 'mail.mt_note',
|
||||
}
|
||||
):
|
||||
|
|
@ -117,6 +131,18 @@ class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
|
|||
self.assertEqual(self.env['mail.activity'].search_count([]), before_count + 1)
|
||||
self.assertEqual(self.env['mail.activity'].search_count([('summary', '=', 'TestNew')]), 1)
|
||||
|
||||
def test_action_next_activity_warning(self):
|
||||
self.action.write({
|
||||
'state': 'next_activity',
|
||||
'activity_user_type': 'generic',
|
||||
"activity_user_field_name": "user_id.name",
|
||||
'activity_type_id': self.env.ref('mail.mail_activity_data_meeting').id,
|
||||
'activity_summary': 'TestNew',
|
||||
})
|
||||
self.assertEqual(self.action.warning, "The field 'Salesperson > Name' is not a user field.")
|
||||
self.action.write({"activity_user_field_name": "parent_id.user_id"})
|
||||
self.assertEqual(self.action.warning, False)
|
||||
|
||||
def test_action_next_activity_due_date(self):
|
||||
""" Make sure we don't crash if a due date is set without a type. """
|
||||
self.action.write({
|
||||
|
|
@ -132,3 +158,66 @@ class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
|
|||
self.assertFalse(run_res, 'ir_actions_server: create next activity action correctly finished should return False')
|
||||
self.assertEqual(self.env['mail.activity'].search_count([]), before_count + 1)
|
||||
self.assertEqual(self.env['mail.activity'].search_count([('summary', '=', 'TestNew')]), 1)
|
||||
|
||||
def test_action_next_activity_from_x2m_user(self):
|
||||
self.test_partner.user_ids = self.user_demo | self.user_admin
|
||||
self.action.write({
|
||||
'state': 'next_activity',
|
||||
'activity_user_type': 'generic',
|
||||
'activity_user_field_name': 'user_ids',
|
||||
'activity_type_id': self.env.ref('mail.mail_activity_data_meeting').id,
|
||||
'activity_summary': 'TestNew',
|
||||
})
|
||||
before_count = self.env['mail.activity'].search_count([])
|
||||
run_res = self.action.with_context(self.context).run()
|
||||
self.assertFalse(run_res, 'ir_actions_server: create next activity action correctly finished should return False')
|
||||
self.assertEqual(self.env['mail.activity'].search_count([]), before_count + 1)
|
||||
self.assertRecordValues(
|
||||
self.env['mail.activity'].search([('res_model', '=', 'res.partner'), ('res_id', '=', self.test_partner.id)]),
|
||||
[{
|
||||
'summary': 'TestNew',
|
||||
'user_id': self.user_demo.id, # the first user found
|
||||
}],
|
||||
)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
|
||||
def test_action_send_mail_without_mail_thread(self):
|
||||
""" Check running a server action to send an email with custom layout on a non mail.thread model """
|
||||
no_thread_record = self.env['mail.test.nothread'].create({'name': 'Test NoMailThread', 'customer_id': self.test_partner.id})
|
||||
no_thread_template = self._create_template(
|
||||
'mail.test.nothread',
|
||||
{
|
||||
'email_from': 'someone@example.com',
|
||||
'partner_to': '{{ object.customer_id.id }}',
|
||||
'subject': 'About {{ object.name }}',
|
||||
'body_html': '<p>Hello <t t-out="object.name"/></p>',
|
||||
'email_layout_xmlid': 'mail.mail_notification_layout',
|
||||
}
|
||||
)
|
||||
|
||||
# update action: send an email
|
||||
self.action.write({
|
||||
'mail_post_method': 'email',
|
||||
'state': 'mail_post',
|
||||
'model_id': self.env['ir.model'].search([('model', '=', 'mail.test.nothread')], limit=1).id,
|
||||
'model_name': 'mail.test.nothread',
|
||||
'template_id': no_thread_template.id,
|
||||
})
|
||||
|
||||
with self.mock_mail_gateway(), self.mock_mail_app():
|
||||
action_ctx = {
|
||||
'active_model': 'mail.test.nothread',
|
||||
'active_id': no_thread_record.id,
|
||||
}
|
||||
self.action.with_context(action_ctx).run()
|
||||
|
||||
mail = self.assertMailMail(
|
||||
self.test_partner,
|
||||
None,
|
||||
content='Hello Test NoMailThread',
|
||||
fields_values={
|
||||
'email_from': 'someone@example.com',
|
||||
'subject': 'About Test NoMailThread',
|
||||
}
|
||||
)
|
||||
self.assertNotIn('Powered by', mail.body_html, 'Body should contain the notification layout')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
import base64
|
||||
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.tests import tagged, users
|
||||
|
||||
|
||||
@tagged("ir_attachment")
|
||||
class TestAttachment(MailCommon):
|
||||
|
||||
@users("employee")
|
||||
def test_register_as_main_attachment(self):
|
||||
""" Test 'register_as_main_attachment', especially the multi support """
|
||||
records_model1 = self.env["mail.test.simple.main.attachment"].create([
|
||||
{
|
||||
"name": f"First model {idx}",
|
||||
}
|
||||
for idx in range(5)
|
||||
])
|
||||
records_model2 = self.env["mail.test.gateway.main.attachment"].create([
|
||||
{
|
||||
"name": f"Second model {idx}",
|
||||
}
|
||||
for idx in range(5)
|
||||
])
|
||||
record_nomain = self.env["mail.test.simple"].create({"name": "No Main Attachment"})
|
||||
attachments = self.env["ir.attachment"].create([
|
||||
{
|
||||
"datas": base64.b64encode(b'AttContent'),
|
||||
"name": f"AttachName_{record.name}.pdf",
|
||||
"mimetype": "application/pdf",
|
||||
"res_id": record.id,
|
||||
"res_model": record._name,
|
||||
}
|
||||
for record in records_model1
|
||||
] + [
|
||||
{
|
||||
"datas": base64.b64encode(b'AttContent'),
|
||||
"name": f"AttachName_{record.name}.pdf",
|
||||
"mimetype": "application/pdf",
|
||||
"res_id": record.id,
|
||||
"res_model": record._name,
|
||||
}
|
||||
for record in records_model2
|
||||
] + [
|
||||
{
|
||||
"datas": base64.b64encode(b'AttContent'),
|
||||
"name": "AttachName_free.pdf",
|
||||
"mimetype": "application/pdf",
|
||||
}, {
|
||||
"datas": base64.b64encode(b'AttContent'),
|
||||
"name": f"AttachName_{record_nomain.name}.pdf",
|
||||
"mimetype": "application/pdf",
|
||||
"res_id": record_nomain.id,
|
||||
"res_model": record_nomain._name,
|
||||
}
|
||||
])
|
||||
attachments.register_as_main_attachment()
|
||||
for record, attachment in zip(records_model1, attachments[:5]):
|
||||
self.assertEqual(record.message_main_attachment_id, attachment)
|
||||
for record, attachment in zip(records_model2, attachments[5:10]):
|
||||
self.assertEqual(record.message_main_attachment_id, attachment)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,730 @@
|
|||
from datetime import date, datetime, timedelta, timezone
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from freezegun import freeze_time
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytz
|
||||
import random
|
||||
|
||||
from odoo import fields, tests
|
||||
from odoo.addons.mail.models.mail_activity import MailActivity
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.test_mail.tests.test_mail_activity import TestActivityCommon
|
||||
from odoo.tests import tagged, users
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('mail_activity', 'mail_activity_mixin')
|
||||
class TestActivityMixin(TestActivityCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.user_utc = mail_new_test_user(
|
||||
cls.env,
|
||||
name='User UTC',
|
||||
login='User UTC',
|
||||
)
|
||||
cls.user_utc.tz = 'UTC'
|
||||
|
||||
cls.user_australia = mail_new_test_user(
|
||||
cls.env,
|
||||
name='user Australia',
|
||||
login='user Australia',
|
||||
)
|
||||
cls.user_australia.tz = 'Australia/Sydney'
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_activity_mixin(self):
|
||||
self.user_employee.tz = self.user_admin.tz
|
||||
with self.with_user('employee'):
|
||||
self.test_record = self.env['mail.test.activity'].browse(self.test_record.id)
|
||||
self.assertEqual(len(self.test_record.message_ids), 1)
|
||||
self.assertEqual(self.test_record.env.user, self.user_employee)
|
||||
|
||||
now_utc = datetime.now(pytz.UTC)
|
||||
now_user = now_utc.astimezone(pytz.timezone(self.env.user.tz or 'UTC'))
|
||||
today_user = now_user.date()
|
||||
|
||||
# Test various scheduling of activities
|
||||
act1 = self.test_record.activity_schedule(
|
||||
'test_mail.mail_act_test_todo',
|
||||
today_user + relativedelta(days=1),
|
||||
user_id=self.user_admin.id)
|
||||
self.assertEqual(act1.automated, True)
|
||||
|
||||
act_type = self.env.ref('test_mail.mail_act_test_todo')
|
||||
self.assertEqual(self.test_record.activity_summary, act_type.summary)
|
||||
self.assertEqual(self.test_record.activity_state, 'planned')
|
||||
self.assertEqual(self.test_record.activity_user_id, self.user_admin)
|
||||
|
||||
act2 = self.test_record.activity_schedule(
|
||||
'test_mail.mail_act_test_meeting',
|
||||
today_user + relativedelta(days=-1),
|
||||
user_id=self.user_employee.id,
|
||||
)
|
||||
self.assertEqual(self.test_record.activity_state, 'overdue')
|
||||
# `activity_user_id` is defined as `fields.Many2one('res.users', 'Responsible User', related='activity_ids.user_id')`
|
||||
# it therefore relies on the natural order of `activity_ids`, according to which activity comes first.
|
||||
# As we just created the activity, its not yet in the right order.
|
||||
# We force it by invalidating it so it gets fetched from database, in the right order.
|
||||
self.test_record.invalidate_recordset(['activity_ids'])
|
||||
self.assertEqual(self.test_record.activity_user_id, self.user_employee)
|
||||
|
||||
act3 = self.test_record.activity_schedule(
|
||||
'test_mail.mail_act_test_todo',
|
||||
today_user + relativedelta(days=3),
|
||||
user_id=self.user_employee.id,
|
||||
)
|
||||
self.assertEqual(self.test_record.activity_state, 'overdue')
|
||||
# `activity_user_id` is defined as `fields.Many2one('res.users', 'Responsible User', related='activity_ids.user_id')`
|
||||
# it therefore relies on the natural order of `activity_ids`, according to which activity comes first.
|
||||
# As we just created the activity, its not yet in the right order.
|
||||
# We force it by invalidating it so it gets fetched from database, in the right order.
|
||||
self.test_record.invalidate_recordset(['activity_ids'])
|
||||
self.assertEqual(self.test_record.activity_user_id, self.user_employee)
|
||||
|
||||
self.test_record.invalidate_recordset()
|
||||
self.assertEqual(self.test_record.activity_ids, act1 | act2 | act3)
|
||||
|
||||
# Perform todo activities for admin
|
||||
self.test_record.activity_feedback(
|
||||
['test_mail.mail_act_test_todo'],
|
||||
user_id=self.user_admin.id,
|
||||
feedback='Test feedback 1',
|
||||
)
|
||||
self.assertEqual(self.test_record.activity_ids, act2 | act3)
|
||||
self.assertFalse(act1.active)
|
||||
|
||||
# Reschedule all activities, should update the record state
|
||||
self.assertEqual(self.test_record.activity_state, 'overdue')
|
||||
self.test_record.activity_reschedule(
|
||||
['test_mail.mail_act_test_meeting', 'test_mail.mail_act_test_todo'],
|
||||
date_deadline=today_user + relativedelta(days=3)
|
||||
)
|
||||
self.assertEqual(self.test_record.activity_state, 'planned')
|
||||
|
||||
# Perform todo activities for remaining people
|
||||
self.test_record.activity_feedback(
|
||||
['test_mail.mail_act_test_todo'],
|
||||
feedback='Test feedback 2')
|
||||
self.assertFalse(act3.active)
|
||||
|
||||
# Setting activities as done should delete them and post messages
|
||||
self.assertEqual(self.test_record.activity_ids, act2)
|
||||
self.assertEqual(len(self.test_record.message_ids), 3)
|
||||
self.assertEqual(len(self.test_record.message_ids), 3)
|
||||
feedback2, feedback1, _create_log = self.test_record.message_ids
|
||||
self.assertEqual((feedback2 + feedback1).subtype_id, self.env.ref('mail.mt_activities'))
|
||||
|
||||
# Unlink meeting activities
|
||||
self.test_record.activity_unlink(['test_mail.mail_act_test_meeting'])
|
||||
|
||||
# Canceling activities should simply remove them
|
||||
self.assertEqual(self.test_record.activity_ids, self.env['mail.activity'])
|
||||
self.assertEqual(len(self.test_record.message_ids), 3, 'Should not produce additional message')
|
||||
self.assertFalse(self.test_record.activity_state)
|
||||
self.assertFalse(act2.exists())
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_activity_mixin_not_only_automated(self):
|
||||
|
||||
# Schedule activity and create manual activity
|
||||
act_type_todo = self.env.ref('test_mail.mail_act_test_todo')
|
||||
auto_act = self.test_record.activity_schedule(
|
||||
'test_mail.mail_act_test_todo',
|
||||
date_deadline=date.today() + relativedelta(days=1),
|
||||
)
|
||||
man_act = self.env['mail.activity'].create({
|
||||
'activity_type_id': act_type_todo.id,
|
||||
'res_id': self.test_record.id,
|
||||
'res_model_id': self.env['ir.model']._get_id(self.test_record._name),
|
||||
'date_deadline': date.today() + relativedelta(days=1)
|
||||
})
|
||||
self.assertEqual(auto_act.automated, True)
|
||||
self.assertEqual(man_act.automated, False)
|
||||
|
||||
# Test activity reschedule on not only automated activities
|
||||
self.test_record.activity_reschedule(
|
||||
['test_mail.mail_act_test_todo'],
|
||||
date_deadline=date.today() + relativedelta(days=2),
|
||||
only_automated=False
|
||||
)
|
||||
self.assertEqual(auto_act.date_deadline, date.today() + relativedelta(days=2))
|
||||
self.assertEqual(man_act.date_deadline, date.today() + relativedelta(days=2))
|
||||
|
||||
# Test activity feedback on not only automated activities
|
||||
self.test_record.activity_feedback(
|
||||
['test_mail.mail_act_test_todo'],
|
||||
feedback='Test feedback',
|
||||
only_automated=False
|
||||
)
|
||||
self.assertEqual(self.test_record.activity_ids, self.env['mail.activity'])
|
||||
self.assertFalse(auto_act.active)
|
||||
self.assertFalse(man_act.active)
|
||||
|
||||
# Test activity unlink on not only automated activities
|
||||
auto_act = self.test_record.activity_schedule(
|
||||
'test_mail.mail_act_test_todo',
|
||||
)
|
||||
man_act = self.env['mail.activity'].create({
|
||||
'activity_type_id': act_type_todo.id,
|
||||
'res_id': self.test_record.id,
|
||||
'res_model_id': self.env['ir.model']._get_id(self.test_record._name)
|
||||
})
|
||||
self.test_record.activity_unlink(['test_mail.mail_act_test_todo'], only_automated=False)
|
||||
self.assertEqual(self.test_record.activity_ids, self.env['mail.activity'])
|
||||
self.assertFalse(auto_act.exists())
|
||||
self.assertFalse(man_act.exists())
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_activity_mixin_archive(self):
|
||||
rec = self.test_record.with_user(self.user_employee)
|
||||
new_act = rec.activity_schedule(
|
||||
'test_mail.mail_act_test_todo',
|
||||
user_id=self.user_admin.id,
|
||||
)
|
||||
self.assertEqual(rec.activity_ids, new_act)
|
||||
rec.action_archive()
|
||||
self.assertEqual(rec.active, False)
|
||||
self.assertEqual(rec.activity_ids, new_act)
|
||||
rec.action_unarchive()
|
||||
self.assertEqual(rec.active, True)
|
||||
self.assertEqual(rec.activity_ids, new_act)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_activity_mixin_archive_user(self):
|
||||
"""
|
||||
Test when archiving an user, we unlink all his related activities
|
||||
"""
|
||||
test_users = self.env['res.users']
|
||||
for i in range(5):
|
||||
test_users += mail_new_test_user(self.env, name=f'test_user_{i}', login=f'test_password_{i}')
|
||||
for user in test_users:
|
||||
self.test_record.activity_schedule(user_id=user.id)
|
||||
archived_users = self.env['res.users'].browse(x.id for x in random.sample(test_users, 2)) # pick 2 users to archive
|
||||
archived_users.action_archive()
|
||||
active_users = test_users - archived_users
|
||||
|
||||
# archive user with company disabled
|
||||
user_admin = self.user_admin
|
||||
user_employee_c2 = self.user_employee_c2
|
||||
self.assertIn(self.company_2, user_admin.company_ids)
|
||||
self.test_record.env['ir.rule'].create({
|
||||
'model_id': self.env.ref('test_mail.model_mail_test_activity').id,
|
||||
'domain_force': "[('company_id', 'in', company_ids)]"
|
||||
})
|
||||
self.test_record.activity_schedule(user_id=user_employee_c2.id)
|
||||
user_employee_c2.with_user(user_admin).with_context(
|
||||
allowed_company_ids=(user_admin.company_ids - self.company_2).ids
|
||||
).action_archive()
|
||||
archived_users += user_employee_c2
|
||||
|
||||
self.assertFalse(any(archived_users.mapped('active')), "Users should be archived.")
|
||||
|
||||
# activities of active users shouldn't be touched, each has exactly 1 activity present
|
||||
activities = self.env['mail.activity'].search([('user_id', 'in', active_users.ids)])
|
||||
self.assertEqual(len(activities), 3, "We should have only 3 activities in total linked to our active users")
|
||||
self.assertEqual(activities.mapped('user_id'), active_users,
|
||||
"We should have 3 different users linked to the activities of the active users")
|
||||
|
||||
# ensure the user's activities are removed
|
||||
activities = self.env['mail.activity'].search([('user_id', 'in', archived_users.ids)])
|
||||
self.assertFalse(activities, "Activities of archived users should be deleted.")
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_activity_mixin_reschedule_user(self):
|
||||
rec = self.test_record.with_user(self.user_employee)
|
||||
rec.activity_schedule(
|
||||
'test_mail.mail_act_test_todo',
|
||||
user_id=self.user_admin.id)
|
||||
self.assertEqual(rec.activity_ids[0].user_id, self.user_admin)
|
||||
|
||||
# reschedule its own should not alter other's activities
|
||||
rec.activity_reschedule(
|
||||
['test_mail.mail_act_test_todo'],
|
||||
user_id=self.user_employee.id,
|
||||
new_user_id=self.user_employee.id)
|
||||
self.assertEqual(rec.activity_ids[0].user_id, self.user_admin)
|
||||
|
||||
rec.activity_reschedule(
|
||||
['test_mail.mail_act_test_todo'],
|
||||
user_id=self.user_admin.id,
|
||||
new_user_id=self.user_employee.id)
|
||||
self.assertEqual(rec.activity_ids[0].user_id, self.user_employee)
|
||||
|
||||
@users('employee')
|
||||
def test_feedback_w_attachments(self):
|
||||
test_record = self.env['mail.test.activity'].browse(self.test_record.ids)
|
||||
|
||||
activity = self.env['mail.activity'].create({
|
||||
'activity_type_id': 1,
|
||||
'res_id': test_record.id,
|
||||
'res_model_id': self.env['ir.model']._get_id('mail.test.activity'),
|
||||
'summary': 'Test',
|
||||
})
|
||||
attachments = self.env['ir.attachment'].create([{
|
||||
'name': 'test',
|
||||
'res_name': 'test',
|
||||
'res_model': 'mail.activity',
|
||||
'res_id': activity.id,
|
||||
'datas': 'test',
|
||||
}, {
|
||||
'name': 'test2',
|
||||
'res_name': 'test',
|
||||
'res_model': 'mail.activity',
|
||||
'res_id': activity.id,
|
||||
'datas': 'testtest',
|
||||
}])
|
||||
|
||||
# Checking if the attachment has been forwarded to the message
|
||||
# when marking an activity as "Done"
|
||||
activity.action_feedback()
|
||||
activity_message = test_record.message_ids[0]
|
||||
self.assertEqual(set(activity_message.attachment_ids.ids), set(attachments.ids))
|
||||
for attachment in attachments:
|
||||
self.assertEqual(attachment.res_id, activity_message.id)
|
||||
self.assertEqual(attachment.res_model, activity_message._name)
|
||||
|
||||
@users('employee')
|
||||
def test_feedback_chained_current_date(self):
|
||||
frozen_now = datetime(2021, 10, 10, 14, 30, 15)
|
||||
|
||||
test_record = self.env['mail.test.activity'].browse(self.test_record.ids)
|
||||
first_activity = self.env['mail.activity'].create({
|
||||
'activity_type_id': self.env.ref('test_mail.mail_act_test_chained_1').id,
|
||||
'date_deadline': frozen_now + relativedelta(days=-2),
|
||||
'res_id': test_record.id,
|
||||
'res_model_id': self.env['ir.model']._get_id('mail.test.activity'),
|
||||
'summary': 'Test',
|
||||
})
|
||||
first_activity_id = first_activity.id
|
||||
|
||||
with freeze_time(frozen_now):
|
||||
first_activity.action_feedback(feedback='Done')
|
||||
self.assertFalse(first_activity.active)
|
||||
|
||||
# check chained activity
|
||||
new_activity = test_record.activity_ids
|
||||
self.assertNotEqual(new_activity.id, first_activity_id)
|
||||
self.assertEqual(new_activity.summary, 'Take the second step.')
|
||||
self.assertEqual(new_activity.date_deadline, frozen_now.date() + relativedelta(days=10))
|
||||
|
||||
@users('employee')
|
||||
def test_feedback_chained_previous(self):
|
||||
self.env.ref('test_mail.mail_act_test_chained_2').sudo().write({'delay_from': 'previous_activity'})
|
||||
frozen_now = datetime(2021, 10, 10, 14, 30, 15)
|
||||
|
||||
test_record = self.env['mail.test.activity'].browse(self.test_record.ids)
|
||||
first_activity = self.env['mail.activity'].create({
|
||||
'activity_type_id': self.env.ref('test_mail.mail_act_test_chained_1').id,
|
||||
'date_deadline': frozen_now + relativedelta(days=-2),
|
||||
'res_id': test_record.id,
|
||||
'res_model_id': self.env['ir.model']._get_id('mail.test.activity'),
|
||||
'summary': 'Test',
|
||||
})
|
||||
first_activity_id = first_activity.id
|
||||
|
||||
with freeze_time(frozen_now):
|
||||
first_activity.action_feedback(feedback='Done')
|
||||
self.assertFalse(first_activity.active)
|
||||
|
||||
# check chained activity
|
||||
new_activity = test_record.activity_ids
|
||||
self.assertNotEqual(new_activity.id, first_activity_id)
|
||||
self.assertEqual(new_activity.summary, 'Take the second step.')
|
||||
self.assertEqual(new_activity.date_deadline, frozen_now.date() + relativedelta(days=8),
|
||||
'New deadline should take into account original activity deadline, not current date')
|
||||
|
||||
def test_mail_activity_state(self):
|
||||
"""Create 3 activity for 2 different users in 2 different timezones.
|
||||
|
||||
User UTC (+0h)
|
||||
User Australia (+11h)
|
||||
Today datetime: 1/1/2020 16h
|
||||
|
||||
Activity 1 & User UTC
|
||||
1/1/2020 - 16h UTC -> The state is today
|
||||
|
||||
Activity 2 & User Australia
|
||||
1/1/2020 - 16h UTC
|
||||
2/1/2020 - 1h Australia -> State is overdue
|
||||
|
||||
Activity 3 & User UTC
|
||||
1/1/2020 - 23h UTC -> The state is today
|
||||
"""
|
||||
record = self.env['mail.test.activity'].create({'name': 'Record'})
|
||||
|
||||
with freeze_time(datetime(2020, 1, 1, 16)):
|
||||
today_utc = datetime.today()
|
||||
activity_1 = self.env['mail.activity'].create({
|
||||
'summary': 'Test',
|
||||
'activity_type_id': 1,
|
||||
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
|
||||
'res_id': record.id,
|
||||
'date_deadline': today_utc,
|
||||
'user_id': self.user_utc.id,
|
||||
})
|
||||
|
||||
activity_2 = activity_1.copy()
|
||||
activity_2.user_id = self.user_australia
|
||||
activity_3 = activity_1.copy()
|
||||
activity_3.date_deadline += relativedelta(hours=7)
|
||||
|
||||
self.assertEqual(activity_1.state, 'today')
|
||||
self.assertEqual(activity_2.state, 'overdue')
|
||||
self.assertEqual(activity_3.state, 'today')
|
||||
|
||||
@users('employee')
|
||||
def test_mail_activity_mixin_search_activity_user_id_false(self):
|
||||
"""Test the search method on the "activity_user_id" when searching for non-set user"""
|
||||
MailTestActivity = self.env['mail.test.activity']
|
||||
test_records = self.test_record | self.test_record_2
|
||||
self.assertFalse(test_records.activity_ids)
|
||||
self.assertEqual(MailTestActivity.search([('activity_user_id', '=', False)]), test_records)
|
||||
|
||||
self.env['mail.activity'].create({
|
||||
'summary': 'Test',
|
||||
'activity_type_id': self.env.ref('test_mail.mail_act_test_todo').id,
|
||||
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
|
||||
'res_id': self.test_record.id,
|
||||
})
|
||||
self.assertEqual(MailTestActivity.search([('activity_user_id', '!=', True)]), self.test_record_2)
|
||||
|
||||
def test_mail_activity_mixin_search_exception_decoration(self):
|
||||
"""Test the search on "activity_exception_decoration".
|
||||
|
||||
Domain ('activity_exception_decoration', '!=', False) should only return
|
||||
records that have at least one warning/danger activity.
|
||||
"""
|
||||
record_warning, record_normal, _ = self.test_record, self.test_record_2, self.env['mail.test.activity'].create({'name': 'No activities'})
|
||||
record_warning.activity_schedule('mail.mail_activity_data_warning', user_id=self.env.user.id)
|
||||
record_normal.activity_schedule('test_mail.mail_act_test_todo', user_id=self.env.user.id)
|
||||
|
||||
records = self.env['mail.test.activity'].search([('activity_exception_decoration', '!=', False)])
|
||||
self.assertEqual(records, record_warning)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests')
|
||||
def test_mail_activity_mixin_search_state_basic(self):
|
||||
"""Test the search method on the "activity_state".
|
||||
|
||||
Test all the operators and also test the case where the "activity_state" is
|
||||
different because of the timezone. There's also a tricky case for which we
|
||||
"reverse" the domain for performance purpose.
|
||||
"""
|
||||
|
||||
# Create some records without activity schedule on it for testing
|
||||
self.env['mail.test.activity'].create([
|
||||
{'name': 'Record %i' % record_i}
|
||||
for record_i in range(5)
|
||||
])
|
||||
|
||||
origin_1, origin_2 = self.env['mail.test.activity'].search([], limit=2)
|
||||
activity_type = self.env.ref('test_mail.mail_act_test_todo')
|
||||
|
||||
with freeze_time(datetime(2020, 1, 1, 16)):
|
||||
today_utc = datetime.today()
|
||||
origin_1_activity_1 = self.env['mail.activity'].create({
|
||||
'summary': 'Test',
|
||||
'activity_type_id': activity_type.id,
|
||||
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
|
||||
'res_id': origin_1.id,
|
||||
'date_deadline': today_utc,
|
||||
'user_id': self.user_utc.id,
|
||||
})
|
||||
|
||||
origin_1_activity_2 = origin_1_activity_1.copy()
|
||||
origin_1_activity_2.user_id = self.user_australia
|
||||
origin_1_activity_3 = origin_1_activity_1.copy()
|
||||
origin_1_activity_3.date_deadline += relativedelta(hours=8)
|
||||
|
||||
self.assertEqual(origin_1_activity_1.state, 'today')
|
||||
self.assertEqual(origin_1_activity_2.state, 'overdue')
|
||||
self.assertEqual(origin_1_activity_3.state, 'today')
|
||||
|
||||
origin_2_activity_1 = self.env['mail.activity'].create({
|
||||
'summary': 'Test',
|
||||
'activity_type_id': activity_type.id,
|
||||
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
|
||||
'res_id': origin_2.id,
|
||||
'date_deadline': today_utc + relativedelta(hours=8),
|
||||
'user_id': self.user_utc.id,
|
||||
})
|
||||
|
||||
origin_2_activity_2 = origin_2_activity_1.copy()
|
||||
origin_2_activity_2.user_id = self.user_australia
|
||||
origin_2_activity_3 = origin_2_activity_1.copy()
|
||||
origin_2_activity_3.date_deadline -= relativedelta(hours=8)
|
||||
origin_2_activity_4 = origin_2_activity_1.copy()
|
||||
origin_2_activity_4.date_deadline = datetime(2020, 1, 2, 0, 0, 0)
|
||||
|
||||
self.assertEqual(origin_2_activity_1.state, 'planned')
|
||||
self.assertEqual(origin_2_activity_2.state, 'today')
|
||||
self.assertEqual(origin_2_activity_3.state, 'today')
|
||||
self.assertEqual(origin_2_activity_4.state, 'planned')
|
||||
|
||||
all_activity_mixin_record = self.env['mail.test.activity'].search([])
|
||||
|
||||
result = self.env['mail.test.activity'].search([('activity_state', '=', 'today')])
|
||||
self.assertTrue(len(result) > 0)
|
||||
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state == 'today'))
|
||||
|
||||
result = self.env['mail.test.activity'].search([('activity_state', 'in', ('today', 'overdue'))])
|
||||
self.assertTrue(len(result) > 0)
|
||||
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state in ('today', 'overdue')))
|
||||
|
||||
result = self.env['mail.test.activity'].search([('activity_state', 'not in', ('today'))])
|
||||
self.assertTrue(len(result) > 0)
|
||||
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state != 'today'))
|
||||
|
||||
result = self.env['mail.test.activity'].search([('activity_state', '=', False)])
|
||||
self.assertTrue(len(result) >= 3, "There is more than 3 records without an activity schedule on it")
|
||||
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: not p.activity_state))
|
||||
|
||||
result = self.env['mail.test.activity'].search([('activity_state', 'not in', ('planned', 'overdue', 'today'))])
|
||||
self.assertTrue(len(result) >= 3, "There is more than 3 records without an activity schedule on it")
|
||||
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: not p.activity_state))
|
||||
|
||||
# test tricky case when the domain will be reversed in the search method
|
||||
# because of falsy value
|
||||
result = self.env['mail.test.activity'].search([('activity_state', 'not in', ('today', False))])
|
||||
self.assertTrue(len(result) > 0)
|
||||
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state not in ('today', False)))
|
||||
|
||||
result = self.env['mail.test.activity'].search([('activity_state', 'in', ('today', False))])
|
||||
self.assertTrue(len(result) > 0)
|
||||
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state in ('today', False)))
|
||||
|
||||
# Check that activity done are not taken into account by group and search by activity_state.
|
||||
Model = self.env['mail.test.activity']
|
||||
search_params = {
|
||||
'domain': [('id', 'in', (origin_1 | origin_2).ids), ('activity_state', '=', 'overdue')]}
|
||||
read_group_params = {
|
||||
'domain': [('id', 'in', (origin_1 | origin_2).ids)],
|
||||
'groupby': ['activity_state'],
|
||||
'aggregates': ['__count'],
|
||||
}
|
||||
self.assertEqual(Model.search(**search_params), origin_1)
|
||||
self.assertEqual(
|
||||
{(e['activity_state'], e['__count']) for e in Model.formatted_read_group(**read_group_params)},
|
||||
{('today', 1), ('overdue', 1)})
|
||||
origin_1_activity_2.action_feedback(feedback='Done')
|
||||
self.assertFalse(Model.search(**search_params))
|
||||
self.assertEqual(
|
||||
{(e['activity_state'], e['__count']) for e in Model.formatted_read_group(**read_group_params)},
|
||||
{('today', 2)})
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests')
|
||||
def test_mail_activity_mixin_search_state_different_day_but_close_time(self):
|
||||
"""Test the case where there's less than 24 hours between the deadline and now_tz,
|
||||
but one day of difference (e.g. 23h 01/01/2020 & 1h 02/02/2020). So the state
|
||||
should be "planned" and not "today". This case was tricky to implement in SQL
|
||||
that's why it has its own test.
|
||||
"""
|
||||
|
||||
# Create some records without activity schedule on it for testing
|
||||
self.env['mail.test.activity'].create([
|
||||
{'name': 'Record %i' % record_i}
|
||||
for record_i in range(5)
|
||||
])
|
||||
|
||||
origin_1 = self.env['mail.test.activity'].search([], limit=1)
|
||||
|
||||
with freeze_time(datetime(2020, 1, 1, 23)):
|
||||
today_utc = datetime.today()
|
||||
origin_1_activity_1 = self.env['mail.activity'].create({
|
||||
'summary': 'Test',
|
||||
'activity_type_id': 1,
|
||||
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
|
||||
'res_id': origin_1.id,
|
||||
'date_deadline': today_utc + relativedelta(hours=2),
|
||||
'user_id': self.user_utc.id,
|
||||
})
|
||||
|
||||
self.assertEqual(origin_1_activity_1.state, 'planned')
|
||||
result = self.env['mail.test.activity'].search([('activity_state', '=', 'today')])
|
||||
self.assertNotIn(origin_1, result, 'The activity state miss calculated during the search')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_my_activity_flow_employee(self):
|
||||
Activity = self.env['mail.activity']
|
||||
date_today = date.today()
|
||||
Activity.create({
|
||||
'activity_type_id': self.env.ref('test_mail.mail_act_test_todo').id,
|
||||
'date_deadline': date_today,
|
||||
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
|
||||
'res_id': self.test_record.id,
|
||||
'user_id': self.user_admin.id,
|
||||
})
|
||||
Activity.create({
|
||||
'activity_type_id': self.env.ref('test_mail.mail_act_test_call').id,
|
||||
'date_deadline': date_today + relativedelta(days=1),
|
||||
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
|
||||
'res_id': self.test_record.id,
|
||||
'user_id': self.user_employee.id,
|
||||
})
|
||||
|
||||
test_record_1 = self.env['mail.test.activity'].with_context(self._test_context).create({'name': 'Test 1'})
|
||||
test_record_1_late_activity = Activity.create({
|
||||
'activity_type_id': self.env.ref('test_mail.mail_act_test_todo').id,
|
||||
'date_deadline': date_today,
|
||||
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
|
||||
'res_id': test_record_1.id,
|
||||
'user_id': self.user_employee.id,
|
||||
})
|
||||
with self.with_user('employee'):
|
||||
record = self.env['mail.test.activity'].search([('my_activity_date_deadline', '=', date_today)])
|
||||
self.assertEqual(test_record_1, record)
|
||||
test_record_1_late_activity._action_done()
|
||||
record = self.env['mail.test.activity'].with_context(active_test=False).search([
|
||||
('my_activity_date_deadline', '=', date_today)
|
||||
])
|
||||
self.assertFalse(record, "Should not find record if the only late activity is done")
|
||||
|
||||
@users('employee')
|
||||
def test_record_unlink(self):
|
||||
test_record = self.test_record.with_user(self.env.user)
|
||||
act1 = test_record.activity_schedule(summary='Active', user_id=self.env.uid)
|
||||
act2 = test_record.activity_schedule(summary='Archived', active=False, user_id=self.env.uid)
|
||||
test_record.unlink()
|
||||
self.assertFalse((act1 + act2).exists(), 'Removing records should remove activities, even archived')
|
||||
|
||||
@users('employee')
|
||||
def test_record_unlinked_orphan_activities(self):
|
||||
"""Test the fix preventing error on corrupted database where activities without related record are present."""
|
||||
test_record = self.env['mail.test.activity'].with_context(
|
||||
self._test_context).create({'name': 'Test'}).with_user(self.user_employee)
|
||||
act = test_record.activity_schedule("test_mail.mail_act_test_todo", summary='Orphan activity')
|
||||
act.action_done()
|
||||
# Delete the record while preventing the cascade deletion of the activity to simulate a corrupted database
|
||||
with patch.object(MailActivity, 'unlink', lambda self: None):
|
||||
test_record.unlink()
|
||||
self.assertTrue(act.exists())
|
||||
self.assertFalse(act.active)
|
||||
self.assertFalse(test_record.exists())
|
||||
|
||||
self.env.invalidate_all()
|
||||
self.assertEqual(
|
||||
self.env['mail.activity'].with_user(self.user_admin).with_context(active_test=False).search(
|
||||
[('active', '=', False)]), act,
|
||||
'Should consider unassigned activity on removed record = access without crash'
|
||||
)
|
||||
self.env.invalidate_all()
|
||||
_dummy = act.with_user(self.user_admin).read(['summary'])
|
||||
|
||||
|
||||
@tests.tagged('mail_activity', 'mail_activity_mixin')
|
||||
class TestORM(TestActivityCommon):
|
||||
"""Test for read_progress_bar"""
|
||||
|
||||
def test_groupby_activity_state_progress_bar_behavior(self):
|
||||
""" Test activity_state groupby logic on mail.test.lead when 'activity_state'
|
||||
is present multiple times in the groupby field list. """
|
||||
lead_timedelta_setup = [0, 0, -2, -2, -2, 2]
|
||||
|
||||
leads = self.env["mail.test.lead"].create([
|
||||
{"name": f"CRM Lead {i}"}
|
||||
for i in range(1, len(lead_timedelta_setup) + 1)
|
||||
])
|
||||
|
||||
with freeze_time("2025-05-21 10:00:00"):
|
||||
self.env["mail.activity"].create([
|
||||
{
|
||||
"date_deadline": datetime.now(timezone.utc) + timedelta(days=delta_days),
|
||||
"res_id": lead.id,
|
||||
"res_model_id": self.env["ir.model"]._get_id("mail.test.lead"),
|
||||
"summary": f"Test activity for CRM lead {lead.id}",
|
||||
"user_id": self.env.user.id,
|
||||
} for lead, delta_days in zip(leads, lead_timedelta_setup)
|
||||
])
|
||||
|
||||
# grouping by 'activity_state' and 'activity_state' as the progress bar
|
||||
domain = [("name", "!=", "")]
|
||||
groupby = "activity_state"
|
||||
progress_bar = {
|
||||
"field": "activity_state",
|
||||
"colors": {
|
||||
"overdue": "danger",
|
||||
"today": "warning",
|
||||
"planned": "success",
|
||||
},
|
||||
}
|
||||
progressbars = self.env["mail.test.lead"].read_progress_bar(
|
||||
domain=domain, group_by=groupby, progress_bar=progress_bar,
|
||||
)
|
||||
|
||||
self.assertEqual(len(progressbars), 3)
|
||||
expected_progressbars = {
|
||||
"overdue": {"overdue": 3, "today": 0, "planned": 0},
|
||||
"today": {"overdue": 0, "today": 2, "planned": 0},
|
||||
"planned": {"overdue": 0, "today": 0, "planned": 1},
|
||||
}
|
||||
self.assertEqual(dict(progressbars), expected_progressbars)
|
||||
|
||||
def test_week_grouping(self):
|
||||
"""The labels associated to each record in read_progress_bar should match
|
||||
the ones from read_group, even in edge cases like en_US locale on sundays
|
||||
"""
|
||||
MailTestActivityCtx = self.env['mail.test.activity'].with_context({"lang": "en_US"})
|
||||
|
||||
# Don't mistake fields date and date_deadline:
|
||||
# * date is just a random value
|
||||
# * date_deadline defines activity_state
|
||||
with freeze_time("2024-09-24 10:00:00"):
|
||||
self.env['mail.test.activity'].create({
|
||||
'date': '2021-05-02',
|
||||
'name': "Yesterday, all my troubles seemed so far away",
|
||||
}).activity_schedule(
|
||||
'test_mail.mail_act_test_todo',
|
||||
summary="Make another test super asap (yesterday)",
|
||||
date_deadline=fields.Date.context_today(MailTestActivityCtx) - timedelta(days=7),
|
||||
user_id=self.env.uid,
|
||||
)
|
||||
self.env['mail.test.activity'].create({
|
||||
'date': '2021-05-09',
|
||||
'name': "Things we said today",
|
||||
}).activity_schedule(
|
||||
'test_mail.mail_act_test_todo',
|
||||
summary="Make another test asap",
|
||||
date_deadline=fields.Date.context_today(MailTestActivityCtx),
|
||||
user_id=self.env.uid,
|
||||
)
|
||||
self.env['mail.test.activity'].create({
|
||||
'date': '2021-05-16',
|
||||
'name': "Tomorrow Never Knows",
|
||||
}).activity_schedule(
|
||||
'test_mail.mail_act_test_todo',
|
||||
summary="Make a test tomorrow",
|
||||
date_deadline=fields.Date.context_today(MailTestActivityCtx) + timedelta(days=7),
|
||||
user_id=self.env.uid,
|
||||
)
|
||||
|
||||
domain = [('date', "!=", False)]
|
||||
groupby = "date:week"
|
||||
progress_bar = {
|
||||
'field': 'activity_state',
|
||||
'colors': {
|
||||
"overdue": 'danger',
|
||||
"today": 'warning',
|
||||
"planned": 'success',
|
||||
}
|
||||
}
|
||||
|
||||
# call read_group to compute group names
|
||||
groups = MailTestActivityCtx.formatted_read_group(domain, groupby=[groupby])
|
||||
progressbars = MailTestActivityCtx.read_progress_bar(domain, group_by=groupby, progress_bar=progress_bar)
|
||||
self.assertEqual(len(groups), 3)
|
||||
self.assertEqual(len(progressbars), 3)
|
||||
|
||||
# format the read_progress_bar result to get a dictionary under this
|
||||
# format: {activity_state: group_name}; the original format
|
||||
# (after read_progress_bar) is {group_name: {activity_state: count}}
|
||||
pg_groups = {
|
||||
next(state for state, count in data.items() if count): group_name
|
||||
for group_name, data in progressbars.items()
|
||||
}
|
||||
|
||||
self.assertEqual(groups[0][groupby][0], pg_groups["overdue"])
|
||||
self.assertEqual(groups[1][groupby][0], pg_groups["today"])
|
||||
self.assertEqual(groups[2][groupby][0], pg_groups["planned"])
|
||||
|
|
@ -0,0 +1,410 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo import fields
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.mail.tests.common_activity import ActivityScheduleCase
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tests import Form, tagged, users
|
||||
from odoo.tools.misc import format_date
|
||||
|
||||
|
||||
@tagged('mail_activity', 'mail_activity_plan')
|
||||
class TestActivitySchedule(ActivityScheduleCase):
|
||||
""" Test plan and activity schedule
|
||||
|
||||
- activity scheduling on a single record and in batch
|
||||
- plan scheduling on a single record and in batch
|
||||
- plan creation and consistency
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# add some triggered and suggested next activitities
|
||||
cls.test_type_1, cls.test_type_2, cls.test_type_3 = cls.env['mail.activity.type'].create([
|
||||
{'name': 'TestAct1', 'res_model': 'mail.test.activity',},
|
||||
{'name': 'TestAct2', 'res_model': 'mail.test.activity',},
|
||||
{'name': 'TestAct3', 'res_model': 'mail.test.activity',},
|
||||
])
|
||||
cls.test_type_1.write({
|
||||
'chaining_type': 'trigger',
|
||||
'delay_count': 2,
|
||||
'delay_from': 'current_date',
|
||||
'delay_unit': 'days',
|
||||
'triggered_next_type_id': cls.test_type_2.id,
|
||||
})
|
||||
cls.test_type_2.write({
|
||||
'chaining_type': 'suggest',
|
||||
'delay_count': 3,
|
||||
'delay_unit': 'weeks',
|
||||
'suggested_next_type_ids': [(4, cls.test_type_1.id), (4, cls.test_type_3.id)],
|
||||
})
|
||||
|
||||
# prepare plans
|
||||
cls.plan_party = cls.env['mail.activity.plan'].create({
|
||||
'name': 'Test Plan A Party',
|
||||
'res_model': 'mail.test.activity',
|
||||
'template_ids': [
|
||||
(0, 0, {
|
||||
'activity_type_id': cls.activity_type_todo.id,
|
||||
'delay_count': 1,
|
||||
'delay_from': 'before_plan_date',
|
||||
'delay_unit': 'days',
|
||||
'responsible_type': 'on_demand',
|
||||
'sequence': 10,
|
||||
'summary': 'Book a place',
|
||||
}), (0, 0, {
|
||||
'activity_type_id': cls.activity_type_todo.id,
|
||||
'delay_count': 1,
|
||||
'delay_from': 'after_plan_date',
|
||||
'delay_unit': 'weeks',
|
||||
'responsible_id': cls.user_admin.id,
|
||||
'responsible_type': 'other',
|
||||
'sequence': 20,
|
||||
'summary': 'Invite special guest',
|
||||
}),
|
||||
],
|
||||
})
|
||||
cls.plan_onboarding = cls.env['mail.activity.plan'].create({
|
||||
'name': 'Test Onboarding',
|
||||
'res_model': 'mail.test.activity',
|
||||
'template_ids': [
|
||||
(0, 0, {
|
||||
'activity_type_id': cls.activity_type_todo.id,
|
||||
'delay_count': 3,
|
||||
'delay_from': 'before_plan_date',
|
||||
'delay_unit': 'days',
|
||||
'responsible_id': cls.user_admin.id,
|
||||
'responsible_type': 'other',
|
||||
'sequence': 10,
|
||||
'summary': 'Plan training',
|
||||
}), (0, 0, {
|
||||
'activity_type_id': cls.activity_type_todo.id,
|
||||
'delay_count': 2,
|
||||
'delay_from': 'after_plan_date',
|
||||
'delay_unit': 'weeks',
|
||||
'responsible_id': cls.user_admin.id,
|
||||
'responsible_type': 'other',
|
||||
'sequence': 20,
|
||||
'summary': 'Training',
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
# test records
|
||||
cls.reference_now = fields.Datetime.from_string('2023-09-30 14:00:00')
|
||||
cls.test_records = cls.env['mail.test.activity'].create([
|
||||
{
|
||||
'date': cls.reference_now + timedelta(days=(idx - 10)),
|
||||
'email_from': f'customer.activity.{idx}@test.example.com',
|
||||
'name': f'test_record_{idx}'
|
||||
} for idx in range(5)
|
||||
])
|
||||
|
||||
# some big dict comparisons
|
||||
cls.maxDiff = None
|
||||
|
||||
@users('employee')
|
||||
def test_activity_schedule(self):
|
||||
""" Test schedule of an activity on a single or multiple records. """
|
||||
test_records_all = [self.test_records[0], self.test_records[:3]]
|
||||
# sanity check: new activity created without specifying activiy type
|
||||
# will have default type of the available activity type with the lowest sequence, then lowest id
|
||||
self.assertTrue(self.activity_type_todo.sequence < self.activity_type_call.sequence)
|
||||
for test_idx, test_case in enumerate(['mono', 'multi']):
|
||||
test_records = test_records_all[test_idx].with_env(self.env)
|
||||
with self.subTest(test_case=test_case, test_records=test_records):
|
||||
# 1. SCHEDULE ACTIVITIES
|
||||
with freeze_time(self.reference_now):
|
||||
form = self._instantiate_activity_schedule_wizard(test_records)
|
||||
form.summary = 'Write specification'
|
||||
form.note = '<p>Useful link ...</p>'
|
||||
form.activity_user_id = self.user_admin
|
||||
with self._mock_activities():
|
||||
form.save().action_schedule_activities()
|
||||
|
||||
for record in test_records:
|
||||
self.assertActivityCreatedOnRecord(record, {
|
||||
'activity_type_id': self.activity_type_todo,
|
||||
'automated': False,
|
||||
'date_deadline': self.reference_now.date() + timedelta(days=4), # activity type delay
|
||||
'note': '<p>Useful link ...</p>',
|
||||
'summary': 'Write specification',
|
||||
'user_id': self.user_admin,
|
||||
})
|
||||
|
||||
# 2. LOG DONE ACTIVITIES
|
||||
with freeze_time(self.reference_now):
|
||||
form = self._instantiate_activity_schedule_wizard(test_records)
|
||||
form.activity_type_id = self.activity_type_call
|
||||
form.activity_user_id = self.user_admin
|
||||
with self._mock_activities(), freeze_time(self.reference_now):
|
||||
form.save().with_context(
|
||||
mail_activity_quick_update=True
|
||||
).action_schedule_activities_done()
|
||||
|
||||
for record in test_records:
|
||||
self.assertActivityDoneOnRecord(record, self.activity_type_call)
|
||||
|
||||
# 3. CONTINUE WITH SCHEDULE ACTIVITIES
|
||||
# implies deadline addition on top of previous activities
|
||||
with freeze_time(self.reference_now):
|
||||
form = self._instantiate_activity_schedule_wizard(test_records)
|
||||
form.activity_type_id = self.activity_type_call
|
||||
form.activity_user_id = self.user_admin
|
||||
with self._mock_activities():
|
||||
form.save().with_context(
|
||||
mail_activity_quick_update=True
|
||||
).action_schedule_activities()
|
||||
|
||||
for record in test_records:
|
||||
self.assertActivityCreatedOnRecord(record, {
|
||||
'activity_type_id': self.activity_type_call,
|
||||
'automated': False,
|
||||
'date_deadline': self.reference_now.date() + timedelta(days=1), # activity call delay
|
||||
'note': False,
|
||||
'summary': 'TodoSumCallSummary',
|
||||
'user_id': self.user_admin,
|
||||
})
|
||||
|
||||
# global activity creation from tests
|
||||
self.assertEqual(len(self.test_records[0].activity_ids), 4)
|
||||
self.assertEqual(len(self.test_records[1].activity_ids), 2)
|
||||
self.assertEqual(len(self.test_records[2].activity_ids), 2)
|
||||
self.assertEqual(len(self.test_records[3].activity_ids), 0)
|
||||
self.assertEqual(len(self.test_records[4].activity_ids), 0)
|
||||
|
||||
@users('admin')
|
||||
def test_activity_schedule_rights_upload(self):
|
||||
user = mail_new_test_user(
|
||||
self.env,
|
||||
groups='base.group_public',
|
||||
login='bert',
|
||||
name='Bert Tartignole',
|
||||
)
|
||||
demo_record = self.env['mail.test.access'].create({'access': 'admin', 'name': 'Record'})
|
||||
form = self._instantiate_activity_schedule_wizard(demo_record)
|
||||
form.activity_type_id = self.env.ref('test_mail.mail_act_test_upload_document')
|
||||
with self.assertRaises(UserError):
|
||||
form.activity_user_id = user
|
||||
form.save()
|
||||
|
||||
@users('employee')
|
||||
def test_activity_schedule_norecord(self):
|
||||
""" Test scheduling free activities, supported if assigned user. """
|
||||
scheduler = self._instantiate_activity_schedule_wizard(None)
|
||||
self.assertEqual(scheduler.activity_type_id, self.activity_type_todo)
|
||||
with self._mock_activities():
|
||||
scheduler.save().action_schedule_activities()
|
||||
self.assertActivityValues(self._new_activities, {
|
||||
'res_id': False,
|
||||
'res_model': False,
|
||||
'summary': 'TodoSummary',
|
||||
'user_id': self.user_employee,
|
||||
})
|
||||
|
||||
# cannot scheduler unassigned personal activities
|
||||
scheduler = self._instantiate_activity_schedule_wizard(None)
|
||||
scheduler = scheduler.save()
|
||||
with self.assertRaises(ValidationError):
|
||||
scheduler.activity_user_id = False
|
||||
|
||||
def test_plan_copy(self):
|
||||
"""Test plan copy"""
|
||||
copied_plan = self.plan_onboarding.copy()
|
||||
self.assertEqual(copied_plan.name, f'{self.plan_onboarding.name} (copy)')
|
||||
self.assertEqual(len(copied_plan.template_ids), len(self.plan_onboarding.template_ids))
|
||||
|
||||
@users('employee')
|
||||
def test_plan_mode(self):
|
||||
""" Test the plan_mode that allows to preselect a compatible plan. """
|
||||
test_record = self.test_records[0].with_env(self.env)
|
||||
context = {
|
||||
'active_id': test_record.id,
|
||||
'active_ids': test_record.ids,
|
||||
'active_model': test_record._name
|
||||
}
|
||||
plan_mode_context = {**context, 'plan_mode': True}
|
||||
|
||||
with Form(self.env['mail.activity.schedule'].with_context(context)) as form:
|
||||
self.assertFalse(form.plan_id)
|
||||
with Form(self.env['mail.activity.schedule'].with_context(plan_mode_context)) as form:
|
||||
self.assertEqual(form.plan_id, self.plan_party)
|
||||
# should select only model-plans
|
||||
self.plan_party.res_model = 'res.partner'
|
||||
with Form(self.env['mail.activity.schedule'].with_context(plan_mode_context)) as form:
|
||||
self.assertEqual(form.plan_id, self.plan_onboarding)
|
||||
|
||||
@users('admin')
|
||||
def test_plan_next_activities(self):
|
||||
""" Test that next activities are displayed correctly. """
|
||||
test_plan = self.env['mail.activity.plan'].create({
|
||||
'name': 'Test Plan',
|
||||
'res_model': 'mail.test.activity',
|
||||
'template_ids': [
|
||||
(0, 0, {'activity_type_id': self.test_type_1.id}),
|
||||
(0, 0, {'activity_type_id': self.test_type_2.id}),
|
||||
(0, 0, {'activity_type_id': self.test_type_3.id}),
|
||||
],
|
||||
})
|
||||
# Assert expected next activities
|
||||
expected_next_activities = [['TestAct2'], ['TestAct1', 'TestAct3'], []]
|
||||
for template, expected_names in zip(test_plan.template_ids, expected_next_activities, strict=True):
|
||||
self.assertEqual(template.next_activity_ids.mapped('name'), expected_names)
|
||||
# Test the plan summary
|
||||
with self.subTest(test_case='Check plan summary'), \
|
||||
freeze_time(self.reference_now):
|
||||
form = self._instantiate_activity_schedule_wizard(self.test_records[0])
|
||||
form.plan_id = test_plan
|
||||
expected_values = [
|
||||
{'description': 'TestAct1', 'deadline': datetime(2023, 9, 30).date()},
|
||||
{'description': 'TestAct2', 'deadline': datetime(2023, 10, 21).date()},
|
||||
{'description': 'TestAct2', 'deadline': datetime(2023, 9, 30).date()},
|
||||
{'description': 'TestAct1', 'deadline': datetime(2023, 10, 2).date()},
|
||||
{'description': 'TestAct3', 'deadline': datetime(2023, 9, 30).date()},
|
||||
{'description': 'TestAct3', 'deadline': datetime(2023, 9, 30).date()},
|
||||
]
|
||||
for line, expected in zip(form.plan_schedule_line_ids._records, expected_values):
|
||||
with self.subTest(line=line, expected_values=expected):
|
||||
self.assertEqual(line['line_description'], expected['description'])
|
||||
self.assertEqual(line['line_date_deadline'], expected['deadline'])
|
||||
|
||||
@users('employee')
|
||||
def test_plan_schedule(self):
|
||||
""" Test schedule of a plan on a single or multiple records. """
|
||||
test_records_all = [self.test_records[0], self.test_records[:3]]
|
||||
for test_idx, test_case in enumerate(['mono', 'multi']):
|
||||
test_records = test_records_all[test_idx].with_env(self.env)
|
||||
with self.subTest(test_case=test_case, test_records=test_records), \
|
||||
freeze_time(self.reference_now):
|
||||
# No plan_date specified (-> self.reference_now is used), No responsible specified
|
||||
form = self._instantiate_activity_schedule_wizard(test_records)
|
||||
self.assertFalse(form.plan_schedule_line_ids)
|
||||
form.plan_id = self.plan_onboarding
|
||||
expected_values = [
|
||||
{'description': 'Plan training', 'deadline': datetime(2023, 9, 27).date()},
|
||||
{'description': 'Training', 'deadline': datetime(2023, 10, 14).date()},
|
||||
]
|
||||
for line, expected in zip(form.plan_schedule_line_ids._records, expected_values):
|
||||
self.assertEqual(line['line_description'], expected['description'])
|
||||
self.assertEqual(line['line_date_deadline'], expected['deadline'])
|
||||
self.assertTrue(form._get_modifier('plan_on_demand_user_id', 'invisible'))
|
||||
form.plan_id = self.plan_party
|
||||
expected_values = [
|
||||
{'description': 'Book a place', 'deadline': datetime(2023, 9, 29).date()},
|
||||
{'description': 'Invite special guest', 'deadline': datetime(2023, 10, 7).date()},
|
||||
]
|
||||
for line, expected in zip(form.plan_schedule_line_ids._records, expected_values):
|
||||
self.assertEqual(line['line_description'], expected['description'])
|
||||
self.assertEqual(line['line_date_deadline'], expected['deadline'])
|
||||
self.assertFalse(form._get_modifier('plan_on_demand_user_id', 'invisible'))
|
||||
with self._mock_activities():
|
||||
form.save().action_schedule_plan()
|
||||
|
||||
self.assertPlanExecution(
|
||||
self.plan_party, test_records,
|
||||
expected_deadlines=[(self.reference_now + relativedelta(days=-1)).date(),
|
||||
(self.reference_now + relativedelta(days=7)).date()])
|
||||
|
||||
# plan_date specified, responsible specified
|
||||
plan_date = self.reference_now.date() + relativedelta(days=14)
|
||||
responsible_id = self.user_admin
|
||||
form = self._instantiate_activity_schedule_wizard(test_records)
|
||||
form.plan_id = self.plan_party
|
||||
form.plan_date = plan_date
|
||||
form.plan_on_demand_user_id = self.env['res.users']
|
||||
self.assertTrue(form.has_error)
|
||||
self.assertIn(f'No responsible specified for {self.activity_type_todo.name}: Book a place',
|
||||
form.error)
|
||||
form.plan_on_demand_user_id = responsible_id
|
||||
self.assertFalse(form.has_error)
|
||||
deadline_1 = plan_date + relativedelta(days=-1)
|
||||
deadline_2 = plan_date + relativedelta(days=7)
|
||||
expected_values = [
|
||||
{'description': 'Book a place', 'deadline': deadline_1},
|
||||
{'description': 'Invite special guest', 'deadline': deadline_2},
|
||||
]
|
||||
for line, expected in zip(form.plan_schedule_line_ids._records, expected_values):
|
||||
self.assertEqual(line['line_description'], expected['description'])
|
||||
self.assertEqual(line['line_date_deadline'], expected['deadline'])
|
||||
with self._mock_activities():
|
||||
form.save().action_schedule_plan()
|
||||
|
||||
self.assertPlanExecution(
|
||||
self.plan_party, test_records,
|
||||
expected_deadlines=[plan_date + relativedelta(days=-1),
|
||||
plan_date + relativedelta(days=7)],
|
||||
expected_responsible=responsible_id)
|
||||
|
||||
@users('admin')
|
||||
def test_plan_setup_model_consistency(self):
|
||||
""" Test the model consistency of a plan.
|
||||
|
||||
Model consistency between activity_type - activity_template - plan:
|
||||
- a plan is restricted to a model
|
||||
- a plan contains activity plan templates which can be limited to some model
|
||||
through activity type
|
||||
"""
|
||||
# Setup independent activities type to avoid interference with existing data
|
||||
activity_type_1, activity_type_2, activity_type_3 = self.env['mail.activity.type'].create([
|
||||
{'name': 'Todo'},
|
||||
{'name': 'Call'},
|
||||
{'name': 'Partner-specific', 'res_model': 'res.partner'},
|
||||
])
|
||||
test_plan = self.env['mail.activity.plan'].create({
|
||||
'name': 'Test Plan',
|
||||
'res_model': 'mail.test.activity',
|
||||
'template_ids': [
|
||||
(0, 0, {'activity_type_id': activity_type_1.id}),
|
||||
(0, 0, {'activity_type_id': activity_type_2.id})
|
||||
],
|
||||
})
|
||||
|
||||
# ok, all activities generic
|
||||
test_plan.res_model = 'res.partner'
|
||||
test_plan.res_model = 'mail.test.activity'
|
||||
|
||||
with self.assertRaises(
|
||||
ValidationError,
|
||||
msg='Cannot set activity type to res.partner as linked to a plan of another model'):
|
||||
activity_type_1.res_model = 'res.partner'
|
||||
|
||||
activity_type_1.res_model = 'mail.test.activity'
|
||||
with self.assertRaises(
|
||||
ValidationError,
|
||||
msg='Cannot set plan to res.partner as using activities linked to another model'):
|
||||
test_plan.res_model = 'res.partner'
|
||||
|
||||
with self.assertRaises(
|
||||
ValidationError,
|
||||
msg='Cannot create activity template for res.partner as linked to a plan of another model'):
|
||||
self.env['mail.activity.plan.template'].create({
|
||||
'activity_type_id': activity_type_3.id,
|
||||
'plan_id': test_plan.id,
|
||||
})
|
||||
|
||||
@users('admin')
|
||||
def test_plan_setup_validation(self):
|
||||
""" Test plan consistency. """
|
||||
plan = self.env['mail.activity.plan'].create({
|
||||
'name': 'test',
|
||||
'res_model': 'mail.test.activity',
|
||||
})
|
||||
template = self.env['mail.activity.plan.template'].create({
|
||||
'activity_type_id': self.activity_type_todo.id,
|
||||
'plan_id': plan.id,
|
||||
'responsible_type': 'other',
|
||||
'responsible_id': self.user_admin.id,
|
||||
})
|
||||
template.responsible_type = 'on_demand'
|
||||
self.assertFalse(template.responsible_id)
|
||||
with self.assertRaises(
|
||||
ValidationError, msg='When selecting responsible "other", you must specify a responsible.'):
|
||||
template.responsible_type = 'other'
|
||||
template.write({'responsible_type': 'other', 'responsible_id': self.user_admin})
|
||||
1002
odoo-bringout-oca-ocb-test_mail/test_mail/tests/test_mail_alias.py
Normal file
1002
odoo-bringout-oca-ocb-test_mail/test_mail/tests/test_mail_alias.py
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,22 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
|
||||
from odoo.addons.mail.tests.common import MailCommon, mail_new_test_user
|
||||
from odoo.addons.test_mail.tests.common import TestRecipients
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import users
|
||||
|
||||
|
||||
@tagged('mail_composer_mixin')
|
||||
class TestMailComposerMixin(TestMailCommon, TestRecipients):
|
||||
class TestMailComposerMixin(MailCommon, TestRecipients):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailComposerMixin, cls).setUpClass()
|
||||
|
||||
# ensure employee can create partners, necessary for templates
|
||||
cls.user_employee.write({
|
||||
'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)],
|
||||
})
|
||||
super().setUpClass()
|
||||
|
||||
cls.mail_template = cls.env['mail.template'].create({
|
||||
'body_html': '<p>EnglishBody for <t t-out="object.name"/></p>',
|
||||
|
|
@ -30,6 +27,22 @@ class TestMailComposerMixin(TestMailCommon, TestRecipients):
|
|||
'customer_id': cls.partner_1.id,
|
||||
})
|
||||
|
||||
# Enable group-based template management
|
||||
cls.env['ir.config_parameter'].set_param('mail.restrict.template.rendering', True)
|
||||
|
||||
# User without the group "mail.group_mail_template_editor"
|
||||
cls.user_rendering_restricted = mail_new_test_user(
|
||||
cls.env,
|
||||
company_id=cls.company_admin.id,
|
||||
groups='base.group_user',
|
||||
login='user_rendering_restricted',
|
||||
name='Code Template Restricted User',
|
||||
notification_type='inbox',
|
||||
signature='--\nErnest'
|
||||
)
|
||||
cls.user_rendering_restricted.group_ids -= cls.env.ref('mail.group_mail_template_editor')
|
||||
cls.user_employee.group_ids += cls.env.ref('mail.group_mail_template_editor')
|
||||
|
||||
cls._activate_multi_lang(
|
||||
layout_arch_db='<body><t t-out="message.body"/> English Layout for <t t-esc="model_description"/></body>',
|
||||
lang_code='es_ES',
|
||||
|
|
@ -41,19 +54,86 @@ class TestMailComposerMixin(TestMailCommon, TestRecipients):
|
|||
def test_content_sync(self):
|
||||
""" Test updating template updates the dynamic fields accordingly. """
|
||||
source = self.test_record.with_env(self.env)
|
||||
template = self.mail_template.with_env(self.env)
|
||||
template_void = template.copy()
|
||||
template_void.write({
|
||||
'body_html': '<p><br /></p>',
|
||||
'lang': False,
|
||||
'subject': False,
|
||||
})
|
||||
|
||||
composer = self.env['mail.test.composer.mixin'].create({
|
||||
'name': 'Invite',
|
||||
'template_id': template.id,
|
||||
'source_ids': [(4, source.id)],
|
||||
})
|
||||
self.assertEqual(composer.body, template.body_html)
|
||||
self.assertTrue(composer.body_has_template_value)
|
||||
self.assertEqual(composer.lang, template.lang)
|
||||
self.assertEqual(composer.subject, template.subject)
|
||||
|
||||
# check rendering
|
||||
body = composer._render_field('body', source.ids)[source.id]
|
||||
self.assertEqual(body, f'<p>EnglishBody for {source.name}</p>')
|
||||
subject = composer._render_field('subject', source.ids)[source.id]
|
||||
self.assertEqual(subject, f'EnglishSubject for {source.name}')
|
||||
|
||||
# manual values > template default values
|
||||
composer.write({
|
||||
'body': '<p>CustomBody for <t t-out="object.name"/></p>',
|
||||
'subject': 'CustomSubject for {{ object.name }}',
|
||||
})
|
||||
self.assertFalse(composer.body_has_template_value)
|
||||
|
||||
body = composer._render_field('body', source.ids)[source.id]
|
||||
self.assertEqual(body, f'<p>CustomBody for {source.name}</p>')
|
||||
subject = composer._render_field('subject', source.ids)[source.id]
|
||||
self.assertEqual(subject, f'CustomSubject for {source.name}')
|
||||
|
||||
# template with void values: should not force void (TODO)
|
||||
composer.template_id = template_void.id
|
||||
self.assertEqual(composer.body, '<p>CustomBody for <t t-out="object.name"/></p>')
|
||||
self.assertFalse(composer.body_has_template_value)
|
||||
self.assertEqual(composer.lang, template.lang)
|
||||
self.assertEqual(composer.subject, 'CustomSubject for {{ object.name }}')
|
||||
|
||||
# reset template TOOD should reset
|
||||
composer.write({'template_id': False})
|
||||
self.assertFalse(composer.body)
|
||||
self.assertFalse(composer.body_has_template_value)
|
||||
self.assertFalse(composer.lang)
|
||||
self.assertFalse(composer.subject)
|
||||
|
||||
@users("user_rendering_restricted")
|
||||
def test_mail_composer_mixin_render_lang(self):
|
||||
""" Test _render_lang when rendering is involved, depending on template
|
||||
editor rights. """
|
||||
source = self.test_record.with_env(self.env)
|
||||
composer = self.env['mail.test.composer.mixin'].create({
|
||||
'description': '<p>Description for <t t-esc="object.name"/></p>',
|
||||
'name': 'Invite',
|
||||
'template_id': self.mail_template.id,
|
||||
'source_ids': [(4, source.id)],
|
||||
})
|
||||
self.assertEqual(composer.body, self.mail_template.body_html)
|
||||
self.assertEqual(composer.subject, self.mail_template.subject)
|
||||
self.assertFalse(composer.lang, 'Fixme: lang is not propagated currently')
|
||||
|
||||
subject = composer._render_field('subject', source.ids)[source.id]
|
||||
self.assertEqual(subject, f'EnglishSubject for {source.name}')
|
||||
body = composer._render_field('body', source.ids)[source.id]
|
||||
self.assertEqual(body, f'<p>EnglishBody for {source.name}</p>')
|
||||
# _render_lang should be ok when content is the same as template
|
||||
rendered = composer._render_lang(source.ids)
|
||||
self.assertEqual(rendered, {source.id: self.partner_1.lang})
|
||||
|
||||
# _render_lang should crash when content is dynamic and not coming from template
|
||||
composer.lang = " {{ 'en_US' }}"
|
||||
with self.assertRaises(AccessError):
|
||||
rendered = composer._render_lang(source.ids)
|
||||
|
||||
# _render_lang should not crash when content is not coming from template
|
||||
# but not dynamic and/or is actually the default computed based on partner
|
||||
for lang_value, expected in [
|
||||
(False, self.partner_1.lang), ("", self.partner_1.lang), ("fr_FR", "fr_FR")
|
||||
]:
|
||||
with self.subTest(lang_value=lang_value):
|
||||
composer.lang = lang_value
|
||||
rendered = composer._render_lang(source.ids)
|
||||
self.assertEqual(rendered, {source.id: expected})
|
||||
|
||||
@users("employee")
|
||||
def test_rendering_custom(self):
|
||||
|
|
@ -84,7 +164,6 @@ class TestMailComposerMixin(TestMailCommon, TestRecipients):
|
|||
source = self.test_record.with_env(self.env)
|
||||
composer = self.env['mail.test.composer.mixin'].create({
|
||||
'description': '<p>Description for <t t-esc="object.name"/></p>',
|
||||
'lang': '{{ object.customer_id.lang }}',
|
||||
'name': 'Invite',
|
||||
'template_id': self.mail_template.id,
|
||||
'source_ids': [(4, source.id)],
|
||||
|
|
@ -103,11 +182,22 @@ class TestMailComposerMixin(TestMailCommon, TestRecipients):
|
|||
|
||||
# ask for dynamic language computation
|
||||
subject = composer._render_field('subject', source.ids, compute_lang=True)[source.id]
|
||||
self.assertEqual(subject, f'EnglishSubject for {source.name}',
|
||||
'Fixme: translations are not done, as taking composer translations and not template one')
|
||||
self.assertEqual(subject, f'SpanishSubject for {source.name}',
|
||||
'Translation comes from the template, as both values equal')
|
||||
body = composer._render_field('body', source.ids, compute_lang=True)[source.id]
|
||||
self.assertEqual(body, f'<p>EnglishBody for {source.name}</p>',
|
||||
'Fixme: translations are not done, as taking composer translations and not template one'
|
||||
)
|
||||
self.assertEqual(body, f'<p>SpanishBody for {source.name}</p>',
|
||||
'Translation comes from the template, as both values equal')
|
||||
description = composer._render_field('description', source.ids)[source.id]
|
||||
self.assertEqual(description, f'<p>Description for {source.name}</p>')
|
||||
|
||||
# check default computation when 'lang' is void -> actually rerouted to template lang
|
||||
composer.lang = False
|
||||
subject = composer._render_field('subject', source.ids, compute_lang=True)[source.id]
|
||||
self.assertEqual(subject, f'SpanishSubject for {source.name}',
|
||||
'Translation comes from the template, as both values equal')
|
||||
|
||||
# check default computation when 'lang' is void in both -> main customer lang
|
||||
self.mail_template.lang = False
|
||||
subject = composer._render_field('subject', source.ids, compute_lang=True)[source.id]
|
||||
self.assertEqual(subject, f'SpanishSubject for {source.name}',
|
||||
'Translation comes from customer lang, being default when no value is rendered')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,558 @@
|
|||
from odoo.addons.mail.tests.common import mail_new_test_user, MailCommon
|
||||
from odoo.addons.test_mail.data.test_mail_data import MAIL_TEMPLATE, MAIL_TEMPLATE_SHORT
|
||||
from odoo.addons.test_mail.tests.common import TestRecipients
|
||||
from odoo.tools.mail import formataddr
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('mail_gateway', 'mail_flow', 'post_install', '-at_install')
|
||||
class TestMailFlow(MailCommon, TestRecipients):
|
||||
""" Test flows matching business cases with incoming / outgoing emails. """
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.user_employee_2 = mail_new_test_user(
|
||||
cls.env,
|
||||
company_id=cls.user_employee.company_id.id,
|
||||
email='eglantine@example.com',
|
||||
groups='base.group_user,base.group_partner_manager',
|
||||
login='employee2',
|
||||
name='Eglantine Employee',
|
||||
notification_type='email',
|
||||
signature='--\nEglantine',
|
||||
)
|
||||
cls.partner_employee_2 = cls.user_employee_2.partner_id
|
||||
cls.user_employee_3 = mail_new_test_user(
|
||||
cls.env,
|
||||
company_id=cls.user_employee.company_id.id,
|
||||
email='emmanuel@example.com',
|
||||
groups='base.group_user,base.group_partner_manager',
|
||||
login='employee3',
|
||||
name='Emmanuel Employee',
|
||||
notification_type='email',
|
||||
signature='--\nEmmanuel',
|
||||
)
|
||||
cls.partner_employee_3 = cls.user_employee_3.partner_id
|
||||
cls.user_portal = cls._create_portal_user()
|
||||
cls.partner_portal = cls.user_portal.partner_id
|
||||
|
||||
cls.test_emails = [
|
||||
# emails only
|
||||
'"Sylvie Lelitre" <sylvie.lelitre@zboing.com>',
|
||||
'"Josiane Quichopoils" <accounting@zboing.com>',
|
||||
'pay@zboing.com',
|
||||
'invoicing@zboing.com',
|
||||
# existing partners
|
||||
'"Robert Brutijus" <robert@zboing.com>',
|
||||
# existing portal users
|
||||
'"Portal Zboing" <portal@zboing.com>',
|
||||
]
|
||||
cls.test_emails_normalized = [
|
||||
'sylvie.lelitre@zboing.com', 'accounting@zboing.com', 'invoicing@zboing.com',
|
||||
'pay@zboing.com', 'robert@zboing.com', 'portal@zboing.com',
|
||||
]
|
||||
cls.customer_zboing = cls.env['res.partner'].create({
|
||||
'email': cls.test_emails[4],
|
||||
'name': 'Robert Brutijus',
|
||||
'phone': '+32455335577',
|
||||
})
|
||||
cls.user_portal_zboing = mail_new_test_user(
|
||||
cls.env,
|
||||
email=cls.test_emails[5],
|
||||
groups='base.group_portal',
|
||||
login='portal_zboing',
|
||||
name='Portal Zboing',
|
||||
)
|
||||
cls.customer_portal_zboing = cls.user_portal_zboing.partner_id
|
||||
|
||||
# lead@test.mycompany.com will cause the creation of new mail.test.lead
|
||||
cls.mail_test_lead_model = cls.env['ir.model']._get('mail.test.lead')
|
||||
cls.alias = cls.env['mail.alias'].create({
|
||||
'alias_domain_id': cls.mail_alias_domain.id,
|
||||
'alias_contact': 'everyone',
|
||||
'alias_model_id': cls.mail_test_lead_model.id,
|
||||
'alias_name': 'lead',
|
||||
})
|
||||
# help@test.mycompany.com will cause the creation of new mail.test.ticket.mc
|
||||
cls.ticket_template = cls.env['mail.template'].create({
|
||||
'auto_delete': True,
|
||||
'body_html': '<p>Received <t t-out="object.name"/></p>',
|
||||
'email_from': '{{ object.user_id.email_formatted or user.email_formatted }}',
|
||||
'lang': '{{ object.customer_id.lang }}',
|
||||
'model_id': cls.env['ir.model']._get_id('mail.test.ticket.partner'),
|
||||
'name': 'Received',
|
||||
'subject': 'Received {{ object.name }}',
|
||||
'use_default_to': True,
|
||||
})
|
||||
cls.container = cls.env['mail.test.container.mc'].create({
|
||||
# triggers automatic answer yay !
|
||||
'alias_defaults': {'state': 'new', 'state_template_id': cls.ticket_template.id},
|
||||
'alias_name': 'help',
|
||||
'company_id': cls.user_employee.company_id.id,
|
||||
'name': 'help',
|
||||
})
|
||||
cls.container.alias_id.write({
|
||||
'alias_model_id': cls.env['ir.model']._get_id('mail.test.ticket.partner')
|
||||
})
|
||||
|
||||
def test_assert_initial_values(self):
|
||||
""" Assert base values for tests """
|
||||
self.assertEqual(
|
||||
self.env['res.partner'].search([('email_normalized', 'in', self.test_emails_normalized)]),
|
||||
self.customer_zboing + self.customer_portal_zboing,
|
||||
)
|
||||
|
||||
def test_lead_email_to_email(self):
|
||||
""" Test email-to-email (e.g. gmail) usage """
|
||||
self.user_employee.notification_type = 'email'
|
||||
lead = self.env['mail.test.lead'].with_user(self.user_employee).create({
|
||||
'partner_id': self.customer_zboing.id,
|
||||
})
|
||||
# employee posts, pinging the customer
|
||||
recipients = lead._message_get_suggested_recipients(
|
||||
reply_discussion=True, no_create=False,
|
||||
)
|
||||
self.assertEqual(recipients, [{
|
||||
'create_values': {},
|
||||
'email': self.customer_zboing.email_normalized,
|
||||
'name': self.customer_zboing.name,
|
||||
'partner_id': self.customer_zboing.id,
|
||||
}])
|
||||
with self.mock_mail_gateway(), self.mock_mail_app():
|
||||
emp_msg = lead.message_post(
|
||||
body='Hello @customer',
|
||||
message_type='comment',
|
||||
partner_ids=[recipients[0]['partner_id']],
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
reply_to_emp = emp_msg.reply_to
|
||||
self.assertEqual(reply_to_emp, formataddr((self.user_employee.name, f'{self.alias_catchall}@{self.alias_domain}')))
|
||||
self.assertSMTPEmailsSent(
|
||||
mail_server=self.mail_server_notification,
|
||||
msg_from=formataddr(
|
||||
(self.partner_employee.name, f'{self.default_from}@{self.alias_domain}')
|
||||
),
|
||||
smtp_from=self.mail_server_notification.from_filter,
|
||||
smtp_to_list=[self.customer_zboing.email_normalized],
|
||||
msg_to_lst=[self.customer_zboing.email_formatted],
|
||||
)
|
||||
|
||||
# customer replies from their email reader, adds a CC and someone in the To
|
||||
cust_reply = self.gateway_mail_reply_from_smtp_email(
|
||||
MAIL_TEMPLATE_SHORT, [self.customer_zboing.email_normalized], reply_all=True,
|
||||
add_to_lst=[self.test_emails[0]], cc=self.test_emails[1],
|
||||
)
|
||||
self.assertMailNotifications(
|
||||
cust_reply,
|
||||
[
|
||||
{
|
||||
'content': "Eli alla à l'eau",
|
||||
'message_type': 'email',
|
||||
'message_values': {
|
||||
'author_id': self.customer_zboing,
|
||||
'email_from': self.customer_zboing.email_formatted,
|
||||
'incoming_email_cc': self.test_emails[1],
|
||||
# be sure to not have catchall.test inside the incoming_email_to !
|
||||
'incoming_email_to': self.test_emails[0],
|
||||
'notified_partner_ids': self.user_employee.partner_id,
|
||||
# only recognized partners
|
||||
'partner_ids': self.env['res.partner'],
|
||||
'reply_to': formataddr((self.customer_zboing.name, f'{self.alias_catchall}@{self.alias_domain}')),
|
||||
'subject': 'Re: False',
|
||||
'subtype_id': self.env.ref('mail.mt_comment'),
|
||||
},
|
||||
'notif': [{'partner': self.user_employee.partner_id, 'type': 'email'}],
|
||||
},
|
||||
],
|
||||
)
|
||||
self.assertSMTPEmailsSent(
|
||||
mail_server=self.mail_server_notification,
|
||||
msg_from=formataddr(
|
||||
(self.customer_zboing.name, f'{self.default_from}@{self.alias_domain}')
|
||||
),
|
||||
smtp_from=self.mail_server_notification.from_filter,
|
||||
smtp_to_list=[self.user_employee.email_normalized],
|
||||
# customers in To/Cc of reply added in envelope to keep them in discussions
|
||||
msg_to_lst=[self.user_employee.email_formatted, self.test_emails[0], self.test_emails[1]],
|
||||
msg_cc_lst=[],
|
||||
)
|
||||
|
||||
# employee replies from their email reader, adds their colleague
|
||||
emp_reply = self.gateway_mail_reply_from_smtp_email(
|
||||
MAIL_TEMPLATE_SHORT, [self.user_employee.email_normalized], reply_all=True,
|
||||
cc=self.partner_employee_2.email_formatted,
|
||||
)
|
||||
self.assertMailNotifications(
|
||||
emp_reply,
|
||||
[
|
||||
{
|
||||
'content': "Eli alla à l'eau",
|
||||
'message_type': 'email',
|
||||
'message_values': {
|
||||
'author_id': self.partner_employee,
|
||||
'email_from': self.partner_employee.email_formatted,
|
||||
'incoming_email_cc': self.partner_employee_2.email_formatted,
|
||||
# be sure not to have catchall reply-to ! customers are in 'To' due to Reply-All
|
||||
'incoming_email_to': f'{self.test_emails[0]}, {self.test_emails[1]}',
|
||||
'notified_partner_ids': self.customer_zboing,
|
||||
# only recognized partners
|
||||
'partner_ids': self.partner_employee_2,
|
||||
'subject': 'Re: Re: False',
|
||||
'subtype_id': self.env.ref('mail.mt_comment'),
|
||||
},
|
||||
# partner_employee_2 received an email, hence no duplicate notification
|
||||
'notif': [{'partner': self.customer_zboing, 'type': 'email'}],
|
||||
},
|
||||
],
|
||||
)
|
||||
self.assertSMTPEmailsSent(
|
||||
mail_server=self.mail_server_notification,
|
||||
msg_from=formataddr(
|
||||
(self.partner_employee.name, f'{self.default_from}@{self.alias_domain}')
|
||||
),
|
||||
smtp_from=self.mail_server_notification.from_filter,
|
||||
smtp_to_list=[self.customer_zboing.email_normalized],
|
||||
# customers are still in discussion
|
||||
msg_to_lst=[self.customer_zboing.email_formatted, self.partner_employee_2.email_formatted, self.test_emails[0], self.test_emails[1]],
|
||||
msg_cc_lst=[],
|
||||
)
|
||||
|
||||
def test_lead_mailgateway(self):
|
||||
""" Flow of this test
|
||||
* incoming email creating a lead -> email set as first message
|
||||
* a salesperson is assigned
|
||||
* - he adds followers (internal and portal)
|
||||
* - he replies through chatter, using suggested recipients
|
||||
* - customer replies, adding other people
|
||||
|
||||
Tested features
|
||||
* cc / to support
|
||||
* suggested recipients computation
|
||||
* outgoing SMTP envelope
|
||||
|
||||
Recipients
|
||||
* incoming: From: sylvie (email) - To: employee, accounting (email) - Cc: pay (email), portal (portal)
|
||||
* reply: creates partner for sylvie and pay through suggested recipients
|
||||
* customer reply: Cc: invoicing (email) and robert (partner)
|
||||
"""
|
||||
# incoming customer email: lead alias + recipients (to + cc)
|
||||
# ------------------------------------------------------------
|
||||
email_to = f'lead@{self.alias_domain}, {self.test_emails[1]}, {self.partner_employee.email_formatted}'
|
||||
email_to_filtered = f'{self.test_emails[1]}, {self.partner_employee.email_formatted}'
|
||||
email_cc = f'{self.test_emails[2]}, {self.test_emails[5]}'
|
||||
with self.mock_mail_gateway(), self.mock_mail_app():
|
||||
lead = self.format_and_process(
|
||||
MAIL_TEMPLATE,
|
||||
self.test_emails[0],
|
||||
email_to,
|
||||
cc=email_cc,
|
||||
subject='Inquiry',
|
||||
target_model='mail.test.lead',
|
||||
)
|
||||
self.assertEqual(lead.email_cc, email_cc, 'Filled by mail.thread.cc mixin')
|
||||
self.assertEqual(lead.email_from, self.test_emails[0])
|
||||
self.assertEqual(lead.name, 'Inquiry')
|
||||
self.assertFalse(lead.partner_id)
|
||||
# followers
|
||||
self.assertFalse(lead.message_partner_ids)
|
||||
# messages
|
||||
self.assertEqual(len(lead.message_ids), 1, 'Incoming email should be only message, no creation message')
|
||||
incoming_email = lead.message_ids
|
||||
self.assertMailNotifications(
|
||||
incoming_email,
|
||||
[
|
||||
{
|
||||
'content': 'Please call me as soon as possible',
|
||||
'message_type': 'email',
|
||||
'message_values': {
|
||||
'author_id': self.env['res.partner'],
|
||||
'email_from': self.test_emails[0],
|
||||
'incoming_email_cc': email_cc,
|
||||
'incoming_email_to': email_to_filtered,
|
||||
'mail_server_id': self.env['ir.mail_server'],
|
||||
'parent_id': self.env['mail.message'],
|
||||
'notified_partner_ids': self.env['res.partner'],
|
||||
# only recognized partners
|
||||
'partner_ids': self.partner_employee + self.customer_portal_zboing,
|
||||
'subject': 'Inquiry',
|
||||
'subtype_id': self.env.ref('mail.mt_comment'),
|
||||
},
|
||||
'notif': [], # no notif, mailgateway sets recipients without notification
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
# user is assigned, should notify him
|
||||
with self.mock_mail_gateway(), self.mock_mail_app():
|
||||
lead.write({'user_id': self.user_employee.id})
|
||||
lead_as_emp = lead.with_user(self.user_employee.id)
|
||||
self.assertEqual(lead_as_emp.message_partner_ids, self.partner_employee)
|
||||
|
||||
# adds other employee and a portal customer as followers
|
||||
lead_as_emp.message_subscribe(partner_ids=(self.partner_employee_2 + self.partner_portal).ids)
|
||||
self.assertEqual(lead_as_emp.message_partner_ids, self.partner_employee + self.partner_employee_2 + self.partner_portal)
|
||||
# updates some customer information
|
||||
lead_as_emp.write({
|
||||
'customer_name': 'Sylvie Lelitre (Zboing)',
|
||||
'phone': '+32455001122',
|
||||
'lang_code': 'fr_FR',
|
||||
})
|
||||
|
||||
# uses Chatter: fetches suggested recipients, post a message
|
||||
# - checks all suggested: email_cc field, primary email
|
||||
# ------------------------------------------------------------
|
||||
suggested_all = lead_as_emp._message_get_suggested_recipients(
|
||||
reply_discussion=True, no_create=False,
|
||||
)
|
||||
partner_sylvie = self.env['res.partner'].search(
|
||||
[('email_normalized', '=', 'sylvie.lelitre@zboing.com')]
|
||||
)
|
||||
partner_pay = self.env['res.partner'].search(
|
||||
[('email_normalized', '=', 'pay@zboing.com')]
|
||||
)
|
||||
partner_accounting = self.env['res.partner'].search(
|
||||
[('email_normalized', '=', 'accounting@zboing.com')]
|
||||
)
|
||||
expected_all = [
|
||||
{ # existing partners come first
|
||||
'create_values': {},
|
||||
'email': 'portal@zboing.com',
|
||||
'name': 'Portal Zboing',
|
||||
'partner_id': self.customer_portal_zboing.id,
|
||||
},
|
||||
{ # primary email comes first
|
||||
'create_values': {},
|
||||
'email': 'sylvie.lelitre@zboing.com',
|
||||
'name': 'Sylvie Lelitre (Zboing)',
|
||||
'partner_id': partner_sylvie.id,
|
||||
},
|
||||
{ # mail.thread.cc: email_cc field
|
||||
'create_values': {},
|
||||
'email': 'pay@zboing.com',
|
||||
'name': 'pay@zboing.com',
|
||||
'partner_id': partner_pay.id,
|
||||
},
|
||||
{ # reply message
|
||||
'create_values': {},
|
||||
'email': 'accounting@zboing.com',
|
||||
'name': 'Josiane Quichopoils',
|
||||
'partner_id': partner_accounting.id,
|
||||
},
|
||||
]
|
||||
for suggested, expected in zip(suggested_all, expected_all):
|
||||
self.assertDictEqual(suggested, expected)
|
||||
|
||||
# finally post the message with recipients
|
||||
with self.mock_mail_gateway():
|
||||
responsible_answer = lead_as_emp.message_post(
|
||||
body='<p>Well received !',
|
||||
partner_ids=(partner_sylvie + partner_pay + partner_accounting + self.customer_portal_zboing).ids,
|
||||
message_type='comment',
|
||||
subject=f'Re: {lead.name}',
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
self.assertEqual(lead_as_emp.message_partner_ids, self.partner_employee + self.partner_employee_2 + self.partner_portal)
|
||||
|
||||
external_partners = partner_sylvie + partner_pay + partner_accounting + self.customer_portal_zboing + self.partner_portal
|
||||
internal_partners = self.partner_employee + self.partner_employee_2
|
||||
self.assertMailNotifications(
|
||||
responsible_answer,
|
||||
[
|
||||
{
|
||||
'content': 'Well received !',
|
||||
'mail_mail_values': {
|
||||
'mail_server_id': self.env['ir.mail_server'], # no specified server
|
||||
},
|
||||
'message_type': 'comment',
|
||||
'message_values': {
|
||||
'author_id': self.partner_employee,
|
||||
'email_from': self.partner_employee.email_formatted,
|
||||
'incoming_email_cc': False,
|
||||
'incoming_email_to': False,
|
||||
'mail_server_id': self.env['ir.mail_server'],
|
||||
# followers + recipients - author
|
||||
'notified_partner_ids': external_partners + self.partner_employee_2,
|
||||
'parent_id': incoming_email,
|
||||
# matches posted message
|
||||
'partner_ids': partner_sylvie + partner_pay + partner_accounting + self.customer_portal_zboing,
|
||||
'reply_to': formataddr((
|
||||
self.partner_employee.name, f'{self.alias_catchall}@{self.alias_domain}'
|
||||
)),
|
||||
'subtype_id': self.env.ref('mail.mt_comment'),
|
||||
},
|
||||
'notif': [
|
||||
{'partner': partner_sylvie, 'type': 'email'},
|
||||
{'partner': partner_pay, 'type': 'email'},
|
||||
{'partner': partner_accounting, 'type': 'email'},
|
||||
{'partner': self.customer_portal_zboing, 'type': 'email'},
|
||||
{'partner': self.partner_employee_2, 'type': 'email'},
|
||||
{'partner': self.partner_portal, 'type': 'email'},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
# expected Msg['To'] : Reply-All behavior: actual recipient, then
|
||||
# all "not internal partners" and catchall (to receive answers)
|
||||
for partner in responsible_answer.notified_partner_ids:
|
||||
exp_msg_to_partners = partner | external_partners
|
||||
exp_msg_to = exp_msg_to_partners.mapped('email_formatted')
|
||||
with self.subTest(name=partner.name):
|
||||
self.assertSMTPEmailsSent(
|
||||
mail_server=self.mail_server_notification,
|
||||
msg_from=formataddr(
|
||||
(self.partner_employee.name, f'{self.default_from}@{self.alias_domain}')
|
||||
),
|
||||
smtp_from=self.mail_server_notification.from_filter,
|
||||
smtp_to_list=[partner.email_normalized],
|
||||
msg_to_lst=exp_msg_to,
|
||||
)
|
||||
|
||||
# customer replies using "Reply All" + adds new people
|
||||
# added: Cc: invoicing (email) and robert (partner)
|
||||
# ------------------------------------------------------------
|
||||
self.gateway_mail_reply_from_smtp_email(
|
||||
MAIL_TEMPLATE, [partner_sylvie.email_normalized], reply_all=True,
|
||||
cc=f'{self.test_emails[3]}, {self.test_emails[4]}', # used mainly for existing partners currently
|
||||
)
|
||||
external_partners += self.customer_zboing # added in CC just above
|
||||
self.assertEqual(len(lead.message_ids), 3, 'Incoming email + chatter reply + customer reply')
|
||||
self.assertEqual(
|
||||
lead.message_partner_ids,
|
||||
internal_partners + self.partner_portal,
|
||||
'Mail gateway: author (partner_sylvie) should not added in followers if external')
|
||||
|
||||
customer_reply = lead.message_ids[0]
|
||||
self.assertMailNotifications(
|
||||
customer_reply,
|
||||
[
|
||||
{
|
||||
'content': 'Please call me as soon as possible',
|
||||
'message_type': 'email',
|
||||
'message_values': {
|
||||
'author_id': partner_sylvie,
|
||||
'email_from': partner_sylvie.email_formatted,
|
||||
# Cc: received email CC - an email still not partnerized (invoicing) and customer_zboing
|
||||
'incoming_email_cc': f'{self.test_emails[3]}, {self.test_emails[4]}',
|
||||
# To: received email Msg-To - customer who replies + email Reply-To
|
||||
'incoming_email_to': ', '.join((external_partners - partner_sylvie - self.customer_zboing).mapped('email_formatted')),
|
||||
'mail_server_id': self.env['ir.mail_server'],
|
||||
# notified: followers - already mailed, aka internal only
|
||||
'notified_partner_ids': internal_partners,
|
||||
'parent_id': responsible_answer,
|
||||
# same reasoning as email_to/cc
|
||||
'partner_ids': external_partners - partner_sylvie,
|
||||
'reply_to': formataddr((
|
||||
partner_sylvie.name, f'{self.alias_catchall}@{self.alias_domain}'
|
||||
)),
|
||||
'subject': f'Re: Re: {lead.name}',
|
||||
'subtype_id': self.env.ref('mail.mt_comment'),
|
||||
},
|
||||
# portal was already in email_to, hence not notified twice through odoo
|
||||
'notif': [
|
||||
{'partner': self.partner_employee, 'type': 'inbox'},
|
||||
{'partner': self.partner_employee_2, 'type': 'email'},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
def test_ticket_mailgateway(self):
|
||||
""" Flow of this test
|
||||
* incoming email creating a ticket in 'new' state
|
||||
* automatic answer based on template
|
||||
"""
|
||||
# incoming customer email: help alias + recipients (to + cc)
|
||||
# ------------------------------------------------------------
|
||||
email_to = f'help@{self.alias_domain}, {self.test_emails[1]}, {self.partner_employee.email_formatted}'
|
||||
email_to_filtered = f'{self.test_emails[1]}, {self.partner_employee.email_formatted}'
|
||||
email_cc = f'{self.test_emails[2]}, {self.test_emails[5]}'
|
||||
with self.mock_mail_gateway(), self.mock_mail_app():
|
||||
ticket = self.format_and_process(
|
||||
MAIL_TEMPLATE,
|
||||
self.test_emails[0],
|
||||
email_to,
|
||||
cc=email_cc,
|
||||
subject='Inquiry',
|
||||
target_model='mail.test.ticket.partner',
|
||||
)
|
||||
self.flush_tracking()
|
||||
|
||||
# author -> partner, as automatic email creates partner
|
||||
partner_sylvie = self.env['res.partner'].search([('email_normalized', '=', 'sylvie.lelitre@zboing.com')])
|
||||
self.assertTrue(partner_sylvie, 'Acknowledgement template should create a partner for incoming email')
|
||||
self.assertEqual(partner_sylvie.email, 'sylvie.lelitre@zboing.com', 'Should parse name/email correctly')
|
||||
self.assertEqual(partner_sylvie.name, 'sylvie.lelitre@zboing.com', 'TDE FIXME: should parse name/email correctly')
|
||||
# create ticket
|
||||
self.assertEqual(ticket.container_id, self.container)
|
||||
self.assertEqual(
|
||||
ticket.customer_id, partner_sylvie,
|
||||
'Should put partner as customer, due to after hook')
|
||||
self.assertEqual(ticket.email_from, self.test_emails[0])
|
||||
self.assertEqual(ticket.name, 'Inquiry')
|
||||
self.assertEqual(ticket.state, 'new', 'Should come from alias defaults')
|
||||
self.assertEqual(ticket.state_template_id, self.ticket_template, 'Should come from alias defaults')
|
||||
# followers
|
||||
self.assertFalse(ticket.message_partner_ids)
|
||||
# messages
|
||||
self.assertEqual(len(ticket.message_ids), 3, 'Incoming email + Acknowledgement + Tracking')
|
||||
|
||||
# first message: incoming email
|
||||
incoming_email = ticket.message_ids[2]
|
||||
self.assertMailNotifications(
|
||||
incoming_email,
|
||||
[
|
||||
{
|
||||
'content': 'Please call me as soon as possible',
|
||||
'message_type': 'email',
|
||||
'message_values': {
|
||||
'author_id': self.env['res.partner'],
|
||||
'email_from': self.test_emails[0],
|
||||
# coming from incoming email
|
||||
'incoming_email_cc': email_cc,
|
||||
'incoming_email_to': email_to_filtered,
|
||||
'mail_server_id': self.env['ir.mail_server'],
|
||||
'parent_id': self.env['mail.message'],
|
||||
'notified_partner_ids': self.env['res.partner'],
|
||||
# only recognized partners
|
||||
'partner_ids': self.partner_employee + self.customer_portal_zboing,
|
||||
'subject': 'Inquiry',
|
||||
# subtype from '_creation_subtype'
|
||||
'subtype_id': self.env.ref('test_mail.st_mail_test_ticket_partner_new'),
|
||||
},
|
||||
'notif': [], # no notif, mailgateway sets recipients without notification
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
# second message: acknowledgement
|
||||
acknowledgement = ticket.message_ids[1]
|
||||
self.assertMailNotifications(
|
||||
acknowledgement,
|
||||
[
|
||||
{
|
||||
'content': f'Received {ticket.name}',
|
||||
'message_type': 'auto_comment',
|
||||
'message_values': {
|
||||
# defined by template, root is the cron user as no responsible
|
||||
'author_id': self.partner_root,
|
||||
'email_from': self.partner_root.email_formatted,
|
||||
'incoming_email_cc': False,
|
||||
'incoming_email_to': False,
|
||||
'mail_server_id': self.env['ir.mail_server'],
|
||||
# no followers, hence only template default_to
|
||||
'notified_partner_ids': partner_sylvie,
|
||||
'parent_id': incoming_email,
|
||||
# no followers, hence only template default_to
|
||||
'partner_ids': partner_sylvie,
|
||||
'subject': f'Received {ticket.name}',
|
||||
# subtype from '_track_template'
|
||||
'subtype_id': self.env.ref('mail.mt_note'),
|
||||
},
|
||||
'notif': [
|
||||
{'partner': partner_sylvie, 'type': 'email',},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
|
@ -1,14 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon
|
||||
import re
|
||||
from unittest.mock import patch
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
from odoo import Command
|
||||
from odoo.addons.mail.models.mail_mail import _UNFOLLOW_REGEX
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests import tagged, users
|
||||
from odoo.tests.common import HttpCase
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('mail_followers')
|
||||
class BaseFollowersTest(TestMailCommon):
|
||||
class BaseFollowersTest(MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
|
@ -16,9 +25,6 @@ class BaseFollowersTest(TestMailCommon):
|
|||
cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
|
||||
cls._create_portal_user()
|
||||
|
||||
# allow employee to update partners
|
||||
cls.user_employee.write({'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)]})
|
||||
|
||||
Subtype = cls.env['mail.message.subtype']
|
||||
# global
|
||||
cls.mt_al_def = Subtype.create({'name': 'mt_al_def', 'default': True, 'res_model': False})
|
||||
|
|
@ -52,6 +58,8 @@ class BaseFollowersTest(TestMailCommon):
|
|||
followed_after = self.env['mail.test.simple'].search([('message_partner_ids', 'in', partner.ids)])
|
||||
self.assertTrue(partner in test_record.message_partner_ids)
|
||||
self.assertEqual(followed_before + test_record, followed_after)
|
||||
with self.assertRaisesRegex(AccessError, 'Portal users can only filter threads'):
|
||||
self.env['mail.test.simple'].with_user(self.user_portal).search([('message_partner_ids', 'in', partner.ids)])
|
||||
|
||||
def test_field_followers(self):
|
||||
test_record = self.test_record.with_user(self.user_employee)
|
||||
|
|
@ -141,7 +149,7 @@ class BaseFollowersTest(TestMailCommon):
|
|||
'name': 'Valid Lelitre',
|
||||
'email': 'valid.lelitre@agrolait.com',
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
'mobile': '0456001122',
|
||||
'phone': '0456001122',
|
||||
'active': False,
|
||||
})
|
||||
document = self.env['mail.test.simple'].browse(self.test_record.id)
|
||||
|
|
@ -195,6 +203,18 @@ class BaseFollowersTest(TestMailCommon):
|
|||
test_record.write({'message_partner_ids': [(4, partner0.id), (4, partner1.id)]})
|
||||
self.assertEqual(test_record.message_follower_ids.partner_id, partner1)
|
||||
|
||||
# Test when the method inverse is called in batch
|
||||
other_record = test_record.create({
|
||||
'name': 'Other',
|
||||
})
|
||||
records = test_record + other_record
|
||||
|
||||
records.message_partner_ids = (partner2 + partner3)
|
||||
self.assertEqual(records.message_partner_ids, partner2 + partner3)
|
||||
|
||||
records.message_partner_ids -= partner2
|
||||
self.assertEqual(records.message_partner_ids, partner3)
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
|
||||
def test_followers_inverse_message_partner_access_rights(self):
|
||||
""" Make sure we're not bypassing security checks by setting a partner
|
||||
|
|
@ -218,14 +238,16 @@ class BaseFollowersTest(TestMailCommon):
|
|||
|
||||
@users('employee')
|
||||
def test_followers_private_address(self):
|
||||
""" Test standard API does not subscribe private addresses """
|
||||
private_address = self.env['res.partner'].sudo().create({
|
||||
""" Test standard API does subscribe IDs the user can't read """
|
||||
other_company = self.env['res.company'].sudo().create({'name': 'Other Company'})
|
||||
private_address = self.env['res.partner'].create({
|
||||
'name': 'Private Address',
|
||||
'type': 'private',
|
||||
'company_id': other_company.id,
|
||||
})
|
||||
self.env.user.write({'company_ids': [(3, other_company.id)]})
|
||||
document = self.env['mail.test.simple'].browse(self.test_record.id)
|
||||
document.message_subscribe(partner_ids=(self.partner_portal | private_address).ids)
|
||||
self.assertEqual(document.message_follower_ids.partner_id, self.partner_portal)
|
||||
self.assertEqual(document.message_follower_ids.partner_id, self.partner_portal | private_address)
|
||||
|
||||
# works through low-level API
|
||||
document._message_subscribe(partner_ids=(self.partner_portal | private_address).ids)
|
||||
|
|
@ -255,7 +277,7 @@ class BaseFollowersTest(TestMailCommon):
|
|||
|
||||
|
||||
@tagged('mail_followers')
|
||||
class AdvancedFollowersTest(TestMailCommon):
|
||||
class AdvancedFollowersTest(MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
|
@ -288,6 +310,10 @@ class AdvancedFollowersTest(TestMailCommon):
|
|||
'name': 'Default track subtype', 'default': True, 'internal': False,
|
||||
'res_model': 'mail.test.track'
|
||||
})
|
||||
cls.sub_track_parent_def = Subtype.create({
|
||||
'name': 'Parent track subtype', 'default': False, 'res_model': 'mail.test.track',
|
||||
'parent_id': cls.sub_track_def.id, 'relation_field': 'parent_id'
|
||||
})
|
||||
|
||||
# mail.test.container subtypes (aka: project records)
|
||||
cls.umb_nodef = Subtype.create({
|
||||
|
|
@ -327,7 +353,18 @@ class AdvancedFollowersTest(TestMailCommon):
|
|||
|
||||
def test_auto_subscribe_create(self):
|
||||
""" Creator of records are automatically added as followers """
|
||||
self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id)
|
||||
for user, should_subscribe in [
|
||||
(self.user_root, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_portal, False),
|
||||
]:
|
||||
with self.subTest(user_name=user.name):
|
||||
# sudo, as done through mailgateway for example
|
||||
if user == self.user_portal:
|
||||
new_rec = self.env['mail.test.track'].with_user(user).sudo().create({})
|
||||
else:
|
||||
new_rec = self.env['mail.test.track'].with_user(user).create({})
|
||||
self.assertEqual(new_rec.message_partner_ids, user.partner_id if should_subscribe else self.env['res.partner'])
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_auto_subscribe_inactive(self):
|
||||
|
|
@ -355,19 +392,27 @@ class AdvancedFollowersTest(TestMailCommon):
|
|||
'Does not subscribe inactive partner')
|
||||
|
||||
def test_auto_subscribe_post(self):
|
||||
""" People posting a message are automatically added as followers """
|
||||
self.test_track.with_user(self.user_admin).message_post(body='Coucou hibou', message_type='comment')
|
||||
self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id | self.user_admin.partner_id)
|
||||
|
||||
def test_auto_subscribe_post_email(self):
|
||||
""" People posting an email are automatically added as followers """
|
||||
self.test_track.with_user(self.user_admin).message_post(body='Coucou hibou', message_type='email')
|
||||
self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id | self.user_admin.partner_id)
|
||||
|
||||
def test_auto_subscribe_not_on_notification(self):
|
||||
""" People posting an automatic notification are not subscribed """
|
||||
self.test_track.with_user(self.user_admin).message_post(body='Coucou hibou', message_type='notification')
|
||||
self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id)
|
||||
""" People posting a discussion message are automatically added as
|
||||
followers """
|
||||
record = self.test_track.with_user(self.user_admin)
|
||||
for message_type, subtype, should_subscribe in [
|
||||
('comment', self.env.ref('mail.mt_note'), False),
|
||||
('comment', self.env.ref('mail.mt_comment'), True),
|
||||
('email_outgoing', self.env.ref('mail.mt_note'), False),
|
||||
('email_outgoing', self.env.ref('mail.mt_comment'), True),
|
||||
('notification', self.env.ref('mail.mt_comment'), False),
|
||||
]:
|
||||
with self.subTest(message_type=message_type, subtype_name=subtype.name):
|
||||
record.message_unsubscribe(partner_ids=self.user_admin.partner_id.ids)
|
||||
record.message_post(
|
||||
body=f'Posting with {message_type} {subtype.name}',
|
||||
message_type=message_type,
|
||||
subtype_id=subtype.id,
|
||||
)
|
||||
if should_subscribe:
|
||||
self.assertIn(self.user_admin.partner_id, record.message_partner_ids)
|
||||
else:
|
||||
self.assertNotIn(self.user_admin.partner_id, record.message_partner_ids)
|
||||
|
||||
def test_auto_subscribe_responsible(self):
|
||||
""" Responsibles are tracked and added as followers """
|
||||
|
|
@ -465,8 +510,23 @@ class AdvancedFollowersTest(TestMailCommon):
|
|||
'AutoSubscribe: at create auto subscribe as creator + from parent take both subtypes'
|
||||
)
|
||||
|
||||
container.message_follower_ids = [Command.clear()]
|
||||
parent_track = self.env['mail.test.track'].with_user(self.user_employee).create({
|
||||
'name': 'Task-Like',
|
||||
'container_id': container.id,
|
||||
})
|
||||
|
||||
child_track = self.env['mail.test.track'].with_user(self.user_admin).create({
|
||||
'name': 'Task-Like Test-sub-task',
|
||||
'parent_id': parent_track.id,
|
||||
'container_id': container.id,
|
||||
})
|
||||
self.assertIn(self.user_employee.partner_id, child_track.message_follower_ids.partner_id, 'The partner from the parent has not been added as follower.')
|
||||
|
||||
|
||||
@tagged('mail_followers')
|
||||
class AdvancedResponsibleNotifiedTest(MailCommon):
|
||||
|
||||
class AdvancedResponsibleNotifiedTest(TestMailCommon):
|
||||
def setUp(self):
|
||||
super(AdvancedResponsibleNotifiedTest, self).setUp()
|
||||
|
||||
|
|
@ -478,7 +538,7 @@ class AdvancedResponsibleNotifiedTest(TestMailCommon):
|
|||
|
||||
def test_auto_subscribe_notify_email(self):
|
||||
""" Responsible is notified when assigned """
|
||||
partner = self.env['res.partner'].create({"name": "demo1", "email": "demo1@test.com"})
|
||||
partner = self.env['res.partner'].create({"name": "demo1", "email": "demo1@test.mycompany.com"})
|
||||
notified_user = self.env['res.users'].create({
|
||||
'login': 'demo1',
|
||||
'partner_id': partner.id,
|
||||
|
|
@ -512,7 +572,7 @@ class AdvancedResponsibleNotifiedTest(TestMailCommon):
|
|||
|
||||
|
||||
@tagged('mail_followers', 'post_install', '-at_install')
|
||||
class RecipientsNotificationTest(TestMailCommon):
|
||||
class RecipientsNotificationTest(MailCommon):
|
||||
""" Test advanced and complex recipients computation / notification, such
|
||||
as multiple users, batch computation, ... Post install because we need the
|
||||
registry to be ready to send notifications."""
|
||||
|
|
@ -539,12 +599,12 @@ class RecipientsNotificationTest(TestMailCommon):
|
|||
'phone': '+32455998877',
|
||||
})
|
||||
cls.user_1, cls.user_2 = cls.env['res.users'].with_context(no_reset_password=True).create([
|
||||
{'groups_id': [(4, cls.env.ref('base.group_portal').id)],
|
||||
{'group_ids': [(4, cls.env.ref('base.group_portal').id)],
|
||||
'login': '_login_portal',
|
||||
'notification_type': 'email',
|
||||
'partner_id': cls.common_partner.id,
|
||||
},
|
||||
{'groups_id': [(4, cls.env.ref('base.group_user').id)],
|
||||
{'group_ids': [(4, cls.env.ref('base.group_user').id)],
|
||||
'login': '_login_internal',
|
||||
'notification_type': 'inbox',
|
||||
'partner_id': cls.common_partner.id,
|
||||
|
|
@ -572,8 +632,11 @@ class RecipientsNotificationTest(TestMailCommon):
|
|||
if not user:
|
||||
user = next((user for user in partner.user_ids), self.env['res.users'])
|
||||
self.assertEqual(partner_data['active'], partner.active)
|
||||
self.assertEqual(partner_data['email_normalized'], partner.email_normalized)
|
||||
self.assertEqual(partner_data['lang'], partner.lang)
|
||||
self.assertEqual(partner_data['name'], partner.name)
|
||||
if user:
|
||||
self.assertEqual(partner_data['groups'], set(user.groups_id.ids))
|
||||
self.assertEqual(partner_data['groups'], set(user.all_group_ids.ids))
|
||||
self.assertEqual(partner_data['notif'], user.notification_type)
|
||||
self.assertEqual(partner_data['uid'], user.id)
|
||||
else:
|
||||
|
|
@ -649,21 +712,21 @@ class RecipientsNotificationTest(TestMailCommon):
|
|||
user_2_1, user_2_2, user_2_3 = self.env['res.users'].sudo().with_context(no_reset_password=True).create([
|
||||
{'company_ids': [(6, 0, cids)],
|
||||
'company_id': self.company_admin.id,
|
||||
'groups_id': [(4, self.env.ref('base.group_portal').id)],
|
||||
'group_ids': [(4, self.env.ref('base.group_portal').id)],
|
||||
'login': '_login2_portal',
|
||||
'notification_type': 'email',
|
||||
'partner_id': shared_partner.id,
|
||||
},
|
||||
{'company_ids': [(6, 0, cids)],
|
||||
'company_id': self.company_admin.id,
|
||||
'groups_id': [(4, self.env.ref('base.group_user').id)],
|
||||
'group_ids': [(4, self.env.ref('base.group_user').id)],
|
||||
'login': '_login2_internal',
|
||||
'notification_type': 'inbox',
|
||||
'partner_id': shared_partner.id,
|
||||
},
|
||||
{'company_ids': [(6, 0, cids)],
|
||||
'company_id': company_other.id,
|
||||
'groups_id': [(4, self.env.ref('base.group_user').id), (4, self.env.ref('base.group_partner_manager').id)],
|
||||
'group_ids': [(4, self.env.ref('base.group_user').id), (4, self.env.ref('base.group_partner_manager').id)],
|
||||
'login': '_login2_manager',
|
||||
'notification_type': 'inbox',
|
||||
'partner_id': shared_partner.id,
|
||||
|
|
@ -684,7 +747,7 @@ class RecipientsNotificationTest(TestMailCommon):
|
|||
'status': 'sent', 'type': 'inbox'}],
|
||||
message_info={'content': 'User Choice Notification'}):
|
||||
test.message_post(
|
||||
body='<p>User Choice Notification</p>',
|
||||
body=Markup('<p>User Choice Notification</p>'),
|
||||
message_type='comment',
|
||||
partner_ids=shared_partner.ids,
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
|
|
@ -788,3 +851,213 @@ class RecipientsNotificationTest(TestMailCommon):
|
|||
pids=test_partners.ids
|
||||
)
|
||||
self.assertRecipientsData(recipients_data, False, test_partners)
|
||||
|
||||
def test_subscribe_post_author(self):
|
||||
""" Test author is added in followers, unless it is archived / odoobot """
|
||||
# some automated action post on behalf of author
|
||||
test_record = self.env['mail.test.simple'].create({'name': 'Test'})
|
||||
self.partner_root.active = True # edge case, people activating Odoobot partner (not user)
|
||||
(self.user_1 + self.user_2).active = False # archived users should not be subscribed
|
||||
self.user_1.partner_id.active = False # archived authors should not be subscribed
|
||||
self.assertFalse(test_record.message_partner_ids)
|
||||
for user, author, exp_followers in [
|
||||
# active user = real author
|
||||
(self.user_employee, self.user_2.partner_id, self.user_employee.partner_id),
|
||||
# inactive user -> check for author
|
||||
(self.user_2, self.user_employee.partner_id, self.user_employee.partner_id),
|
||||
(self.user_2, self.user_1.partner_id, self.env['res.partner']), # no inactive !
|
||||
(self.user_2, self.user_root.partner_id, self.env['res.partner']), # no odoobot !
|
||||
]:
|
||||
with self.subTest(user=user.name, author=author.name):
|
||||
test_record.with_user(user).message_post(
|
||||
author_id=author.id,
|
||||
body='Youpie',
|
||||
message_type='comment',
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
self.assertEqual(test_record.message_partner_ids, exp_followers)
|
||||
if exp_followers:
|
||||
test_record.message_unsubscribe(partner_ids=exp_followers.ids)
|
||||
|
||||
@tagged('mail_followers', 'post_install', '-at_install')
|
||||
class UnfollowLinkTest(MailCommon, HttpCase):
|
||||
""" Test unfollow links, notably used in notification emails """
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.user_portal = cls._create_portal_user()
|
||||
cls.partner_portal = cls.user_portal.partner_id
|
||||
cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test'})
|
||||
cls.test_record_copy = cls.test_record.copy()
|
||||
cls.test_record_unfollow = cls.env['mail.test.simple.unfollow'].with_context(cls._test_context).create(
|
||||
{'name': 'unfollow'})
|
||||
cls.partner_without_user = cls.env['res.partner'].create({
|
||||
'name': 'Dave',
|
||||
'email': 'dave@odoo.com',
|
||||
})
|
||||
cls.user_employee.write({'notification_type': 'email'})
|
||||
|
||||
def _message_unsubscribe_unreadable_record(self, user):
|
||||
def raise_access_error(*args, **kwargs):
|
||||
raise AccessError('Unreadable')
|
||||
|
||||
with patch.object(self.test_record.__class__, 'check_access', side_effect=raise_access_error):
|
||||
self.test_record.with_user(user).message_unsubscribe(user.partner_id.ids)
|
||||
|
||||
|
||||
def _test_tampered_unfollow_url(self, record, unfollow_url, partner):
|
||||
""" Test that tampered urls doesn't work.
|
||||
|
||||
Test that:
|
||||
- when the following parameters are altered, the browsing the URL returns
|
||||
a 403 and doesn't unsubscribe the partner.
|
||||
- when trying to use the same URL with another partner, it also returns a
|
||||
403 and doesn't unsubscribe the other partner.
|
||||
"""
|
||||
for param, value in (
|
||||
('token', '0000000000000000000000000000000000000000'),
|
||||
('model', 'mail.test.gateway'),
|
||||
('res_id', self.test_record_copy.id),
|
||||
('partner_id', self.partner_admin.id),
|
||||
):
|
||||
with self.subTest(f'Tampered {param}'):
|
||||
tampered_unfollow_url = self._url_update_query_parameters(unfollow_url, **{param: value})
|
||||
response = self.url_open(tampered_unfollow_url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertIn(partner, record.message_partner_ids)
|
||||
|
||||
def _test_unfollow_url(self, record, unfollow_url, partner):
|
||||
""" Test that the unfollow url works.
|
||||
|
||||
Test that: that browsing the unfollow URL unsubscribe the user from the record
|
||||
"""
|
||||
with self.subTest('Legitimate unfollow'):
|
||||
# We test that the URL still work a second time if the user has been re-added
|
||||
for _ in range(2):
|
||||
try:
|
||||
self.assertIn(partner, record.message_partner_ids)
|
||||
response = self.url_open(unfollow_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn(partner, record.message_partner_ids)
|
||||
self.assertEqual(urlparse(response.url).path, '/mail/unfollow')
|
||||
self.assertIn("You are no longer following the document", response.text)
|
||||
self.assertIn('o_access_record_link', response.text)
|
||||
finally:
|
||||
record._message_subscribe(partner_ids=partner.ids)
|
||||
|
||||
def test_assert_initial_data(self):
|
||||
""" Test some initial value. """
|
||||
record_employee = self.test_record.with_user(self.user_employee)
|
||||
record_employee.check_access('read')
|
||||
record_portal = self.test_record.with_user(self.user_portal)
|
||||
with self.assertRaises(AccessError):
|
||||
record_portal.check_access('write')
|
||||
for template_ref in ('mail.mail_notification_layout', 'mail.mail_notification_light'):
|
||||
with self.subTest(f'Unfollow link in {template_ref}'):
|
||||
mail_template_arch = self.env.ref(template_ref).arch
|
||||
self.assertIn('/mail/unfollow', mail_template_arch)
|
||||
self.assertNotIn('/mail/unfollow', re.sub(_UNFOLLOW_REGEX, '', mail_template_arch))
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models')
|
||||
def test_inbox_unfollow_information(self):
|
||||
""" Check follow-up information for displaying inbox messages used to
|
||||
implement "unfollow" in the inbox.
|
||||
|
||||
Note that the actual mechanism to unfollow a record from a message is
|
||||
tested in the client part.
|
||||
"""
|
||||
self.user_employee.write({'notification_type': 'inbox'})
|
||||
|
||||
test_record = self.env['mail.test.simple'].browse(self.test_record.ids)
|
||||
_message = test_record.with_user(self.user_admin).message_post(
|
||||
body="test message",
|
||||
subtype_id=self.env.ref("mail.mt_comment").id,
|
||||
partner_ids=self.partner_employee.ids,
|
||||
)
|
||||
# The user doesn't follow the record
|
||||
self.authenticate(self.env.user.login, self.env.user.login)
|
||||
message_data = self.make_jsonrpc_request("/mail/inbox/messages")["data"]
|
||||
self.assertFalse(message_data["mail.thread"][0]["selfFollower"])
|
||||
self.assertFalse(message_data.get("mail.followers"), "Should not have void followers data")
|
||||
self.assertFalse(test_record.with_user(self.user_employee).message_is_follower)
|
||||
|
||||
# The user follows the record
|
||||
test_record._message_subscribe(partner_ids=self.env.user.partner_id.ids)
|
||||
follower = test_record.message_follower_ids.filtered(
|
||||
lambda follower: follower.partner_id == self.env.user.partner_id
|
||||
)
|
||||
message_data = self.make_jsonrpc_request("/mail/inbox/messages")["data"]
|
||||
self.assertEqual(message_data["mail.followers"], [
|
||||
{
|
||||
"id": follower.id,
|
||||
"is_active": True,
|
||||
"partner_id": self.env.user.partner_id.id,
|
||||
},
|
||||
])
|
||||
self.assertEqual(message_data["mail.thread"][0]["selfFollower"], follower.id, "Should have follower ID")
|
||||
|
||||
@mute_logger('odoo.addons.base.models', 'odoo.addons.mail.controllers.mail', 'odoo.http', 'odoo.models')
|
||||
def test_notification_email_unfollow_link(self):
|
||||
""" Internal user must receive an unfollow URL, that cannot be tampered
|
||||
and redirects to the correct page.
|
||||
"""
|
||||
for test_partners, test_record, exp_has_url in [
|
||||
(self.partner_employee, self.test_record, [True]),
|
||||
# customer should not receive an unfollow URL
|
||||
(self.partner_without_user, self.test_record, [False]),
|
||||
(self.partner_portal, self.test_record, [False]),
|
||||
# always unfollow link (model definition)
|
||||
(self.partner_without_user, self.test_record_unfollow, [True]),
|
||||
(self.partner_portal, self.test_record_unfollow, [True]),
|
||||
# multi partners
|
||||
(
|
||||
self.partner_without_user + self.partner_portal + self.partner_employee,
|
||||
self.test_record, [False, False, True],
|
||||
),
|
||||
(
|
||||
self.partner_without_user + self.partner_portal + self.partner_employee,
|
||||
self.test_record_unfollow, [True, True, True],
|
||||
),
|
||||
]:
|
||||
with self.subTest(partners=test_partners.mapped('name')):
|
||||
# Test that the user receives an unfollow URL when following the record
|
||||
test_record._message_subscribe(partner_ids=test_partners.ids)
|
||||
unfollow_urls = self._message_post_and_get_unfollow_urls(test_record, test_partners)
|
||||
for test_partner, unfollow_url, has_url in zip(test_partners, unfollow_urls, exp_has_url):
|
||||
self.assertEqual(bool(unfollow_url), has_url)
|
||||
|
||||
# Test unfollowing URL when user is not logged
|
||||
if has_url:
|
||||
self.authenticate(None, None)
|
||||
self._test_unfollow_url(test_record, unfollow_url, test_partner)
|
||||
self._test_tampered_unfollow_url(test_record, unfollow_url, test_partner)
|
||||
|
||||
if test_partner == self.partner_employee:
|
||||
# Test unfollowing URL when user is logged
|
||||
self.authenticate(self.user_employee.login, self.user_employee.login)
|
||||
self._test_unfollow_url(test_record, unfollow_url, test_partner)
|
||||
|
||||
# Test that the user doesn't receive the unfollow URL when not following the record
|
||||
test_record.message_unsubscribe(partner_ids=test_partners.ids)
|
||||
unfollow_urls = self._message_post_and_get_unfollow_urls(test_record, test_partners)
|
||||
for test_partner, unfollow_url in zip(test_partners, unfollow_urls):
|
||||
self.assertFalse(unfollow_url)
|
||||
|
||||
def test_unsubscribe_unreadable(self):
|
||||
""" Check internal can always unsubscribe form records while portal are
|
||||
limited to records they can access. Other records are considered as customer
|
||||
oriented and we don't want to lose emails. """
|
||||
for user, can_unsubscribe in [
|
||||
(self.user_employee, True),
|
||||
(self.user_portal, False),
|
||||
]:
|
||||
self.test_record._message_subscribe(partner_ids=user.partner_id.ids)
|
||||
self.assertIn(user.partner_id, self.test_record.message_partner_ids)
|
||||
if can_unsubscribe:
|
||||
self._message_unsubscribe_unreadable_record(user)
|
||||
self.assertNotIn(user.partner_id, self.test_record.message_partner_ids)
|
||||
else:
|
||||
with self.assertRaises(AccessError):
|
||||
self._message_unsubscribe_unreadable_record(user)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.addons.test_mail.tests.common import TestRecipients
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('mail_management')
|
||||
class TestMailManagement(TestMailCommon, TestRecipients):
|
||||
class TestMailManagement(MailCommon, TestRecipients):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
|
|
|||
|
|
@ -1,14 +1,162 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon
|
||||
import contextlib
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
from odoo.addons.base.models.ir_mail_server import MailDeliveryException
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user, MailCommon
|
||||
from odoo.addons.mail.tools.discuss import Store
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests.common import tagged, users, HttpCase
|
||||
from odoo.tools import is_html_empty, mute_logger, formataddr
|
||||
from odoo.tests import tagged, users
|
||||
|
||||
|
||||
@tagged('mail_message')
|
||||
class TestMessageValues(TestMailCommon):
|
||||
@tagged('mail_message', 'mail_controller', 'post_install', '-at_install')
|
||||
class TestMessageHelpersRobustness(MailCommon, HttpCase):
|
||||
""" Test message helpers robustness, currently mainly linked to records
|
||||
being removed from DB due to cascading deletion, which let side records
|
||||
alive in DB. """
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.user_employee_2 = mail_new_test_user(
|
||||
cls.env,
|
||||
email='eglantine@example.com',
|
||||
groups='base.group_user',
|
||||
login='employee2',
|
||||
notification_type='email',
|
||||
name='Eglantine Employee',
|
||||
)
|
||||
cls.partner_employee_2 = cls.user_employee_2.partner_id
|
||||
|
||||
cls.test_records_simple, _partners = cls._create_records_for_batch(
|
||||
'mail.test.simple', 3,
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# cleanup db
|
||||
self.env['mail.notification'].search([('author_id', '=', self.partner_employee.id)]).unlink()
|
||||
|
||||
# handy shortcut variables
|
||||
self.deleted_record = self.test_records_simple[2]
|
||||
|
||||
# generate crashed notifications
|
||||
with mute_logger('odoo.addons.mail.models.mail_mail'), self.mock_mail_gateway():
|
||||
def _send_email(*args, **kwargs):
|
||||
raise MailDeliveryException("Some exception")
|
||||
self.send_email_mocked.side_effect = _send_email
|
||||
|
||||
for record in self.test_records_simple.with_user(self.user_employee):
|
||||
record.message_post(
|
||||
body="Setup",
|
||||
message_type='comment',
|
||||
partner_ids=self.partner_employee_2.ids,
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
|
||||
# In the mean time, some FK deletes the record where the message is
|
||||
# # scheduled, skipping its unlink() override
|
||||
self.env.cr.execute(
|
||||
f"DELETE FROM {self.test_records_simple._table} WHERE id = %s", (self.deleted_record.id,)
|
||||
)
|
||||
self.env.invalidate_all()
|
||||
|
||||
def test_assert_initial_values(self):
|
||||
notifs_by_employee = self.env['mail.notification'].search([('author_id', '=', self.partner_employee.id)])
|
||||
self.assertEqual(
|
||||
set(notifs_by_employee.mapped('mail_message_id.res_id')),
|
||||
set(self.test_records_simple.ids)
|
||||
)
|
||||
self.assertEqual(len(notifs_by_employee), 3)
|
||||
self.assertTrue(all(notif.notification_status == 'exception' for notif in notifs_by_employee))
|
||||
self.assertTrue(all(notif.res_partner_id == self.partner_employee_2 for notif in notifs_by_employee))
|
||||
|
||||
def test_load_message_failures(self):
|
||||
self.authenticate(self.user_employee.login, self.user_employee.login)
|
||||
with contextlib.suppress(Exception), mute_logger('odoo.http', 'odoo.sql_db'): # suppress logged error due to readonly route doing an update
|
||||
result = self.make_jsonrpc_request("/mail/data", {"fetch_params": ["failures"]})
|
||||
self.assertEqual(sorted(r['thread']['id'] for r in result['mail.message']), sorted(self.test_records_simple[:2].ids))
|
||||
self.assertEqual(
|
||||
sorted(self.env['mail.notification'].search([('author_id', '=', self.partner_employee.id)]).mapped('mail_message_id.res_id')),
|
||||
sorted((self.test_records_simple - self.deleted_record).ids),
|
||||
'Should have cleaned notifications linked to unexisting records'
|
||||
)
|
||||
|
||||
def test_load_message_failures_use_display_name(self):
|
||||
test_record = self.env['mail.test.simple.unnamed'].create({'description': 'Some description'})
|
||||
test_record.message_subscribe(partner_ids=self.partner_employee_2.ids)
|
||||
|
||||
self.authenticate(self.user_employee.login, self.user_employee.password)
|
||||
msg = test_record.message_post(body='Some body', author_id=self.partner_employee.id)
|
||||
# simulate failure
|
||||
self.env['mail.notification'].create({
|
||||
'author_id': msg.author_id.id,
|
||||
'mail_message_id': msg.id,
|
||||
'res_partner_id': self.partner_employee_2.id,
|
||||
'notification_type': 'email',
|
||||
'notification_status': 'exception',
|
||||
'failure_type': 'mail_email_invalid',
|
||||
})
|
||||
with contextlib.suppress(Exception), mute_logger('odoo.http', 'odoo.sql_db'): # suppress logged error due to readonly route doing an update
|
||||
res = self.make_jsonrpc_request("/mail/data", {"fetch_params": ["failures"]})
|
||||
self.assertEqual(
|
||||
sorted(t["name"] for t in res["mail.thread"]),
|
||||
sorted(['Some description'] + (self.test_records_simple - self.deleted_record).mapped('display_name'))
|
||||
)
|
||||
|
||||
def test_message_fetch(self):
|
||||
# set notifications to unread, so that we can simulate inbox usage
|
||||
p2_notifications = self.env['mail.notification'].search([('res_partner_id', '=', self.partner_employee_2.id)])
|
||||
p2_notifications.is_read = False
|
||||
|
||||
self.authenticate(self.user_employee_2.login, self.user_employee_2.login)
|
||||
result = self.make_jsonrpc_request("/mail/inbox/messages", {})['data']
|
||||
self.assertEqual(
|
||||
{r['thread']['id'] if r['thread'] else False for r in result['mail.message']},
|
||||
set((self.test_records_simple - self.deleted_record).ids + [False]),
|
||||
'Currently reading message on missing record, crash avoided, void thread for missing record'
|
||||
)
|
||||
p2_notifications.with_user(self.user_employee_2).mail_message_id.set_message_done()
|
||||
|
||||
result = self.make_jsonrpc_request("/mail/history/messages", {})['data']
|
||||
self.assertEqual(
|
||||
{r['thread']['id'] if r['thread'] else False for r in result['mail.message']},
|
||||
set((self.test_records_simple - self.deleted_record).ids + [False]),
|
||||
'Currently reading message on missing record, crash avoided'
|
||||
)
|
||||
|
||||
def test_message_link_by_employee(self):
|
||||
record = self.test_records_simple[0]
|
||||
thread_message = record.message_post(body='Thread Message', message_type='comment')
|
||||
deleted_message = record.message_post(body='', message_type='comment')
|
||||
self.authenticate(self.user_employee.login, self.user_employee.login)
|
||||
with self.subTest(thread_message=thread_message):
|
||||
expected_url = self.base_url() + f'/odoo/{thread_message.model}/{thread_message.res_id}?highlight_message_id={thread_message.id}'
|
||||
res = self.url_open(f'/mail/message/{thread_message.id}')
|
||||
self.assertEqual(res.url, expected_url)
|
||||
with self.subTest(deleted_message=deleted_message):
|
||||
res = self.url_open(f'/mail/message/{deleted_message.id}')
|
||||
|
||||
def test_notify_cancel_by_type(self):
|
||||
""" Test canceling notifications, notably when having missing records. """
|
||||
self.env.invalidate_all()
|
||||
notifs_by_employee = self.env['mail.notification'].search([('author_id', '=', self.partner_employee.id)])
|
||||
|
||||
# do not crash even if removed record
|
||||
self.test_records_simple.with_user(self.user_employee).notify_cancel_by_type('email')
|
||||
self.env.invalidate_all()
|
||||
|
||||
notifs_by_employee = notifs_by_employee.exists()
|
||||
self.assertEqual(len(notifs_by_employee), 3, 'Currently keep notifications for missing records')
|
||||
self.assertTrue(all(notif.notification_status == 'canceled' for notif in notifs_by_employee))
|
||||
|
||||
|
||||
@tagged("mail_message", "post_install", "-at_install")
|
||||
class TestMessageValues(MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
|
@ -60,24 +208,28 @@ class TestMessageValues(TestMailCommon):
|
|||
self.assertFalse(message.sudo().tracking_value_ids)
|
||||
|
||||
# Reset body case
|
||||
record._message_update_content(message, '<p><br /></p>', attachment_ids=message.attachment_ids.ids)
|
||||
record._message_update_content(
|
||||
message,
|
||||
body=Markup("<p><br /></p>"),
|
||||
attachment_ids=message.attachment_ids.ids,
|
||||
)
|
||||
self.assertTrue(is_html_empty(message.body))
|
||||
self.assertFalse(message.sudo()._filter_empty(), 'Still having attachments')
|
||||
|
||||
# Subtype content
|
||||
note_subtype.sudo().write({'description': 'Very important discussions'})
|
||||
record._message_update_content(message, '', [])
|
||||
record._message_update_content(message, body="", attachment_ids=[])
|
||||
self.assertFalse(message.attachment_ids)
|
||||
self.assertEqual(message.notified_partner_ids, self.partner_admin)
|
||||
self.assertEqual(message.starred_partner_ids, self.partner_admin)
|
||||
self.assertFalse(message.sudo()._filter_empty(), 'Subtype with description')
|
||||
|
||||
# Completely void now
|
||||
# Completely emptied now
|
||||
note_subtype.sudo().write({'description': ''})
|
||||
self.assertEqual(message.sudo()._filter_empty(), message)
|
||||
record._message_update_content(message, '', [])
|
||||
self.assertFalse(message.notified_partner_ids)
|
||||
self.assertFalse(message.starred_partner_ids)
|
||||
record._message_update_content(message.sudo(), body="", attachment_ids=[])
|
||||
self.assertEqual(message.notified_partner_ids, self.partner_admin) # message still notified (albeit content is removed)
|
||||
self.assertEqual(message.starred_partner_ids, self.partner_admin) # starred messages stay (albeit content is removed)
|
||||
|
||||
# test tracking values
|
||||
record.write({'user_id': self.user_admin.id})
|
||||
|
|
@ -88,13 +240,13 @@ class TestMessageValues(TestMailCommon):
|
|||
self.assertFalse(tracking_message.subtype_id.description)
|
||||
self.assertFalse(tracking_message.sudo()._filter_empty(), 'Has tracking values')
|
||||
with self.assertRaises(UserError, msg='Tracking values prevent from updating content'):
|
||||
record._message_update_content(tracking_message, '', [])
|
||||
record._message_update_content(tracking_message, body="", attachment_ids=[])
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_mail_message_format_access(self):
|
||||
def test_mail_message_to_store_access(self):
|
||||
"""
|
||||
User that doesn't have access to a record should still be able to fetch
|
||||
the record_name inside message_format.
|
||||
the record_name inside message _to_store.
|
||||
"""
|
||||
company_2 = self.env['res.company'].create({'name': 'Second Test Company'})
|
||||
record1 = self.env['mail.test.multi.company'].create({
|
||||
|
|
@ -104,25 +256,78 @@ class TestMessageValues(TestMailCommon):
|
|||
message = record1.message_post(body='', partner_ids=[self.user_employee.partner_id.id])
|
||||
# We need to flush and invalidate the ORM cache since the record_name
|
||||
# is already cached from the creation. Otherwise it will leak inside
|
||||
# message_format.
|
||||
# message _to_store.
|
||||
self.env.flush_all()
|
||||
self.env.invalidate_all()
|
||||
res = message.with_user(self.user_employee).message_format()
|
||||
self.assertEqual(res[0].get('record_name'), 'Test1')
|
||||
res = Store().add(message.with_user(self.user_employee)).get_result()
|
||||
self.assertEqual(res["mail.message"][0].get("record_name"), "Test1")
|
||||
|
||||
record1.write({"name": "Test2"})
|
||||
res = message.with_user(self.user_employee).message_format()
|
||||
self.assertEqual(res[0].get('record_name'), 'Test2')
|
||||
self.env.flush_all()
|
||||
self.env.invalidate_all()
|
||||
res = Store().add(message.with_user(self.user_employee)).get_result()
|
||||
self.assertEqual(res["mail.message"][0].get('record_name'), 'Test2')
|
||||
|
||||
# check model not inheriting from mail.thread -> should not crash
|
||||
record_nothread = self.env['mail.test.nothread'].create({'name': 'NoThread'})
|
||||
message = self.env['mail.message'].create({
|
||||
'model': record_nothread._name,
|
||||
'res_id': record_nothread.id,
|
||||
})
|
||||
formatted = Store().add(message).get_result()["mail.message"][0]
|
||||
self.assertEqual(formatted['record_name'], record_nothread.name)
|
||||
|
||||
def test_records_by_message(self):
|
||||
record1 = self.env["mail.test.simple"].create({"name": "Test1"})
|
||||
record2 = self.env["mail.test.simple"].create({"name": "Test1"})
|
||||
record3 = self.env["mail.test.nothread"].create({"name": "Test2"})
|
||||
messages = self.env["mail.message"].create(
|
||||
[
|
||||
{
|
||||
"model": record._name,
|
||||
"res_id": record.id,
|
||||
}
|
||||
for record in [record1, record2, record3]
|
||||
]
|
||||
)
|
||||
# methods called on batch of message
|
||||
records_by_model_name = messages._records_by_model_name()
|
||||
test_simple_records = records_by_model_name["mail.test.simple"]
|
||||
self.assertEqual(test_simple_records, record1 + record2)
|
||||
self.assertEqual(test_simple_records._prefetch_ids, tuple((record1 + record2).ids))
|
||||
test_no_thread_records = records_by_model_name["mail.test.nothread"]
|
||||
self.assertEqual(test_no_thread_records, record3)
|
||||
self.assertEqual(test_no_thread_records._prefetch_ids, tuple(record3.ids))
|
||||
record_by_message = messages._record_by_message()
|
||||
m0_records = record_by_message[messages[0]]
|
||||
self.assertEqual(m0_records, record1)
|
||||
self.assertEqual(m0_records._prefetch_ids, tuple((record1 + record2).ids))
|
||||
m1_records = record_by_message[messages[1]]
|
||||
self.assertEqual(m1_records, record2)
|
||||
self.assertEqual(m1_records._prefetch_ids, tuple((record1 + record2).ids))
|
||||
m2_records = record_by_message[messages[2]]
|
||||
self.assertEqual(m2_records, record3)
|
||||
self.assertEqual(m2_records._prefetch_ids, tuple(record3.ids))
|
||||
# methods called on individual message from a batch: prefetch from batch is kept
|
||||
records_by_model_name = next(iter(messages))._records_by_model_name()
|
||||
test_simple_records = records_by_model_name["mail.test.simple"]
|
||||
self.assertEqual(test_simple_records, record1)
|
||||
self.assertEqual(test_simple_records._prefetch_ids, tuple((record1 + record2).ids))
|
||||
record_by_message = next(iter(messages))._record_by_message()
|
||||
m0_records = record_by_message[messages[0]]
|
||||
self.assertEqual(m0_records, record1)
|
||||
self.assertEqual(m0_records._prefetch_ids, tuple((record1 + record2).ids))
|
||||
|
||||
def test_mail_message_values_body_base64_image(self):
|
||||
msg = self.env['mail.message'].with_user(self.user_employee).create({
|
||||
'body': 'taratata <img src="data:image/png;base64,iV/+OkI=" width="2"> <img src="data:image/png;base64,iV/+OkI=" width="2">',
|
||||
})
|
||||
self.assertEqual(len(msg.attachment_ids), 1)
|
||||
attachment = msg.attachment_ids[0]
|
||||
self.assertEqual(
|
||||
msg.body,
|
||||
'<p>taratata <img src="/web/image/{attachment.id}?access_token={attachment.access_token}" alt="image0" width="2"> '
|
||||
'<img src="/web/image/{attachment.id}?access_token={attachment.access_token}" alt="image0" width="2"></p>'.format(attachment=msg.attachment_ids[0])
|
||||
f'<p>taratata <img src="/web/image/{attachment.id}?access_token={attachment.access_token}" alt="image0" data-attachment-id="{attachment.id}" width="2"> '
|
||||
f'<img src="/web/image/{attachment.id}?access_token={attachment.access_token}" alt="image0" data-attachment-id="{attachment.id}" width="2"></p>'
|
||||
)
|
||||
|
||||
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.models')
|
||||
|
|
@ -134,7 +339,7 @@ class TestMessageValues(TestMailCommon):
|
|||
+ commit linked to this test). """
|
||||
# name would make it blow up: keep only email
|
||||
test_record = self.env['mail.test.container'].browse(self.alias_record.ids)
|
||||
test_record.write({
|
||||
self.user_employee.write({
|
||||
'name': 'Super Long Name That People May Enter "Even with an internal quoting of stuff"'
|
||||
})
|
||||
msg = self.env['mail.message'].create({
|
||||
|
|
@ -145,40 +350,11 @@ class TestMessageValues(TestMailCommon):
|
|||
self.assertEqual(msg.reply_to, reply_to_email,
|
||||
'Reply-To: use only email when formataddr > 68 chars')
|
||||
|
||||
# name + company_name would make it blow up: keep record_name in formatting
|
||||
self.company_admin.name = "Company name being about 33 chars"
|
||||
test_record.write({'name': 'Name that would be more than 68 with company name'})
|
||||
msg = self.env['mail.message'].create({
|
||||
'model': test_record._name,
|
||||
'res_id': test_record.id
|
||||
})
|
||||
self.assertEqual(msg.reply_to, formataddr((test_record.name, reply_to_email)),
|
||||
'Reply-To: use recordname as name in format if recordname + company > 68 chars')
|
||||
|
||||
# no record_name: keep company_name in formatting if ok
|
||||
test_record.write({'name': ''})
|
||||
msg = self.env['mail.message'].create({
|
||||
'model': test_record._name,
|
||||
'res_id': test_record.id
|
||||
})
|
||||
self.assertEqual(msg.reply_to, formataddr((self.env.user.company_id.name, reply_to_email)),
|
||||
'Reply-To: use company as name in format when no record name and still < 68 chars')
|
||||
|
||||
# no record_name and company_name make it blow up: keep only email
|
||||
self.env.user.company_id.write({'name': 'Super Long Name That People May Enter "Even with an internal quoting of stuff"'})
|
||||
msg = self.env['mail.message'].create({
|
||||
'model': test_record._name,
|
||||
'res_id': test_record.id
|
||||
})
|
||||
self.assertEqual(msg.reply_to, reply_to_email,
|
||||
'Reply-To: use only email when formataddr > 68 chars')
|
||||
|
||||
# whatever the record and company names, email is too long: keep only email
|
||||
test_record.write({
|
||||
'alias_name': 'Waaaay too long alias name that should make any reply-to blow the 68 characters limit',
|
||||
'name': 'Short',
|
||||
})
|
||||
self.env.user.company_id.write({'name': 'Comp'})
|
||||
sanitized_alias_name = 'waaaay-too-long-alias-name-that-should-make-any-reply-to-blow-the-68-characters-limit'
|
||||
msg = self.env['mail.message'].create({
|
||||
'model': test_record._name,
|
||||
|
|
@ -187,6 +363,7 @@ class TestMessageValues(TestMailCommon):
|
|||
self.assertEqual(msg.reply_to, f"{sanitized_alias_name}@{self.alias_domain}",
|
||||
'Reply-To: even a long email is ok as only formataddr is problematic')
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_mail_message_values_fromto_no_document_values(self):
|
||||
msg = self.Message.create({
|
||||
|
|
@ -197,32 +374,26 @@ class TestMessageValues(TestMailCommon):
|
|||
self.assertEqual(msg.reply_to, 'test.reply@example.com')
|
||||
self.assertEqual(msg.email_from, 'test.from@example.com')
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_mail_message_values_fromto_no_document(self):
|
||||
msg = self.Message.create({})
|
||||
self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
|
||||
reply_to_name = self.env.user.company_id.name
|
||||
reply_to_name = self.user_employee.name
|
||||
reply_to_email = '%s@%s' % (self.alias_catchall, self.alias_domain)
|
||||
self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
|
||||
self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
|
||||
|
||||
# no alias domain -> author
|
||||
self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.domain')]).unlink()
|
||||
|
||||
msg = self.Message.create({})
|
||||
self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
|
||||
self.assertEqual(msg.reply_to, formataddr((self.user_employee.name, self.user_employee.email)))
|
||||
self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
|
||||
|
||||
# no alias catchall, no alias -> author
|
||||
self.env['ir.config_parameter'].set_param('mail.catchall.domain', self.alias_domain)
|
||||
self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.alias')]).unlink()
|
||||
self.env.company.sudo().alias_domain_id = False
|
||||
self.assertFalse(self.env.company.catchall_email)
|
||||
|
||||
msg = self.Message.create({})
|
||||
self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
|
||||
self.assertEqual(msg.reply_to, formataddr((self.user_employee.name, self.user_employee.email)))
|
||||
self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_mail_message_values_fromto_document_alias(self):
|
||||
msg = self.Message.create({
|
||||
|
|
@ -230,13 +401,15 @@ class TestMessageValues(TestMailCommon):
|
|||
'res_id': self.alias_record.id
|
||||
})
|
||||
self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id.split('@')[0])
|
||||
reply_to_name = '%s %s' % (self.env.user.company_id.name, self.alias_record.name)
|
||||
reply_to_name = self.user_employee.name
|
||||
reply_to_email = '%s@%s' % (self.alias_record.alias_name, self.alias_domain)
|
||||
self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
|
||||
self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
|
||||
|
||||
# no alias domain -> author
|
||||
self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.domain')]).unlink()
|
||||
# no alias domain, no company catchall -> author
|
||||
self.alias_record.alias_domain_id = False
|
||||
self.env.company.sudo().alias_domain_id = False
|
||||
self.assertFalse(self.env.company.catchall_email)
|
||||
|
||||
msg = self.Message.create({
|
||||
'model': 'mail.test.container',
|
||||
|
|
@ -246,20 +419,18 @@ class TestMessageValues(TestMailCommon):
|
|||
self.assertEqual(msg.reply_to, formataddr((self.user_employee.name, self.user_employee.email)))
|
||||
self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
|
||||
|
||||
# no catchall -> don't care, alias
|
||||
self.env['ir.config_parameter'].set_param('mail.catchall.domain', self.alias_domain)
|
||||
self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.alias')]).unlink()
|
||||
# alias wins over company, hence no catchall is not an issue
|
||||
self.alias_record.alias_domain_id = self.mail_alias_domain
|
||||
|
||||
msg = self.Message.create({
|
||||
'model': 'mail.test.container',
|
||||
'res_id': self.alias_record.id
|
||||
})
|
||||
self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id.split('@')[0])
|
||||
reply_to_name = '%s %s' % (self.env.company.name, self.alias_record.name)
|
||||
reply_to_email = '%s@%s' % (self.alias_record.alias_name, self.alias_domain)
|
||||
self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
|
||||
self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_mail_message_values_fromto_document_no_alias(self):
|
||||
test_record = self.env['mail.test.simple'].create({'name': 'Test', 'email_from': 'ignasse@example.com'})
|
||||
|
|
@ -269,17 +440,17 @@ class TestMessageValues(TestMailCommon):
|
|||
'res_id': test_record.id
|
||||
})
|
||||
self.assertIn('-openerp-%d-mail.test.simple' % test_record.id, msg.message_id.split('@')[0])
|
||||
reply_to_name = '%s %s' % (self.env.user.company_id.name, test_record.name)
|
||||
reply_to_name = self.user_employee.name
|
||||
reply_to_email = '%s@%s' % (self.alias_catchall, self.alias_domain)
|
||||
self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
|
||||
self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_mail_message_values_fromto_document_manual_alias(self):
|
||||
test_record = self.env['mail.test.simple'].create({'name': 'Test', 'email_from': 'ignasse@example.com'})
|
||||
alias = self.env['mail.alias'].create({
|
||||
alias = self.env['mail.alias'].sudo().create({
|
||||
'alias_name': 'MegaLias',
|
||||
'alias_user_id': False,
|
||||
'alias_model_id': self.env['ir.model']._get('mail.test.simple').id,
|
||||
'alias_parent_model_id': self.env['ir.model']._get('mail.test.simple').id,
|
||||
'alias_parent_thread_id': test_record.id,
|
||||
|
|
@ -291,11 +462,12 @@ class TestMessageValues(TestMailCommon):
|
|||
})
|
||||
|
||||
self.assertIn('-openerp-%d-mail.test.simple' % test_record.id, msg.message_id.split('@')[0])
|
||||
reply_to_name = '%s %s' % (self.env.user.company_id.name, test_record.name)
|
||||
reply_to_name = self.user_employee.name
|
||||
reply_to_email = '%s@%s' % (alias.alias_name, self.alias_domain)
|
||||
self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
|
||||
self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
|
||||
|
||||
@users('employee')
|
||||
def test_mail_message_values_fromto_reply_to_force_new(self):
|
||||
msg = self.Message.create({
|
||||
'model': 'mail.test.container',
|
||||
|
|
@ -305,3 +477,8 @@ class TestMessageValues(TestMailCommon):
|
|||
self.assertIn('reply_to', msg.message_id.split('@')[0])
|
||||
self.assertNotIn('mail.test.container', msg.message_id.split('@')[0])
|
||||
self.assertNotIn('-%d-' % self.alias_record.id, msg.message_id.split('@')[0])
|
||||
|
||||
def test_mail_message_values_misc(self):
|
||||
""" Test various values on mail.message, notably default values """
|
||||
msg = self.env['mail.message'].create({'model': self.alias_record._name, 'res_id': self.alias_record.id})
|
||||
self.assertEqual(msg.message_type, 'comment', 'Message should be comments by default')
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
import base64
|
||||
|
||||
from markupsafe import Markup
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo import SUPERUSER_ID
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user, MailCommon
|
||||
from odoo.addons.test_mail.models.mail_test_access import MailTestAccess
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon
|
||||
from odoo.addons.test_mail.models.test_mail_models import MailTestSimple
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
|
||||
class MessageAccessCommon(TestMailCommon):
|
||||
class MessageAccessCommon(MailCommon, HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
|
@ -36,6 +37,13 @@ class MessageAccessCommon(TestMailCommon):
|
|||
name='Chell Gladys',
|
||||
)
|
||||
|
||||
cls.test_subtype_access_internal = cls.env['mail.message.subtype'].create([
|
||||
{
|
||||
'internal': True,
|
||||
'name': 'Test Internal',
|
||||
},
|
||||
])
|
||||
|
||||
(
|
||||
cls.record_public, cls.record_portal, cls.record_portal_ro,
|
||||
cls.record_followers,
|
||||
|
|
@ -133,7 +141,7 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
# - Criterions
|
||||
# - "private message" (no model, no res_id) -> deprecated
|
||||
# - follower of document
|
||||
# - document-based (write or create, using '_get_mail_message_access'
|
||||
# - document-based (write or create, using '_mail_get_operation_for_mail_message_operation'
|
||||
# hence '_mail_post_access' by default)
|
||||
# - notified of parent message
|
||||
# ------------------------------------------------------------
|
||||
|
|
@ -142,7 +150,7 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
def test_access_create(self):
|
||||
""" Test 'group_user' creation rules """
|
||||
# prepare 'notified of parent' condition
|
||||
admin_msg = self.record_admin.message_ids[0]
|
||||
admin_msg = self.record_admin.message_ids[-1]
|
||||
admin_msg.write({'partner_ids': [(4, self.user_employee.partner_id.id)]})
|
||||
|
||||
# prepare 'followers' condition
|
||||
|
|
@ -157,6 +165,7 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
(self.env["mail.test.access"], {}, False, 'Private message like is ok'),
|
||||
# document based
|
||||
(self.record_internal, {}, False, 'W Access on record'),
|
||||
(self.record_internal, {'message_type': 'notification'}, False, 'W Access on record, notification does not change anything'),
|
||||
(self.record_internal_ro, {}, True, 'No W Access on record'),
|
||||
(self.record_admin, {}, True, 'No access on record (and not notified on first message)'),
|
||||
(record_admin_fol, {
|
||||
|
|
@ -168,26 +177,30 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
}, False, 'No access on record but reply to notified parent'),
|
||||
]:
|
||||
with self.subTest(record=record, msg_vals=msg_vals, reason=reason):
|
||||
final_vals = dict(
|
||||
{
|
||||
'body': 'Test',
|
||||
'message_type': 'comment',
|
||||
'subtype_id': self.env.ref('mail.mt_comment').id,
|
||||
}, **msg_vals
|
||||
)
|
||||
if should_crash:
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.message'].with_user(self.user_employee).create({
|
||||
'model': record._name if record else False,
|
||||
'res_id': record.id if record else False,
|
||||
'body': 'Test',
|
||||
**msg_vals,
|
||||
**final_vals,
|
||||
})
|
||||
if record:
|
||||
with self.assertRaises(AccessError):
|
||||
record.with_user(self.user_employee).message_post(
|
||||
body='Test',
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
**final_vals,
|
||||
)
|
||||
else:
|
||||
_message = self.env['mail.message'].with_user(self.user_employee).create({
|
||||
'model': record._name if record else False,
|
||||
'res_id': record.id if record else False,
|
||||
'body': 'Test',
|
||||
**msg_vals,
|
||||
**final_vals,
|
||||
})
|
||||
if record:
|
||||
# TDE note: due to parent_id flattening, doing message_post
|
||||
|
|
@ -197,27 +210,57 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
if record == self.record_admin and 'parent_id' in msg_vals:
|
||||
continue
|
||||
record.with_user(self.user_employee).message_post(
|
||||
body='Test',
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
**msg_vals,
|
||||
**final_vals,
|
||||
)
|
||||
|
||||
def test_access_create_customized(self):
|
||||
""" Test '_get_mail_message_access' support """
|
||||
""" Test '_mail_get_operation_for_mail_message_operation' support """
|
||||
record = self.env['mail.test.access.custo'].with_user(self.user_employee).create({'name': 'Open'})
|
||||
for user in self.user_employee + self.user_portal:
|
||||
_message = record.message_post(
|
||||
body='A message',
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
# lock -> see '_get_mail_message_access'
|
||||
with self.subTest(user_name=user.name):
|
||||
_message = record.with_user(user).message_post(
|
||||
# attachments=[('Attachment', b'My attachment')], # FIXME
|
||||
body='A message',
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
# lock -> see '_mail_get_operation_for_mail_message_operation'
|
||||
record.write({'is_locked': True})
|
||||
record.message_unsubscribe(partner_ids=self.partner_employee.ids) # avoid acl conflict with those follower-based
|
||||
record.invalidate_model()
|
||||
for user in self.user_employee + self.user_portal:
|
||||
with self.assertRaises(AccessError):
|
||||
_message_portal = record.with_user(self.user_portal).message_post(
|
||||
with self.subTest(user_name=user.name):
|
||||
with self.assertRaises(AccessError):
|
||||
_message = record.with_user(user).message_post(
|
||||
body='Another portal message',
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
# readonly -> "read" access sufficient on unlocked records, see '_mail_get_operation_for_mail_message_operation'
|
||||
record.sudo().write({'is_locked': False, 'is_readonly': True})
|
||||
record.invalidate_model()
|
||||
for user in self.user_employee + self.user_portal:
|
||||
with self.subTest(user_name=user.name):
|
||||
# cannot write
|
||||
with self.assertRaises(AccessError):
|
||||
record.with_user(user).write({'name': 'Can Update'})
|
||||
# can post
|
||||
_message = record.with_user(user).message_post(
|
||||
# attachments=[('Attachment', b'My attachment')], # FIXME
|
||||
body='Another portal message',
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
# controller check
|
||||
self.authenticate(user.login, user.login)
|
||||
res = self.make_jsonrpc_request(
|
||||
route="/mail/message/post",
|
||||
params={
|
||||
'thread_model': record._name,
|
||||
'thread_id': record.id,
|
||||
'post_data': {
|
||||
'body': "Test",
|
||||
},
|
||||
},
|
||||
)['store_data']
|
||||
self.assertEqual(len(res['mail.message']), 1)
|
||||
|
||||
def test_access_create_mail_post_access(self):
|
||||
""" Test 'mail_post_access' support that allows creating a message with
|
||||
|
|
@ -270,6 +313,13 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
(self.record_admin, {
|
||||
'parent_id': admin_msg.id,
|
||||
}, False, 'No access on record but reply to notified parent'),
|
||||
# internal = forbidden (internal users only)
|
||||
(self.record_portal, {'is_internal': True}, True, 'Internal subtype always forbidden'),
|
||||
(self.record_portal, {'is_internal': True, 'message_type': 'notification'}, False, 'Automatic log accepted'),
|
||||
(self.record_portal, {'subtype_id': self.env.ref('mail.mt_note').id}, True, 'Internal flag always forbidden'),
|
||||
(self.record_portal, {'subtype_id': self.test_subtype_access_internal.id}, True, 'Internal flag (custom subtype) always forbidden'),
|
||||
(self.record_portal, {'message_type': 'notification', 'subtype_id': self.test_subtype_access_internal.id}, False, 'Automatic log accepted'),
|
||||
(self.record_portal, {'subtype_id': False}, True, 'No subtype = internal = always forbidden'),
|
||||
]:
|
||||
with self.subTest(record=record, msg_vals=msg_vals, reason=reason):
|
||||
if should_crash:
|
||||
|
|
@ -278,6 +328,8 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
'model': record._name if record else False,
|
||||
'res_id': record.id if record else False,
|
||||
'body': 'Test',
|
||||
'message_type': 'comment',
|
||||
'subtype_id': self.env.ref('mail.mt_comment').id,
|
||||
**msg_vals,
|
||||
})
|
||||
else:
|
||||
|
|
@ -285,6 +337,8 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
'model': record._name if record else False,
|
||||
'res_id': record.id if record else False,
|
||||
'body': 'Test',
|
||||
'message_type': 'comment',
|
||||
'subtype_id': self.env.ref('mail.mt_comment').id,
|
||||
**msg_vals,
|
||||
})
|
||||
|
||||
|
|
@ -294,6 +348,7 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
'model': self.record_portal._name,
|
||||
'res_id': self.record_portal.id,
|
||||
'body': 'Test',
|
||||
'subtype_id': self.env.ref('mail.mt_comment').id,
|
||||
})
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule')
|
||||
|
|
@ -329,18 +384,18 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
test_record.message_subscribe((partner_1 | self.user_admin.partner_id).ids)
|
||||
|
||||
message = test_record.message_post(
|
||||
body='<p>This is First Message</p>',
|
||||
body=Markup('<p>This is First Message</p>'),
|
||||
message_type='comment',
|
||||
subject='Subject',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
# portal user have no rights to read the message
|
||||
with self.assertRaises(AccessError):
|
||||
message.with_user(self.user_portal).read(['subject, body'])
|
||||
message.with_user(self.user_portal).read(['subject', 'body'])
|
||||
|
||||
with patch.object(MailTestSimple, 'check_access_rights', return_value=True):
|
||||
with patch.object(MailTestSimple, '_check_access', return_value=None):
|
||||
with self.assertRaises(AccessError):
|
||||
message.with_user(self.user_portal).read(['subject, body'])
|
||||
message.with_user(self.user_portal).read(['subject', 'body'])
|
||||
|
||||
# parent message is accessible to references notification mail values
|
||||
# for _notify method and portal user have no rights to send the message for this model
|
||||
|
|
@ -368,8 +423,9 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
# READ
|
||||
# - Criterions
|
||||
# - author
|
||||
# - creator (might post on behalf of someone else)
|
||||
# - recipients / notified
|
||||
# - document-based: read, using '_get_mail_message_access'
|
||||
# - document-based: read, using '_mail_get_operation_for_mail_message_operation'
|
||||
# - share users: limited to 'not internal' (flag or subtype)
|
||||
# ------------------------------------------------------------
|
||||
|
||||
|
|
@ -384,6 +440,9 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
(self.record_admin.message_ids[0], {
|
||||
'author_id': self.user_employee.partner_id.id,
|
||||
}, False, 'Author > no access on record'),
|
||||
(self.record_admin.message_ids[0], {
|
||||
'create_uid': self.user_employee.id,
|
||||
}, False, 'Creator > no access on record'),
|
||||
# notified
|
||||
(self.record_admin.message_ids[0], {
|
||||
'notification_ids': [(0, 0, {
|
||||
|
|
@ -400,7 +459,12 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
'parent_id': msg.parent_id.id,
|
||||
}
|
||||
with self.subTest(msg=msg, reason=reason):
|
||||
if msg_vals:
|
||||
if 'create_uid' in msg_vals:
|
||||
self.patch(self.env.registry, 'ready', False)
|
||||
msg.with_user(SUPERUSER_ID).write(msg_vals)
|
||||
self.patch(self.env.registry, 'ready', True)
|
||||
self.assertEqual(msg.create_uid.id, msg_vals['create_uid'])
|
||||
elif msg_vals:
|
||||
msg.write(msg_vals)
|
||||
if should_crash:
|
||||
with self.assertRaises(AccessError):
|
||||
|
|
@ -410,6 +474,35 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
if msg_vals:
|
||||
msg.write(original_vals)
|
||||
|
||||
def test_access_read_customized(self):
|
||||
""" Test '_mail_get_operation_for_mail_message_operation' support """
|
||||
records = self.env['mail.test.access.custo'].with_user(self.user_admin).create([
|
||||
{'name': 'Open'},
|
||||
{'name': 'Open RO', 'is_readonly': True},
|
||||
{'is_locked': True, 'name': 'Locked'},
|
||||
])
|
||||
messages_all = self.env['mail.message']
|
||||
for record in records:
|
||||
messages_all += record.with_user(self.user_admin).message_post(
|
||||
body=f'AnchorForTest / A message from {self.user_admin.name} on {record.name}',
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
# lock -> see '_mail_get_operation_for_mail_message_operation', cannot read locked message
|
||||
# without write access, with is not granted for employees
|
||||
with self.assertRaises(AccessError): # write access not granted on locked -> cannot read message
|
||||
messages_all[2].with_user(self.user_employee).read(['subject'])
|
||||
with self.assertRaises(AccessError): # also working in case of batch ok / not ok
|
||||
messages_all.with_user(self.user_employee).read(['subject'])
|
||||
messages_all[0].with_user(self.user_employee).read(['subject'])
|
||||
messages_all[1].with_user(self.user_employee).read(['subject']) # can read message of readonly
|
||||
|
||||
with self.assertRaises(AccessError): # fetch should be symmetric to read
|
||||
_message = messages_all[2].with_user(self.user_employee).copy_data()
|
||||
|
||||
with self.assertRaises(AccessError): # no write access at all
|
||||
messages_all.with_user(self.user_portal).read(['subject'])
|
||||
messages_all.with_user(self.user_admin).read(['subject'])
|
||||
|
||||
def test_access_read_portal(self):
|
||||
""" Read access check for portal users """
|
||||
for msg, msg_vals, should_crash, reason in [
|
||||
|
|
@ -426,17 +519,36 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
'res_partner_id': self.user_portal.partner_id.id,
|
||||
})],
|
||||
}, False, 'Notified > no access on record'),
|
||||
# forbidden
|
||||
# forbidden: internal (subtype / message)
|
||||
(self.record_portal.message_ids[0], {
|
||||
'subtype_id': self.env.ref('mail.mt_note').id,
|
||||
}, True, 'Note cannot be read by portal users'),
|
||||
}, True, 'Note (comment) cannot be read by portal users'),
|
||||
(self.record_portal.message_ids[0], {
|
||||
'subtype_id': self.test_subtype_access_internal.id,
|
||||
}, True, 'Internal subtype (comment) cannot be read by portal users'),
|
||||
(self.record_portal.message_ids[0], {
|
||||
'message_type': 'email_outgoing',
|
||||
'subtype_id': self.env.ref('mail.mt_note').id,
|
||||
}, False, 'Note (email_outgoing) can be read by portal users'),
|
||||
(self.record_portal.message_ids[0], {
|
||||
'subtype_id': False,
|
||||
}, True, 'Pure log (no subtype, even comment) cannot be read by portal users'),
|
||||
(self.record_portal.message_ids[0], {
|
||||
'is_internal': True,
|
||||
}, True, 'Internal message cannot be read by portal users'),
|
||||
}, True, 'Internal message (comment) cannot be read by portal users'),
|
||||
(self.record_portal.message_ids[0], {
|
||||
'is_internal': True,
|
||||
'message_type': 'notification',
|
||||
}, False, 'Internal message (notification) can be read by portal users'),
|
||||
# forbidden: other
|
||||
(self.record_portal.message_ids[0], {
|
||||
'message_type': 'user_notification',
|
||||
}, True, 'User notifications for other people can never be read by portal users'),
|
||||
]:
|
||||
original_vals = {
|
||||
'author_id': msg.author_id.id,
|
||||
'is_internal': False,
|
||||
'message_type': msg.message_type,
|
||||
'notification_ids': [(6, 0, {})],
|
||||
'parent_id': msg.parent_id.id,
|
||||
'subtype_id': self.env.ref('mail.mt_comment').id,
|
||||
|
|
@ -444,6 +556,8 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
with self.subTest(msg=msg, reason=reason):
|
||||
if msg_vals:
|
||||
msg.write(msg_vals)
|
||||
|
||||
self.env.invalidate_all()
|
||||
if should_crash:
|
||||
with self.assertRaises(AccessError):
|
||||
msg.with_user(self.user_portal).read(['body'])
|
||||
|
|
@ -451,6 +565,7 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
msg.with_user(self.user_portal).read(['body'])
|
||||
if msg_vals:
|
||||
msg.write(original_vals)
|
||||
self.env.invalidate_all()
|
||||
|
||||
def test_access_read_public(self):
|
||||
""" Read access check for public users """
|
||||
|
|
@ -479,6 +594,7 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
original_vals = {
|
||||
'author_id': msg.author_id.id,
|
||||
'is_internal': False,
|
||||
'message_type': msg.message_type,
|
||||
'notification_ids': [(6, 0, {})],
|
||||
'parent_id': msg.parent_id.id,
|
||||
'subtype_id': self.env.ref('mail.mt_comment').id,
|
||||
|
|
@ -486,6 +602,8 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
with self.subTest(msg=msg, reason=reason):
|
||||
if msg_vals:
|
||||
msg.write(msg_vals)
|
||||
|
||||
self.env.invalidate_all()
|
||||
if should_crash:
|
||||
with self.assertRaises(AccessError):
|
||||
msg.with_user(self.user_public).read(['body'])
|
||||
|
|
@ -493,10 +611,11 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
msg.with_user(self.user_public).read(['body'])
|
||||
if msg_vals:
|
||||
msg.write(original_vals)
|
||||
self.env.invalidate_all()
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# UNLINK
|
||||
# - Criterion: document-based (write or create), using '_get_mail_message_access'
|
||||
# - Criterion: document-based (write or create), using '_mail_get_operation_for_mail_message_operation'
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def test_access_unlink(self):
|
||||
|
|
@ -548,7 +667,7 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
# - Criterions
|
||||
# - author
|
||||
# - recipients / notified
|
||||
# - document-based (write or create), using '_get_mail_message_access'
|
||||
# - document-based (write or create), using '_mail_get_operation_for_mail_message_operation'
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def test_access_write(self):
|
||||
|
|
@ -590,13 +709,12 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
""" Test updating message envelope require some privileges """
|
||||
message = self.record_internal.with_user(self.user_employee).message_ids[0]
|
||||
message.write({'body': 'Update Me'})
|
||||
# To change in 18+
|
||||
message.write({'model': 'res.partner'})
|
||||
message.sudo().write({'model': self.record_internal._name}) # back to original model
|
||||
with self.assertRaises(AccessError):
|
||||
message.write({'model': 'res.partner'})
|
||||
# To change in 18+
|
||||
message.write({'partner_ids': [(4, self.user_portal_2.partner_id.id)]})
|
||||
# To change in 18+
|
||||
message.write({'res_id': self.record_public.id})
|
||||
with self.assertRaises(AccessError):
|
||||
message.write({'res_id': self.record_public.id})
|
||||
# To change in 18+
|
||||
message.write({'notification_ids': [
|
||||
(0, 0, {'res_partner_id': self.user_portal_2.partner_id.id})
|
||||
|
|
@ -672,12 +790,26 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
res_id=self.record_portal.id,
|
||||
subtype_id=self.ref('mail.mt_comment'),
|
||||
))
|
||||
msg_record_portal_internal = self.env['mail.message'].create(dict(base_msg_vals,
|
||||
body='Internal Comment on Portal',
|
||||
is_internal=True,
|
||||
model=self.record_portal._name,
|
||||
res_id=self.record_portal.id,
|
||||
subtype_id=self.ref('mail.mt_comment'),
|
||||
))
|
||||
msg_record_public = self.env['mail.message'].create(dict(base_msg_vals,
|
||||
body='Public Comment',
|
||||
model=self.record_public._name,
|
||||
res_id=self.record_public.id,
|
||||
subtype_id=self.ref('mail.mt_comment'),
|
||||
))
|
||||
msg_record_public_internal = self.env['mail.message'].create(dict(base_msg_vals,
|
||||
body='Internal Comment on Public',
|
||||
is_internal=True,
|
||||
model=self.record_public._name,
|
||||
res_id=self.record_public.id,
|
||||
subtype_id=self.ref('mail.mt_comment'),
|
||||
))
|
||||
|
||||
for (test_user, add_domain), exp_messages in zip([
|
||||
(self.user_public, []),
|
||||
|
|
@ -686,16 +818,60 @@ class TestMailMessageAccess(MessageAccessCommon):
|
|||
(self.user_employee, [('body', 'ilike', 'Internal')]),
|
||||
(self.user_admin, []),
|
||||
], [
|
||||
# public: record with access
|
||||
msg_record_public,
|
||||
# portal: mentionned + record with access, if published
|
||||
msgs[0] + msgs[3] + msg_record_portal + msg_record_public,
|
||||
msgs[1:6] + msg_record_portal + msg_record_public,
|
||||
msgs[1:6],
|
||||
msgs[1:] + msg_record_admin + msg_record_portal + msg_record_public
|
||||
# employee
|
||||
msgs[1:6] + msg_record_portal + msg_record_portal_internal + msg_record_public + msg_record_public_internal,
|
||||
msgs[1:6] + msg_record_portal_internal + msg_record_public_internal,
|
||||
msgs[1:] + msg_record_admin + msg_record_portal + msg_record_portal_internal + msg_record_public + msg_record_public_internal,
|
||||
]):
|
||||
with self.subTest(test_user=test_user.name, add_domain=add_domain):
|
||||
self.env.invalidate_all()
|
||||
domain = [('subject', 'like', '_ZTest')] + add_domain
|
||||
self.assertEqual(self.env['mail.message'].with_user(test_user).search(domain), exp_messages)
|
||||
|
||||
def test_search_customized(self):
|
||||
""" Test '_mail_get_operation_for_mail_message_operation' support in search """
|
||||
records = self.env['mail.test.access.custo'].with_user(self.user_admin).create([
|
||||
{'name': 'Open'},
|
||||
{'name': 'Open RO', 'is_readonly': True}, # internal can read thus search
|
||||
{'name': 'Soonish Locked'},
|
||||
])
|
||||
messages_all = self.env['mail.message'].sudo()
|
||||
for user in self.user_employee + self.user_portal:
|
||||
for record in records:
|
||||
new = record.message_post(
|
||||
body=f'AnchorForSearch / A message from {user.name}',
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
messages_all += new.sudo()
|
||||
|
||||
found_emp = self.env['mail.message'].with_user(self.user_employee).search([
|
||||
('body', 'ilike', 'AnchorForSearch')
|
||||
])
|
||||
self.assertEqual(found_emp, messages_all)
|
||||
found_por = self.env['mail.message'].with_user(self.user_portal).search([
|
||||
('body', 'ilike', 'AnchorForSearch')
|
||||
])
|
||||
self.assertEqual(found_por, messages_all)
|
||||
|
||||
# lock -> locked records need 'write' access, as defined in '_mail_get_operation_for_mail_message_operation'
|
||||
# hence messages are out of search, symmetrical to reading therm
|
||||
records[2].write({'is_locked': True, 'name': 'Locked !'})
|
||||
records[2].flush_recordset()
|
||||
found_emp = self.env['mail.message'].with_user(self.user_employee).search([
|
||||
('body', 'ilike', 'AnchorForSearch')
|
||||
])
|
||||
self.assertEqual(found_emp, messages_all.filtered(lambda m: m.res_id != records[2].id), 'Should filter like read')
|
||||
found_emp.read(['subject'])
|
||||
found_por = self.env['mail.message'].with_user(self.user_portal).search([
|
||||
('body', 'ilike', 'AnchorForSearch')
|
||||
])
|
||||
self.assertEqual(found_por, messages_all.filtered(lambda m: m.res_id != records[2].id), 'Should filter like read')
|
||||
found_por.read(['subject'])
|
||||
|
||||
|
||||
@tagged('mail_message', 'security', 'post_install', '-at_install')
|
||||
class TestMessageSubModelAccess(MessageAccessCommon):
|
||||
|
|
@ -762,9 +938,11 @@ class TestMessageSubModelAccess(MessageAccessCommon):
|
|||
with self.assertRaises(AccessError):
|
||||
notif_own.write({'res_partner_id': self.user_admin.partner_id.id})
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule')
|
||||
def test_mail_notification_portal(self):
|
||||
""" In any case, portal should not modify notifications """
|
||||
self.assertFalse(self.env['mail.notification'].with_user(self.user_portal).check_access_rights('write', raise_exception=False))
|
||||
with self.assertRaises(AccessError):
|
||||
self.assertFalse(self.env['mail.notification'].with_user(self.user_portal).check_access('write'))
|
||||
portal_record = self.record_portal.with_user(self.user_portal)
|
||||
message = portal_record.message_post(
|
||||
body='Hello People',
|
||||
|
|
@ -776,3 +954,17 @@ class TestMessageSubModelAccess(MessageAccessCommon):
|
|||
self.assertEqual(len(notifications), 2)
|
||||
self.assertTrue(bool(notifications.read(['is_read'])), 'Portal can read')
|
||||
self.assertEqual(notifications.res_partner_id, self.user_portal_2.partner_id + self.user_employee.partner_id)
|
||||
|
||||
internal_record = self.record_internal.with_user(self.user_admin)
|
||||
message = internal_record.message_post(
|
||||
body='Hello People',
|
||||
message_type='comment',
|
||||
partner_ids=self.user_employee.partner_id.ids,
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
notifications = message.notification_ids.with_user(self.user_portal)
|
||||
with self.assertRaises(
|
||||
AccessError,
|
||||
msg="Portal cannot read notifications unless they are the recipient or the author"
|
||||
):
|
||||
notifications.read(['is_read'])
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import json
|
||||
import socket
|
||||
|
||||
from itertools import product
|
||||
from freezegun import freeze_time
|
||||
from unittest.mock import patch
|
||||
from werkzeug.urls import url_parse, url_decode
|
||||
from werkzeug.urls import url_parse
|
||||
|
||||
from odoo.addons.mail.models.mail_message import Message
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.addons.mail.models.mail_message import MailMessage
|
||||
from odoo.addons.mail.tests.common import MailCommon, mail_new_test_user
|
||||
from odoo.addons.test_mail.models.test_mail_corner_case_models import MailTestMultiCompanyWithActivity
|
||||
from odoo.addons.test_mail.tests.common import TestRecipients
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests import tagged, users, HttpCase
|
||||
from odoo.tools import formataddr, mute_logger
|
||||
from odoo.tests.common import JsonRpcException
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('multi_company')
|
||||
class TestMultiCompanySetup(TestMailCommon, TestRecipients):
|
||||
class TestMailMCCommon(MailCommon, TestRecipients):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMultiCompanySetup, cls).setUpClass()
|
||||
cls._activate_multi_company()
|
||||
super().setUpClass()
|
||||
|
||||
cls.test_model = cls.env['ir.model']._get('mail.test.gateway')
|
||||
cls.email_from = '"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>'
|
||||
|
|
@ -38,17 +38,16 @@ class TestMultiCompanySetup(TestMailCommon, TestRecipients):
|
|||
'company_id': cls.user_employee_c2.company_id.id},
|
||||
])
|
||||
|
||||
cls.company_3 = cls.env['res.company'].create({'name': 'ELIT'})
|
||||
cls.partner_1 = cls.env['res.partner'].with_context(cls._test_context).create({
|
||||
'name': 'Valid Lelitre',
|
||||
'email': 'valid.lelitre@agrolait.com',
|
||||
})
|
||||
# groups@.. will cause the creation of new mail.test.gateway
|
||||
cls.alias = cls.env['mail.alias'].create({
|
||||
'alias_name': 'groups',
|
||||
'alias_user_id': False,
|
||||
cls.mail_alias = cls.env['mail.alias'].create({
|
||||
'alias_contact': 'everyone',
|
||||
'alias_model_id': cls.test_model.id,
|
||||
'alias_contact': 'everyone'})
|
||||
'alias_name': 'groups',
|
||||
})
|
||||
|
||||
# Set a first message on public group to test update and hierarchy
|
||||
cls.fake_email = cls.env['mail.message'].create({
|
||||
|
|
@ -61,11 +60,23 @@ class TestMultiCompanySetup(TestMailCommon, TestRecipients):
|
|||
'message_id': '<123456-openerp-%s-mail.test.gateway@%s>' % (cls.test_record.id, socket.gethostname()),
|
||||
})
|
||||
|
||||
cls._create_portal_user()
|
||||
cls.user_portal_c2 = mail_new_test_user(
|
||||
cls.env,
|
||||
groups='base.group_portal',
|
||||
login='portal_user_c2',
|
||||
company_id=cls.company_2.id,
|
||||
name="Portal User C2",
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super(TestMultiCompanySetup, self).setUp()
|
||||
super().setUp()
|
||||
# patch registry to simulate a ready environment
|
||||
self.patch(self.env.registry, 'ready', True)
|
||||
self.flush_tracking()
|
||||
|
||||
|
||||
@tagged('multi_company')
|
||||
class TestMultiCompanySetup(TestMailMCCommon, HttpCase):
|
||||
|
||||
@users('employee_c2')
|
||||
@mute_logger('odoo.addons.base.models.ir_rule')
|
||||
|
|
@ -85,26 +96,35 @@ class TestMultiCompanySetup(TestMailCommon, TestRecipients):
|
|||
with self.assertRaises(AccessError):
|
||||
test_record_c1.write({'name': 'Cannot Write'})
|
||||
|
||||
first_attachment = self.env['ir.attachment'].create({
|
||||
'company_id': self.user_employee_c2.company_id.id,
|
||||
'datas': base64.b64encode(b'First attachment'),
|
||||
'mimetype': 'text/plain',
|
||||
'name': 'TestAttachmentIDS.txt',
|
||||
'res_model': 'mail.compose.message',
|
||||
'res_id': 0,
|
||||
})
|
||||
|
||||
message = test_record_c1.message_post(
|
||||
attachments=[('testAttachment', b'Test attachment')],
|
||||
attachments=[('testAttachment', b'First attachment')],
|
||||
attachment_ids=first_attachment.ids,
|
||||
body='My Body',
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
self.assertEqual(message.attachment_ids.mapped('name'), ['testAttachment'])
|
||||
first_attachment = message.attachment_ids
|
||||
self.assertTrue('testAttachment' in message.attachment_ids.mapped('name'))
|
||||
self.assertEqual(test_record_c1.message_main_attachment_id, first_attachment)
|
||||
|
||||
new_attach = self.env['ir.attachment'].create({
|
||||
'company_id': self.user_employee_c2.company_id.id,
|
||||
'datas': base64.b64encode(b'Test attachment'),
|
||||
'datas': base64.b64encode(b'Second attachment'),
|
||||
'mimetype': 'text/plain',
|
||||
'name': 'TestAttachmentIDS.txt',
|
||||
'res_model': 'mail.compose.message',
|
||||
'res_id': 0,
|
||||
})
|
||||
message = test_record_c1.message_post(
|
||||
attachments=[('testAttachment', b'Test attachment')],
|
||||
attachments=[('testAttachment', b'Second attachment')],
|
||||
attachment_ids=new_attach.ids,
|
||||
body='My Body',
|
||||
message_type='comment',
|
||||
|
|
@ -129,19 +149,19 @@ class TestMultiCompanySetup(TestMailCommon, TestRecipients):
|
|||
# Other company (no access)
|
||||
# ------------------------------------------------------------
|
||||
|
||||
_original_car = Message.check_access_rule
|
||||
with patch.object(Message, 'check_access_rule',
|
||||
_original_car = MailMessage._check_access
|
||||
with patch.object(MailMessage, '_check_access',
|
||||
autospec=True, side_effect=_original_car) as mock_msg_car:
|
||||
with self.assertRaises(AccessError):
|
||||
test_records_mc_c1.message_post(
|
||||
body='<p>Hello</p>',
|
||||
force_record_name='CustomName', # avoid ACL on display_name
|
||||
message_type='comment',
|
||||
record_name='CustomName', # avoid ACL on display_name
|
||||
reply_to='custom.reply.to@test.example.com', # avoid ACL in notify_get_reply_to
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
self.assertEqual(mock_msg_car.call_count, 1,
|
||||
'Purpose is to raise at msg check access level')
|
||||
self.assertEqual(mock_msg_car.call_count, 2,
|
||||
'Check at model level succeeds and check at record level fails')
|
||||
with self.assertRaises(AccessError):
|
||||
_name = test_records_mc_c1.name
|
||||
|
||||
|
|
@ -163,26 +183,17 @@ class TestMultiCompanySetup(TestMailCommon, TestRecipients):
|
|||
# now able to post as was notified of parent message
|
||||
test_records_mc_c1.message_post(
|
||||
body='<p>Hello</p>',
|
||||
force_record_name='CustomName', # avoid ACL on display_name
|
||||
message_type='comment',
|
||||
parent_id=initial_message.id,
|
||||
record_name='CustomName', # avoid ACL on display_name
|
||||
reply_to='custom.reply.to@test.example.com', # avoid ACL in notify_get_reply_to
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
|
||||
# now able to post as was notified of parent message
|
||||
attachments = self.env['ir.attachment'].create(
|
||||
self._generate_attachments_data(
|
||||
2, 'mail.compose.message', 0,
|
||||
prefix='Other'
|
||||
)
|
||||
)
|
||||
# record_name and reply_to may generate ACLs issues when computed by
|
||||
# 'message_post' but should not, hence not specifying them to be sure
|
||||
# testing the complete flow
|
||||
test_records_mc_c1.message_post(
|
||||
attachments=attachments_data,
|
||||
attachment_ids=attachments.ids,
|
||||
body='<p>Hello</p>',
|
||||
message_type='comment',
|
||||
parent_id=initial_message.id,
|
||||
|
|
@ -204,8 +215,8 @@ class TestMultiCompanySetup(TestMailCommon, TestRecipients):
|
|||
attachments=attachments_data,
|
||||
attachment_ids=attachments.ids,
|
||||
body='<p>Hello</p>',
|
||||
force_record_name='CustomName', # avoid ACL on display_name
|
||||
message_type='comment',
|
||||
record_name='CustomName', # avoid ACL on display_name
|
||||
reply_to='custom.reply.to@test.example.com', # avoid ACL in notify_get_reply_to
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
|
|
@ -238,71 +249,106 @@ class TestMultiCompanySetup(TestMailCommon, TestRecipients):
|
|||
attachments=attachments_data,
|
||||
attachment_ids=attachments.ids,
|
||||
body='<p>Hello</p>',
|
||||
force_record_name='CustomName', # avoid ACL on display_name
|
||||
message_type='comment',
|
||||
record_name='CustomName', # avoid ACL on display_name
|
||||
reply_to='custom.reply.to@test.example.com', # avoid ACL in notify_get_reply_to
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
|
||||
def test_systray_get_activities(self):
|
||||
self.env["mail.activity"].search([]).unlink()
|
||||
user_admin = self.user_admin.with_user(self.user_admin)
|
||||
test_records = self.env["mail.test.multi.company.with.activity"].create(
|
||||
[
|
||||
{"name": "Test1", "company_id": user_admin.company_id.id},
|
||||
{"name": "Test2", "company_id": self.company_2.id},
|
||||
]
|
||||
)
|
||||
test_records[0].activity_schedule("test_mail.mail_act_test_todo", user_id=user_admin.id)
|
||||
test_records[1].activity_schedule("test_mail.mail_act_test_todo", user_id=user_admin.id)
|
||||
test_activity = next(
|
||||
a for a in user_admin.systray_get_activities()
|
||||
if a['model'] == 'mail.test.multi.company.with.activity'
|
||||
)
|
||||
self.assertEqual(
|
||||
test_activity,
|
||||
{
|
||||
"actions": [{"icon": "fa-clock-o", "name": "Summary"}],
|
||||
"icon": "/base/static/description/icon.png",
|
||||
"id": self.env["ir.model"]._get_id("mail.test.multi.company.with.activity"),
|
||||
"model": "mail.test.multi.company.with.activity",
|
||||
"name": "Test Multi Company Mail With Activity",
|
||||
"overdue_count": 0,
|
||||
"planned_count": 0,
|
||||
"today_count": 2,
|
||||
"total_count": 2,
|
||||
"type": "activity",
|
||||
}
|
||||
)
|
||||
|
||||
test_activity = next(
|
||||
a for a in user_admin.with_context(allowed_company_ids=[self.company_2.id]).systray_get_activities()
|
||||
if a['model'] == 'mail.test.multi.company.with.activity'
|
||||
)
|
||||
self.assertEqual(
|
||||
test_activity,
|
||||
{
|
||||
"actions": [{"icon": "fa-clock-o", "name": "Summary"}],
|
||||
"icon": "/base/static/description/icon.png",
|
||||
"id": self.env["ir.model"]._get_id("mail.test.multi.company.with.activity"),
|
||||
"model": "mail.test.multi.company.with.activity",
|
||||
"name": "Test Multi Company Mail With Activity",
|
||||
"overdue_count": 0,
|
||||
"planned_count": 0,
|
||||
"today_count": 1,
|
||||
"total_count": 1,
|
||||
"type": "activity",
|
||||
}
|
||||
)
|
||||
def test_recipients_multi_company(self):
|
||||
"""Test mentioning a partner with no common company."""
|
||||
test_records_mc_c2 = self.test_records_mc[1]
|
||||
with self.assertBus([(self.cr.dbname, "res.partner", self.user_employee_c3.partner_id.id)]):
|
||||
test_records_mc_c2.with_user(self.user_employee_c2).with_context(
|
||||
allowed_company_ids=self.company_2.ids
|
||||
).message_post(
|
||||
body="Hello @Freudenbergerg",
|
||||
message_type="comment",
|
||||
partner_ids=self.user_employee_c3.partner_id.ids,
|
||||
subtype_xmlid="mail.mt_comment",
|
||||
)
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'multi_company', 'mail_controller')
|
||||
class TestMultiCompanyRedirect(TestMailCommon, HttpCase):
|
||||
class TestMultiCompanyControllers(TestMailMCCommon, HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMultiCompanyRedirect, cls).setUpClass()
|
||||
cls._activate_multi_company()
|
||||
@mute_logger('odoo.http')
|
||||
def test_mail_thread_data(self):
|
||||
""" Test returned thread data, in MC environment, to test notably MC
|
||||
access issues on partner, ACL support, ... """
|
||||
customer_c3 = self.env["res.partner"].create({
|
||||
"company_id": self.company_3.id,
|
||||
"name": "C3 Customer",
|
||||
})
|
||||
record = self.env["mail.test.multi.company.read"].with_user(self.user_employee_c2).create({
|
||||
"company_id": self.user_employee_c2.company_id.id,
|
||||
"name": "Multi Company Record",
|
||||
})
|
||||
self.assertEqual(record.company_id, self.company_2)
|
||||
|
||||
record.message_subscribe(partner_ids=customer_c3.ids)
|
||||
with self.assertRaises(AccessError):
|
||||
customer_c3.with_user(self.user_employee_c2).check_access("read")
|
||||
|
||||
self.authenticate(self.user_employee_c2.login, self.user_employee_c2.login)
|
||||
result = self.make_jsonrpc_request(
|
||||
"/mail/data", {"fetch_params": [["mail.thread", {
|
||||
"thread_id": record.id,
|
||||
"thread_model": record._name,
|
||||
"request_list": ["followers"],
|
||||
}]]},
|
||||
)
|
||||
self.assertEqual(len(result["mail.followers"]), 2)
|
||||
self.assertEqual(result["mail.followers"][0]["partner_id"], customer_c3.id)
|
||||
self.assertEqual(result["mail.thread"][0]["followersCount"], 2)
|
||||
self.assertTrue(result["mail.thread"][0]["hasWriteAccess"])
|
||||
self.assertTrue(result["mail.thread"][0]["hasReadAccess"])
|
||||
self.assertTrue(result["mail.thread"][0]["canPostOnReadonly"])
|
||||
|
||||
# check read / write / post access info
|
||||
for test_user, (has_w, has_r, can_post) in zip(
|
||||
(self.user_portal, self.user_portal_c2, self.user_employee, self.user_admin),
|
||||
(
|
||||
(False, True, True), # currently not really supported actually, should go through portal controllers
|
||||
(False, True, True), # currently not really supported actually, should go through portal controllers
|
||||
(False, True, True),
|
||||
(True, True, True),
|
||||
),
|
||||
):
|
||||
with self.subTest(user_name=test_user.name):
|
||||
self.authenticate(test_user.login, test_user.login)
|
||||
# crash if calling using portal users -> dedicated portal routes currently
|
||||
if test_user in self.user_portal + self.user_portal_c2:
|
||||
with self.assertRaises(JsonRpcException):
|
||||
result = self.make_jsonrpc_request(
|
||||
"/mail/data", {"fetch_params": [["mail.thread", {
|
||||
"thread_id": record.id,
|
||||
"thread_model": record._name,
|
||||
"request_list": ["followers"],
|
||||
}]]},
|
||||
)
|
||||
else:
|
||||
result = self.make_jsonrpc_request(
|
||||
"/mail/data", {"fetch_params": [["mail.thread", {
|
||||
"thread_id": record.id,
|
||||
"thread_model": record._name,
|
||||
"request_list": ["followers"],
|
||||
}]]},
|
||||
)
|
||||
self.assertEqual(result["mail.thread"][0]["followersCount"], 2)
|
||||
self.assertEqual(result["mail.thread"][0]["hasWriteAccess"], has_w)
|
||||
self.assertEqual(result["mail.thread"][0]["hasReadAccess"], has_r)
|
||||
self.assertEqual(result["mail.thread"][0]["canPostOnReadonly"], can_post)
|
||||
|
||||
record.with_user(self.user_admin).message_post(
|
||||
body='Hello!',
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
partner_ids=[self.partner_employee_c2.id, customer_c3.id],
|
||||
)
|
||||
self.authenticate(self.user_employee_c2.login, self.user_employee_c2.login)
|
||||
messages = self.make_jsonrpc_request("/mail/inbox/messages")
|
||||
self.assertEqual(len(messages['data']['mail.message']), 1)
|
||||
|
||||
def test_redirect_to_records(self):
|
||||
""" Test mail/view redirection in MC environment, notably cids being
|
||||
|
|
@ -336,8 +382,7 @@ class TestMultiCompanyRedirect(TestMailCommon, HttpCase):
|
|||
if not login:
|
||||
path = url_parse(response.url).path
|
||||
self.assertEqual(path, '/web/login')
|
||||
decoded_fragment = url_decode(url_parse(response.url).fragment)
|
||||
self.assertNotIn("cids", decoded_fragment)
|
||||
self.assertNotIn('cids', response.request._cookies)
|
||||
else:
|
||||
user = self.env['res.users'].browse(self.session.uid)
|
||||
self.assertEqual(user.login, login)
|
||||
|
|
@ -346,19 +391,46 @@ class TestMultiCompanyRedirect(TestMailCommon, HttpCase):
|
|||
# Logged into company main, try accessing record in other
|
||||
# company -> _redirect_to_record should redirect to
|
||||
# messaging as the user doesn't have any access
|
||||
fragment = url_parse(response.url).fragment
|
||||
action = url_decode(fragment)['action']
|
||||
self.assertEqual(action, 'mail.action_discuss')
|
||||
parsed_url = url_parse(response.url)
|
||||
self.assertEqual(parsed_url.path, '/odoo/action-mail.action_discuss')
|
||||
else:
|
||||
# Logged into company main, try accessing record in same
|
||||
# company -> _redirect_to_record should add company in
|
||||
# allowed_company_ids
|
||||
fragment = url_parse(response.url).fragment
|
||||
cids = url_decode(fragment)['cids']
|
||||
cids = response.request._cookies.get('cids')
|
||||
if mc_record.company_id == user.company_id:
|
||||
self.assertEqual(cids, f'{mc_record.company_id.id}')
|
||||
else:
|
||||
self.assertEqual(cids, f'{user.company_id.id},{mc_record.company_id.id}')
|
||||
self.assertEqual(cids, f'{user.company_id.id}-{mc_record.company_id.id}')
|
||||
|
||||
def test_multi_redirect_to_records(self):
|
||||
""" Test mail/view redirection in MC environment, notably test a user that is
|
||||
redirected multiple times, the cids needed to access the record are being added
|
||||
recursivelly when in redirect."""
|
||||
mc_records = self.env['mail.test.multi.company'].create([
|
||||
{
|
||||
'company_id': self.user_employee.company_id.id,
|
||||
'name': 'Multi Company Record',
|
||||
},
|
||||
{
|
||||
'company_id': self.user_employee_c2.company_id.id,
|
||||
'name': 'Multi Company Record',
|
||||
}
|
||||
])
|
||||
|
||||
self.authenticate(self.user_admin.login, self.user_admin.login)
|
||||
companies = []
|
||||
for mc_record in mc_records:
|
||||
with self.subTest(mc_record=mc_record):
|
||||
response = self.url_open(
|
||||
f'/mail/view?model={mc_record._name}&res_id={mc_record.id}',
|
||||
timeout=15
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
cids = response.request._cookies.get('cids')
|
||||
companies.append(str(mc_record.company_id.id))
|
||||
self.assertEqual(cids, '-'.join(companies))
|
||||
|
||||
def test_redirect_to_records_nothread(self):
|
||||
""" Test no thread models and redirection """
|
||||
|
|
@ -372,10 +444,10 @@ class TestMultiCompanyRedirect(TestMailCommon, HttpCase):
|
|||
|
||||
# when being logged, cids should be based on current user's company unless
|
||||
# there is an access issue (not tested here, see 'test_redirect_to_records')
|
||||
self.authenticate(self.user_admin.login, self.user_admin.login)
|
||||
for test_record in nothreads:
|
||||
for user_company in self.company_admin, self.company_2:
|
||||
with self.subTest(record_name=test_record.name, user_company=user_company):
|
||||
self.authenticate(self.user_admin.login, self.user_admin.login)
|
||||
self.user_admin.write({'company_id': user_company.id})
|
||||
response = self.url_open(
|
||||
f'/mail/view?model={test_record._name}&res_id={test_record.id}',
|
||||
|
|
@ -383,9 +455,8 @@ class TestMultiCompanyRedirect(TestMailCommon, HttpCase):
|
|||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
decoded_fragment = url_decode(url_parse(response.url).fragment)
|
||||
self.assertTrue("cids" in decoded_fragment)
|
||||
self.assertEqual(decoded_fragment['cids'], str(user_company.id))
|
||||
self.assertTrue('cids' in response.request._cookies)
|
||||
self.assertEqual(response.request._cookies.get('cids'), str(user_company.id))
|
||||
|
||||
# when being not logged, cids should not be added as redirection after
|
||||
# logging will be 'mail/view' again
|
||||
|
|
@ -397,40 +468,41 @@ class TestMultiCompanyRedirect(TestMailCommon, HttpCase):
|
|||
timeout=15
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
decoded_fragment = url_decode(url_parse(response.url).fragment)
|
||||
self.assertNotIn('cids', decoded_fragment)
|
||||
self.assertNotIn('cids', response.request._cookies)
|
||||
|
||||
def test_mail_message_post_other_company_with_cids(self):
|
||||
"""
|
||||
Ensure that a user can post a message on a thread belonging to another
|
||||
company when:
|
||||
|
||||
@tagged("-at_install", "post_install", "multi_company", "mail_controller")
|
||||
class TestMultiCompanyThreadData(TestMailCommon, HttpCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls._activate_multi_company()
|
||||
- The user has access to both companies via `company_ids`.
|
||||
- The active company context only includes the other company.
|
||||
- The target record belongs to a different company than the active one.
|
||||
|
||||
def test_mail_thread_data_follower(self):
|
||||
partner_portal = self.env["res.partner"].create(
|
||||
{"company_id": self.company_3.id, "name": "portal partner"}
|
||||
)
|
||||
record = self.env["mail.test.multi.company"].create({"name": "Multi Company Record"})
|
||||
record.message_subscribe(partner_ids=partner_portal.ids)
|
||||
with self.assertRaises(UserError):
|
||||
partner_portal.with_user(self.user_employee_c2).check_access_rule("read")
|
||||
self.authenticate(self.user_employee_c2.login, self.user_employee_c2.login)
|
||||
response = self.url_open(
|
||||
url="/mail/thread/data",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"thread_id": record.id,
|
||||
"thread_model": "mail.test.multi.company",
|
||||
"request_list": ["followers"],
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
followers = json.loads(response.content)["result"]["followers"]
|
||||
self.assertEqual(len(followers), 1)
|
||||
self.assertEqual(followers[0]["partner"]["id"], partner_portal.id)
|
||||
This reproduces the scenario where a user receives a notification from a
|
||||
record in Company A while being active in Company B, and attempts to reply
|
||||
from the inbox.
|
||||
"""
|
||||
self.user_employee_c2.write({'company_ids': [(6, 0, [self.user_employee.company_id.id, self.company_2.id])]})
|
||||
record_c1 = self.env["mail.test.multi.company"].sudo().create({
|
||||
"name": "Thread in C1",
|
||||
"company_id": self.user_employee.company_id.id, # company 1
|
||||
})
|
||||
self.authenticate('employee_c2', 'employee_c2')
|
||||
self.opener.cookies.set('cids', str(self.company_2.id))
|
||||
payload = {
|
||||
"thread_model": record_c1._name,
|
||||
"thread_id": record_c1.id,
|
||||
"post_data": {
|
||||
"body": "<p>Reply from inbox</p>",
|
||||
"message_type": "comment",
|
||||
"subtype_xmlid": "mail.mt_comment",
|
||||
},
|
||||
"context": {
|
||||
"allowed_company_ids": self.company_2.ids,
|
||||
}
|
||||
}
|
||||
result = self.make_jsonrpc_request("/mail/message/post", payload)
|
||||
message_data = result["store_data"]["mail.message"][0]
|
||||
self.assertEqual(message_data["body"], ["markup", "<p>Reply from inbox</p>"])
|
||||
self.assertTrue(record_c1.message_ids.filtered(lambda m: m.id == message_data["id"]))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,708 @@
|
|||
import json
|
||||
import socket
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import odoo
|
||||
|
||||
from odoo.tools.misc import mute_logger
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.mail.tools.jwt import InvalidVapidError
|
||||
from odoo.addons.mail.tools.web_push import ENCRYPTION_BLOCK_OVERHEAD, ENCRYPTION_HEADER_SIZE
|
||||
from odoo.addons.sms.tests.common import SMSCommon
|
||||
from odoo.addons.test_mail.data.test_mail_data import MAIL_TEMPLATE
|
||||
from odoo.tests import tagged
|
||||
from markupsafe import Markup
|
||||
from unittest.mock import patch
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install', 'mail_push')
|
||||
class TestWebPushNotification(SMSCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.user_email = cls.user_employee
|
||||
cls.user_email.notification_type = 'email'
|
||||
|
||||
cls.user_inbox = mail_new_test_user(
|
||||
cls.env, login='user_inbox', groups='base.group_user', name='User Inbox',
|
||||
notification_type='inbox'
|
||||
)
|
||||
|
||||
cls.record_simple = cls.env['mail.test.simple'].with_context(cls._test_context).create({
|
||||
'name': 'Test',
|
||||
'email_from': 'ignasse@example.com'
|
||||
})
|
||||
cls.record_simple.message_subscribe(partner_ids=[
|
||||
cls.user_email.partner_id.id,
|
||||
cls.user_inbox.partner_id.id,
|
||||
])
|
||||
cls.alias_gateway = cls.env['mail.alias'].create({
|
||||
'alias_contact': 'everyone',
|
||||
'alias_domain': cls.mail_alias_domain.id,
|
||||
'alias_model_id': cls.env['ir.model']._get_id('mail.test.gateway.company'),
|
||||
'alias_name': 'alias.gateway',
|
||||
})
|
||||
|
||||
# generate keys and devices
|
||||
cls.vapid_public_key = cls.env['mail.push.device'].get_web_push_vapid_public_key()
|
||||
cls.env['mail.push.device'].sudo().create([
|
||||
{
|
||||
'endpoint': f'https://test.odoo.com/webpush/user{(idx + 1)}',
|
||||
'expiration_time': None,
|
||||
'keys': json.dumps({
|
||||
'p256dh': 'BGbhnoP_91U7oR59BaaSx0JnDv2oEooYnJRV2AbY5TBeKGCRCf0HcIJ9bOKchUCDH4cHYWo9SYDz3U-8vSxPL_A',
|
||||
'auth': 'DJFdtAgZwrT6yYkUMgUqow'
|
||||
}),
|
||||
'partner_id': user.partner_id.id,
|
||||
} for idx, user in enumerate(cls.user_email + cls.user_inbox)
|
||||
])
|
||||
|
||||
def _trigger_cron_job(self):
|
||||
self.env.ref('mail.ir_cron_web_push_notification').method_direct_trigger()
|
||||
|
||||
def _assert_notification_count_for_cron(self, number_of_notification):
|
||||
notification_count = self.env['mail.push'].search_count([])
|
||||
self.assertEqual(notification_count, number_of_notification)
|
||||
|
||||
@patch.object(odoo.addons.mail.models.mail_thread, 'push_to_end_point')
|
||||
@mute_logger('odoo.tests')
|
||||
def test_notify_by_push(self, push_to_end_point):
|
||||
""" When posting a comment, notify both inbox and people outside of Odoo
|
||||
aka email """
|
||||
self.record_simple.with_user(self.user_admin).message_post(
|
||||
body=Markup('<p>Hello</p>'),
|
||||
message_type='comment',
|
||||
partner_ids=(self.user_email + self.user_inbox).partner_id.ids,
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
# not using cron, as max 1 push notif -> direct send
|
||||
self._assert_notification_count_for_cron(0)
|
||||
# two recipients, comment notifies both inbox and email people
|
||||
self.assertEqual(push_to_end_point.call_count, 2)
|
||||
|
||||
@patch.object(odoo.addons.mail.models.mail_thread, 'push_to_end_point')
|
||||
def test_notify_by_push_channel(self, push_to_end_point):
|
||||
""" Test various use case with discuss.channel. Chat and group channels
|
||||
sends push notifications, channel not. """
|
||||
chat_channel, channel_channel, group_channel = self.env['discuss.channel'].with_user(self.user_email).create([
|
||||
{
|
||||
'channel_partner_ids': [
|
||||
(4, self.user_email.partner_id.id),
|
||||
(4, self.user_inbox.partner_id.id),
|
||||
],
|
||||
'channel_type': channel_type,
|
||||
'name': f'{channel_type} Message' if channel_type != 'group' else '',
|
||||
} for channel_type in ['chat', 'channel', 'group']
|
||||
])
|
||||
group_channel._add_members(guests=self.guest)
|
||||
|
||||
for channel, sender, notification_count in zip(
|
||||
(chat_channel + channel_channel + group_channel + group_channel),
|
||||
(self.user_email, self.user_email, self.user_email, self.guest),
|
||||
(1, 0, 1, 2),
|
||||
):
|
||||
with self.subTest(channel_type=channel.channel_type):
|
||||
if sender == self.guest:
|
||||
channel_as_sender = channel.with_user(self.env.ref('base.public_user')).with_context(guest=sender)
|
||||
else:
|
||||
channel_as_sender = channel.with_user(self.user_email)
|
||||
# sudo: discuss.channel - guest can post as sudo in a test (simulating RPC without using network)
|
||||
channel_as_sender.sudo().message_post(
|
||||
body='Test Push',
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
self.assertEqual(push_to_end_point.call_count, notification_count)
|
||||
if notification_count > 0:
|
||||
payload_value = json.loads(push_to_end_point.call_args.kwargs['payload'])
|
||||
if channel.channel_type == 'chat':
|
||||
self.assertEqual(payload_value['title'], f'{self.user_email.name}')
|
||||
elif channel.channel_type == 'group':
|
||||
self.assertIn(self.user_email.name, payload_value['title'])
|
||||
self.assertIn(self.user_inbox.name, payload_value['title'])
|
||||
self.assertIn(self.guest.name, payload_value['title'])
|
||||
self.assertNotIn("False", payload_value['title'])
|
||||
else:
|
||||
self.assertEqual(payload_value['title'], f'#{channel.name}')
|
||||
icon = (
|
||||
'/web/static/img/odoo-icon-192x192.png'
|
||||
if sender == self.guest
|
||||
else f'/web/image/res.partner/{self.user_email.partner_id.id}/avatar_128'
|
||||
)
|
||||
self.assertEqual(payload_value['options']['icon'], icon)
|
||||
self.assertEqual(payload_value['options']['body'], 'Test Push')
|
||||
self.assertEqual(payload_value['options']['data']['res_id'], channel.id)
|
||||
self.assertEqual(payload_value['options']['data']['model'], channel._name)
|
||||
self.assertEqual(push_to_end_point.call_args.kwargs['device']['endpoint'], 'https://test.odoo.com/webpush/user2')
|
||||
push_to_end_point.reset_mock()
|
||||
|
||||
# Test Direct Message with channel muted -> should skip push notif
|
||||
now = datetime.now()
|
||||
self.env['discuss.channel.member'].search([
|
||||
('partner_id', 'in', (self.user_email.partner_id + self.user_inbox.partner_id).ids),
|
||||
('channel_id', 'in', (chat_channel + channel_channel + group_channel).ids),
|
||||
]).write({
|
||||
'mute_until_dt': now + timedelta(days=5)
|
||||
})
|
||||
chat_channel.with_user(self.user_email).message_post(
|
||||
body='Test',
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
push_to_end_point.assert_not_called()
|
||||
push_to_end_point.reset_mock()
|
||||
|
||||
self.env["discuss.channel.member"].search([
|
||||
("partner_id", "in", (self.user_email.partner_id + self.user_inbox.partner_id).ids),
|
||||
("channel_id", "in", (chat_channel + channel_channel + group_channel).ids),
|
||||
]).write({
|
||||
"mute_until_dt": False,
|
||||
})
|
||||
|
||||
# Test Channel Message
|
||||
group_channel.with_user(self.user_email).message_post(
|
||||
body='Test',
|
||||
partner_ids=self.user_inbox.partner_id.ids,
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
push_to_end_point.assert_called_once()
|
||||
|
||||
@patch.object(odoo.addons.mail.models.mail_thread, "push_to_end_point")
|
||||
def test_notify_by_push_channel_with_channel_notifications_settings(self, push_to_end_point):
|
||||
""" Test various use case with the channel notification settings."""
|
||||
all_test_user = mail_new_test_user(
|
||||
self.env,
|
||||
login="all",
|
||||
name="all",
|
||||
email="all@example.com",
|
||||
notification_type="inbox",
|
||||
groups="base.group_user",
|
||||
)
|
||||
mentions_test_user = mail_new_test_user(
|
||||
self.env,
|
||||
login="mentions",
|
||||
name="mentions",
|
||||
email="mentions@example.com",
|
||||
notification_type="inbox",
|
||||
groups="base.group_user",
|
||||
)
|
||||
nothing_test_user = mail_new_test_user(
|
||||
self.env,
|
||||
login="nothing",
|
||||
name="nothing",
|
||||
email="nothing@example.com",
|
||||
notification_type="inbox",
|
||||
groups="base.group_user",
|
||||
)
|
||||
all_test_user.res_users_settings_ids.write({"channel_notifications": "all"})
|
||||
nothing_test_user.res_users_settings_ids.write({"channel_notifications": "no_notif"})
|
||||
|
||||
# generate devices
|
||||
self.env["mail.push.device"].sudo().create(
|
||||
[
|
||||
{
|
||||
"endpoint": f"https://test.odoo.com/webpush/user{(idx + 20)}",
|
||||
"expiration_time": None,
|
||||
"keys": json.dumps(
|
||||
{
|
||||
"p256dh": "BGbhnoP_91U7oR59BaaSx0JnDv2oEooYnJRV2AbY5TBeKGCRCf0HcIJ9bOKchUCDH4cHYWo9SYDz3U-8vSxPL_A",
|
||||
"auth": "DJFdtAgZwrT6yYkUMgUqow",
|
||||
}
|
||||
),
|
||||
"partner_id": user.partner_id.id,
|
||||
}
|
||||
for idx, user in enumerate(all_test_user + mentions_test_user + nothing_test_user)
|
||||
]
|
||||
)
|
||||
|
||||
channel_channel = self.env["discuss.channel"].with_user(self.user_email).create(
|
||||
[
|
||||
{
|
||||
"channel_partner_ids": [
|
||||
(4, self.user_email.partner_id.id),
|
||||
(4, all_test_user.partner_id.id),
|
||||
(4, mentions_test_user.partner_id.id),
|
||||
(4, nothing_test_user.partner_id.id),
|
||||
],
|
||||
"channel_type": "channel",
|
||||
"name": "channel",
|
||||
}
|
||||
]
|
||||
)
|
||||
# normal messages in channel
|
||||
channel_channel.with_user(self.user_email).message_post(
|
||||
body="Test Push",
|
||||
message_type="comment",
|
||||
subtype_xmlid="mail.mt_comment",
|
||||
)
|
||||
push_to_end_point.assert_called_once()
|
||||
# all_test_user should be notified
|
||||
self.assertEqual(push_to_end_point.call_args.kwargs["device"]["endpoint"], "https://test.odoo.com/webpush/user20")
|
||||
push_to_end_point.reset_mock()
|
||||
|
||||
# mention messages in channel
|
||||
channel_channel.with_user(self.user_email).message_post(
|
||||
body="Test Push @mentions",
|
||||
message_type="comment",
|
||||
partner_ids=(all_test_user + mentions_test_user + nothing_test_user).partner_id.ids,
|
||||
subtype_xmlid="mail.mt_comment",
|
||||
)
|
||||
self.assertEqual(push_to_end_point.call_count, 2)
|
||||
# all_test_user and mentions_test_user should be notified
|
||||
self.assertEqual(push_to_end_point.call_args_list[0].kwargs["device"]["endpoint"], "https://test.odoo.com/webpush/user20")
|
||||
self.assertEqual(push_to_end_point.call_args_list[1].kwargs["device"]["endpoint"], "https://test.odoo.com/webpush/user21")
|
||||
push_to_end_point.reset_mock()
|
||||
|
||||
# muted channel
|
||||
now = datetime.now()
|
||||
self.env["discuss.channel.member"].search(
|
||||
[
|
||||
("partner_id", "in", (all_test_user.partner_id + mentions_test_user.partner_id + nothing_test_user.partner_id).ids),
|
||||
]
|
||||
).write(
|
||||
{
|
||||
"mute_until_dt": now + timedelta(days=5),
|
||||
}
|
||||
)
|
||||
# normal messages in channel
|
||||
channel_channel.with_user(self.user_email).message_post(
|
||||
body="Test Push",
|
||||
message_type="comment",
|
||||
subtype_xmlid="mail.mt_comment",
|
||||
)
|
||||
push_to_end_point.assert_not_called()
|
||||
# mention messages in channel
|
||||
channel_channel.with_user(self.user_email).message_post(
|
||||
body="Test Push",
|
||||
message_type="comment",
|
||||
subtype_xmlid="mail.mt_comment",
|
||||
)
|
||||
push_to_end_point.assert_not_called()
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread')
|
||||
def test_notify_by_push_mail_gateway(self):
|
||||
""" Check mail gateway push notifications """
|
||||
with self.mock_mail_gateway():
|
||||
test_record = self.format_and_process(
|
||||
MAIL_TEMPLATE, self.user_email.email_formatted,
|
||||
f'{self.alias_gateway.display_name}, {self.user_inbox.email_formatted}',
|
||||
subject='Test Record Creation',
|
||||
target_model='mail.test.gateway.company',
|
||||
)
|
||||
self.assertEqual(len(test_record.message_ids), 1)
|
||||
self.assertEqual(test_record.message_partner_ids, self.user_email.partner_id)
|
||||
test_record.message_subscribe(partner_ids=[self.user_inbox.partner_id.id])
|
||||
|
||||
for include_as_external, has_notif in ((False, True), (True, False)):
|
||||
with self.mock_mail_gateway():
|
||||
to = f'{self.alias_gateway.display_name}'
|
||||
if include_as_external:
|
||||
to += f', {self.user_inbox.email_formatted}'
|
||||
self.format_and_process(
|
||||
MAIL_TEMPLATE, self.user_email.email_formatted, to,
|
||||
subject='Repy By Email',
|
||||
extra=f'In-Reply-To:\r\n\t{test_record.message_ids[-1].message_id}\n',
|
||||
)
|
||||
if has_notif:
|
||||
# user_inbox is notified by Odoo, hence receives a push notification
|
||||
self.assertPushNotification(
|
||||
mail_push_count=0, title_content=self.user_email.name,
|
||||
body_content='Please call me as soon as possible this afternoon!\n\n--\nSylvie',
|
||||
)
|
||||
else:
|
||||
self.assertNoPushNotification()
|
||||
|
||||
@mute_logger('odoo.tests')
|
||||
def test_notify_by_push_message_notify(self):
|
||||
""" In case of notification, only inbox users are notified """
|
||||
for recipient, has_notification in [(self.user_email, False), (self.user_inbox, True)]:
|
||||
with self.subTest(recipient=recipient):
|
||||
with self.mock_mail_gateway():
|
||||
self.record_simple.with_user(self.user_admin).message_notify(
|
||||
body='Test Push Body',
|
||||
partner_ids=recipient.partner_id.ids,
|
||||
subject='Test Push Notification',
|
||||
)
|
||||
# not using cron, as max 1 push notif -> direct send
|
||||
self._assert_notification_count_for_cron(0)
|
||||
if has_notification:
|
||||
self.assertPushNotification(
|
||||
mail_push_count=0,
|
||||
endpoint='https://test.odoo.com/webpush/user2', keys=('vapid_private_key', 'vapid_public_key'),
|
||||
title=f'{self.user_admin.name}: {self.record_simple.display_name}',
|
||||
body_content='Test Push Body',
|
||||
options={
|
||||
'data': {'model': self.record_simple._name, 'res_id': self.record_simple.id,},
|
||||
},
|
||||
)
|
||||
else:
|
||||
self.assertNoPushNotification()
|
||||
|
||||
@patch.object(odoo.addons.mail.models.mail_thread, 'push_to_end_point')
|
||||
@mute_logger('odoo.tests')
|
||||
def test_notify_call_invitation(self, push_to_end_point):
|
||||
inviting_user = self.env['res.users'].sudo().create({'name': "Test User", 'login': 'test'})
|
||||
channel = self.env['discuss.channel'].with_user(inviting_user)._get_or_create_chat(
|
||||
partners_to=[self.user_email.partner_id.id])
|
||||
inviting_channel_member = channel.sudo().channel_member_ids.filtered(
|
||||
lambda channel_member: channel_member.partner_id == inviting_user.partner_id)
|
||||
|
||||
inviting_channel_member._rtc_join_call()
|
||||
push_to_end_point.assert_called_once()
|
||||
payload_value = json.loads(push_to_end_point.call_args.kwargs['payload'])
|
||||
self.assertEqual(
|
||||
payload_value['title'],
|
||||
"Incoming call",
|
||||
)
|
||||
options = payload_value['options']
|
||||
self.assertTrue(options['requireInteraction'])
|
||||
self.assertEqual(options['body'], f"Conference: {channel.name}")
|
||||
self.assertEqual(options['actions'], [
|
||||
{
|
||||
"action": "DECLINE",
|
||||
"type": "button",
|
||||
"title": "Decline",
|
||||
},
|
||||
{
|
||||
"action": "ACCEPT",
|
||||
"type": "button",
|
||||
"title": "Accept",
|
||||
},
|
||||
])
|
||||
data = options['data']
|
||||
self.assertEqual(data['type'], "CALL")
|
||||
self.assertEqual(data['res_id'], channel.id)
|
||||
self.assertEqual(data['model'], "discuss.channel")
|
||||
push_to_end_point.reset_mock()
|
||||
|
||||
inviting_channel_member._rtc_leave_call()
|
||||
push_to_end_point.assert_called_once()
|
||||
payload_value = json.loads(push_to_end_point.call_args.kwargs['payload'])
|
||||
self.assertEqual(payload_value['options']['data']['type'], "CANCEL")
|
||||
push_to_end_point.reset_mock()
|
||||
|
||||
@patch.object(odoo.addons.mail.models.mail_thread, 'push_to_end_point')
|
||||
def test_notify_by_push_tracking(self, push_to_end_point):
|
||||
""" Test tracking message included in push notifications """
|
||||
container_update_subtype = self.env.ref('test_mail.st_mail_test_ticket_container_upd')
|
||||
ticket = self.env['mail.test.ticket'].with_user(self.user_email).create({
|
||||
'name': 'Test',
|
||||
})
|
||||
ticket.message_subscribe(
|
||||
partner_ids=[self.user_email.partner_id.id],
|
||||
subtype_ids=[container_update_subtype.id],
|
||||
)
|
||||
|
||||
container = self.env['mail.test.container'].create({'name': 'Container'})
|
||||
ticket.write({
|
||||
'name': 'Test2',
|
||||
'email_from': 'noone@example.com',
|
||||
'container_id': container.id,
|
||||
})
|
||||
self.flush_tracking()
|
||||
self._assert_notification_count_for_cron(0)
|
||||
push_to_end_point.assert_not_called()
|
||||
|
||||
container2 = self.env['mail.test.container'].create({'name': 'Container Two'})
|
||||
ticket.message_subscribe(
|
||||
partner_ids=[self.user_inbox.partner_id.id],
|
||||
subtype_ids=[container_update_subtype.id],
|
||||
)
|
||||
ticket.write({
|
||||
'name': 'Test3',
|
||||
'email_from': 'noone@example.com',
|
||||
'container_id': container2.id,
|
||||
})
|
||||
self.flush_tracking()
|
||||
self._assert_notification_count_for_cron(0)
|
||||
push_to_end_point.assert_called_once()
|
||||
payload_value = json.loads(push_to_end_point.call_args.kwargs['payload'])
|
||||
self.assertIn(
|
||||
f'{container_update_subtype.description}\nContainer: {container.name} → {container2.name}',
|
||||
payload_value['options']['body'],
|
||||
'Tracking changes should be included in push notif payload'
|
||||
)
|
||||
|
||||
@patch.object(odoo.addons.mail.models.mail_push, 'push_to_end_point')
|
||||
def test_push_notifications_cron(self, push_to_end_point):
|
||||
# Add 4 more devices to force sending via cron queue
|
||||
for index in range(10, 14):
|
||||
self.env['mail.push.device'].sudo().create([{
|
||||
'endpoint': 'https://test.odoo.com/webpush/user%d' % index,
|
||||
'expiration_time': None,
|
||||
'keys': json.dumps({
|
||||
'p256dh': 'BGbhnoP_91U7oR59BaaSx0JnDv2oEooYnJRV2AbY5TBeKGCRCf0HcIJ9bOKchUCDH4cHYWo9SYDz3U-8vSxPL_A',
|
||||
'auth': 'DJFdtAgZwrT6yYkUMgUqow'
|
||||
}),
|
||||
'partner_id': self.user_inbox.partner_id.id,
|
||||
}])
|
||||
|
||||
self.record_simple.with_user(self.user_email).message_notify(
|
||||
partner_ids=self.user_inbox.partner_id.ids,
|
||||
body='Test message send via Web Push',
|
||||
subject='Test Activity',
|
||||
)
|
||||
|
||||
self._assert_notification_count_for_cron(5)
|
||||
# Force the execution of the cron
|
||||
self._trigger_cron_job()
|
||||
self.assertEqual(push_to_end_point.call_count, 5)
|
||||
|
||||
@patch.object(odoo.addons.mail.models.mail_thread.Session, 'post',
|
||||
return_value=SimpleNamespace(**{'status_code': 404, 'text': 'Device Unreachable'}))
|
||||
def test_push_notifications_error_device_unreachable(self, post):
|
||||
with mute_logger('odoo.addons.mail.tools.web_push'):
|
||||
self.record_simple.with_user(self.user_email).message_notify(
|
||||
partner_ids=self.user_inbox.partner_id.ids,
|
||||
body='Test message send via Web Push',
|
||||
subject='Test Activity',
|
||||
)
|
||||
|
||||
self._assert_notification_count_for_cron(0)
|
||||
post.assert_called_once()
|
||||
# Test that the unreachable device is deleted from the DB
|
||||
notification_count = self.env['mail.push.device'].search_count([('endpoint', '=', 'https://test.odoo.com/webpush/user2')])
|
||||
self.assertEqual(notification_count, 0)
|
||||
|
||||
@patch.object(odoo.addons.mail.models.mail_thread.Session, 'post',
|
||||
return_value=SimpleNamespace(**{'status_code': 201, 'text': 'Ok'}))
|
||||
def test_push_notifications_error_encryption_simple(self, post):
|
||||
""" Test to see if all parameters sent to the endpoint are present.
|
||||
This test doesn't test if the cryptographic values are correct. """
|
||||
self.record_simple.with_user(self.user_email).message_notify(
|
||||
partner_ids=self.user_inbox.partner_id.ids,
|
||||
body='Test message send via Web Push',
|
||||
subject='Test Activity',
|
||||
)
|
||||
|
||||
self._assert_notification_count_for_cron(0)
|
||||
post.assert_called_once()
|
||||
self.assertEqual(post.call_args.args[0], 'https://test.odoo.com/webpush/user2')
|
||||
self.assertIn('headers', post.call_args.kwargs)
|
||||
self.assertIn('vapid', post.call_args.kwargs['headers']['Authorization'])
|
||||
self.assertIn('t=', post.call_args.kwargs['headers']['Authorization'])
|
||||
self.assertIn('k=', post.call_args.kwargs['headers']['Authorization'])
|
||||
self.assertEqual('aes128gcm', post.call_args.kwargs['headers']['Content-Encoding'])
|
||||
self.assertEqual('60', post.call_args.kwargs['headers']['TTL'])
|
||||
self.assertIn('data', post.call_args.kwargs)
|
||||
self.assertIn('timeout', post.call_args.kwargs)
|
||||
|
||||
@patch.object(odoo.addons.mail.models.mail_thread.Session, 'post',
|
||||
return_value=SimpleNamespace(status_code=201, text='Ok'))
|
||||
def test_push_notifications_device_invalid_tld_domain(self, post):
|
||||
self.env['mail.push.device'].sudo().create([{
|
||||
'endpoint': 'https://test.odoo.invalid/webpush/user',
|
||||
'expiration_time': None,
|
||||
'keys': json.dumps({
|
||||
'p256dh': 'BGbhnoP_91U7oR59BaaSx0JnDv2oEooYnJRV2AbY5TBeKGCRCf0HcIJ9bOKchUCDH4cHYWo9SYDz3U-8vSxPL_A',
|
||||
'auth': 'DJFdtAgZwrT6yYkUMgUqow'
|
||||
}),
|
||||
'partner_id': self.user_inbox.partner_id.id,
|
||||
}])
|
||||
|
||||
device_count = self.env['mail.push.device'].search_count([('endpoint', '=', 'https://test.odoo.invalid/webpush/user')])
|
||||
self.assertEqual(device_count, 1)
|
||||
|
||||
self.record_simple.with_user(self.user_email).message_notify(
|
||||
partner_ids=self.user_inbox.partner_id.ids,
|
||||
body='Test message send via Web Push',
|
||||
subject='Test Activity',
|
||||
)
|
||||
|
||||
self._assert_notification_count_for_cron(0)
|
||||
post.assert_called_once()
|
||||
# Test that the device with the invalid TLD is deleted from the DB
|
||||
device_count = self.env['mail.push.device'].search_count([('endpoint', '=', 'https://test.odoo.invalid/webpush/user')])
|
||||
self.assertEqual(device_count, 0)
|
||||
|
||||
@patch.object(odoo.addons.mail.models.mail_thread.Session, 'post', side_effect=ConnectionError("Oops, network error"))
|
||||
def test_push_notifications_device_raise_exception(self, post):
|
||||
# Add 4 more devices to force sending via cron queue
|
||||
for index in range(10, 14):
|
||||
self.env['mail.push.device'].sudo().create([{
|
||||
'endpoint': 'https://test.odoo.com/webpush/user%d' % index,
|
||||
'expiration_time': None,
|
||||
'keys': json.dumps({
|
||||
'p256dh': 'BGbhnoP_91U7oR59BaaSx0JnDv2oEooYnJRV2AbY5TBeKGCRCf0HcIJ9bOKchUCDH4cHYWo9SYDz3U-8vSxPL_A',
|
||||
'auth': 'DJFdtAgZwrT6yYkUMgUqow'
|
||||
}),
|
||||
'partner_id': self.user_inbox.partner_id.id,
|
||||
}])
|
||||
|
||||
self.record_simple.with_user(self.user_email).message_notify(
|
||||
partner_ids=self.user_inbox.partner_id.ids,
|
||||
body='Test message send via Web Push',
|
||||
subject='Test Activity',
|
||||
)
|
||||
|
||||
with self.assertLogs('odoo.addons.mail.models.mail_push', level="ERROR") as capture:
|
||||
self._assert_notification_count_for_cron(5)
|
||||
self._trigger_cron_job()
|
||||
self.assertEqual(capture.output, [
|
||||
'ERROR:odoo.addons.mail.models.mail_push:An error occurred while trying to send web push: Oops, network error',
|
||||
] * 5)
|
||||
|
||||
def test_push_notification_regenerate_vapid_keys(self):
|
||||
ir_params_sudo = self.env['ir.config_parameter'].sudo()
|
||||
ir_params_sudo.search([('key', 'in', [
|
||||
'mail.web_push_vapid_private_key',
|
||||
'mail.web_push_vapid_public_key'
|
||||
])]).unlink()
|
||||
new_vapid_public_key = self.env['mail.push.device'].get_web_push_vapid_public_key()
|
||||
self.assertNotEqual(self.vapid_public_key, new_vapid_public_key)
|
||||
with self.assertRaises(InvalidVapidError):
|
||||
self.env['mail.push.device'].register_devices(
|
||||
endpoint='https://test.odoo.com/webpush/user1',
|
||||
expiration_time=None,
|
||||
keys=json.dumps({
|
||||
'p256dh': 'BGbhnoP_91U7oR59BaaSx0JnDv2oEooYnJRV2AbY5TBeKGCRCf0HcIJ9bOKchUCDH4cHYWo9SYDz3U-8vSxPL_A',
|
||||
'auth': 'DJFdtAgZwrT6yYkUMgUqow'
|
||||
}),
|
||||
partner_id=self.user_email.partner_id.id,
|
||||
vapid_public_key=self.vapid_public_key,
|
||||
)
|
||||
|
||||
@patch.object(
|
||||
odoo.addons.mail.models.mail_thread.Session, 'post', return_value=SimpleNamespace(status_code=201, text='Ok')
|
||||
)
|
||||
@patch.object(
|
||||
odoo.addons.mail.models.mail_thread, 'push_to_end_point',
|
||||
wraps=odoo.addons.mail.tools.web_push.push_to_end_point,
|
||||
)
|
||||
def test_push_notifications_truncate_payload(self, thread_push_mock, session_post_mock):
|
||||
"""Ensure that when we send large bodies with various character types,
|
||||
the final encrypted data (post-encryption) never exceeds 4096 bytes.
|
||||
|
||||
This test checks the behavior for the current size limits and encryption overhead.
|
||||
See below test for a more illustrative example.
|
||||
See MailThread._truncate_payload for a more thorough explanation.
|
||||
|
||||
Test scenarios include:
|
||||
- ASCII characters (X)
|
||||
- UTF-8 characters (Ø), at various offsets
|
||||
"""
|
||||
# compute the size of an empty notification with these parameters
|
||||
# this could change based on the id of record_simple for example
|
||||
# but is otherwise constant for any notification sent with the same parameters
|
||||
self.record_simple.with_user(self.user_email).message_notify(
|
||||
partner_ids=self.user_inbox.partner_id.ids,
|
||||
body='',
|
||||
subject='Test Payload',
|
||||
)
|
||||
base_payload_size = len(thread_push_mock.call_args.kwargs['payload'].encode())
|
||||
effective_payload_size_limit = self.env['mail.thread']._truncate_payload_get_max_payload_length()
|
||||
# this is just a sanity check that the value makes sense, feel free to update as needed
|
||||
self.assertEqual(effective_payload_size_limit, 3993, "Payload limit should come out to 3990.")
|
||||
body_size_limit = effective_payload_size_limit - base_payload_size
|
||||
encryption_overhead = ENCRYPTION_HEADER_SIZE + ENCRYPTION_BLOCK_OVERHEAD
|
||||
|
||||
test_cases = [
|
||||
# (description, body)
|
||||
('empty string', '', 0, 0),
|
||||
('1-byte ASCII characters (below limit)', 'X' * (body_size_limit - 1), body_size_limit - 1, body_size_limit - 1),
|
||||
('1-byte ASCII characters (at limit)', 'X' * body_size_limit, body_size_limit, body_size_limit),
|
||||
('1-byte ASCII characters (past limit)', 'X' * (body_size_limit + 1), body_size_limit, body_size_limit),
|
||||
('1-byte ASCII characters (way past limit)', 'X' * 5000, body_size_limit, body_size_limit),
|
||||
] + [ # \u00d8 check that it can be cut anywhere by offsetting the string by 1 byte each time
|
||||
(
|
||||
f'2-bytes UTF-8 characters (near limit + {offset}-byte offset)',
|
||||
('+' * offset) + ('Ø' * (body_size_limit // 6)),
|
||||
offset + ((body_size_limit - offset) // 6), # length truncated to nearest full character (\u00f8)
|
||||
offset * 1 + ((body_size_limit - offset) // 6) * 6,
|
||||
)
|
||||
for offset in range(0, 8)
|
||||
]
|
||||
|
||||
for description, body, expected_body_length, expected_body_size in test_cases:
|
||||
with self.subTest(description):
|
||||
self.record_simple.with_user(self.user_email).message_notify(
|
||||
partner_ids=self.user_inbox.partner_id.ids,
|
||||
body=body,
|
||||
subject='Test Payload',
|
||||
)
|
||||
|
||||
encrypted_payload = session_post_mock.call_args.kwargs['data']
|
||||
payload_before_encryption = thread_push_mock.call_args.kwargs['payload']
|
||||
self.assertLessEqual(
|
||||
len(encrypted_payload), 4096, 'Final encrypted payload should not exceed 4096 bytes'
|
||||
)
|
||||
self.assertEqual(
|
||||
len(json.loads(payload_before_encryption)['options']['body']), expected_body_length
|
||||
)
|
||||
self.assertEqual(
|
||||
len(encrypted_payload),
|
||||
base_payload_size + expected_body_size + encryption_overhead,
|
||||
'Encrypted size should be exactly the base payload size + body size + encryption overhead.'
|
||||
)
|
||||
|
||||
@patch.object(
|
||||
odoo.addons.mail.models.mail_thread.Session, 'post', return_value=SimpleNamespace(status_code=201, text='Ok')
|
||||
)
|
||||
@patch.object(
|
||||
odoo.addons.mail.models.mail_thread, 'push_to_end_point',
|
||||
wraps=odoo.addons.mail.tools.web_push.push_to_end_point,
|
||||
)
|
||||
@patch.object(
|
||||
odoo.addons.mail.tools.web_push, '_encrypt_payload',
|
||||
wraps=odoo.addons.mail.tools.web_push._encrypt_payload,
|
||||
)
|
||||
def test_push_notifications_truncate_payload_mocked_size_limit(self, web_push_encrypt_payload_mock, thread_push_mock, session_post_mock):
|
||||
"""Illustrative test for text contents truncation.
|
||||
|
||||
We want to ensure we truncate utf-8 values properly based on maximum payload size.
|
||||
Here max payload size is mocked, so that we can test on the same body each time to ease reading.
|
||||
|
||||
See MailThread._truncate_payload for a more thorough explanation.
|
||||
"""
|
||||
self.record_simple.with_user(self.user_email).message_notify(
|
||||
partner_ids=self.user_inbox.partner_id.ids,
|
||||
body="",
|
||||
subject='Test Payload',
|
||||
)
|
||||
base_payload = thread_push_mock.call_args.kwargs['payload'].encode()
|
||||
base_payload_size = len(base_payload)
|
||||
encryption_overhead = ENCRYPTION_HEADER_SIZE + ENCRYPTION_BLOCK_OVERHEAD
|
||||
|
||||
body = "BØDY"
|
||||
body_json = json.dumps(body)[1:-1]
|
||||
for size_limit, expected_body in [
|
||||
(base_payload_size + len(body_json), "BØDY"),
|
||||
(base_payload_size + len(body_json) - 1, "BØD"),
|
||||
(base_payload_size + len(body_json) - 2, "BØ"),
|
||||
] + [ # truncating anywhere in \u00d8 (Ø) should truncate to the nearest full character (B)
|
||||
(base_payload_size + len(body_json) - n, "B")
|
||||
for n in range(3, 9)
|
||||
] + [
|
||||
(base_payload_size + len(body_json) - 9, ""),
|
||||
(base_payload_size + len(body_json) - 10, ""), # should still work even if it would still be too big after truncate
|
||||
]:
|
||||
with self.subTest(size_limit=size_limit), patch.object(
|
||||
odoo.addons.mail.models.mail_thread.MailThread, '_truncate_payload_get_max_payload_length',
|
||||
return_value=size_limit,
|
||||
):
|
||||
self.record_simple.with_user(self.user_email).message_notify(
|
||||
partner_ids=self.user_inbox.partner_id.ids,
|
||||
body=body,
|
||||
subject='Test Payload',
|
||||
)
|
||||
payload_at_push = thread_push_mock.call_args.kwargs['payload']
|
||||
payload_before_encrypt = web_push_encrypt_payload_mock.call_args.args[0]
|
||||
encrypted_payload = session_post_mock.call_args.kwargs['data']
|
||||
self.assertEqual(payload_before_encrypt.decode(), payload_at_push, "Payload should not change between encryption and push call.")
|
||||
self.assertEqual(len(payload_before_encrypt), len(payload_at_push), "Encoded body should be same size as decoded.")
|
||||
self.assertEqual(
|
||||
len(encrypted_payload), len(payload_before_encrypt) + encryption_overhead,
|
||||
'Final encrypted payload should just be the size of the unencrypted payload + the size of encryption overhead.'
|
||||
)
|
||||
self.assertEqual(
|
||||
json.loads(payload_at_push)['options']['body'], expected_body
|
||||
)
|
||||
if not expected_body:
|
||||
self.assertEqual(
|
||||
payload_before_encrypt, base_payload,
|
||||
"Only the contents of the body should be truncated, not the rest of the payload."
|
||||
)
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.base.tests.test_ir_cron import CronMixinCase
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.addons.test_mail.models.mail_test_lead import MailTestTLead
|
||||
from odoo.addons.test_mail.tests.common import TestRecipients
|
||||
from odoo.exceptions import AccessError, UserError, ValidationError
|
||||
from odoo.fields import Datetime as FieldDatetime
|
||||
from odoo.tests import tagged, users
|
||||
from odoo.tools import mute_logger
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@tagged('mail_scheduled_message')
|
||||
class TestScheduledMessage(MailCommon, TestRecipients):
|
||||
""" Test Scheduled Message internals """
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# force 'now' to ease test about schedulers
|
||||
cls.reference_now = FieldDatetime.to_datetime('2022-12-24 12:00:00')
|
||||
|
||||
with cls.mock_datetime_and_now(cls, cls.reference_now):
|
||||
cls.test_record = cls.env['mail.test.ticket'].with_context(cls._test_context).create([{
|
||||
'name': 'Test Record',
|
||||
'customer_id': cls.partner_1.id,
|
||||
'user_id': cls.user_employee.id,
|
||||
}])
|
||||
cls.private_record = cls.env['mail.test.access'].create({
|
||||
'access': 'admin',
|
||||
'name': 'Private Record',
|
||||
})
|
||||
cls.hidden_scheduled_message, cls.visible_scheduled_message = cls.env['mail.scheduled.message'].create([
|
||||
{
|
||||
'author_id': cls.partner_admin.id,
|
||||
'model': cls.private_record._name,
|
||||
'res_id': cls.private_record.id,
|
||||
'body': 'Hidden Scheduled Message',
|
||||
'scheduled_date': '2022-12-24 15:00:00',
|
||||
},
|
||||
{
|
||||
'author_id': cls.partner_admin.id,
|
||||
'model': cls.test_record._name,
|
||||
'res_id': cls.test_record.id,
|
||||
'body': 'Visible Scheduled Message',
|
||||
'scheduled_date': '2022-12-24 15:00:00',
|
||||
},
|
||||
]).with_user(cls.user_employee)
|
||||
|
||||
def schedule_message(self, target_record=None, author_id=None, **kwargs):
|
||||
with self.mock_datetime_and_now(self.reference_now):
|
||||
return self.env['mail.scheduled.message'].create({
|
||||
'author_id': author_id or self.env.user.partner_id.id,
|
||||
'model': target_record._name if target_record else kwargs.pop('model'),
|
||||
'res_id': target_record.id if target_record else kwargs.pop('res_id'),
|
||||
'body': kwargs.pop('body', 'Test Body'),
|
||||
'scheduled_date': kwargs.pop('scheduled_date', '2022-12-24 15:00:00'),
|
||||
**kwargs,
|
||||
})
|
||||
|
||||
|
||||
class TestScheduledMessageAccess(TestScheduledMessage):
|
||||
|
||||
@users('employee')
|
||||
def test_scheduled_message_model_without_post_right(self):
|
||||
# creation on a record that the user cannot post to
|
||||
with self.assertRaises(AccessError):
|
||||
self.schedule_message(self.private_record)
|
||||
# read a message scheduled on a record the user can't post to
|
||||
with self.assertRaises(AccessError):
|
||||
self.hidden_scheduled_message.read()
|
||||
# search a message scheduled on a record the user can't post to
|
||||
self.assertFalse(self.env['mail.scheduled.message'].search([['id', '=', self.hidden_scheduled_message.id]]))
|
||||
# write on a message scheduled on a record the user can't post to
|
||||
with self.assertRaises(AccessError):
|
||||
self.hidden_scheduled_message.write({'body': 'boum'})
|
||||
# post a message scheduled on a record the user can't post to
|
||||
with self.assertRaises(AccessError):
|
||||
self.hidden_scheduled_message.post_message()
|
||||
# unlink a message scheduled on a record the user can't post to
|
||||
with self.assertRaises(AccessError):
|
||||
self.hidden_scheduled_message.unlink()
|
||||
|
||||
@users('employee')
|
||||
def test_scheduled_message_model_with_post_right(self):
|
||||
# read a message scheduled by another user on a record the user can post to
|
||||
self.visible_scheduled_message.read()
|
||||
# search a message scheduled by another user on a record the user can post to
|
||||
self.assertEqual(self.env['mail.scheduled.message'].search([['id', '=', self.visible_scheduled_message.id]]), self.visible_scheduled_message)
|
||||
# write on a message scheduled by another user on a record the user can post to
|
||||
with self.assertRaises(AccessError):
|
||||
self.visible_scheduled_message.write({'body': 'boum'})
|
||||
# post a message scheduled on a record the user can post to
|
||||
with self.assertRaises(UserError):
|
||||
self.visible_scheduled_message.post_message()
|
||||
# unlink a message scheduled on a record the user can post to
|
||||
self.visible_scheduled_message.unlink()
|
||||
|
||||
@users('employee')
|
||||
def test_own_scheduled_message(self):
|
||||
# create a scheduled message on a record the user can post to
|
||||
scheduled_message = self.schedule_message(self.test_record)
|
||||
# read own scheduled message
|
||||
scheduled_message.read()
|
||||
# search own scheduled message
|
||||
self.assertEqual(self.env['mail.scheduled.message'].search([['id', '=', scheduled_message.id]]), scheduled_message)
|
||||
# write on own scheduled message
|
||||
scheduled_message.write({'body': 'Hello!'})
|
||||
# unlink own scheduled message
|
||||
scheduled_message.unlink()
|
||||
|
||||
|
||||
class TestScheduledMessageBusiness(TestScheduledMessage, CronMixinCase):
|
||||
|
||||
@users('employee')
|
||||
def test_scheduled_message_restrictions(self):
|
||||
# cannot schedule a message in the past
|
||||
with self.assertRaises(ValidationError):
|
||||
self.schedule_message(self.test_record, scheduled_date='2022-12-24 10:00:00')
|
||||
# cannot schedule a message on a model without thread
|
||||
# with admin as employee does not have write access on res.users)
|
||||
with self.with_user("admin"), self.assertRaises(ValidationError):
|
||||
self.schedule_message(self.user_employee)
|
||||
scheduled_message = self.schedule_message(self.test_record)
|
||||
# cannot reschedule a message in the past
|
||||
with self.assertRaises(ValidationError):
|
||||
scheduled_message.write({'scheduled_date': '2022-12-24 14:00:00'})
|
||||
# cannot change target record of scheduled message
|
||||
with self.assertRaises(UserError):
|
||||
scheduled_message.write({'res_id': 2})
|
||||
with self.assertRaises(UserError):
|
||||
scheduled_message.write({'model': 'mail.test.track'})
|
||||
# unlink the test record should also unlink the test message
|
||||
self.test_record.sudo().unlink()
|
||||
self.assertFalse(scheduled_message.exists())
|
||||
|
||||
@users('employee')
|
||||
def test_scheduled_message_posting(self):
|
||||
schedule_cron_id = self.env.ref('mail.ir_cron_post_scheduled_message').id
|
||||
test_lead = self.env["mail.test.lead"].create({})
|
||||
with self.mock_mail_gateway(), \
|
||||
self.mock_mail_app(), \
|
||||
self.capture_triggers(schedule_cron_id) as capt:
|
||||
scheduled_message_id = self.schedule_message(
|
||||
self.test_record,
|
||||
scheduled_date='2022-12-24 14:00:00',
|
||||
partner_ids=self.test_record.customer_id,
|
||||
body="success",
|
||||
send_context={"mail_post_autofollow": True},
|
||||
subject="Test subject",
|
||||
).id
|
||||
# cron should be triggered at scheduled date
|
||||
self.assertEqual(capt.records['call_at'], FieldDatetime.to_datetime('2022-12-24 14:00:00'))
|
||||
# no message created or mail sent
|
||||
self.assertFalse(self.test_record.message_ids)
|
||||
self.assertFalse(self._new_mails)
|
||||
|
||||
# add a scheduled message that will fail to check that it won't block the cron
|
||||
failing_schedueld_message_id = self.schedule_message(
|
||||
test_lead,
|
||||
scheduled_date='2022-12-24 14:00:00',
|
||||
partner_ids=self.test_record.customer_id,
|
||||
body="fail",
|
||||
).id
|
||||
|
||||
def _message_post_after_hook(self, message, values):
|
||||
raise Exception("Boum!")
|
||||
|
||||
with self.mock_datetime_and_now('2022-12-24 14:00:00'),\
|
||||
patch.object(MailTestTLead, '_message_post_after_hook', _message_post_after_hook),\
|
||||
mute_logger('odoo.addons.mail.models.mail_scheduled_message'):
|
||||
self.env['mail.scheduled.message'].with_user(self.user_root)._post_messages_cron()
|
||||
# one scheduled message failed, only one mail should be sent
|
||||
self.assertEqual(len(self._new_mails), 1)
|
||||
# user should be notified about the failed posting
|
||||
self.assertMailNotifications(
|
||||
self._new_msgs.filtered(lambda m: not m.model),
|
||||
[{
|
||||
'content': f"<p>The message scheduled on {test_lead._name}({test_lead.id}) with"
|
||||
" the following content could not be sent:<br>-----<br></p><p>fail</p><br>-----<br>",
|
||||
'message_type': 'user_notification',
|
||||
'subtype': 'mail.mt_note',
|
||||
'message_values': {
|
||||
'author_id': self.partner_root,
|
||||
'model': False,
|
||||
'res_id': False,
|
||||
'subject': "A scheduled message could not be sent",
|
||||
},
|
||||
'notif': [
|
||||
{'partner': self.partner_employee, 'type': 'inbox'}
|
||||
]
|
||||
}])
|
||||
# other message should be posted and mail should be sent
|
||||
self.assertMailNotifications(
|
||||
self._new_msgs.filtered(lambda m: m.model == self.test_record._name),
|
||||
[{
|
||||
'content': "<p>success</p>",
|
||||
'message_type': 'notification',
|
||||
'message_values': {
|
||||
'author_id': self.partner_employee,
|
||||
'model': self.test_record._name,
|
||||
'res_id': self.test_record.id,
|
||||
'subject': "Test subject",
|
||||
},
|
||||
'notif': [
|
||||
{'partner': self.test_record.customer_id, 'type': 'email'}
|
||||
]
|
||||
}]
|
||||
)
|
||||
self.assertEqual(self._new_mails[0].state, 'sent')
|
||||
# customer should be a follower of the thread (mail_post_autofollow context key)
|
||||
self.assertIn(self.test_record.customer_id, self.test_record.message_partner_ids)
|
||||
# scheduled messages shouldn't exist anymore
|
||||
self.assertFalse(self.env['mail.scheduled.message'].search([['id', 'in', [scheduled_message_id, failing_schedueld_message_id]]]))
|
||||
|
||||
@users('employee')
|
||||
def test_scheduled_message_posting_on_scheduled_time(self):
|
||||
""" Ensure scheduled message is posted and sent at the scheduled time. """
|
||||
self.test_record.message_subscribe(partner_ids=[self.partner_1.id])
|
||||
|
||||
self.schedule_message(
|
||||
self.test_record,
|
||||
scheduled_date=FieldDatetime.to_string(self.reference_now),
|
||||
)
|
||||
|
||||
with self.mock_mail_gateway(), self.mock_datetime_and_now(self.reference_now), self.enter_registry_test_mode():
|
||||
# Needed to get force_send disabled due to mail_notify_force_send in the context
|
||||
self.env.ref('mail.ir_cron_post_scheduled_message').with_user(self.user_admin).method_direct_trigger()
|
||||
|
||||
# Message is posted and mail is sent on time
|
||||
self.assertEqual(len(self._new_mails), 1)
|
||||
self.assertMailMailWRecord(
|
||||
self.test_record,
|
||||
[self.partner_1],
|
||||
'sent',
|
||||
author=self.env.user.partner_id,
|
||||
)
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user, MailCommon
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class TestSubtypeAccess(TestMailCommon):
|
||||
class TestSubtypeAccess(MailCommon):
|
||||
|
||||
def test_subtype_access(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -7,25 +7,22 @@ import datetime
|
|||
from freezegun import freeze_time
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
|
||||
from odoo.tests import tagged, users
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.addons.test_mail.tests.common import TestRecipients
|
||||
from odoo.tests import tagged, users, warmup
|
||||
from odoo.tools import mute_logger, safe_eval
|
||||
|
||||
|
||||
class TestMailTemplateCommon(TestMailCommon, TestRecipients):
|
||||
class TestMailTemplateCommon(MailCommon, TestRecipients):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailTemplateCommon, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.test_record = cls.env['mail.test.lang'].with_context(cls._test_context).create({
|
||||
'email_from': 'ignasse@example.com',
|
||||
'name': 'Test',
|
||||
})
|
||||
|
||||
cls.user_employee.write({
|
||||
'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)],
|
||||
})
|
||||
|
||||
cls._attachments = [{
|
||||
'name': 'first.txt',
|
||||
'datas': base64.b64encode(b'My first attachment'),
|
||||
|
|
@ -51,6 +48,7 @@ class TestMailTemplateCommon(TestMailCommon, TestRecipients):
|
|||
'email_cc': '%s' % cls.email_3,
|
||||
'partner_to': '%s,%s' % (cls.partner_2.id, cls.user_admin.partner_id.id),
|
||||
'subject': 'EnglishSubject for {{ object.name }}',
|
||||
'use_default_to': False,
|
||||
})
|
||||
|
||||
# activate translations
|
||||
|
|
@ -64,6 +62,17 @@ class TestMailTemplateCommon(TestMailCommon, TestRecipients):
|
|||
# Force the attachments of the template to be in the natural order.
|
||||
cls.test_template.invalidate_recordset(['attachment_ids'])
|
||||
|
||||
# dynamic reports
|
||||
cls.test_report = cls.env['ir.actions.report'].create([
|
||||
{
|
||||
'name': 'Test Report 3 with variable data on Mail Test Ticket',
|
||||
'model': 'mail.test.ticket.mc',
|
||||
'print_report_name': "'TestReport3 for %s' % object.name",
|
||||
'report_type': 'qweb-pdf',
|
||||
'report_name': 'test_mail.mail_test_ticket_test_variable_template',
|
||||
},
|
||||
])
|
||||
|
||||
|
||||
@tagged('mail_template')
|
||||
class TestMailTemplate(TestMailTemplateCommon):
|
||||
|
|
@ -79,6 +88,19 @@ class TestMailTemplate(TestMailTemplateCommon):
|
|||
self.assertEqual(action.name, 'Send Mail (%s)' % self.test_template.name)
|
||||
self.assertEqual(action.binding_model_id.model, 'mail.test.lang')
|
||||
|
||||
def test_template_fields(self):
|
||||
""" Test computed fields """
|
||||
# has_dynamic_reports: based on ir.actions.report
|
||||
test_template_lang = self.test_template.with_user(self.user_employee)
|
||||
self.assertFalse(test_template_lang.has_dynamic_reports)
|
||||
test_template_ticket_mc = self.env['mail.template'].with_user(self.user_employee).create({
|
||||
'model_id': self.env['ir.model']._get_id('mail.test.ticket.mc'),
|
||||
})
|
||||
self.assertTrue(test_template_ticket_mc.has_dynamic_reports)
|
||||
# has_mail_server: based on ir.mail_server available
|
||||
self.assertTrue(test_template_lang.has_mail_server)
|
||||
self.assertTrue(test_template_ticket_mc.has_mail_server)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@users('employee')
|
||||
def test_template_schedule_email(self):
|
||||
|
|
@ -119,48 +141,275 @@ class TestMailTemplate(TestMailTemplateCommon):
|
|||
self.assertFalse(mail.scheduled_date)
|
||||
self.assertEqual(mail.state, 'outgoing')
|
||||
|
||||
|
||||
@tagged('mail_template', 'multi_lang')
|
||||
class TestMailTemplateLanguages(TestMailTemplateCommon):
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_template_send_email(self):
|
||||
def test_template_send_mail_body(self):
|
||||
""" Test that the body and body_html is set correctly in 'mail.mail'
|
||||
when sending an email from mail.template """
|
||||
mail_id = self.test_template.send_mail(self.test_record.id)
|
||||
mail = self.env['mail.mail'].sudo().browse(mail_id)
|
||||
body_result = '<p>EnglishBody for %s</p>' % self.test_record.name
|
||||
|
||||
self.assertEqual(mail.body_html, body_result)
|
||||
self.assertEqual(mail.body, body_result)
|
||||
|
||||
|
||||
@tagged('mail_template', 'multi_lang', 'mail_performance', 'post_install', '-at_install')
|
||||
class TestMailTemplateLanguages(TestMailTemplateCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
""" Create lang-based records and templates, to test batch and performances
|
||||
with language involved. """
|
||||
super().setUpClass()
|
||||
|
||||
# use test notification layout
|
||||
cls.test_template.write({
|
||||
'email_layout_xmlid': 'mail.test_layout',
|
||||
})
|
||||
|
||||
# double record, one in each lang
|
||||
cls.test_records = cls.test_record + cls.env['mail.test.lang'].create({
|
||||
'email_from': 'ignasse.es@example.com',
|
||||
'lang': 'es_ES',
|
||||
'name': 'Test Record 2',
|
||||
})
|
||||
|
||||
# pure batch, 100 records
|
||||
cls.test_records_batch, test_partners = cls._create_records_for_batch(
|
||||
'mail.test.lang', 100,
|
||||
)
|
||||
test_partners[:50].lang = 'es_ES'
|
||||
|
||||
# have a template with dynamic templates to check impact
|
||||
cls.test_template_wreports = cls.test_template.copy({
|
||||
'email_layout_xmlid': 'mail.test_layout',
|
||||
})
|
||||
cls.test_reports = cls.env['ir.actions.report'].create([
|
||||
{
|
||||
'name': f'Test Report on {cls.test_record._name}',
|
||||
'model': cls.test_record._name,
|
||||
'print_report_name': "f'TestReport for {object.name}'",
|
||||
'report_type': 'qweb-pdf',
|
||||
'report_name': 'test_mail.mail_test_ticket_test_template',
|
||||
}, {
|
||||
'name': f'Test Report 2 on {cls.test_record._name}',
|
||||
'model': cls.test_record._name,
|
||||
'print_report_name': "f'TestReport2 for {object.name}'",
|
||||
'report_type': 'qweb-pdf',
|
||||
'report_name': 'test_mail.mail_test_ticket_test_template_2',
|
||||
}
|
||||
])
|
||||
cls.test_template_wreports.report_template_ids = cls.test_reports
|
||||
|
||||
cls.env.flush_all()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# warm up group access cache: 5 queries + 1 query per user
|
||||
self.user_employee.has_group('base.group_user')
|
||||
# we don't use mock_mail_gateway thus want to mock smtp to test the stack
|
||||
self._mock_smtplib_connection()
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@warmup
|
||||
def test_template_send_email(self):
|
||||
""" Test 'send_email' on template on a given record, used notably as
|
||||
contextual action. """
|
||||
self.env.invalidate_all()
|
||||
with self.with_user(self.user_employee.login), self.assertQueryCount(13):
|
||||
mail_id = self.test_template.with_env(self.env).send_mail(self.test_record.id)
|
||||
mail = self.env['mail.mail'].sudo().browse(mail_id)
|
||||
|
||||
self.assertEqual(sorted(mail.attachment_ids.mapped('name')), ['first.txt', 'second.txt'])
|
||||
self.assertEqual(mail.body_html,
|
||||
f'<body><p>EnglishBody for {self.test_record.name}</p> English Layout for Lang Chatter Model</body>')
|
||||
self.assertEqual(mail.email_cc, self.test_template.email_cc)
|
||||
self.assertEqual(mail.email_to, self.test_template.email_to)
|
||||
self.assertEqual(mail.recipient_ids, self.partner_2 | self.user_admin.partner_id)
|
||||
self.assertEqual(mail.subject, 'EnglishSubject for %s' % self.test_record.name)
|
||||
self.assertEqual(mail.subject, f'EnglishSubject for {self.test_record.name}')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@warmup
|
||||
def test_template_send_email_nolayout(self):
|
||||
""" Test without layout, just to check impact """
|
||||
self.test_template.email_layout_xmlid = False
|
||||
self.env.invalidate_all()
|
||||
with self.with_user(self.user_employee.login), self.assertQueryCount(12):
|
||||
mail_id = self.test_template.with_env(self.env).send_mail(self.test_record.id)
|
||||
mail = self.env['mail.mail'].sudo().browse(mail_id)
|
||||
|
||||
self.assertEqual(sorted(mail.attachment_ids.mapped('name')), ['first.txt', 'second.txt'])
|
||||
self.assertEqual(mail.body_html,
|
||||
f'<p>EnglishBody for {self.test_record.name}</p>')
|
||||
self.assertEqual(mail.email_cc, self.test_template.email_cc)
|
||||
self.assertEqual(mail.email_to, self.test_template.email_to)
|
||||
self.assertEqual(mail.recipient_ids, self.partner_2 | self.user_admin.partner_id)
|
||||
self.assertEqual(mail.subject, f'EnglishSubject for {self.test_record.name}')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@warmup
|
||||
def test_template_send_email_batch(self):
|
||||
""" Test 'send_email' on template in batch """
|
||||
self.env.invalidate_all()
|
||||
with self.with_user(self.user_employee.login), self.assertQueryCount(25):
|
||||
template = self.test_template.with_env(self.env)
|
||||
mails_sudo = template.send_mail_batch(self.test_records_batch.ids)
|
||||
|
||||
self.assertEqual(len(mails_sudo), 100)
|
||||
for idx, (mail, record) in enumerate(zip(mails_sudo, self.test_records_batch)):
|
||||
self.assertEqual(sorted(mail.attachment_ids.mapped('name')), ['first.txt', 'second.txt'])
|
||||
self.assertEqual(mail.attachment_ids.mapped("res_id"), [template.id] * 2)
|
||||
self.assertEqual(mail.attachment_ids.mapped("res_model"), [template._name] * 2)
|
||||
self.assertEqual(mail.email_cc, template.email_cc)
|
||||
self.assertEqual(mail.email_to, template.email_to)
|
||||
self.assertEqual(mail.recipient_ids, self.partner_2 | self.user_admin.partner_id)
|
||||
if idx >= 50:
|
||||
self.assertEqual(mail.subject, f'EnglishSubject for {record.name}')
|
||||
else:
|
||||
self.assertEqual(mail.subject, f'SpanishSubject for {record.name}')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@warmup
|
||||
def test_template_send_email_wreport(self):
|
||||
""" Test 'send_email' on template on a given record, used notably as
|
||||
contextual action, with dynamic reports involved """
|
||||
self.env.invalidate_all()
|
||||
# tm: 22, nightly: +1
|
||||
with self.with_user(self.user_employee.login), self.assertQueryCount(21):
|
||||
mail_id = self.test_template_wreports.with_env(self.env).send_mail(self.test_record.id)
|
||||
mail = self.env['mail.mail'].sudo().browse(mail_id)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(mail.attachment_ids.mapped('name')),
|
||||
[f'TestReport for {self.test_record.name}.html', f'TestReport2 for {self.test_record.name}.html', 'first.txt', 'second.txt']
|
||||
)
|
||||
self.assertEqual(mail.recipient_ids, self.partner_2 | self.user_admin.partner_id)
|
||||
self.assertEqual(mail.subject, f'EnglishSubject for {self.test_record.name}')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@warmup
|
||||
def test_template_send_email_wreport_batch(self):
|
||||
""" Test 'send_email' on template in batch with dynamic reports """
|
||||
self.env.invalidate_all()
|
||||
# tm: 233, nightly: +1
|
||||
with self.with_user(self.user_employee.login), self.assertQueryCount(232):
|
||||
template = self.test_template_wreports.with_env(self.env)
|
||||
mails_sudo = template.send_mail_batch(self.test_records_batch.ids)
|
||||
|
||||
self.assertEqual(len(mails_sudo), 100)
|
||||
for idx, (mail, record) in enumerate(zip(mails_sudo, self.test_records_batch)):
|
||||
self.assertEqual(
|
||||
sorted(mail.attachment_ids.mapped('name')),
|
||||
[f'TestReport for {record.name}.html', f'TestReport2 for {record.name}.html', 'first.txt', 'second.txt']
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(mail.attachment_ids.mapped("res_id")),
|
||||
sorted([self.test_template_wreports.id] * 2 + [mail.mail_message_id.id] * 2),
|
||||
"Attachments: attachment_ids -> linked to template, attachments -> to mail.message"
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(mail.attachment_ids.mapped("res_model")),
|
||||
sorted([template._name] * 2 + ["mail.message"] * 2),
|
||||
"Attachments: attachment_ids -> linked to template, attachments -> to mail.message"
|
||||
)
|
||||
self.assertEqual(mail.email_cc, self.test_template.email_cc)
|
||||
self.assertEqual(mail.email_to, self.test_template.email_to)
|
||||
self.assertEqual(mail.recipient_ids, self.partner_2 | self.user_admin.partner_id)
|
||||
if idx >= 50:
|
||||
self.assertEqual(mail.subject, f'EnglishSubject for {record.name}')
|
||||
self.assertEqual(mail.body_html,
|
||||
f'<body><p>EnglishBody for {record.name}</p> English Layout for Lang Chatter Model</body>')
|
||||
else:
|
||||
self.assertEqual(mail.subject, f'SpanishSubject for {record.name}')
|
||||
self.assertEqual(mail.body_html,
|
||||
f'<body><p>SpanishBody for {record.name}</p> Spanish Layout para Spanish Model Description</body>')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_template_send_email_wreport_batch_scalability(self):
|
||||
""" Test 'send_email' on template in batch, using configuration parameter
|
||||
for batch rendering. """
|
||||
for batch_size, exp_mail_create_count in [
|
||||
(False, 2), # unset, default is 50
|
||||
(0, 2), # 0: fallbacks on default
|
||||
(30, 4), # 100 / 30 -> 4 iterations
|
||||
]:
|
||||
with self.subTest(batch_size=batch_size):
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
"mail.batch_size", batch_size
|
||||
)
|
||||
with self.with_user(self.user_employee.login), \
|
||||
self.mock_mail_gateway():
|
||||
template = self.test_template_wreports.with_env(self.env)
|
||||
mails_sudo = template.send_mail_batch(self.test_records_batch.ids)
|
||||
|
||||
self.assertEqual(self.mail_mail_create_mocked.call_count, exp_mail_create_count)
|
||||
self.assertEqual(len(mails_sudo), 100)
|
||||
for idx, (mail, record) in enumerate(zip(mails_sudo, self.test_records_batch)):
|
||||
self.assertEqual(
|
||||
sorted(mail.attachment_ids.mapped('name')),
|
||||
[f'TestReport for {record.name}.html', f'TestReport2 for {record.name}.html', 'first.txt', 'second.txt']
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(mail.attachment_ids.mapped("res_id")),
|
||||
sorted([self.test_template_wreports.id] * 2 + [mail.mail_message_id.id] * 2),
|
||||
"Attachments: attachment_ids -> linked to template, attachments -> to mail.message"
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(mail.attachment_ids.mapped("res_model")),
|
||||
sorted([template._name] * 2 + ["mail.message"] * 2),
|
||||
"Attachments: attachment_ids -> linked to template, attachments -> to mail.message"
|
||||
)
|
||||
self.assertEqual(mail.email_cc, self.test_template.email_cc)
|
||||
self.assertEqual(mail.email_to, self.test_template.email_to)
|
||||
self.assertEqual(mail.recipient_ids, self.partner_2 | self.user_admin.partner_id)
|
||||
if idx >= 50:
|
||||
self.assertEqual(mail.subject, f'EnglishSubject for {record.name}')
|
||||
else:
|
||||
self.assertEqual(mail.subject, f'SpanishSubject for {record.name}')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_template_translation_lang(self):
|
||||
test_record = self.env['mail.test.lang'].browse(self.test_record.ids)
|
||||
""" Test template rendering using lang defined directly on the record """
|
||||
test_record = self.test_record.with_env(self.env)
|
||||
test_record.write({
|
||||
'lang': 'es_ES',
|
||||
})
|
||||
test_template = self.env['mail.template'].browse(self.test_template.ids)
|
||||
test_template = self.test_template.with_env(self.env)
|
||||
|
||||
mail_id = test_template.send_mail(test_record.id, email_layout_xmlid='mail.test_layout')
|
||||
mail_id = test_template.send_mail(test_record.id)
|
||||
mail = self.env['mail.mail'].sudo().browse(mail_id)
|
||||
self.assertEqual(mail.body_html,
|
||||
'<body><p>SpanishBody for %s</p> Spanish Layout para Spanish Model Description</body>' % self.test_record.name)
|
||||
self.assertEqual(mail.subject, 'SpanishSubject for %s' % self.test_record.name)
|
||||
f'<body><p>SpanishBody for {self.test_record.name}</p> Spanish Layout para Spanish Model Description</body>')
|
||||
self.assertEqual(mail.subject, f'SpanishSubject for {self.test_record.name}')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@warmup
|
||||
def test_template_translation_partner_lang(self):
|
||||
test_record = self.env['mail.test.lang'].browse(self.test_record.ids)
|
||||
customer = self.env['res.partner'].create({
|
||||
'email': 'robert.carlos@test.example.com',
|
||||
'lang': 'es_ES',
|
||||
'name': 'Roberto Carlos',
|
||||
})
|
||||
test_record.write({
|
||||
'customer_id': customer.id,
|
||||
})
|
||||
test_template = self.env['mail.template'].browse(self.test_template.ids)
|
||||
""" Test template rendering using lang defined on a sub-record aka
|
||||
'partner_id.lang' """
|
||||
test_records = self.env['mail.test.lang'].browse(self.test_records.ids)
|
||||
customers = self.env['res.partner'].create([
|
||||
{
|
||||
'email': 'roberto.carlos@test.example.com',
|
||||
'lang': 'es_ES',
|
||||
'name': 'Roberto Carlos',
|
||||
}, {
|
||||
'email': 'rob.charly@test.example.com',
|
||||
'lang': 'en_US',
|
||||
'name': 'Rob Charly',
|
||||
}
|
||||
])
|
||||
test_records[0].write({'customer_id': customers[0].id})
|
||||
test_records[1].write({'customer_id': customers[1].id})
|
||||
|
||||
mail_id = test_template.send_mail(test_record.id, email_layout_xmlid='mail.test_layout')
|
||||
mail = self.env['mail.mail'].sudo().browse(mail_id)
|
||||
self.assertEqual(mail.body_html,
|
||||
'<body><p>SpanishBody for %s</p> Spanish Layout para Spanish Model Description</body>' % self.test_record.name)
|
||||
self.assertEqual(mail.subject, 'SpanishSubject for %s' % self.test_record.name)
|
||||
self.env.invalidate_all()
|
||||
with self.with_user(self.user_employee.login), self.assertQueryCount(18):
|
||||
template = self.test_template.with_env(self.env)
|
||||
mails_sudo = template.send_mail_batch(self.test_records.ids, email_layout_xmlid='mail.test_layout')
|
||||
|
||||
self.assertEqual(mails_sudo[0].body_html,
|
||||
f'<body><p>SpanishBody for {test_records[0].name}</p> Spanish Layout para Spanish Model Description</body>')
|
||||
self.assertEqual(mails_sudo[0].subject, f'SpanishSubject for {test_records[0].name}')
|
||||
self.assertEqual(mails_sudo[1].body_html,
|
||||
f'<body><p>EnglishBody for {test_records[1].name}</p> English Layout for Lang Chatter Model</body>')
|
||||
self.assertEqual(mails_sudo[1].subject, f'EnglishSubject for {test_records[1].name}')
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.test_mail.tests.test_mail_template import TestMailTemplateCommon
|
||||
from odoo.tests import tagged, users
|
||||
from odoo.tests.common import Form
|
||||
from odoo.tests import Form, tagged, users
|
||||
|
||||
|
||||
@tagged('mail_template', 'multi_lang')
|
||||
class TestMailTemplateTools(TestMailTemplateCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
|
@ -20,6 +21,63 @@ class TestMailTemplateTools(TestMailTemplateCommon):
|
|||
self.assertEqual(len(self.test_template.partner_to.split(',')), 2)
|
||||
self.assertTrue(self.test_record.email_from)
|
||||
|
||||
@users('employee')
|
||||
def test_mail_template_preview_fields(self):
|
||||
test_record = self.test_record.with_user(self.env.user)
|
||||
test_record_ref = f'{test_record._name},{test_record.id}'
|
||||
test_template = self.test_template.with_user(self.env.user)
|
||||
|
||||
# resource_ref: should not crash if no template (hence no model)
|
||||
preview = Form(self.env['mail.template.preview'])
|
||||
self.assertFalse(preview.has_attachments)
|
||||
self.assertTrue(preview.has_several_languages_installed)
|
||||
self.assertFalse(preview.resource_ref)
|
||||
|
||||
# mail_template_id being invisible, create a new one for template check
|
||||
preview = Form(self.env['mail.template.preview'].with_context(default_mail_template_id=test_template.id))
|
||||
self.assertTrue(preview.has_attachments)
|
||||
self.assertTrue(preview.has_several_languages_installed)
|
||||
self.assertEqual(preview.resource_ref, test_record_ref, 'Should take first (only) record by default')
|
||||
|
||||
def test_mail_template_preview_empty_database(self):
|
||||
"""Check behaviour of the wizard when there is no record for the target model."""
|
||||
self.env['mail.test.lang'].search([]).unlink()
|
||||
test_template = self.env['mail.template'].browse(self.test_template.ids)
|
||||
preview = self.env['mail.template.preview'].create({
|
||||
'mail_template_id': test_template.id,
|
||||
})
|
||||
|
||||
self.assertFalse(preview.error_msg)
|
||||
for field in preview._MAIL_TEMPLATE_FIELDS:
|
||||
if field in ['partner_to', 'report_template_ids']:
|
||||
continue
|
||||
self.assertEqual(test_template[field], preview[field])
|
||||
|
||||
def test_mail_template_preview_dynamic_attachment(self):
|
||||
"""Check behaviour with templates that use reports."""
|
||||
test_record = self.env['mail.test.lang'].browse(self.test_record.ids)
|
||||
test_report = self.env['ir.actions.report'].sudo().create({
|
||||
'name': 'Test Report',
|
||||
'model': test_record._name,
|
||||
'print_report_name': "'TestReport for %s' % object.name",
|
||||
'report_type': 'qweb-pdf',
|
||||
'report_name': 'test_mail.mail_test_ticket_test_template',
|
||||
})
|
||||
self.test_template.write({
|
||||
'report_template_ids': test_report.ids,
|
||||
'attachment_ids': False,
|
||||
})
|
||||
|
||||
preview = self.env['mail.template.preview'].with_context({
|
||||
'force_report_rendering': False, # this also invalidates the test records...
|
||||
}).create({
|
||||
'mail_template_id': self.test_template.id,
|
||||
'resource_ref': test_record,
|
||||
})
|
||||
|
||||
self.assertEqual(preview.body_html, f'<p>EnglishBody for {test_record.name}</p>')
|
||||
self.assertFalse(preview.attachment_ids, 'Reports should not be listed in attachments')
|
||||
|
||||
def test_mail_template_preview_force_lang(self):
|
||||
test_record = self.env['mail.test.lang'].browse(self.test_record.ids)
|
||||
test_record.write({
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,14 +1,187 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
from odoo import exceptions, tools
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.addons.mail.tests.common_tracking import MailTrackingDurationMixinCase
|
||||
from odoo.addons.test_mail.tests.common import TestRecipients
|
||||
from odoo.tests.common import tagged, users
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('mail_thread', 'mail_track', 'is_query_count')
|
||||
class TestMailTrackingDurationMixin(MailTrackingDurationMixinCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass('mail.test.track.duration.mixin')
|
||||
|
||||
def test_mail_tracking_duration(self):
|
||||
self._test_record_duration_tracking()
|
||||
|
||||
def test_mail_tracking_duration_batch(self):
|
||||
self._test_record_duration_tracking_batch()
|
||||
|
||||
def test_queries_batch_mail_tracking_duration(self):
|
||||
self._test_queries_batch_duration_tracking()
|
||||
|
||||
|
||||
@tagged('mail_thread', 'mail_track')
|
||||
class TestMailThreadRottingMixin(MailTrackingDurationMixinCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass('mail.test.rotting.resource')
|
||||
|
||||
[cls.stage_new, cls.stage_qualification, cls.stage_finished] = cls.env['mail.test.rotting.stage'].create([
|
||||
{
|
||||
'name': 'stage_new',
|
||||
'rotting_threshold_days': 3,
|
||||
}, {
|
||||
'name': 'stage_qualification',
|
||||
'rotting_threshold_days': 5,
|
||||
}, {
|
||||
'name': 'stage_finished',
|
||||
'rotting_threshold_days': 1,
|
||||
'no_rot': True,
|
||||
},
|
||||
])
|
||||
|
||||
def test_resource_rotting(self):
|
||||
# create dates for the test
|
||||
jan1 = datetime(2025, 1, 1)
|
||||
jan5 = datetime(2025, 1, 5)
|
||||
jan7 = datetime(2025, 1, 7)
|
||||
jan12 = datetime(2025, 1, 12)
|
||||
jan28 = datetime(2025, 1, 28)
|
||||
|
||||
# create resources for the test, created on jan 1
|
||||
with self.mock_datetime_and_now(jan1):
|
||||
items = [item1, item2, item3, item_done, item_won] = self.env['mail.test.rotting.resource'].create([
|
||||
{
|
||||
'name': 'item1',
|
||||
'stage_id': self.stage_new.id,
|
||||
}, {
|
||||
'name': 'item2',
|
||||
'stage_id': self.stage_qualification.id,
|
||||
}, {
|
||||
'name': 'item3',
|
||||
'stage_id': self.stage_new.id,
|
||||
}, {
|
||||
'name': 'item_done',
|
||||
'stage_id': self.stage_qualification.id,
|
||||
'done': True,
|
||||
}, {
|
||||
'name': 'item_wonStage',
|
||||
'stage_id': self.stage_finished.id,
|
||||
},
|
||||
])
|
||||
items.flush_recordset(['date_last_stage_update']) # precalculate stage update()
|
||||
|
||||
with self.mock_datetime_and_now(jan5):
|
||||
# need to invalidate on date change to ensure rotting computations
|
||||
items.invalidate_recordset(['is_rotting'])
|
||||
for item in [item1, item3]:
|
||||
self.assertTrue(
|
||||
item.is_rotting,
|
||||
'on jan 5: it\'s been four days, so only items in stage_new should be rotting',
|
||||
)
|
||||
self.assertEqual(item.rotting_days, 4)
|
||||
for item in [item2, item_done, item_won]:
|
||||
self.assertFalse(
|
||||
item.is_rotting,
|
||||
'on jan 5: it\'s been four days, so only items in stage_new should be rotting',
|
||||
)
|
||||
self.assertEqual(item.rotting_days, 0)
|
||||
|
||||
item3.name = 'item3 edited'
|
||||
self.assertTrue(
|
||||
item3.is_rotting,
|
||||
'writing to an item doesn\'t affect its rotting status',
|
||||
)
|
||||
|
||||
with self.mock_datetime_and_now(jan7):
|
||||
items.invalidate_recordset(['is_rotting'])
|
||||
self.assertTrue(
|
||||
item2.is_rotting,
|
||||
'on jan 7: items belonging to stage_qualification should be rotting, except if their state forbids it',
|
||||
)
|
||||
self.assertEqual(item2.rotting_days, 6)
|
||||
self.assertFalse(
|
||||
item_done.is_rotting,
|
||||
'item_done is marked as done, it should not be able to rot',
|
||||
)
|
||||
|
||||
self.assertTrue(item1.is_rotting)
|
||||
item1.message_post(body='Message received', message_type='email')
|
||||
self.assertTrue(
|
||||
item1.is_rotting,
|
||||
'Receiving an email should not remove rotting',
|
||||
)
|
||||
|
||||
item1.message_post(body='Message sent', message_type='email_outgoing')
|
||||
self.assertTrue(
|
||||
item1.is_rotting,
|
||||
'Nor should sending an email',
|
||||
)
|
||||
|
||||
self.assertFalse(
|
||||
item_won.is_rotting,
|
||||
'Items in stage_finished cannot rot',
|
||||
)
|
||||
self.stage_finished.no_rot = False
|
||||
self.assertTrue(
|
||||
item_won.is_rotting,
|
||||
'However if the stage no longer disallows rotting, then all items in the stage may once more rot',
|
||||
)
|
||||
|
||||
self.stage_finished.no_rot = True
|
||||
self.assertFalse(
|
||||
item_won.is_rotting,
|
||||
'Disallowing rotting once again should disable rotting once more',
|
||||
)
|
||||
|
||||
with self.mock_datetime_and_now(jan12):
|
||||
items.invalidate_recordset(['rotting_days', 'is_rotting'])
|
||||
|
||||
self.assertTrue(item3.is_rotting)
|
||||
self.stage_new.rotting_threshold_days = 40
|
||||
self.assertFalse(
|
||||
item3.is_rotting,
|
||||
'Changing the threshold should affect the status immediately)',
|
||||
)
|
||||
|
||||
self.stage_new.rotting_threshold_days = 1
|
||||
|
||||
item3.stage_id = self.stage_qualification
|
||||
self.assertFalse(
|
||||
item3.is_rotting,
|
||||
'Changing stages always removes rotting',
|
||||
)
|
||||
|
||||
self.stage_qualification.rotting_threshold_days = 0
|
||||
self.assertFalse(
|
||||
item2.is_rotting,
|
||||
'Setting rotting_threshold_days at 0 on a stage immediately disables rotting for the stage',
|
||||
)
|
||||
|
||||
with self.mock_datetime_and_now(jan28):
|
||||
items.invalidate_recordset(['rotting_days', 'is_rotting'])
|
||||
# After a significant amount of time has passed:
|
||||
self.assertTrue(
|
||||
item1.is_rotting,
|
||||
'Items that are not done or won are rotting',
|
||||
)
|
||||
for item in [item2, item3, item_done, item_won]:
|
||||
self.assertFalse(
|
||||
item.is_rotting,
|
||||
'Items that are not done, won, or in a disabled rotting stage are not rotting',
|
||||
)
|
||||
|
||||
|
||||
@tagged('mail_thread', 'mail_blacklist')
|
||||
class TestMailThread(TestMailCommon, TestRecipients):
|
||||
class TestMailThread(MailCommon, TestRecipients):
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_blacklist_mixin_email_normalized(self):
|
||||
|
|
@ -58,3 +231,31 @@ class TestMailThread(TestMailCommon, TestRecipients):
|
|||
self.assertTrue(new_record.is_blacklisted)
|
||||
|
||||
bl_record.unlink()
|
||||
|
||||
|
||||
@tagged('mail_thread', 'mail_thread_cc', 'mail_tools')
|
||||
class TestMailThreadCC(MailCommon):
|
||||
|
||||
@users("employee")
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_suggested_recipients_mail_cc(self):
|
||||
""" MailThreadCC mixin adds its own suggested recipients management
|
||||
coming from CC (carbon copy) management. """
|
||||
record = self.env['mail.test.cc'].create({
|
||||
'email_cc': 'cc1@example.com, cc2@example.com, cc3 <cc3@example.com>',
|
||||
})
|
||||
suggestions = record._message_get_suggested_recipients(no_create=True)
|
||||
expected_list = [
|
||||
{
|
||||
'name': '', 'email': 'cc1@example.com',
|
||||
'partner_id': False, 'create_values': {},
|
||||
}, {
|
||||
'name': '', 'email': 'cc2@example.com',
|
||||
'partner_id': False, 'create_values': {},
|
||||
}, {
|
||||
'name': 'cc3', 'email': 'cc3@example.com',
|
||||
'partner_id': False, 'create_values': {},
|
||||
}]
|
||||
self.assertEqual(len(suggestions), len(expected_list))
|
||||
for suggestion, expected in zip(suggestions, expected_list):
|
||||
self.assertDictEqual(suggestion, expected)
|
||||
|
|
|
|||
|
|
@ -1,130 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('mail_wizards')
|
||||
class TestMailResend(TestMailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailResend, cls).setUpClass()
|
||||
cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
|
||||
|
||||
#Two users
|
||||
cls.user1 = mail_new_test_user(cls.env, login='e1', groups='base.group_user', name='Employee 1', notification_type='email', email='e1') # invalid email
|
||||
cls.user2 = mail_new_test_user(cls.env, login='e2', groups='base.group_portal', name='Employee 2', notification_type='email', email='e2@example.com')
|
||||
#Two partner
|
||||
cls.partner1 = cls.env['res.partner'].with_context(cls._test_context).create({
|
||||
'name': 'Partner 1',
|
||||
'email': 'p1' # invalid email
|
||||
})
|
||||
cls.partner2 = cls.env['res.partner'].with_context(cls._test_context).create({
|
||||
'name': 'Partner 2',
|
||||
'email': 'p2@example.com'
|
||||
})
|
||||
cls.partners = cls.env['res.partner'].concat(cls.user1.partner_id, cls.user2.partner_id, cls.partner1, cls.partner2)
|
||||
cls.invalid_email_partners = cls.env['res.partner'].concat(cls.user1.partner_id, cls.partner1)
|
||||
|
||||
# @mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_resend_workflow(self):
|
||||
with self.assertSinglePostNotifications(
|
||||
[{'partner': partner, 'type': 'email', 'status': 'exception'} for partner in self.partners],
|
||||
message_info={'message_type': 'notification'}):
|
||||
def _connect(*args, **kwargs):
|
||||
raise Exception("Some exception")
|
||||
self.connect_mocked.side_effect = _connect
|
||||
message = self.test_record.with_user(self.user_admin).message_post(partner_ids=self.partners.ids, subtype_xmlid='mail.mt_comment', message_type='notification')
|
||||
|
||||
wizard = self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({})
|
||||
self.assertEqual(wizard.notification_ids.mapped('res_partner_id'), self.partners, "wizard should manage notifications for each failed partner")
|
||||
|
||||
# three more failure sent on bus, one for each mail in failure and one for resend
|
||||
self._reset_bus()
|
||||
expected_bus_notifications = [
|
||||
(self.cr.dbname, 'res.partner', self.partner_admin.id),
|
||||
(self.cr.dbname, 'res.partner', self.env.user.partner_id.id),
|
||||
]
|
||||
with self.mock_mail_gateway(), self.assertBus(expected_bus_notifications * 3):
|
||||
wizard.resend_mail_action()
|
||||
done_msgs, done_notifs = self.assertMailNotifications(message, [
|
||||
{'content': '', 'message_type': 'notification',
|
||||
'notif': [{'partner': partner, 'type': 'email', 'status': 'exception' if partner in self.user1.partner_id | self.partner1 else 'sent'} for partner in self.partners]}]
|
||||
)
|
||||
self.assertEqual(wizard.notification_ids, done_notifs)
|
||||
self.assertEqual(done_msgs, message)
|
||||
|
||||
self.user1.write({"email": 'u1@example.com'})
|
||||
|
||||
# two more failure update sent on bus, one for failed mail and one for resend
|
||||
self._reset_bus()
|
||||
with self.mock_mail_gateway(), self.assertBus(expected_bus_notifications * 2):
|
||||
self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({}).resend_mail_action()
|
||||
done_msgs, done_notifs = self.assertMailNotifications(message, [
|
||||
{'content': '', 'message_type': 'notification',
|
||||
'notif': [{'partner': partner, 'type': 'email', 'status': 'exception' if partner == self.partner1 else 'sent', 'check_send': partner == self.partner1} for partner in self.partners]}]
|
||||
)
|
||||
self.assertEqual(wizard.notification_ids, done_notifs)
|
||||
self.assertEqual(done_msgs, message)
|
||||
|
||||
self.partner1.write({"email": 'p1@example.com'})
|
||||
|
||||
# A success update should be sent on bus once the email has no more failure
|
||||
self._reset_bus()
|
||||
with self.mock_mail_gateway(), self.assertBus(expected_bus_notifications):
|
||||
self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({}).resend_mail_action()
|
||||
self.assertMailNotifications(message, [
|
||||
{'content': '', 'message_type': 'notification',
|
||||
'notif': [{'partner': partner, 'type': 'email', 'status': 'sent', 'check_send': partner == self.partner1} for partner in self.partners]}]
|
||||
)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_remove_mail_become_canceled(self):
|
||||
# two failure sent on bus, one for each mail
|
||||
self._reset_bus()
|
||||
with self.mock_mail_gateway(), self.assertBus([(self.cr.dbname, 'res.partner', self.partner_admin.id)] * 2):
|
||||
message = self.test_record.with_user(self.user_admin).message_post(partner_ids=self.partners.ids, subtype_xmlid='mail.mt_comment', message_type='notification')
|
||||
|
||||
self.assertMailNotifications(message, [
|
||||
{'content': '', 'message_type': 'notification',
|
||||
'notif': [{'partner': partner, 'type': 'email', 'status': 'exception' if partner in self.user1.partner_id | self.partner1 else 'sent'} for partner in self.partners]}]
|
||||
)
|
||||
|
||||
wizard = self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({})
|
||||
partners = wizard.partner_ids.mapped("partner_id")
|
||||
self.assertEqual(self.invalid_email_partners, partners)
|
||||
wizard.partner_ids.filtered(lambda p: p.partner_id == self.partner1).write({"resend": False})
|
||||
wizard.resend_mail_action()
|
||||
|
||||
self.assertMailNotifications(message, [
|
||||
{'content': '', 'message_type': 'notification',
|
||||
'notif': [{'partner': partner, 'type': 'email',
|
||||
'status': (partner == self.user1.partner_id and 'exception') or (partner == self.partner1 and 'canceled') or 'sent'} for partner in self.partners]}]
|
||||
)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_cancel_all(self):
|
||||
self._reset_bus()
|
||||
with self.mock_mail_gateway(), self.assertBus([(self.cr.dbname, 'res.partner', self.partner_admin.id)] * 2):
|
||||
message = self.test_record.with_user(self.user_admin).message_post(partner_ids=self.partners.ids, subtype_xmlid='mail.mt_comment', message_type='notification')
|
||||
|
||||
wizard = self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({})
|
||||
# one update for cancell
|
||||
self._reset_bus()
|
||||
expected_bus_notifications = [
|
||||
(self.cr.dbname, 'res.partner', self.partner_admin.id),
|
||||
(self.cr.dbname, 'res.partner', self.env.user.partner_id.id),
|
||||
]
|
||||
with self.mock_mail_gateway(), self.assertBus(expected_bus_notifications):
|
||||
wizard.cancel_mail_action()
|
||||
|
||||
self.assertMailNotifications(message, [
|
||||
{'content': '', 'message_type': 'notification',
|
||||
'notif': [{'partner': partner, 'type': 'email',
|
||||
'check_send': partner in self.user1.partner_id | self.partner1,
|
||||
'status': 'canceled' if partner in self.user1.partner_id | self.partner1 else 'sent'} for partner in self.partners]}]
|
||||
)
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,21 +0,0 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo.tests
|
||||
from odoo import Command
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class TestUi(odoo.tests.HttpCase):
|
||||
|
||||
def test_01_mail_tour(self):
|
||||
self.start_tour("/web", 'mail_tour', login="admin")
|
||||
|
||||
def test_02_mail_create_channel_no_mail_tour(self):
|
||||
self.env['res.users'].create({
|
||||
'email': '', # User should be able to create a channel even if no email is defined
|
||||
'groups_id': [Command.set([self.ref('base.group_user')])],
|
||||
'name': 'Test User',
|
||||
'login': 'testuser',
|
||||
'password': 'testuser',
|
||||
})
|
||||
self.start_tour("/web", 'mail_tour', login='testuser')
|
||||
Loading…
Add table
Add a link
Reference in a new issue