mirror of
https://github.com/bringout/oca-ocb-test.git
synced 2026-04-21 23:42:08 +02:00
Initial commit: Test packages
This commit is contained in:
commit
080accd21c
338 changed files with 32413 additions and 0 deletions
49
odoo-bringout-oca-ocb-test_mail/README.md
Normal file
49
odoo-bringout-oca-ocb-test_mail/README.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Mail Tests
|
||||
|
||||
This module contains tests related to mail. Those are
|
||||
present in a separate module as it contains models used only to perform
|
||||
tests independently to functional aspects of other models.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-ocb-test_mail
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- mail
|
||||
- test_performance
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Mail Tests
|
||||
- **Version**: 1.0
|
||||
- **Category**: Hidden
|
||||
- **License**: LGPL-3
|
||||
- **Installable**: True
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `test_mail`.
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original LGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Reports: doc/REPORTS.md
|
||||
- Security: doc/SECURITY.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
32
odoo-bringout-oca-ocb-test_mail/doc/ARCHITECTURE.md
Normal file
32
odoo-bringout-oca-ocb-test_mail/doc/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[Users] -->|HTTP| V[Views and QWeb Templates]
|
||||
V --> C[Controllers]
|
||||
V --> W[Wizards – Transient Models]
|
||||
C --> M[Models and ORM]
|
||||
W --> M
|
||||
M --> R[Reports]
|
||||
DX[Data XML] --> M
|
||||
S[Security – ACLs and Groups] -. enforces .-> M
|
||||
|
||||
subgraph Test_mail Module - test_mail
|
||||
direction LR
|
||||
M:::layer
|
||||
W:::layer
|
||||
C:::layer
|
||||
V:::layer
|
||||
R:::layer
|
||||
S:::layer
|
||||
DX:::layer
|
||||
end
|
||||
|
||||
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
|
||||
```
|
||||
|
||||
Notes
|
||||
- Views include tree/form/kanban templates and report templates.
|
||||
- Controllers provide website/portal routes when present.
|
||||
- Wizards are UI flows implemented with `models.TransientModel`.
|
||||
- Data XML loads data/demo records; Security defines groups and access.
|
||||
3
odoo-bringout-oca-ocb-test_mail/doc/CONFIGURATION.md
Normal file
3
odoo-bringout-oca-ocb-test_mail/doc/CONFIGURATION.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for test_mail. Configure related models, access rights, and options as needed.
|
||||
3
odoo-bringout-oca-ocb-test_mail/doc/CONTROLLERS.md
Normal file
3
odoo-bringout-oca-ocb-test_mail/doc/CONTROLLERS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Controllers
|
||||
|
||||
This module does not define custom HTTP controllers.
|
||||
6
odoo-bringout-oca-ocb-test_mail/doc/DEPENDENCIES.md
Normal file
6
odoo-bringout-oca-ocb-test_mail/doc/DEPENDENCIES.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [mail](../../odoo-bringout-oca-ocb-mail)
|
||||
- test_performance
|
||||
4
odoo-bringout-oca-ocb-test_mail/doc/FAQ.md
Normal file
4
odoo-bringout-oca-ocb-test_mail/doc/FAQ.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# FAQ
|
||||
|
||||
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
|
||||
- Q: How to enable? A: Start server with --addon test_mail or install in UI.
|
||||
7
odoo-bringout-oca-ocb-test_mail/doc/INSTALL.md
Normal file
7
odoo-bringout-oca-ocb-test_mail/doc/INSTALL.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-ocb-test_mail"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-ocb-test_mail"
|
||||
```
|
||||
39
odoo-bringout-oca-ocb-test_mail/doc/MODELS.md
Normal file
39
odoo-bringout-oca-ocb-test_mail/doc/MODELS.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in test_mail.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class mail_performance_thread
|
||||
class mail_performance_tracking
|
||||
class mail_test_access
|
||||
class mail_test_access_custo
|
||||
class mail_test_activity
|
||||
class mail_test_cc
|
||||
class mail_test_composer_mixin
|
||||
class mail_test_composer_source
|
||||
class mail_test_container
|
||||
class mail_test_container_mc
|
||||
class mail_test_field_type
|
||||
class mail_test_gateway
|
||||
class mail_test_gateway_groups
|
||||
class mail_test_lang
|
||||
class mail_test_multi_company
|
||||
class mail_test_multi_company_read
|
||||
class mail_test_multi_company_with_activity
|
||||
class mail_test_nothread
|
||||
class mail_test_simple
|
||||
class mail_test_ticket
|
||||
class mail_test_ticket_el
|
||||
class mail_test_ticket_mc
|
||||
class mail_test_track
|
||||
class mail_test_track_all
|
||||
class mail_test_track_compute
|
||||
class mail_test_track_monetary
|
||||
class mail_test_track_selection
|
||||
class mail_thread
|
||||
```
|
||||
|
||||
Notes
|
||||
- Classes show model technical names; fields omitted for brevity.
|
||||
- Items listed under _inherit are extensions of existing models.
|
||||
6
odoo-bringout-oca-ocb-test_mail/doc/OVERVIEW.md
Normal file
6
odoo-bringout-oca-ocb-test_mail/doc/OVERVIEW.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Overview
|
||||
|
||||
Packaged Odoo addon: test_mail. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon test_mail
|
||||
- License: LGPL-3
|
||||
3
odoo-bringout-oca-ocb-test_mail/doc/REPORTS.md
Normal file
3
odoo-bringout-oca-ocb-test_mail/doc/REPORTS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
41
odoo-bringout-oca-ocb-test_mail/doc/SECURITY.md
Normal file
41
odoo-bringout-oca-ocb-test_mail/doc/SECURITY.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Security
|
||||
|
||||
Access control and security definitions in test_mail.
|
||||
|
||||
## Access Control Lists (ACLs)
|
||||
|
||||
Model access permissions defined in:
|
||||
- **[ir.model.access.csv](../test_mail/security/ir.model.access.csv)**
|
||||
- 50 model access rules
|
||||
|
||||
## Record Rules
|
||||
|
||||
Row-level security rules defined in:
|
||||
|
||||
## Security Groups & Configuration
|
||||
|
||||
Security groups and permissions defined in:
|
||||
- **[test_mail_security.xml](../test_mail/security/test_mail_security.xml)**
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Security Layers"
|
||||
A[Users] --> B[Groups]
|
||||
B --> C[Access Control Lists]
|
||||
C --> D[Models]
|
||||
B --> E[Record Rules]
|
||||
E --> F[Individual Records]
|
||||
end
|
||||
```
|
||||
|
||||
Security files overview:
|
||||
- **[ir.model.access.csv](../test_mail/security/ir.model.access.csv)**
|
||||
- Model access permissions (CRUD rights)
|
||||
- **[test_mail_security.xml](../test_mail/security/test_mail_security.xml)**
|
||||
- Security groups, categories, and XML-based rules
|
||||
|
||||
Notes
|
||||
- Access Control Lists define which groups can access which models
|
||||
- Record Rules provide row-level security (filter records by user/group)
|
||||
- Security groups organize users and define permission sets
|
||||
- All security is enforced at the ORM level by Odoo
|
||||
5
odoo-bringout-oca-ocb-test_mail/doc/TROUBLESHOOTING.md
Normal file
5
odoo-bringout-oca-ocb-test_mail/doc/TROUBLESHOOTING.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Troubleshooting
|
||||
|
||||
- Ensure Python and Odoo environment matches repo guidance.
|
||||
- Check database connectivity and logs if startup fails.
|
||||
- Validate that dependent addons listed in DEPENDENCIES.md are installed.
|
||||
7
odoo-bringout-oca-ocb-test_mail/doc/USAGE.md
Normal file
7
odoo-bringout-oca-ocb-test_mail/doc/USAGE.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Usage
|
||||
|
||||
Start Odoo including this addon (from repo root):
|
||||
|
||||
```bash
|
||||
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon test_mail
|
||||
```
|
||||
3
odoo-bringout-oca-ocb-test_mail/doc/WIZARDS.md
Normal file
3
odoo-bringout-oca-ocb-test_mail/doc/WIZARDS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Wizards
|
||||
|
||||
This module does not include UI wizards.
|
||||
43
odoo-bringout-oca-ocb-test_mail/pyproject.toml
Normal file
43
odoo-bringout-oca-ocb-test_mail/pyproject.toml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[project]
|
||||
name = "odoo-bringout-oca-ocb-test_mail"
|
||||
version = "16.0.0"
|
||||
description = "Mail Tests - Mail Tests: performances and tests specific to mail"
|
||||
authors = [
|
||||
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
|
||||
]
|
||||
dependencies = [
|
||||
"odoo-bringout-oca-ocb-mail>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-test_performance>=16.0.0",
|
||||
"requests>=2.25.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.11"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Office/Business",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/bringout/0"
|
||||
repository = "https://github.com/bringout/0"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["test_mail"]
|
||||
|
||||
[tool.rye]
|
||||
managed = true
|
||||
dev-dependencies = [
|
||||
"pytest>=8.4.1",
|
||||
]
|
||||
4
odoo-bringout-oca-ocb-test_mail/test_mail/__init__.py
Normal file
4
odoo-bringout-oca-ocb-test_mail/test_mail/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import data
|
||||
from . import models
|
||||
36
odoo-bringout-oca-ocb-test_mail/test_mail/__manifest__.py
Normal file
36
odoo-bringout-oca-ocb-test_mail/test_mail/__manifest__.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
'name': 'Mail Tests',
|
||||
'version': '1.0',
|
||||
'category': 'Hidden',
|
||||
'sequence': 9876,
|
||||
'summary': 'Mail Tests: performances and tests specific to mail',
|
||||
'description': """This module contains tests related to mail. Those are
|
||||
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',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'security/test_mail_security.xml',
|
||||
'data/data.xml',
|
||||
'data/mail_template_data.xml',
|
||||
'data/subtype_data.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.qunit_suite_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/*',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_mail_data
|
||||
40
odoo-bringout-oca-ocb-test_mail/test_mail/data/data.xml
Normal file
40
odoo-bringout-oca-ocb-test_mail/test_mail/data/data.xml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="mail_act_test_todo" model="mail.activity.type">
|
||||
<field name="name">Do Stuff</field>
|
||||
<field name="summary">Really?! Wow! A superpowers drug you can just rub onto your skin?</field>
|
||||
<field name="category">default</field>
|
||||
<field name="res_model">mail.test.activity</field>
|
||||
</record>
|
||||
<record id="mail_act_test_meeting" model="mail.activity.type">
|
||||
<field name="name">Meet People</field>
|
||||
<field name="summary">You'd think it would be something you'd have to freebase. Noooooo!</field>
|
||||
<field name="category">default</field>
|
||||
<field name="res_model">mail.test.activity</field>
|
||||
</record>
|
||||
<record id="mail_act_test_call" model="mail.activity.type">
|
||||
<field name="name">Call People</field>
|
||||
<field name="summary">Then throw her in the laundry room, which will hereafter be referred to as "the brig".</field>
|
||||
<field name="category">default</field>
|
||||
<field name="res_model">mail.test.activity</field>
|
||||
</record>
|
||||
<record id="mail_act_test_chained_2" model="mail.activity.type">
|
||||
<field name="name">Step 2</field>
|
||||
<field name="summary">Take the second step.</field>
|
||||
<field name="category">default</field>
|
||||
<field name="res_model">mail.test.activity</field>
|
||||
<field name="delay_count">10</field>
|
||||
<field name="delay_from">current_date</field>
|
||||
<field name="delay_unit">days</field>
|
||||
</record>
|
||||
<record id="mail_act_test_chained_1" model="mail.activity.type">
|
||||
<field name="name">Step 1</field>
|
||||
<field name="summary">Take the first step.</field>
|
||||
<field name="category">default</field>
|
||||
<field name="res_model">mail.test.activity</field>
|
||||
<field name="chaining_type">trigger</field>
|
||||
<field name="triggered_next_type_id" ref="test_mail.mail_act_test_chained_2"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="mail_test_ticket_tracking_tpl" model="mail.template">
|
||||
<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="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"/>
|
||||
</record>
|
||||
|
||||
<record id="mail_test_container_tpl" model="mail.template">
|
||||
<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="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"/>
|
||||
</record>
|
||||
|
||||
<template id="mail_test_ticket_tracking_view">
|
||||
<p><span t-out="object.name or ''"/> datetime has been updated to <span t-out="object.datetime or ''"/></p>
|
||||
</template>
|
||||
|
||||
<template id="mail_test_ticket_test_template">
|
||||
<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 a sample of an external report.</p>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- mail.test.simple -->
|
||||
<record id="st_mail_test_simple_external" model="mail.message.subtype">
|
||||
<field name="name">External subtype</field>
|
||||
<field name="description">External subtype</field>
|
||||
<field name="res_model">mail.test.simple</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="internal" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- mail.test.ticket -->
|
||||
<record id="st_mail_test_ticket_container_upd" model="mail.message.subtype">
|
||||
<field name="name">Container Changed Subtype</field>
|
||||
<field name="description">Container Changed</field>
|
||||
<field name="res_model">mail.test.ticket</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="internal" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- mail.test.container -->
|
||||
<record id="st_mail_test_container_default" model="mail.message.subtype">
|
||||
<field name="name">Container Default Subtype</field>
|
||||
<field name="res_model">mail.test.container</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="internal" eval="False"/>
|
||||
</record>
|
||||
<!-- when subscribing to mail.test.container, delegate to mail.test.ticket child -->
|
||||
<record id="st_mail_test_container_child_full" model="mail.message.subtype">
|
||||
<field name="name">Container Child Full Subtype</field>
|
||||
<field name="res_model">mail.test.container</field>
|
||||
<field name="parent_id" ref="test_mail.st_mail_test_ticket_container_upd"/>
|
||||
<field name="relation_field">container_id</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="internal" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- mail.test.ticket.mc -->
|
||||
<record id="st_mail_test_ticket_container_mc_upd" model="mail.message.subtype">
|
||||
<field name="name">Container MC Changed Subtype</field>
|
||||
<field name="description">Container Changed</field>
|
||||
<field name="res_model">mail.test.ticket.mc</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="internal" eval="False"/>
|
||||
</record>
|
||||
<record id="st_mail_test_ticket_internal" model="mail.message.subtype">
|
||||
<field name="name">Ticket MC Internal</field>
|
||||
<field name="description">Ticket MC Internal</field>
|
||||
<field name="res_model">mail.test.ticket.mc</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="internal" eval="True"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
1426
odoo-bringout-oca-ocb-test_mail/test_mail/data/test_mail_data.py
Normal file
1426
odoo-bringout-oca-ocb-test_mail/test_mail/data/test_mail_data.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mail_test_access
|
||||
from . import test_mail_corner_case_models
|
||||
from . import test_mail_models
|
||||
from . import test_mail_thread_models
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
from odoo import exceptions, fields, models
|
||||
|
||||
|
||||
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. """
|
||||
_description = 'Mail Access Test'
|
||||
_name = 'mail.test.access'
|
||||
_inherit = ['mail.thread.blacklist']
|
||||
_mail_post_access = 'write' # default value but ease mock
|
||||
_order = 'id DESC'
|
||||
_primary_email = 'email_from'
|
||||
|
||||
name = fields.Char()
|
||||
email_from = fields.Char()
|
||||
phone = fields.Char()
|
||||
customer_id = fields.Many2one('res.partner', 'Customer')
|
||||
access = fields.Selection(
|
||||
[
|
||||
('public', 'public'),
|
||||
('logged', 'Logged'),
|
||||
('logged_ro', 'Logged readonly for portal'),
|
||||
('followers', 'Followers'),
|
||||
('internal', 'Internal'),
|
||||
('internal_ro', 'Internal readonly'),
|
||||
('admin', 'Admin'),
|
||||
],
|
||||
name='Access', default='public')
|
||||
|
||||
def _mail_get_partner_fields(self):
|
||||
return ['customer_id']
|
||||
|
||||
|
||||
class MailTestAccessCusto(models.Model):
|
||||
""" Test access on mail models without depending on real models like channel
|
||||
or partner which have their own set of ACLs. """
|
||||
_description = 'Mail Access Test with Custo'
|
||||
_name = 'mail.test.access.custo'
|
||||
_inherit = ['mail.thread.blacklist']
|
||||
_mail_post_access = 'write' # default value but ease mock
|
||||
_order = 'id DESC'
|
||||
_primary_email = 'email_from'
|
||||
|
||||
name = fields.Char()
|
||||
email_from = fields.Char()
|
||||
phone = fields.Char()
|
||||
customer_id = fields.Many2one('res.partner', 'Customer')
|
||||
is_locked = fields.Boolean()
|
||||
|
||||
def _mail_get_partner_fields(self):
|
||||
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)
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class MailPerformanceThread(models.Model):
|
||||
_name = 'mail.performance.thread'
|
||||
_description = 'Performance: mail.thread'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char()
|
||||
value = fields.Integer()
|
||||
value_pc = fields.Float(compute="_value_pc", store=True)
|
||||
track = fields.Char(default='test', tracking=True)
|
||||
partner_id = fields.Many2one('res.partner', string='Customer')
|
||||
|
||||
@api.depends('value')
|
||||
def _value_pc(self):
|
||||
for record in self:
|
||||
record.value_pc = float(record.value) / 100
|
||||
|
||||
|
||||
class MailPerformanceTracking(models.Model):
|
||||
_name = 'mail.performance.tracking'
|
||||
_description = 'Performance: multi tracking'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char(required=True, tracking=True)
|
||||
field_0 = fields.Char(tracking=True)
|
||||
field_1 = fields.Char(tracking=True)
|
||||
field_2 = fields.Char(tracking=True)
|
||||
|
||||
|
||||
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'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char()
|
||||
email_from = fields.Char()
|
||||
datetime = fields.Datetime(default=fields.Datetime.now)
|
||||
customer_id = fields.Many2one('res.partner', 'Customer')
|
||||
type = fields.Selection([('first', 'First'), ('second', 'Second')])
|
||||
user_id = fields.Many2one('res.users', 'Responsible', tracking=True)
|
||||
|
||||
@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'):
|
||||
self = self.with_context(default_type='first')
|
||||
return super(MailTestFieldType, self).create(vals_list)
|
||||
|
||||
def _mail_get_partner_fields(self):
|
||||
return ['customer_id']
|
||||
|
||||
|
||||
class MailTestLang(models.Model):
|
||||
""" A simple chatter model with lang-based capabilities, allowing to
|
||||
test translations. """
|
||||
_description = 'Lang Chatter Model'
|
||||
_name = 'mail.test.lang'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char()
|
||||
email_from = fields.Char()
|
||||
customer_id = fields.Many2one('res.partner')
|
||||
lang = fields.Char('Lang')
|
||||
|
||||
def _mail_get_partner_fields(self):
|
||||
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 {})
|
||||
|
||||
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 MailTestTrackCompute(models.Model):
|
||||
_name = 'mail.test.track.compute'
|
||||
_description = "Test tracking with computed fields"
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
partner_id = fields.Many2one('res.partner', tracking=True)
|
||||
partner_name = fields.Char(related='partner_id.name', store=True, tracking=True)
|
||||
partner_email = fields.Char(related='partner_id.email', store=True, tracking=True)
|
||||
partner_phone = fields.Char(related='partner_id.phone', tracking=True)
|
||||
|
||||
|
||||
class MailTestTrackMonetary(models.Model):
|
||||
_name = 'mail.test.track.monetary'
|
||||
_description = 'Test tracking monetary field'
|
||||
_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):
|
||||
""" Test tracking for selection fields """
|
||||
_description = 'Test Selection Tracking'
|
||||
_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'
|
||||
_description = "Test Multi Company Mail"
|
||||
_inherit = 'mail.thread'
|
||||
|
||||
name = fields.Char()
|
||||
company_id = fields.Many2one('res.company')
|
||||
|
||||
|
||||
class MailTestMultiCompanyRead(models.Model):
|
||||
""" Just mail.test.simple, but multi company and supporting posting
|
||||
even if the user has no write access. """
|
||||
_description = 'Simple Chatter Model '
|
||||
_name = 'mail.test.multi.company.read'
|
||||
_inherit = ['mail.test.multi.company']
|
||||
_mail_post_access = 'read'
|
||||
|
||||
|
||||
class MailTestNotMailThread(models.Model):
|
||||
""" Models not inheriting from mail.thread but using some cross models
|
||||
capabilities of mail. """
|
||||
_name = 'mail.test.nothread'
|
||||
_description = "NoThread Model"
|
||||
|
||||
name = fields.Char()
|
||||
company_id = fields.Many2one('res.company')
|
||||
customer_id = fields.Many2one('res.partner')
|
||||
|
|
@ -0,0 +1,330 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
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'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char()
|
||||
email_from = fields.Char()
|
||||
|
||||
|
||||
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'
|
||||
_inherit = ['mail.thread.blacklist']
|
||||
_primary_email = 'email_from'
|
||||
|
||||
name = fields.Char()
|
||||
email_from = fields.Char()
|
||||
custom_field = fields.Char()
|
||||
|
||||
@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'),
|
||||
}
|
||||
defaults.update(custom_values or {})
|
||||
return super().message_new(msg_dict, custom_values=defaults)
|
||||
|
||||
|
||||
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'
|
||||
_inherit = ['mail.thread.blacklist', 'mail.alias.mixin']
|
||||
_mail_flat_thread = False
|
||||
_primary_email = 'email_from'
|
||||
|
||||
name = fields.Char()
|
||||
email_from = fields.Char()
|
||||
custom_field = fields.Char()
|
||||
customer_id = fields.Many2one('res.partner', 'Customer')
|
||||
|
||||
def _alias_get_creation_values(self):
|
||||
values = super(MailTestGatewayGroups, self)._alias_get_creation_values()
|
||||
values['alias_model_id'] = self.env['ir.model']._get('mail.test.gateway.groups').id
|
||||
if self.id:
|
||||
values['alias_force_thread_id'] = self.id
|
||||
values['alias_parent_thread_id'] = self.id
|
||||
return values
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
class MailTestStandard(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'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char()
|
||||
email_from = fields.Char()
|
||||
user_id = fields.Many2one('res.users', 'Responsible', tracking=True)
|
||||
container_id = fields.Many2one('mail.test.container', tracking=True)
|
||||
company_id = fields.Many2one('res.company')
|
||||
|
||||
|
||||
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'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
name = fields.Char()
|
||||
date = fields.Date()
|
||||
email_from = fields.Char()
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
def action_start(self, action_summary):
|
||||
return self.activity_schedule(
|
||||
'test_mail.mail_act_test_todo',
|
||||
summary=action_summary
|
||||
)
|
||||
|
||||
def action_close(self, action_feedback, attachment_ids=None):
|
||||
self.activity_feedback(['test_mail.mail_act_test_todo'],
|
||||
feedback=action_feedback,
|
||||
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'
|
||||
_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)
|
||||
source_ids = fields.Many2many('mail.test.composer.source', string='Invite source')
|
||||
|
||||
def _compute_render_model(self):
|
||||
self.render_model = 'mail.test.composer.source'
|
||||
|
||||
|
||||
class MailTestComposerSource(models.Model):
|
||||
""" A simple model on which invites are sent. """
|
||||
_description = 'Invite-like Wizard'
|
||||
_name = 'mail.test.composer.source'
|
||||
_inherit = ['mail.thread.blacklist']
|
||||
_primary_email = 'email_from'
|
||||
|
||||
name = fields.Char('Name')
|
||||
customer_id = fields.Many2one('res.partner', 'Main customer')
|
||||
email_from = fields.Char(
|
||||
'Email',
|
||||
compute='_compute_email_from', readonly=False, store=True)
|
||||
|
||||
@api.depends('customer_id')
|
||||
def _compute_email_from(self):
|
||||
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):
|
||||
return ['customer_id']
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class MailTestCC(models.Model):
|
||||
_name = 'mail.test.cc'
|
||||
_description = "Test Email CC Thread"
|
||||
_inherit = ['mail.thread.cc']
|
||||
|
||||
name = fields.Char()
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
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_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_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_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_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
|
||||
access_mail_test_track_user,mail.test.track.user.employee,model_mail_test_track,base.group_user,1,1,1,1
|
||||
access_mail_test_activity_portal,mail.test.activity.portal,model_mail_test_activity,base.group_portal,1,0,0,0
|
||||
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_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_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_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
|
||||
access_mail_test_container_mc_portal,mail.test.container.mc.portal,model_mail_test_container_mc,base.group_portal,1,0,0,0
|
||||
access_mail_test_container_mc_user,mail.test.container.mc.user,model_mail_test_container_mc,base.group_user,1,1,1,1
|
||||
access_mail_test_cc_portal,mail.test.cc.portal,model_mail_test_cc,base.group_portal,1,0,0,0
|
||||
access_mail_test_cc_user,mail.test.cc.user,model_mail_test_cc,base.group_user,1,1,1,1
|
||||
access_mail_test_lang_portal,mail.test.lang.portal,model_mail_test_lang,base.group_portal,1,0,0,0
|
||||
access_mail_test_lang_user,mail.test.lang.user,model_mail_test_lang,base.group_user,1,1,1,1
|
||||
access_mail_test_multi_company_user,mail.test.multi.company.user,model_mail_test_multi_company,base.group_user,1,1,1,1
|
||||
access_mail_test_multi_company_portal,mail.test.multi.company.portal,model_mail_test_multi_company,base.group_portal,1,0,0,0
|
||||
access_mail_test_multi_company_read_user,mail.test.multi.company.read.user,model_mail_test_multi_company_read,base.group_user,1,1,1,1
|
||||
access_mail_test_multi_company_read_portal,mail.test.multi.company.read.portal,model_mail_test_multi_company_read,base.group_portal,1,0,0,0
|
||||
access_mail_test_multi_company_with_activity_user,mail.test.multi.company.with.activity.user,model_mail_test_multi_company_with_activity,base.group_user,1,1,1,1
|
||||
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_track_all,mail.test.track.all,model_mail_test_track_all,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_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
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<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"/>
|
||||
<field name="domain_force">[('access', '=', 'public')]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_public'))]"/>
|
||||
</record>
|
||||
<record id="ir_rule_mail_test_access_portal" model="ir.rule">
|
||||
<field name="name">Portal: public/logged/logged readonly only</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_access"/>
|
||||
<field name="domain_force">[
|
||||
'|', ('access', 'in', ('public', 'logged', 'logged_ro')),
|
||||
'&', ('access', '=', 'followers'), ('message_partner_ids', 'in', [user.partner_id.id])]</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_portal_update" model="ir.rule">
|
||||
<field name="name">Portal: update logged only</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_access"/>
|
||||
<field name="domain_force">[
|
||||
'|', ('access', '=', 'logged'),
|
||||
'&', ('access', '=', 'followers'), ('message_partner_ids', 'in', [user.partner_id.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
</record>
|
||||
<record id="ir_rule_mail_test_access_internal" model="ir.rule">
|
||||
<field name="name">Internal: read not admin</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_access"/>
|
||||
<field name="domain_force">[('access', '!=', 'admin')]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<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_internal_update" model="ir.rule">
|
||||
<field name="name">Internal: update not admin and not readonly</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_access"/>
|
||||
<field name="domain_force">[('access', 'not in', ('internal_ro', 'admin'))]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<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_admin" model="ir.rule">
|
||||
<field name="name">Admin: all</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_access"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<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>
|
||||
</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', '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"/>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<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"/>
|
||||
<field eval="True" name="global"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
|
||||
<!-- 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"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
<record id="mail_test_ticket_mc_rule_portal" model="ir.rule">
|
||||
<field name="name">Portal Mail Test Ticket Multi Company</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_ticket_mc"/>
|
||||
<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 -->
|
||||
<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"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
<field name="global" eval="True"/>
|
||||
</record>
|
||||
<record id="mail_test_container_mc_rule_portal" model="ir.rule">
|
||||
<field name="name">Portal Mail Test Container Multi Company</field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_container_mc"/>
|
||||
<field name="domain_force">[('message_partner_ids', 'in', [user.partner_id.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,676 @@
|
|||
/** @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,77 @@
|
|||
/** @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');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
/** @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']);
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/** @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,165 @@
|
|||
/** @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,438 @@
|
|||
/** @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))"
|
||||
);
|
||||
});
|
||||
});
|
||||
24
odoo-bringout-oca-ocb-test_mail/test_mail/tests/__init__.py
Normal file
24
odoo-bringout-oca-ocb-test_mail/test_mail/tests/__init__.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_invite
|
||||
from . import test_ir_actions
|
||||
from . import test_mail_activity
|
||||
from . import test_mail_composer
|
||||
from . import test_mail_composer_mixin
|
||||
from . import test_mail_followers
|
||||
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_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
|
||||
37
odoo-bringout-oca-ocb-test_mail/test_mail/tests/common.py
Normal file
37
odoo-bringout-oca-ocb-test_mail/test_mail/tests/common.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# -*- 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
|
||||
def setUpClass(cls):
|
||||
super(TestRecipients, cls).setUpClass()
|
||||
Partner = cls.env['res.partner'].with_context({
|
||||
'mail_create_nolog': True,
|
||||
'mail_create_nosubscribe': True,
|
||||
'mail_notrack': True,
|
||||
'no_reset_password': True,
|
||||
})
|
||||
cls.partner_1 = Partner.create({
|
||||
'name': 'Valid Lelitre',
|
||||
'email': 'valid.lelitre@agrolait.com',
|
||||
'country_id': cls.env.ref('base.be').id,
|
||||
'mobile': '0456001122',
|
||||
'phone': False,
|
||||
})
|
||||
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,
|
||||
})
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# -*- 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.tests import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('mail_followers')
|
||||
class TestInvite(TestMailCommon):
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_invite_email(self):
|
||||
test_record = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
|
||||
test_partner = self.env['res.partner'].with_context(self._test_context).create({
|
||||
'name': 'Valid Lelitre',
|
||||
'email': 'valid.lelitre@agrolait.com'})
|
||||
|
||||
mail_invite = self.env['mail.wizard.invite'].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()
|
||||
|
||||
# check added followers and that emails were sent
|
||||
self.assertEqual(test_record.message_partner_ids,
|
||||
test_partner | self.user_admin.partner_id)
|
||||
self.assertSentEmail(self.partner_employee, [test_partner])
|
||||
self.assertSentEmail(self.partner_employee, [self.partner_admin])
|
||||
self.assertEqual(len(self._mails), 2)
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 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.tests import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('ir_actions')
|
||||
class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestServerActionsEmail, self).setUp()
|
||||
self.template = self._create_template(
|
||||
'res.partner',
|
||||
{'email_from': '{{ object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted }}',
|
||||
'partner_to': '%s' % self.test_partner.id,
|
||||
}
|
||||
)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
|
||||
def test_action_email(self):
|
||||
# initial state
|
||||
self.assertEqual(len(self.test_partner.message_ids), 1,
|
||||
'Contains Contact created message')
|
||||
self.assertFalse(self.test_partner.message_partner_ids)
|
||||
|
||||
# update action: send an email
|
||||
self.action.write({
|
||||
'mail_post_method': 'email',
|
||||
'state': 'mail_post',
|
||||
'template_id': self.template.id,
|
||||
})
|
||||
self.assertFalse(self.action.mail_post_autofollow, 'Email action does not support autofollow')
|
||||
|
||||
with self.mock_mail_app():
|
||||
self.action.with_context(self.context).run()
|
||||
|
||||
# check an email is waiting for sending
|
||||
mail = self.env['mail.mail'].sudo().search([('subject', '=', 'About TestingPartner')])
|
||||
self.assertEqual(len(mail), 1)
|
||||
self.assertTrue(mail.auto_delete)
|
||||
self.assertEqual(mail.body_html, '<p>Hello TestingPartner</p>')
|
||||
self.assertFalse(mail.is_notification)
|
||||
with self.mock_mail_gateway(mail_unlink_sent=True):
|
||||
mail.send()
|
||||
|
||||
# no archive (message)
|
||||
self.assertEqual(len(self.test_partner.message_ids), 1,
|
||||
'Contains Contact created message')
|
||||
self.assertFalse(self.test_partner.message_partner_ids)
|
||||
|
||||
def test_action_followers(self):
|
||||
self.test_partner.message_unsubscribe(self.test_partner.message_partner_ids.ids)
|
||||
random_partner = self.env['res.partner'].create({'name': 'Thierry Wololo'})
|
||||
self.action.write({
|
||||
'state': 'followers',
|
||||
'partner_ids': [(4, self.env.ref('base.partner_admin').id), (4, random_partner.id)],
|
||||
})
|
||||
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_message_post(self):
|
||||
# initial state
|
||||
self.assertEqual(len(self.test_partner.message_ids), 1,
|
||||
'Contains Contact created message')
|
||||
self.assertFalse(self.test_partner.message_partner_ids)
|
||||
|
||||
# test without autofollow and comment
|
||||
self.action.write({
|
||||
'mail_post_autofollow': False,
|
||||
'mail_post_method': 'comment',
|
||||
'state': 'mail_post',
|
||||
'template_id': self.template.id
|
||||
})
|
||||
|
||||
with self.assertSinglePostNotifications(
|
||||
[{'partner': self.test_partner, 'type': 'email', 'status': 'ready'}],
|
||||
message_info={'content': 'Hello %s' % self.test_partner.name,
|
||||
'message_type': 'notification',
|
||||
'subtype': 'mail.mt_comment',
|
||||
}
|
||||
):
|
||||
self.action.with_context(self.context).run()
|
||||
# NOTE: template using current user will have funny email_from
|
||||
self.assertEqual(self.test_partner.message_ids[0].email_from, self.partner_root.email_formatted)
|
||||
self.assertFalse(self.test_partner.message_partner_ids)
|
||||
|
||||
# test with autofollow and note
|
||||
self.action.write({
|
||||
'mail_post_autofollow': True,
|
||||
'mail_post_method': 'note'
|
||||
})
|
||||
with self.assertSinglePostNotifications(
|
||||
[{'partner': self.test_partner, 'type': 'email', 'status': 'ready'}],
|
||||
message_info={'content': 'Hello %s' % self.test_partner.name,
|
||||
'message_type': 'notification',
|
||||
'subtype': 'mail.mt_note',
|
||||
}
|
||||
):
|
||||
self.action.with_context(self.context).run()
|
||||
self.assertEqual(len(self.test_partner.message_ids), 3,
|
||||
'2 new messages produced')
|
||||
self.assertEqual(self.test_partner.message_partner_ids, self.test_partner)
|
||||
|
||||
def test_action_next_activity(self):
|
||||
self.action.write({
|
||||
'state': 'next_activity',
|
||||
'activity_user_type': 'specific',
|
||||
'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.assertEqual(self.env['mail.activity'].search_count([('summary', '=', 'TestNew')]), 1)
|
||||
|
||||
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({
|
||||
'state': 'next_activity',
|
||||
'activity_user_type': 'specific',
|
||||
'activity_type_id': self.env.ref('mail.mail_activity_data_meeting').id,
|
||||
'activity_summary': 'TestNew',
|
||||
'activity_date_deadline_range': 1,
|
||||
'activity_date_deadline_range_type': False,
|
||||
})
|
||||
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.assertEqual(self.env['mail.activity'].search_count([('summary', '=', 'TestNew')]), 1)
|
||||
|
|
@ -0,0 +1,762 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import date, datetime, timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from freezegun import freeze_time
|
||||
from psycopg2 import IntegrityError
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import DEFAULT
|
||||
|
||||
import pytz
|
||||
|
||||
from odoo import fields, exceptions, tests
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon
|
||||
from odoo.addons.test_mail.models.test_mail_models import MailTestActivity
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tests.common import Form, users
|
||||
|
||||
|
||||
class TestActivityCommon(TestMailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestActivityCommon, cls).setUpClass()
|
||||
cls.test_record = cls.env['mail.test.activity'].with_context(cls._test_context).create({'name': 'Test'})
|
||||
# reset ctx
|
||||
cls._reset_mail_context(cls.test_record)
|
||||
|
||||
|
||||
@tests.tagged('mail_activity')
|
||||
class TestActivityRights(TestActivityCommon):
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_activity_security_user_access_other(self):
|
||||
activity = self.test_record.with_user(self.user_employee).activity_schedule(
|
||||
'test_mail.mail_act_test_todo',
|
||||
user_id=self.user_admin.id)
|
||||
self.assertTrue(activity.can_write)
|
||||
activity.write({'user_id': self.user_employee.id})
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_activity_security_user_access_own(self):
|
||||
activity = self.test_record.with_user(self.user_employee).activity_schedule(
|
||||
'test_mail.mail_act_test_todo')
|
||||
self.assertTrue(activity.can_write)
|
||||
activity.write({'user_id': self.user_admin.id})
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_activity_security_user_noaccess_automated(self):
|
||||
def _employee_crash(*args, **kwargs):
|
||||
""" If employee is test employee, consider they have no access on document """
|
||||
recordset = args[0]
|
||||
if recordset.env.uid == self.user_employee.id:
|
||||
raise exceptions.AccessError('Hop hop hop Ernest, please step back.')
|
||||
return DEFAULT
|
||||
|
||||
with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash):
|
||||
activity = self.test_record.activity_schedule(
|
||||
'test_mail.mail_act_test_todo',
|
||||
user_id=self.user_employee.id)
|
||||
|
||||
activity2 = self.test_record.activity_schedule('test_mail.mail_act_test_todo')
|
||||
activity2.write({'user_id': self.user_employee.id})
|
||||
|
||||
def test_activity_security_user_noaccess_manual(self):
|
||||
def _employee_crash(*args, **kwargs):
|
||||
""" If employee is test employee, consider they have no access on document """
|
||||
recordset = args[0]
|
||||
if recordset.env.uid == self.user_employee.id:
|
||||
raise exceptions.AccessError('Hop hop hop Ernest, please step back.')
|
||||
return DEFAULT
|
||||
|
||||
test_activity = self.env['mail.activity'].with_user(self.user_admin).create({
|
||||
'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,
|
||||
'user_id': self.user_admin.id,
|
||||
'summary': 'Summary',
|
||||
})
|
||||
test_activity.flush_recordset()
|
||||
|
||||
# can _search activities if access to the document
|
||||
self.env['mail.activity'].with_user(self.user_employee)._search(
|
||||
[('id', '=', test_activity.id)], count=False)
|
||||
|
||||
# cannot _search activities if no access to the document
|
||||
with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash):
|
||||
with self.assertRaises(exceptions.AccessError):
|
||||
searched_activity = self.env['mail.activity'].with_user(self.user_employee)._search(
|
||||
[('id', '=', test_activity.id)], count=False)
|
||||
|
||||
# can read_group activities if access to the document
|
||||
read_group_result = self.env['mail.activity'].with_user(self.user_employee).read_group(
|
||||
[('id', '=', test_activity.id)],
|
||||
['summary'],
|
||||
['summary'],
|
||||
)
|
||||
self.assertEqual(1, read_group_result[0]['summary_count'])
|
||||
self.assertEqual('Summary', read_group_result[0]['summary'])
|
||||
|
||||
# cannot read_group activities if no access to the document
|
||||
with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash):
|
||||
with self.assertRaises(exceptions.AccessError):
|
||||
self.env['mail.activity'].with_user(self.user_employee).read_group(
|
||||
[('id', '=', test_activity.id)],
|
||||
['summary'],
|
||||
['summary'],
|
||||
)
|
||||
|
||||
# cannot read activities if no access to the document
|
||||
with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash):
|
||||
with self.assertRaises(exceptions.AccessError):
|
||||
searched_activity = self.env['mail.activity'].with_user(self.user_employee).search(
|
||||
[('id', '=', test_activity.id)])
|
||||
searched_activity.read(['summary'])
|
||||
|
||||
# cannot search_read activities if no access to the document
|
||||
with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash):
|
||||
with self.assertRaises(exceptions.AccessError):
|
||||
self.env['mail.activity'].with_user(self.user_employee).search_read(
|
||||
[('id', '=', test_activity.id)],
|
||||
['summary'])
|
||||
|
||||
# cannot create activities for people that cannot access record
|
||||
with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash):
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
activity = self.env['mail.activity'].create({
|
||||
'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,
|
||||
'user_id': self.user_employee.id,
|
||||
})
|
||||
|
||||
# cannot create activities if no access to the document
|
||||
with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash):
|
||||
with self.assertRaises(exceptions.AccessError):
|
||||
activity = self.test_record.with_user(self.user_employee).activity_schedule(
|
||||
'test_mail.mail_act_test_todo',
|
||||
user_id=self.user_admin.id)
|
||||
|
||||
|
||||
@tests.tagged('mail_activity')
|
||||
class TestActivityFlow(TestActivityCommon):
|
||||
|
||||
def test_activity_flow_employee(self):
|
||||
with self.with_user('employee'):
|
||||
test_record = self.env['mail.test.activity'].browse(self.test_record.id)
|
||||
self.assertEqual(test_record.activity_ids, self.env['mail.activity'])
|
||||
|
||||
# employee record an activity and check the deadline
|
||||
self.env['mail.activity'].create({
|
||||
'summary': 'Test Activity',
|
||||
'date_deadline': date.today() + relativedelta(days=1),
|
||||
'activity_type_id': self.env.ref('mail.mail_activity_data_email').id,
|
||||
'res_model_id': self.env['ir.model']._get(test_record._name).id,
|
||||
'res_id': test_record.id,
|
||||
})
|
||||
self.assertEqual(test_record.activity_summary, 'Test Activity')
|
||||
self.assertEqual(test_record.activity_state, 'planned')
|
||||
|
||||
test_record.activity_ids.write({'date_deadline': date.today() - relativedelta(days=1)})
|
||||
self.assertEqual(test_record.activity_state, 'overdue')
|
||||
|
||||
test_record.activity_ids.write({'date_deadline': date.today()})
|
||||
self.assertEqual(test_record.activity_state, 'today')
|
||||
|
||||
# activity is done
|
||||
test_record.activity_ids.action_feedback(feedback='So much feedback')
|
||||
self.assertEqual(test_record.activity_ids, self.env['mail.activity'])
|
||||
self.assertEqual(test_record.message_ids[0].subtype_id, self.env.ref('mail.mt_activities'))
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_activity_notify_other_user(self):
|
||||
self.user_admin.notification_type = 'email'
|
||||
rec = self.test_record.with_user(self.user_employee)
|
||||
with self.assertSinglePostNotifications(
|
||||
[{'partner': self.partner_admin, 'type': 'email'}],
|
||||
message_info={'content': 'assigned you the following activity', 'subtype': 'mail.mt_note', 'message_type': 'user_notification'}):
|
||||
activity = rec.activity_schedule(
|
||||
'test_mail.mail_act_test_todo',
|
||||
user_id=self.user_admin.id)
|
||||
self.assertEqual(activity.create_uid, self.user_employee)
|
||||
self.assertEqual(activity.user_id, self.user_admin)
|
||||
|
||||
def test_activity_notify_same_user(self):
|
||||
self.user_employee.notification_type = 'email'
|
||||
rec = self.test_record.with_user(self.user_employee)
|
||||
with self.assertNoNotifications():
|
||||
activity = rec.activity_schedule(
|
||||
'test_mail.mail_act_test_todo',
|
||||
user_id=self.user_employee.id)
|
||||
self.assertEqual(activity.create_uid, self.user_employee)
|
||||
self.assertEqual(activity.user_id, self.user_employee)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_activity_dont_notify_no_user_change(self):
|
||||
self.user_employee.notification_type = 'email'
|
||||
activity = self.test_record.activity_schedule('test_mail.mail_act_test_todo', user_id=self.user_employee.id)
|
||||
with self.assertNoNotifications():
|
||||
activity.with_user(self.user_admin).write({'user_id': self.user_employee.id})
|
||||
self.assertEqual(activity.user_id, self.user_employee)
|
||||
|
||||
def test_activity_summary_sync(self):
|
||||
""" Test summary from type is copied on activities if set (currently only in form-based onchange) """
|
||||
ActivityType = self.env['mail.activity.type']
|
||||
email_activity_type = ActivityType.create({
|
||||
'name': 'email',
|
||||
'summary': 'Email Summary',
|
||||
})
|
||||
call_activity_type = ActivityType.create({'name': 'call'})
|
||||
with Form(self.env['mail.activity'].with_context(default_res_model_id=self.env['ir.model']._get_id('mail.test.activity'), default_res_id=self.test_record.id)) as ActivityForm:
|
||||
# `res_model_id` and `res_id` are invisible, see view `mail.mail_activity_view_form_popup`
|
||||
# they must be set using defaults, see `action_feedback_schedule_next`
|
||||
ActivityForm.activity_type_id = call_activity_type
|
||||
# activity summary should be empty
|
||||
self.assertEqual(ActivityForm.summary, False)
|
||||
|
||||
ActivityForm.activity_type_id = email_activity_type
|
||||
# activity summary should be replaced with email's default summary
|
||||
self.assertEqual(ActivityForm.summary, email_activity_type.summary)
|
||||
|
||||
ActivityForm.activity_type_id = call_activity_type
|
||||
# activity summary remains unchanged from change of activity type as call activity doesn't have default summary
|
||||
self.assertEqual(ActivityForm.summary, email_activity_type.summary)
|
||||
|
||||
@mute_logger('odoo.sql_db')
|
||||
def test_activity_values(self):
|
||||
""" Test activities are created with right model / res_id values linking
|
||||
to records without void values. 0 as res_id especially is not wanted. """
|
||||
# creating activities on a temporary record generates activities with res_id
|
||||
# being 0, which is annoying -> never create activities in transient mode
|
||||
temp_record = self.env['mail.test.activity'].new({'name': 'Test'})
|
||||
with self.assertRaises(IntegrityError):
|
||||
activity = temp_record.activity_schedule('test_mail.mail_act_test_todo', user_id=self.user_employee.id)
|
||||
|
||||
test_record = self.env['mail.test.activity'].browse(self.test_record.ids)
|
||||
|
||||
with self.assertRaises(IntegrityError):
|
||||
self.env['mail.activity'].create({
|
||||
'res_model_id': self.env['ir.model']._get_id(test_record._name),
|
||||
})
|
||||
with self.assertRaises(IntegrityError):
|
||||
self.env['mail.activity'].create({
|
||||
'res_model_id': self.env['ir.model']._get_id(test_record._name),
|
||||
'res_id': False,
|
||||
})
|
||||
with self.assertRaises(IntegrityError):
|
||||
self.env['mail.activity'].create({
|
||||
'res_id': test_record.id,
|
||||
})
|
||||
|
||||
activity = self.env['mail.activity'].create({
|
||||
'res_id': test_record.id,
|
||||
'res_model_id': self.env['ir.model']._get_id(test_record._name),
|
||||
})
|
||||
with self.assertRaises(IntegrityError):
|
||||
activity.write({'res_model_id': False})
|
||||
self.env.flush_all()
|
||||
with self.assertRaises(IntegrityError):
|
||||
activity.write({'res_id': False})
|
||||
self.env.flush_all()
|
||||
with self.assertRaises(IntegrityError):
|
||||
activity.write({'res_id': 0})
|
||||
self.env.flush_all()
|
||||
|
||||
|
||||
@tests.tagged('mail_activity')
|
||||
class TestActivityMixin(TestActivityCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestActivityMixin, cls).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(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))
|
||||
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',)
|
||||
self.assertEqual(self.test_record.activity_ids, act2 | act3)
|
||||
|
||||
# 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')
|
||||
|
||||
# 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), 2)
|
||||
self.assertEqual(self.test_record.message_ids.mapped('subtype_id'), self.env.ref('mail.mt_activities'))
|
||||
|
||||
# Perform 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), 2)
|
||||
|
||||
@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.toggle_active()
|
||||
self.assertEqual(rec.active, False)
|
||||
self.assertEqual(rec.activity_ids, self.env['mail.activity'])
|
||||
rec.toggle_active()
|
||||
self.assertEqual(rec.active, True)
|
||||
self.assertEqual(rec.activity_ids, self.env['mail.activity'])
|
||||
|
||||
@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[-1]
|
||||
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.exists())
|
||||
|
||||
# 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.exists())
|
||||
|
||||
# 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
|
||||
"""
|
||||
today_utc = datetime(2020, 1, 1, 16, 0, 0)
|
||||
|
||||
class MockedDatetime(datetime):
|
||||
@classmethod
|
||||
def utcnow(cls):
|
||||
return today_utc
|
||||
|
||||
record = self.env['mail.test.activity'].create({'name': 'Record'})
|
||||
|
||||
with patch('odoo.addons.mail.models.mail_activity.datetime', MockedDatetime):
|
||||
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')
|
||||
|
||||
@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.
|
||||
"""
|
||||
today_utc = datetime(2020, 1, 1, 16, 0, 0)
|
||||
|
||||
class MockedDatetime(datetime):
|
||||
@classmethod
|
||||
def utcnow(cls):
|
||||
return today_utc
|
||||
|
||||
# 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)
|
||||
|
||||
with patch('odoo.addons.mail.models.mail_activity.datetime', MockedDatetime), \
|
||||
patch('odoo.addons.mail.models.mail_activity_mixin.datetime', MockedDatetime):
|
||||
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,
|
||||
'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': 1,
|
||||
'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 not in ('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)))
|
||||
|
||||
@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.
|
||||
"""
|
||||
today_utc = datetime(2020, 1, 1, 23, 0, 0)
|
||||
|
||||
class MockedDatetime(datetime):
|
||||
@classmethod
|
||||
def utcnow(cls):
|
||||
return today_utc
|
||||
|
||||
# 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 patch('odoo.addons.mail.models.mail_activity.datetime', MockedDatetime):
|
||||
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'})
|
||||
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)
|
||||
|
||||
|
||||
@tests.tagged('mail_activity')
|
||||
class TestORM(TestActivityCommon):
|
||||
"""Test for read_progress_bar"""
|
||||
|
||||
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
|
||||
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),
|
||||
)
|
||||
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),
|
||||
)
|
||||
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),
|
||||
)
|
||||
|
||||
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.read_group(domain, fields=['date'], 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], pg_groups["overdue"])
|
||||
self.assertEqual(groups[1][groupby], pg_groups["today"])
|
||||
self.assertEqual(groups[2][groupby], pg_groups["planned"])
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,113 @@
|
|||
# -*- 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.tests import tagged
|
||||
from odoo.tests.common import users
|
||||
|
||||
|
||||
@tagged('mail_composer_mixin')
|
||||
class TestMailComposerMixin(TestMailCommon, 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)],
|
||||
})
|
||||
|
||||
cls.mail_template = cls.env['mail.template'].create({
|
||||
'body_html': '<p>EnglishBody for <t t-out="object.name"/></p>',
|
||||
'model_id': cls.env['ir.model']._get('mail.test.composer.source').id,
|
||||
'name': 'Test Template for mail.test.composer.source',
|
||||
'lang': '{{ object.customer_id.lang }}',
|
||||
'subject': 'EnglishSubject for {{ object.name }}',
|
||||
})
|
||||
cls.test_record = cls.env['mail.test.composer.source'].create({
|
||||
'name': cls.partner_1.name,
|
||||
'customer_id': cls.partner_1.id,
|
||||
})
|
||||
|
||||
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',
|
||||
test_record=cls.test_record,
|
||||
test_template=cls.mail_template,
|
||||
)
|
||||
|
||||
@users("employee")
|
||||
def test_content_sync(self):
|
||||
""" Test updating template updates the dynamic fields accordingly. """
|
||||
source = self.test_record.with_env(self.env)
|
||||
composer = self.env['mail.test.composer.mixin'].create({
|
||||
'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>')
|
||||
|
||||
@users("employee")
|
||||
def test_rendering_custom(self):
|
||||
""" Test rendering with custom strings (not coming from template) """
|
||||
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>',
|
||||
'body': '<p>SpecificBody from <t t-out="user.name"/></p>',
|
||||
'name': 'Invite',
|
||||
'subject': 'SpecificSubject for {{ object.name }}',
|
||||
})
|
||||
self.assertEqual(composer.body, '<p>SpecificBody from <t t-out="user.name"/></p>')
|
||||
self.assertEqual(composer.subject, 'SpecificSubject for {{ object.name }}')
|
||||
|
||||
subject = composer._render_field('subject', source.ids)[source.id]
|
||||
self.assertEqual(subject, f'SpecificSubject for {source.name}')
|
||||
body = composer._render_field('body', source.ids)[source.id]
|
||||
self.assertEqual(body, f'<p>SpecificBody from {self.env.user.name}</p>')
|
||||
description = composer._render_field('description', source.ids)[source.id]
|
||||
self.assertEqual(description, f'<p>Description for {source.name}</p>')
|
||||
|
||||
@users("employee")
|
||||
def test_rendering_lang(self):
|
||||
""" Test rendering with language involved """
|
||||
template = self.mail_template.with_env(self.env)
|
||||
customer = self.partner_1.with_env(self.env)
|
||||
customer.lang = 'es_ES'
|
||||
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)],
|
||||
})
|
||||
self.assertEqual(composer.body, template.body_html)
|
||||
self.assertEqual(composer.subject, template.subject)
|
||||
self.assertEqual(composer.lang, '{{ object.customer_id.lang }}')
|
||||
|
||||
# do not specifically ask for language computation
|
||||
subject = composer._render_field('subject', source.ids, compute_lang=False)[source.id]
|
||||
self.assertEqual(subject, f'EnglishSubject for {source.name}')
|
||||
body = composer._render_field('body', source.ids, compute_lang=False)[source.id]
|
||||
self.assertEqual(body, f'<p>EnglishBody for {source.name}</p>')
|
||||
description = composer._render_field('description', source.ids)[source.id]
|
||||
self.assertEqual(description, f'<p>Description for {source.name}</p>')
|
||||
|
||||
# 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')
|
||||
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'
|
||||
)
|
||||
description = composer._render_field('description', source.ids)[source.id]
|
||||
self.assertEqual(description, f'<p>Description for {source.name}</p>')
|
||||
|
|
@ -0,0 +1,790 @@
|
|||
# -*- 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.exceptions import AccessError
|
||||
from odoo.tests import tagged, users
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('mail_followers')
|
||||
class BaseFollowersTest(TestMailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(BaseFollowersTest, cls).setUpClass()
|
||||
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})
|
||||
cls.mt_al_nodef = Subtype.create({'name': 'mt_al_nodef', 'default': False, 'res_model': False})
|
||||
# mail.test.simple
|
||||
cls.mt_mg_def = Subtype.create({'name': 'mt_mg_def', 'default': True, 'res_model': 'mail.test.simple'})
|
||||
cls.mt_mg_nodef = Subtype.create({'name': 'mt_mg_nodef', 'default': False, 'res_model': 'mail.test.simple'})
|
||||
cls.mt_mg_def_int = Subtype.create({'name': 'mt_mg_def', 'default': True, 'res_model': 'mail.test.simple', 'internal': True})
|
||||
# mail.test.container
|
||||
cls.mt_cl_def = Subtype.create({'name': 'mt_cl_def', 'default': True, 'res_model': 'mail.test.container'})
|
||||
|
||||
cls.default_group_subtypes = Subtype.search([('default', '=', True), '|', ('res_model', '=', 'mail.test.simple'), ('res_model', '=', False)])
|
||||
cls.default_group_subtypes_portal = Subtype.search([('internal', '=', False), ('default', '=', True), '|', ('res_model', '=', 'mail.test.simple'), ('res_model', '=', False)])
|
||||
|
||||
def test_field_message_is_follower(self):
|
||||
test_record = self.test_record.with_user(self.user_employee)
|
||||
followed_before = test_record.search([('message_is_follower', '=', True)])
|
||||
self.assertFalse(test_record.message_is_follower)
|
||||
test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id])
|
||||
followed_after = test_record.search([('message_is_follower', '=', True)])
|
||||
self.assertTrue(test_record.message_is_follower)
|
||||
self.assertEqual(followed_before | test_record, followed_after)
|
||||
|
||||
def test_field_message_partner_ids(self):
|
||||
test_record = self.test_record.with_user(self.user_employee)
|
||||
partner = self.user_employee.partner_id
|
||||
followed_before = self.env['mail.test.simple'].search([('message_partner_ids', 'in', partner.ids)])
|
||||
self.assertFalse(partner in test_record.message_partner_ids)
|
||||
self.assertNotIn(test_record, followed_before)
|
||||
test_record.message_subscribe(partner_ids=[partner.id])
|
||||
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)
|
||||
|
||||
def test_field_followers(self):
|
||||
test_record = self.test_record.with_user(self.user_employee)
|
||||
test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id, self.user_admin.partner_id.id])
|
||||
followers = self.env['mail.followers'].search([
|
||||
('res_model', '=', 'mail.test.simple'),
|
||||
('res_id', '=', test_record.id)])
|
||||
self.assertEqual(followers, test_record.message_follower_ids)
|
||||
self.assertEqual(test_record.message_partner_ids, self.user_employee.partner_id | self.user_admin.partner_id)
|
||||
|
||||
def test_followers_subtypes_default(self):
|
||||
test_record = self.test_record.with_user(self.user_employee)
|
||||
test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id])
|
||||
self.assertEqual(test_record.message_partner_ids, self.user_employee.partner_id)
|
||||
follower = self.env['mail.followers'].search([
|
||||
('res_model', '=', 'mail.test.simple'),
|
||||
('res_id', '=', test_record.id),
|
||||
('partner_id', '=', self.user_employee.partner_id.id)])
|
||||
self.assertEqual(follower, test_record.message_follower_ids)
|
||||
self.assertEqual(follower.subtype_ids, self.default_group_subtypes)
|
||||
|
||||
def test_followers_subtypes_default_internal(self):
|
||||
test_record = self.test_record.with_user(self.user_employee)
|
||||
test_record.message_subscribe(partner_ids=[self.partner_portal.id])
|
||||
self.assertEqual(test_record.message_partner_ids, self.partner_portal)
|
||||
follower = self.env['mail.followers'].search([
|
||||
('res_model', '=', 'mail.test.simple'),
|
||||
('res_id', '=', test_record.id),
|
||||
('partner_id', '=', self.partner_portal.id)])
|
||||
self.assertEqual(follower.subtype_ids, self.default_group_subtypes_portal)
|
||||
|
||||
def test_followers_subtypes_specified(self):
|
||||
test_record = self.test_record.with_user(self.user_employee)
|
||||
test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id], subtype_ids=[self.mt_mg_nodef.id])
|
||||
self.assertEqual(test_record.message_partner_ids, self.user_employee.partner_id)
|
||||
follower = self.env['mail.followers'].search([
|
||||
('res_model', '=', 'mail.test.simple'),
|
||||
('res_id', '=', test_record.id),
|
||||
('partner_id', '=', self.user_employee.partner_id.id)])
|
||||
self.assertEqual(follower, test_record.message_follower_ids)
|
||||
self.assertEqual(follower.subtype_ids, self.mt_mg_nodef)
|
||||
|
||||
def test_followers_multiple_subscription_force(self):
|
||||
test_record = self.test_record.with_user(self.user_employee)
|
||||
|
||||
test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id], subtype_ids=[self.mt_mg_nodef.id])
|
||||
self.assertEqual(test_record.message_partner_ids, self.user_admin.partner_id)
|
||||
self.assertEqual(test_record.message_follower_ids.subtype_ids, self.mt_mg_nodef)
|
||||
|
||||
test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id], subtype_ids=[self.mt_mg_nodef.id, self.mt_al_nodef.id])
|
||||
self.assertEqual(test_record.message_partner_ids, self.user_admin.partner_id)
|
||||
self.assertEqual(test_record.message_follower_ids.subtype_ids, self.mt_mg_nodef | self.mt_al_nodef)
|
||||
|
||||
def test_followers_multiple_subscription_noforce(self):
|
||||
""" Calling message_subscribe without subtypes on an existing subscription should not do anything (default < existing) """
|
||||
test_record = self.test_record.with_user(self.user_employee)
|
||||
|
||||
test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id], subtype_ids=[self.mt_mg_nodef.id, self.mt_al_nodef.id])
|
||||
self.assertEqual(test_record.message_partner_ids, self.user_admin.partner_id)
|
||||
self.assertEqual(test_record.message_follower_ids.subtype_ids, self.mt_mg_nodef | self.mt_al_nodef)
|
||||
|
||||
# set new subtypes with force=False, meaning no rewriting of the subscription is done -> result should not change
|
||||
test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id])
|
||||
self.assertEqual(test_record.message_partner_ids, self.user_admin.partner_id)
|
||||
self.assertEqual(test_record.message_follower_ids.subtype_ids, self.mt_mg_nodef | self.mt_al_nodef)
|
||||
|
||||
def test_followers_multiple_subscription_update(self):
|
||||
""" Calling message_subscribe with subtypes on an existing subscription should replace them (new > existing) """
|
||||
test_record = self.test_record.with_user(self.user_employee)
|
||||
test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id], subtype_ids=[self.mt_mg_def.id, self.mt_cl_def.id])
|
||||
self.assertEqual(test_record.message_partner_ids, self.user_employee.partner_id)
|
||||
follower = self.env['mail.followers'].search([
|
||||
('res_model', '=', 'mail.test.simple'),
|
||||
('res_id', '=', test_record.id),
|
||||
('partner_id', '=', self.user_employee.partner_id.id)])
|
||||
self.assertEqual(follower, test_record.message_follower_ids)
|
||||
self.assertEqual(follower.subtype_ids, self.mt_mg_def | self.mt_cl_def)
|
||||
|
||||
# remove one subtype `mt_mg_def` and set new subtype `mt_al_def`
|
||||
test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id], subtype_ids=[self.mt_cl_def.id, self.mt_al_def.id])
|
||||
self.assertEqual(follower.subtype_ids, self.mt_cl_def | self.mt_al_def)
|
||||
|
||||
@users('employee')
|
||||
def test_followers_inactive(self):
|
||||
""" Test standard API does not subscribe inactive partners """
|
||||
customer = self.env['res.partner'].create({
|
||||
'name': 'Valid Lelitre',
|
||||
'email': 'valid.lelitre@agrolait.com',
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
'mobile': '0456001122',
|
||||
'active': False,
|
||||
})
|
||||
document = self.env['mail.test.simple'].browse(self.test_record.id)
|
||||
self.assertEqual(document.message_partner_ids, self.env['res.partner'])
|
||||
document.message_subscribe(partner_ids=(self.partner_portal | customer).ids)
|
||||
self.assertEqual(document.message_partner_ids, self.partner_portal)
|
||||
self.assertEqual(document.message_follower_ids.partner_id, self.partner_portal)
|
||||
|
||||
# works through low-level API
|
||||
document._message_subscribe(partner_ids=(self.partner_portal | customer).ids)
|
||||
self.assertEqual(document.message_partner_ids, self.partner_portal, 'No active test: customer not visible')
|
||||
self.assertEqual(document.message_follower_ids.partner_id, self.partner_portal | customer)
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_followers_inverse_message_partner(self):
|
||||
test_record = self.test_record.with_env(self.env)
|
||||
partner0, partner1, partner2, partner3 = self.env['res.partner'].create(
|
||||
[{'email': f'partner.{n}@test.lan', 'name': f'partner{n}'} for n in range(4)]
|
||||
)
|
||||
self.assertFalse(test_record.message_follower_ids)
|
||||
self.assertFalse(test_record.message_partner_ids)
|
||||
|
||||
# fillup with API
|
||||
test_record.message_subscribe(partner_ids=partner3.ids)
|
||||
self.assertEqual(test_record.message_follower_ids.partner_id, partner3)
|
||||
# set empty
|
||||
test_record.message_partner_ids = None
|
||||
self.assertFalse(test_record.message_follower_ids.partner_id)
|
||||
# set 1
|
||||
test_record.message_partner_ids = partner0
|
||||
self.assertEqual(test_record.message_follower_ids.partner_id, partner0)
|
||||
# set multiple when non-empty
|
||||
test_record.message_partner_ids = partner1 + partner2
|
||||
self.assertEqual(test_record.message_follower_ids.partner_id, partner1 + partner2)
|
||||
# remove 1
|
||||
test_record.message_partner_ids -= partner1
|
||||
self.assertEqual(test_record.message_follower_ids.partner_id, partner2)
|
||||
# add multiple with one already set
|
||||
test_record.message_partner_ids += partner1 + partner2
|
||||
self.assertEqual(test_record.message_follower_ids.partner_id, partner1 + partner2)
|
||||
# remove outside of existing
|
||||
test_record.message_partner_ids -= partner3
|
||||
self.assertEqual(test_record.message_follower_ids.partner_id, partner1 + partner2)
|
||||
# reset
|
||||
test_record.message_partner_ids = False
|
||||
self.assertFalse(test_record.message_follower_ids.partner_id)
|
||||
|
||||
# test with inactive and commands
|
||||
partner0.write({'active': False})
|
||||
test_record.write({'message_partner_ids': [(4, partner0.id), (4, partner1.id)]})
|
||||
self.assertEqual(test_record.message_follower_ids.partner_id, partner1)
|
||||
|
||||
@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
|
||||
instead of a follower """
|
||||
test_record = self.test_record.with_user(self.user_portal)
|
||||
partner0 = self.env['res.partner'].create({
|
||||
'email': 'partner1@test.lan',
|
||||
'name': 'partner1',
|
||||
})
|
||||
_name = test_record.name # check portal user can read
|
||||
|
||||
# set empty
|
||||
with self.assertRaises(AccessError):
|
||||
test_record.message_partner_ids = None
|
||||
# set 1
|
||||
with self.assertRaises(AccessError):
|
||||
test_record.message_partner_ids = partner0
|
||||
# remove 1
|
||||
with self.assertRaises(AccessError):
|
||||
test_record.message_partner_ids -= partner0
|
||||
|
||||
@users('employee')
|
||||
def test_followers_private_address(self):
|
||||
""" Test standard API does not subscribe private addresses """
|
||||
private_address = self.env['res.partner'].sudo().create({
|
||||
'name': 'Private Address',
|
||||
'type': 'private',
|
||||
})
|
||||
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)
|
||||
|
||||
# works through low-level API
|
||||
document._message_subscribe(partner_ids=(self.partner_portal | private_address).ids)
|
||||
self.assertEqual(document.message_follower_ids.partner_id, self.partner_portal | private_address)
|
||||
|
||||
@users('employee')
|
||||
def test_create_multi_followers(self):
|
||||
documents = self.env['mail.test.simple'].create([{'name': 'ninja'}] * 5)
|
||||
for document in documents:
|
||||
self.assertEqual(document.message_follower_ids.partner_id, self.env.user.partner_id)
|
||||
self.assertEqual(document.message_follower_ids.subtype_ids, self.default_group_subtypes)
|
||||
|
||||
@users('employee')
|
||||
def test_subscriptions_data_fetch(self):
|
||||
""" Test that _get_subscription_data gives correct values when modifying followers manually."""
|
||||
test_record = self.test_record
|
||||
test_record_copy = self.test_record.copy()
|
||||
test_records = test_record + test_record_copy
|
||||
test_record.message_subscribe([self.user_employee.partner_id.id])
|
||||
subscription_data = self.env['mail.followers']._get_subscription_data([(test_records._name, test_records.ids)], None)
|
||||
self.assertEqual(len(subscription_data), 1)
|
||||
self.assertEqual(subscription_data[0][1], test_record.id)
|
||||
self.env['mail.followers'].browse(subscription_data[0][0]).sudo().res_id = test_record_copy
|
||||
subscription_data = self.env['mail.followers']._get_subscription_data([(test_records._name, test_records.ids)], None)
|
||||
self.assertEqual(len(subscription_data), 1)
|
||||
self.assertEqual(subscription_data[0][1], test_record_copy.id)
|
||||
|
||||
|
||||
@tagged('mail_followers')
|
||||
class AdvancedFollowersTest(TestMailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(AdvancedFollowersTest, cls).setUpClass()
|
||||
cls._create_portal_user()
|
||||
|
||||
cls.test_track = cls.env['mail.test.track'].with_user(cls.user_employee).create({
|
||||
'name': 'Test',
|
||||
})
|
||||
|
||||
Subtype = cls.env['mail.message.subtype']
|
||||
|
||||
# clean demo data to avoid interferences
|
||||
Subtype.search([('res_model', 'in', ['mail.test.container', 'mail.test.track'])]).unlink()
|
||||
|
||||
# mail.test.track subtypes (aka: task records)
|
||||
cls.sub_track_1 = Subtype.create({
|
||||
'name': 'Track (with child relation) 1', 'default': False,
|
||||
'res_model': 'mail.test.track'
|
||||
})
|
||||
cls.sub_track_2 = Subtype.create({
|
||||
'name': 'Track (with child relation) 2', 'default': False,
|
||||
'res_model': 'mail.test.track'
|
||||
})
|
||||
cls.sub_track_nodef = Subtype.create({
|
||||
'name': 'Generic Track subtype', 'default': False, 'internal': False,
|
||||
'res_model': 'mail.test.track'
|
||||
})
|
||||
cls.sub_track_def = Subtype.create({
|
||||
'name': 'Default track subtype', 'default': True, 'internal': False,
|
||||
'res_model': 'mail.test.track'
|
||||
})
|
||||
|
||||
# mail.test.container subtypes (aka: project records)
|
||||
cls.umb_nodef = Subtype.create({
|
||||
'name': 'Container NoDefault', 'default': False,
|
||||
'res_model': 'mail.test.container'
|
||||
})
|
||||
cls.umb_def = Subtype.create({
|
||||
'name': 'Container Default', 'default': True,
|
||||
'res_model': 'mail.test.container'
|
||||
})
|
||||
cls.umb_def_int = Subtype.create({
|
||||
'name': 'Container Default', 'default': True, 'internal': True,
|
||||
'res_model': 'mail.test.container'
|
||||
})
|
||||
# -> subtypes for auto subscription from container to sub records
|
||||
cls.umb_autosub_def = Subtype.create({
|
||||
'name': 'Container AutoSub (default)', 'default': True, 'res_model': 'mail.test.container',
|
||||
'parent_id': cls.sub_track_1.id, 'relation_field': 'container_id'
|
||||
})
|
||||
cls.umb_autosub_nodef = Subtype.create({
|
||||
'name': 'Container AutoSub 2', 'default': False, 'res_model': 'mail.test.container',
|
||||
'parent_id': cls.sub_track_2.id, 'relation_field': 'container_id'
|
||||
})
|
||||
|
||||
# generic subtypes
|
||||
cls.sub_comment = cls.env.ref('mail.mt_comment')
|
||||
cls.sub_generic_int_nodef = Subtype.create({
|
||||
'name': 'Generic internal subtype',
|
||||
'default': False,
|
||||
'internal': True,
|
||||
})
|
||||
cls.sub_generic_int_def = Subtype.create({
|
||||
'name': 'Generic internal subtype (default)',
|
||||
'default': True,
|
||||
'internal': True,
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_auto_subscribe_inactive(self):
|
||||
""" Test inactive are not added as followers in automated subscription """
|
||||
self.test_track.user_id = False
|
||||
self.user_admin.active = False
|
||||
self.user_admin.flush_recordset()
|
||||
self.partner_admin.active = False
|
||||
self.partner_admin.flush_recordset()
|
||||
|
||||
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.assertEqual(self.test_track.message_follower_ids.partner_id, self.user_employee.partner_id)
|
||||
|
||||
self.test_track.write({'user_id': self.user_admin.id})
|
||||
self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id)
|
||||
self.assertEqual(self.test_track.message_follower_ids.partner_id, self.user_employee.partner_id)
|
||||
|
||||
new_record = self.env['mail.test.track'].with_user(self.user_admin).create({
|
||||
'name': 'Test',
|
||||
})
|
||||
self.assertFalse(new_record.message_partner_ids,
|
||||
'Filters out inactive partners')
|
||||
self.assertFalse(new_record.message_follower_ids.partner_id,
|
||||
'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)
|
||||
|
||||
def test_auto_subscribe_responsible(self):
|
||||
""" Responsibles are tracked and added as followers """
|
||||
sub = self.env['mail.test.track'].with_user(self.user_employee).create({
|
||||
'name': 'Test',
|
||||
'user_id': self.user_admin.id,
|
||||
})
|
||||
self.assertEqual(sub.message_partner_ids, (self.user_employee.partner_id | self.user_admin.partner_id))
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_auto_subscribe_defaults(self):
|
||||
""" Test auto subscription based on an container record. This mimics
|
||||
the behavior of addons like project and task where subscribing to
|
||||
some project's subtypes automatically subscribe the follower to its tasks.
|
||||
|
||||
Functional rules applied here
|
||||
|
||||
* subscribing to an container subtype with parent_id / relation_field set
|
||||
automatically create subscription with matching subtypes
|
||||
* subscribing to a sub-record as creator applies default subtype values
|
||||
* portal user should not have access to internal subtypes
|
||||
|
||||
Inactive partners should not be auto subscribed.
|
||||
"""
|
||||
container = self.env['mail.test.container'].with_context(self._test_context).create({
|
||||
'name': 'Project-Like',
|
||||
})
|
||||
|
||||
# have an inactive partner to check auto subscribe does not subscribe it
|
||||
user_root = self.env.ref('base.user_root')
|
||||
self.assertFalse(user_root.active)
|
||||
self.assertFalse(user_root.partner_id.active)
|
||||
|
||||
container.message_subscribe(partner_ids=(self.partner_portal | user_root.partner_id).ids)
|
||||
container.message_subscribe(partner_ids=self.partner_admin.ids, subtype_ids=(self.sub_comment | self.umb_autosub_nodef | self.sub_generic_int_nodef).ids)
|
||||
self.assertEqual(container.message_partner_ids, self.partner_portal | self.partner_admin)
|
||||
follower_por = container.message_follower_ids.filtered(lambda f: f.partner_id == self.partner_portal)
|
||||
follower_adm = container.message_follower_ids.filtered(lambda f: f.partner_id == self.partner_admin)
|
||||
self.assertEqual(
|
||||
follower_por.subtype_ids,
|
||||
self.sub_comment | self.umb_def | self.umb_autosub_def,
|
||||
'Subscribe: Default subtypes: comment (default generic) and two model-related defaults')
|
||||
self.assertEqual(
|
||||
follower_adm.subtype_ids,
|
||||
self.sub_comment | self.umb_autosub_nodef | self.sub_generic_int_nodef,
|
||||
'Subscribe: Asked subtypes when subscribing')
|
||||
|
||||
sub1 = self.env['mail.test.track'].with_user(self.user_employee).create({
|
||||
'name': 'Task-Like Test',
|
||||
'container_id': container.id,
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
sub1.message_partner_ids, self.partner_portal | self.partner_admin | self.user_employee.partner_id,
|
||||
'Followers: creator (employee) + auto subscribe from parent (portal)')
|
||||
follower_por = sub1.message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_portal)
|
||||
follower_adm = sub1.message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_admin)
|
||||
follower_emp = sub1.message_follower_ids.filtered(lambda fol: fol.partner_id == self.user_employee.partner_id)
|
||||
self.assertEqual(
|
||||
follower_por.subtype_ids, self.sub_comment | self.sub_track_1,
|
||||
'AutoSubscribe: comment (generic checked), Track (with child relation) 1 as Umbrella AutoSub (default) was checked'
|
||||
)
|
||||
self.assertEqual(
|
||||
follower_adm.subtype_ids, self.sub_comment | self.sub_track_2 | self.sub_generic_int_nodef,
|
||||
'AutoSubscribe: comment (generic checked), Track (with child relation) 2) as Umbrella AutoSub 2 was checked, Generic internal subtype (generic checked)'
|
||||
)
|
||||
self.assertEqual(
|
||||
follower_emp.subtype_ids, self.sub_comment | self.sub_track_def | self.sub_generic_int_def,
|
||||
'AutoSubscribe: only default one as no subscription on parent'
|
||||
)
|
||||
|
||||
# check portal generic subscribe
|
||||
sub1.message_unsubscribe(partner_ids=self.partner_portal.ids)
|
||||
sub1.message_subscribe(partner_ids=self.partner_portal.ids)
|
||||
follower_por = sub1.message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_portal)
|
||||
|
||||
self.assertEqual(
|
||||
follower_por.subtype_ids, self.sub_comment | self.sub_track_def,
|
||||
'AutoSubscribe: only default one as no subscription on parent (no internal as portal)'
|
||||
)
|
||||
|
||||
# check auto subscribe as creator + auto subscribe as parent follower takes both subtypes
|
||||
container.message_subscribe(
|
||||
partner_ids=self.user_employee.partner_id.ids,
|
||||
subtype_ids=(self.sub_comment | self.sub_generic_int_nodef | self.umb_autosub_nodef).ids)
|
||||
sub2 = self.env['mail.test.track'].with_user(self.user_employee).create({
|
||||
'name': 'Task-Like Test',
|
||||
'container_id': container.id,
|
||||
})
|
||||
follower_emp = sub2.message_follower_ids.filtered(lambda fol: fol.partner_id == self.user_employee.partner_id)
|
||||
defaults = self.sub_comment | self.sub_track_def | self.sub_generic_int_def
|
||||
parents = self.sub_generic_int_nodef | self.sub_track_2
|
||||
self.assertEqual(
|
||||
follower_emp.subtype_ids, defaults + parents,
|
||||
'AutoSubscribe: at create auto subscribe as creator + from parent take both subtypes'
|
||||
)
|
||||
|
||||
|
||||
class AdvancedResponsibleNotifiedTest(TestMailCommon):
|
||||
def setUp(self):
|
||||
super(AdvancedResponsibleNotifiedTest, self).setUp()
|
||||
|
||||
# patch registry to simulate a ready environment so that _message_auto_subscribe_notify
|
||||
# will be executed with the associated notification
|
||||
old = self.env.registry.ready
|
||||
self.env.registry.ready = True
|
||||
self.addCleanup(setattr, self.env.registry, 'ready', old)
|
||||
|
||||
def test_auto_subscribe_notify_email(self):
|
||||
""" Responsible is notified when assigned """
|
||||
partner = self.env['res.partner'].create({"name": "demo1", "email": "demo1@test.com"})
|
||||
notified_user = self.env['res.users'].create({
|
||||
'login': 'demo1',
|
||||
'partner_id': partner.id,
|
||||
'notification_type': 'email',
|
||||
})
|
||||
|
||||
# TODO master: add a 'state' selection field on 'mail.test.track' with a 'done' value to have a complete test
|
||||
# check that 'default_state' context does not collide with mail.mail default values
|
||||
sub = self.env['mail.test.track'].with_user(self.user_employee).with_context({
|
||||
'default_state': 'done',
|
||||
'mail_notify_force_send': False
|
||||
}).create({
|
||||
'name': 'Test',
|
||||
'user_id': notified_user.id,
|
||||
})
|
||||
|
||||
self.assertEqual(sub.message_partner_ids, (self.user_employee.partner_id | notified_user.partner_id))
|
||||
# fetch created "You have been assigned to 'Test'" mail.message
|
||||
mail_message = self.env['mail.message'].search([
|
||||
('model', '=', 'mail.test.track'),
|
||||
('res_id', '=', sub.id),
|
||||
('partner_ids', 'in', partner.id),
|
||||
])
|
||||
self.assertEqual(1, len(mail_message))
|
||||
|
||||
# verify that a mail.mail is attached to it with the correct state ('outgoing')
|
||||
mail_notification = mail_message.notification_ids
|
||||
self.assertEqual(1, len(mail_notification))
|
||||
self.assertTrue(bool(mail_notification.mail_mail_id))
|
||||
self.assertEqual(mail_notification.mail_mail_id.state, 'outgoing')
|
||||
|
||||
|
||||
@tagged('mail_followers', 'post_install', '-at_install')
|
||||
class RecipientsNotificationTest(TestMailCommon):
|
||||
""" 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."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(RecipientsNotificationTest, cls).setUpClass()
|
||||
|
||||
# portal user for testing share status / internal subtypes
|
||||
cls.user_portal = cls._create_portal_user()
|
||||
cls.partner_portal = cls.user_portal.partner_id
|
||||
|
||||
# simple customer
|
||||
cls.customer = cls.env['res.partner'].create({
|
||||
'email': 'customer@test.customer.com',
|
||||
'name': 'Customer',
|
||||
'phone': '+32455778899',
|
||||
})
|
||||
|
||||
# Simulate case of 2 users that got their partner merged
|
||||
cls.common_partner = cls.env['res.partner'].create({
|
||||
'email': 'common.partner@test.customer.com',
|
||||
'name': 'Common Partner',
|
||||
'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)],
|
||||
'login': '_login_portal',
|
||||
'notification_type': 'email',
|
||||
'partner_id': cls.common_partner.id,
|
||||
},
|
||||
{'groups_id': [(4, cls.env.ref('base.group_user').id)],
|
||||
'login': '_login_internal',
|
||||
'notification_type': 'inbox',
|
||||
'partner_id': cls.common_partner.id,
|
||||
}
|
||||
])
|
||||
cls.env.flush_all()
|
||||
|
||||
def assertRecipientsData(self, recipients_data, records, partners, partner_to_users=None):
|
||||
""" Custom assert as recipients structure is custom and may change due
|
||||
to some implementation choice. """
|
||||
if records:
|
||||
self.assertEqual(set(recipients_data.keys()), set(records.ids))
|
||||
record_ids = records.ids
|
||||
else:
|
||||
records, record_ids = [False], [0]
|
||||
for record, record_id in zip(records, record_ids):
|
||||
record_data = recipients_data[record_id]
|
||||
self.assertEqual(set(record_data.keys()), set(partners.ids))
|
||||
for partner in partners:
|
||||
partner_data = record_data[partner.id]
|
||||
if partner_to_users and partner_to_users.get(partner.id): #helps making test explicit
|
||||
user = partner_to_users[partner.id]
|
||||
else:
|
||||
user = next((user for user in partner.user_ids if not user.share), self.env['res.users'])
|
||||
if not user:
|
||||
user = next((user for user in partner.user_ids), self.env['res.users'])
|
||||
self.assertEqual(partner_data['active'], partner.active)
|
||||
if user:
|
||||
self.assertEqual(partner_data['groups'], set(user.groups_id.ids))
|
||||
self.assertEqual(partner_data['notif'], user.notification_type)
|
||||
self.assertEqual(partner_data['uid'], user.id)
|
||||
else:
|
||||
self.assertEqual(partner_data['groups'], set())
|
||||
self.assertEqual(partner_data['notif'], 'email')
|
||||
self.assertFalse(partner_data['uid'])
|
||||
if record:
|
||||
self.assertEqual(partner_data['is_follower'], partner in record.message_partner_ids)
|
||||
else:
|
||||
self.assertFalse(partner_data['is_follower'])
|
||||
self.assertEqual(partner_data['share'], partner.partner_share)
|
||||
self.assertEqual(partner_data['ushare'], user.share)
|
||||
|
||||
@users('employee')
|
||||
def test_notification_nodupe(self):
|
||||
""" Check that we only create one mail.notification per partner. """
|
||||
# Trigger auto subscribe notification
|
||||
test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": self.user_2.id})
|
||||
mail_message = self.env['mail.message'].search([
|
||||
('res_id', '=', test.id),
|
||||
('model', '=', 'mail.test.track'),
|
||||
('message_type', '=', 'user_notification')
|
||||
])
|
||||
notif = self.env['mail.notification'].search([
|
||||
('mail_message_id', '=', mail_message.id),
|
||||
('res_partner_id', '=', self.common_partner.id)
|
||||
])
|
||||
self.assertEqual(len(notif), 1)
|
||||
self.assertEqual(notif.notification_type, 'inbox', 'Multi users should take internal users if possible')
|
||||
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test, 'comment', self.env.ref('mail.mt_comment').id,
|
||||
pids=self.common_partner.ids)
|
||||
self.assertRecipientsData(recipients_data, test, self.common_partner + self.partner_employee,
|
||||
partner_to_users={self.common_partner.id: self.user_2})
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_notification_unlink(self):
|
||||
""" Check that we unlink the created user_notification after unlinked the
|
||||
related document. """
|
||||
test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": self.user_1.id})
|
||||
mail_message = self.env['mail.message'].search([
|
||||
('res_id', '=', test.id),
|
||||
('model', '=', 'mail.test.track'),
|
||||
('message_type', '=', 'user_notification')
|
||||
])
|
||||
self.assertEqual(len(mail_message), 1)
|
||||
test.unlink()
|
||||
self.assertEqual(
|
||||
self.env['mail.message'].search_count([
|
||||
('res_id', '=', test.id),
|
||||
('model', '=', 'mail.test.track'),
|
||||
('message_type', '=', 'user_notification')
|
||||
]), 0
|
||||
)
|
||||
|
||||
@users('employee')
|
||||
def test_notification_user_choice(self):
|
||||
""" Check fetching user information when notifying someone with multiple
|
||||
users (more complex use case). """
|
||||
company_other = self.env['res.company'].sudo().create({
|
||||
'currency_id': self.env.ref('base.CAD').id,
|
||||
'email': 'company_other@test.example.com',
|
||||
'name': 'Company Other',
|
||||
})
|
||||
shared_partner = self.env['res.partner'].sudo().create({
|
||||
'email': 'common.partner@test.customer.com',
|
||||
'name': 'Common Partner',
|
||||
'phone': '+32455998877',
|
||||
})
|
||||
cids = (company_other + self.company_admin).ids
|
||||
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)],
|
||||
'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)],
|
||||
'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)],
|
||||
'login': '_login2_manager',
|
||||
'notification_type': 'inbox',
|
||||
'partner_id': shared_partner.id,
|
||||
}
|
||||
])
|
||||
(user_2_1 + user_2_2 + user_2_3).flush_recordset()
|
||||
|
||||
# just ensure current share status
|
||||
self.assertFalse(shared_partner.partner_share)
|
||||
self.assertTrue(user_2_1.share)
|
||||
self.assertFalse(user_2_2.share or user_2_3.share)
|
||||
|
||||
test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": False})
|
||||
self.assertEqual(test.message_partner_ids, self.partner_employee)
|
||||
|
||||
with self.assertSinglePostNotifications(
|
||||
[{'group': 'customer', 'partner': shared_partner,
|
||||
'status': 'sent', 'type': 'inbox'}],
|
||||
message_info={'content': 'User Choice Notification'}):
|
||||
test.message_post(
|
||||
body='<p>User Choice Notification</p>',
|
||||
message_type='comment',
|
||||
partner_ids=shared_partner.ids,
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test, 'comment', self.env.ref('mail.mt_comment').id,
|
||||
pids=shared_partner.ids)
|
||||
self.assertRecipientsData(recipients_data, test, self.partner_employee + shared_partner,
|
||||
partner_to_users={shared_partner.id: user_2_2})
|
||||
|
||||
@users('employee')
|
||||
def test_recipients_fetch(self):
|
||||
test_records = self.env['mail.test.simple'].create([
|
||||
{'email_from': 'ignasse@example.com',
|
||||
'name': 'Test %s' % idx,
|
||||
} for idx in range(5)
|
||||
])
|
||||
# make followers listen to notes to use it and check portal will never be notified of it (internal)
|
||||
test_records.message_follower_ids.sudo().write({'subtype_ids': [(4, self.env.ref('mail.mt_note').id)]})
|
||||
for test_record in test_records:
|
||||
self.assertEqual(test_record.message_partner_ids, self.env.user.partner_id)
|
||||
|
||||
test_records[0].message_subscribe(self.partner_portal.ids)
|
||||
self.assertNotIn(
|
||||
self.env.ref('mail.mt_note'),
|
||||
test_records[0].message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_portal).subtype_ids,
|
||||
'Portal user should not follow notes by default')
|
||||
|
||||
# just fetch followers
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test_records[0], 'comment', self.env.ref('mail.mt_comment').id,
|
||||
pids=None
|
||||
)
|
||||
self.assertRecipientsData(recipients_data, test_records[0], self.env.user.partner_id + self.partner_portal)
|
||||
|
||||
# followers + additional recipients
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test_records[0], 'comment', self.env.ref('mail.mt_comment').id,
|
||||
pids=(self.customer + self.common_partner + self.partner_admin).ids
|
||||
)
|
||||
self.assertRecipientsData(recipients_data, test_records[0],
|
||||
self.env.user.partner_id + self.partner_portal + self.customer + self.common_partner + self.partner_admin)
|
||||
|
||||
# ensure filtering on internal: should exclude Portal even if misconfiguration
|
||||
follower_portal = test_records[0].message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_portal).sudo()
|
||||
follower_portal.write({'subtype_ids': [(4, self.env.ref('mail.mt_note').id)]})
|
||||
follower_portal.flush_recordset()
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test_records[0], 'comment', self.env.ref('mail.mt_note').id,
|
||||
pids=(self.common_partner + self.partner_admin).ids
|
||||
)
|
||||
self.assertRecipientsData(recipients_data, test_records[0], self.env.user.partner_id + self.common_partner + self.partner_admin)
|
||||
|
||||
# ensure filtering on subtype: should exclude Portal as it does not follow comment anymore
|
||||
follower_portal.write({'subtype_ids': [(3, self.env.ref('mail.mt_comment').id)]})
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test_records[0], 'comment', self.env.ref('mail.mt_comment').id,
|
||||
pids=(self.common_partner + self.partner_admin).ids
|
||||
)
|
||||
self.assertRecipientsData(recipients_data, test_records[0], self.env.user.partner_id + self.common_partner + self.partner_admin)
|
||||
|
||||
# check without subtype
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test_records[0], 'comment', False,
|
||||
pids=(self.common_partner + self.partner_admin).ids
|
||||
)
|
||||
self.assertRecipientsData(recipients_data, test_records[0], self.common_partner + self.partner_admin)
|
||||
|
||||
# multi mode
|
||||
test_records[1].message_subscribe(self.partner_portal.ids)
|
||||
test_records[0:4].message_subscribe(self.common_partner.ids)
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test_records, 'comment', self.env.ref('mail.mt_comment').id,
|
||||
pids=self.partner_admin.ids
|
||||
)
|
||||
# 0: portal is follower but does not follow comment + common partner (+ admin as pid)
|
||||
recipients_data_1 = dict((r, recipients_data[r]) for r in recipients_data if r in test_records[0:1].ids)
|
||||
self.assertRecipientsData(recipients_data_1, test_records[0:1], self.env.user.partner_id + self.common_partner + self.partner_admin)
|
||||
# 1: portal is follower with comment + common partner (+ admin as pid)
|
||||
recipients_data_1 = dict((r, recipients_data[r]) for r in recipients_data if r in test_records[1:2].ids)
|
||||
self.assertRecipientsData(recipients_data_1, test_records[1:2], self.env.user.partner_id + self.common_partner + self.partner_portal + self.partner_admin)
|
||||
# 2-3: common partner (+ admin as pid)
|
||||
recipients_data_2 = dict((r, recipients_data[r]) for r in recipients_data if r in test_records[2:4].ids)
|
||||
self.assertRecipientsData(recipients_data_2, test_records[2:4], self.env.user.partner_id + self.common_partner + self.partner_admin)
|
||||
# 4+: env user partner (+ admin as pid)
|
||||
recipients_data_3 = dict((r, recipients_data[r]) for r in recipients_data if r in test_records[4:].ids)
|
||||
self.assertRecipientsData(recipients_data_3, test_records[4:], self.env.user.partner_id + self.partner_admin)
|
||||
|
||||
# multi mode, pids only
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test_records, 'comment', False,
|
||||
pids=(self.env.user.partner_id + self.partner_admin).ids
|
||||
)
|
||||
self.assertRecipientsData(recipients_data, test_records, self.env.user.partner_id + self.partner_admin)
|
||||
|
||||
# on mail.thread, False everywhere: pathologic case
|
||||
test_partners = self.partner_admin + self.partner_employee + self.common_partner
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
self.env['mail.thread'], False, False,
|
||||
pids=test_partners.ids
|
||||
)
|
||||
self.assertRecipientsData(recipients_data, False, test_partners)
|
||||
2473
odoo-bringout-oca-ocb-test_mail/test_mail/tests/test_mail_gateway.py
Normal file
2473
odoo-bringout-oca-ocb-test_mail/test_mail/tests/test_mail_gateway.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,907 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import psycopg2
|
||||
import pytz
|
||||
import smtplib
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from freezegun import freeze_time
|
||||
from OpenSSL.SSL import Error as SSLError
|
||||
from socket import gaierror, timeout
|
||||
from unittest.mock import call, patch
|
||||
|
||||
from odoo import api, Command, tools
|
||||
from odoo.addons.base.models.ir_mail_server import MailDeliveryException
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests import common, tagged, users
|
||||
from odoo.tools import mute_logger, DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
|
||||
@tagged('mail_mail')
|
||||
class TestMailMail(TestMailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailMail, cls).setUpClass()
|
||||
cls._init_mail_servers()
|
||||
|
||||
cls.server_domain_2 = cls.env['ir.mail_server'].create({
|
||||
'name': 'Server 2',
|
||||
'smtp_host': 'test_2.com',
|
||||
'from_filter': 'test_2.com',
|
||||
})
|
||||
|
||||
cls.test_record = cls.env['mail.test.gateway'].with_context(cls._test_context).create({
|
||||
'name': 'Test',
|
||||
'email_from': 'ignasse@example.com',
|
||||
}).with_context({})
|
||||
|
||||
cls.test_message = cls.test_record.message_post(body='<p>Message</p>', subject='Subject')
|
||||
cls.test_mail = cls.env['mail.mail'].create([{
|
||||
'body': '<p>Body</p>',
|
||||
'email_from': False,
|
||||
'email_to': 'test@example.com',
|
||||
'is_notification': True,
|
||||
'subject': 'Subject',
|
||||
}])
|
||||
cls.test_notification = cls.env['mail.notification'].create({
|
||||
'is_read': False,
|
||||
'mail_mail_id': cls.test_mail.id,
|
||||
'mail_message_id': cls.test_message.id,
|
||||
'notification_type': 'email',
|
||||
'res_partner_id': cls.partner_employee.id, # not really used for matching except multi-recipients
|
||||
})
|
||||
|
||||
cls.emails_falsy = [False, '', ' ']
|
||||
cls.emails_invalid = ['buggy', 'buggy, wrong']
|
||||
cls.emails_invalid_ascii = ['raoul@example¢¡.com']
|
||||
cls.emails_valid = ['raoul¢¡@example.com', 'raoul@example.com']
|
||||
|
||||
def _reset_data(self):
|
||||
self._init_mail_mock()
|
||||
self.test_mail.write({'failure_reason': False, 'failure_type': False, 'state': 'outgoing'})
|
||||
self.test_notification.write({'failure_reason': False, 'failure_type': False, 'notification_status': 'ready'})
|
||||
|
||||
@users('admin')
|
||||
def test_mail_mail_attachment_access(self):
|
||||
mail = self.env['mail.mail'].create({
|
||||
'body_html': 'Test',
|
||||
'email_to': 'test@example.com',
|
||||
'partner_ids': [(4, self.user_employee.partner_id.id)],
|
||||
'attachment_ids': [
|
||||
(0, 0, {'name': 'file 1', 'datas': 'c2VjcmV0'}),
|
||||
(0, 0, {'name': 'file 2', 'datas': 'c2VjcmV0'}),
|
||||
(0, 0, {'name': 'file 3', 'datas': 'c2VjcmV0'}),
|
||||
(0, 0, {'name': 'file 4', 'datas': 'c2VjcmV0'}),
|
||||
],
|
||||
})
|
||||
|
||||
def _patched_check(self, *args, **kwargs):
|
||||
if self.env.is_superuser():
|
||||
return
|
||||
if any(attachment.name in ('file 2', 'file 4') for attachment in self):
|
||||
raise AccessError('No access')
|
||||
|
||||
mail.invalidate_recordset()
|
||||
|
||||
new_attachment = self.env['ir.attachment'].create({
|
||||
'name': 'new file',
|
||||
'datas': 'c2VjcmV0',
|
||||
})
|
||||
|
||||
with patch.object(type(self.env['ir.attachment']), 'check', _patched_check):
|
||||
# Sanity check
|
||||
self.assertEqual(mail.restricted_attachment_count, 2)
|
||||
self.assertEqual(len(mail.unrestricted_attachment_ids), 2)
|
||||
self.assertEqual(mail.unrestricted_attachment_ids.mapped('name'), ['file 1', 'file 3'])
|
||||
|
||||
# Add a new attachment
|
||||
mail.write({
|
||||
'unrestricted_attachment_ids': [Command.link(new_attachment.id)],
|
||||
})
|
||||
self.assertEqual(mail.restricted_attachment_count, 2)
|
||||
self.assertEqual(len(mail.unrestricted_attachment_ids), 3)
|
||||
self.assertEqual(mail.unrestricted_attachment_ids.mapped('name'), ['file 1', 'file 3', 'new file'])
|
||||
self.assertEqual(len(mail.attachment_ids), 5)
|
||||
|
||||
# Remove an attachment
|
||||
mail.write({
|
||||
'unrestricted_attachment_ids': [Command.unlink(new_attachment.id)],
|
||||
})
|
||||
self.assertEqual(mail.restricted_attachment_count, 2)
|
||||
self.assertEqual(len(mail.unrestricted_attachment_ids), 2)
|
||||
self.assertEqual(mail.unrestricted_attachment_ids.mapped('name'), ['file 1', 'file 3'])
|
||||
self.assertEqual(len(mail.attachment_ids), 4)
|
||||
|
||||
# Reset command
|
||||
mail.invalidate_recordset()
|
||||
mail.write({'unrestricted_attachment_ids': [Command.clear()]})
|
||||
self.assertEqual(len(mail.unrestricted_attachment_ids), 0)
|
||||
self.assertEqual(len(mail.attachment_ids), 2)
|
||||
|
||||
# Read in SUDO
|
||||
mail.invalidate_recordset()
|
||||
self.assertEqual(mail.sudo().restricted_attachment_count, 2)
|
||||
self.assertEqual(len(mail.sudo().unrestricted_attachment_ids), 0)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_mail_recipients(self):
|
||||
""" Partner_ids is a field used from mail_message, but not from mail_mail. """
|
||||
mail = self.env['mail.mail'].sudo().create({
|
||||
'body_html': '<p>Test</p>',
|
||||
'email_to': 'test@example.com',
|
||||
'partner_ids': [(4, self.user_employee.partner_id.id)]
|
||||
})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send()
|
||||
self.assertSentEmail(mail.env.user.partner_id, ['test@example.com'])
|
||||
self.assertEqual(len(self._mails), 1)
|
||||
|
||||
mail = self.env['mail.mail'].sudo().create({
|
||||
'body_html': '<p>Test</p>',
|
||||
'email_to': 'test@example.com',
|
||||
'recipient_ids': [(4, self.user_employee.partner_id.id)],
|
||||
})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send()
|
||||
self.assertSentEmail(mail.env.user.partner_id, ['test@example.com'])
|
||||
self.assertSentEmail(mail.env.user.partner_id, [self.user_employee.email_formatted])
|
||||
self.assertEqual(len(self._mails), 2)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_mail_recipients_cc(self):
|
||||
""" Partner_ids is a field used from mail_message, but not from mail_mail. """
|
||||
mail = self.env['mail.mail'].sudo().create({
|
||||
'body_html': '<p>Test</p>',
|
||||
'email_cc': 'test.cc.1@example.com, "Herbert" <test.cc.2@example.com>',
|
||||
'email_to': 'test.rec.1@example.com, "Raoul" <test.rec.2@example.com>',
|
||||
'recipient_ids': [(4, self.user_employee.partner_id.id)],
|
||||
})
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
mail.send()
|
||||
# note that formatting is lost for cc
|
||||
self.assertSentEmail(mail.env.user.partner_id,
|
||||
['test.rec.1@example.com', '"Raoul" <test.rec.2@example.com>'],
|
||||
email_cc=['test.cc.1@example.com', '"Herbert" <test.cc.2@example.com>'])
|
||||
# Mail: currently cc are put as copy of all sent emails (aka spam)
|
||||
self.assertSentEmail(mail.env.user.partner_id, [self.user_employee.email_formatted],
|
||||
email_cc=['test.cc.1@example.com', '"Herbert" <test.cc.2@example.com>'])
|
||||
self.assertEqual(len(self._mails), 2)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_mail_recipients_formatting(self):
|
||||
""" Check support of email / formatted email """
|
||||
mail = self.env['mail.mail'].sudo().create({
|
||||
'author_id': False,
|
||||
'body_html': '<p>Test</p>',
|
||||
'email_cc': 'test.cc.1@example.com, "Herbert" <test.cc.2@example.com>',
|
||||
'email_from': '"Ignasse" <test.from@example.com>',
|
||||
'email_to': 'test.rec.1@example.com, "Raoul" <test.rec.2@example.com>',
|
||||
})
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
mail.send()
|
||||
# note that formatting is lost for cc
|
||||
self.assertSentEmail('"Ignasse" <test.from@example.com>',
|
||||
['test.rec.1@example.com', '"Raoul" <test.rec.2@example.com>'],
|
||||
email_cc=['test.cc.1@example.com', '"Herbert" <test.cc.2@example.com>'])
|
||||
self.assertEqual(len(self._mails), 1)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_mail_return_path(self):
|
||||
# mail without thread-enabled record
|
||||
base_values = {
|
||||
'body_html': '<p>Test</p>',
|
||||
'email_to': 'test@example.com',
|
||||
}
|
||||
|
||||
mail = self.env['mail.mail'].create(base_values)
|
||||
with self.mock_mail_gateway():
|
||||
mail.send()
|
||||
self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s@%s' % (self.alias_bounce, self.alias_domain))
|
||||
|
||||
# mail on thread-enabled record
|
||||
mail = self.env['mail.mail'].create(dict(base_values, **{
|
||||
'model': self.test_record._name,
|
||||
'res_id': self.test_record.id,
|
||||
}))
|
||||
with self.mock_mail_gateway():
|
||||
mail.send()
|
||||
self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s@%s' % (self.alias_bounce, self.alias_domain))
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests')
|
||||
def test_mail_mail_schedule(self):
|
||||
"""Test that a mail scheduled in the past/future are sent or not"""
|
||||
now = datetime(2022, 6, 28, 14, 0, 0)
|
||||
scheduled_datetimes = [
|
||||
# falsy values
|
||||
False, '', 'This is not a date format',
|
||||
# datetimes (UTC/GMT +10 hours for Australia/Brisbane)
|
||||
now, pytz.timezone('Australia/Brisbane').localize(now),
|
||||
# string
|
||||
(now - timedelta(days=1)).strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
(now + timedelta(days=1)).strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
(now + timedelta(days=1)).strftime("%H:%M:%S %d-%m-%Y"),
|
||||
# tz: is actually 1 hour before now in UTC
|
||||
(now + timedelta(hours=3)).strftime("%H:%M:%S %d-%m-%Y") + " +0400",
|
||||
# tz: is actually 1 hour after now in UTC
|
||||
(now + timedelta(hours=-3)).strftime("%H:%M:%S %d-%m-%Y") + " -0400",
|
||||
]
|
||||
expected_datetimes = [
|
||||
False, False, False,
|
||||
now, now - pytz.timezone('Australia/Brisbane').utcoffset(now),
|
||||
now - timedelta(days=1), now + timedelta(days=1), now + timedelta(days=1),
|
||||
now + timedelta(hours=-1),
|
||||
now + timedelta(hours=1),
|
||||
]
|
||||
expected_states = [
|
||||
# falsy values = send now
|
||||
'sent', 'sent', 'sent',
|
||||
'sent', 'sent',
|
||||
'sent', 'outgoing', 'outgoing',
|
||||
'sent', 'outgoing'
|
||||
]
|
||||
|
||||
mails = self.env['mail.mail'].create([
|
||||
{'body_html': '<p>Test</p>',
|
||||
'email_to': 'test@example.com',
|
||||
'scheduled_date': scheduled_datetime,
|
||||
} for scheduled_datetime in scheduled_datetimes
|
||||
])
|
||||
|
||||
for mail, expected_datetime, scheduled_datetime in zip(mails, expected_datetimes, scheduled_datetimes):
|
||||
self.assertEqual(mail.scheduled_date, expected_datetime,
|
||||
'Scheduled date: %s should be stored as %s, received %s' % (scheduled_datetime, expected_datetime, mail.scheduled_date))
|
||||
self.assertEqual(mail.state, 'outgoing')
|
||||
|
||||
with freeze_time(now):
|
||||
self.env['mail.mail'].process_email_queue()
|
||||
for mail, expected_state in zip(mails, expected_states):
|
||||
self.assertEqual(mail.state, expected_state)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_mail_send_exceptions_origin(self):
|
||||
""" Test various use case with exceptions and errors and see how they are
|
||||
managed and stored at mail and notification level. """
|
||||
mail, notification = self.test_mail, self.test_notification
|
||||
|
||||
# MailServer.build_email(): invalid from
|
||||
self.env['ir.config_parameter'].set_param('mail.default.from', '')
|
||||
self._reset_data()
|
||||
with self.mock_mail_gateway(), mute_logger('odoo.addons.mail.models.mail_mail'):
|
||||
mail.send(raise_exception=False)
|
||||
self.assertFalse(self._mails[0]['email_from'])
|
||||
self.assertEqual(
|
||||
mail.failure_reason,
|
||||
'You must either provide a sender address explicitly or configure using the combination of `mail.catchall.domain` and `mail.default.from` ICPs, in the server configuration file or with the --email-from startup parameter.')
|
||||
self.assertFalse(mail.failure_type, 'Mail: void from: no failure type, should be updated')
|
||||
self.assertEqual(mail.state, 'exception')
|
||||
self.assertEqual(
|
||||
notification.failure_reason,
|
||||
'You must either provide a sender address explicitly or configure using the combination of `mail.catchall.domain` and `mail.default.from` ICPs, in the server configuration file or with the --email-from startup parameter.')
|
||||
self.assertEqual(notification.failure_type, 'unknown', 'Mail: void from: unknown failure type, should be updated')
|
||||
self.assertEqual(notification.notification_status, 'exception')
|
||||
|
||||
# MailServer.send_email(): _prepare_email_message: unexpected ASCII
|
||||
# Force catchall domain to void otherwise bounce is set to postmaster-odoo@domain
|
||||
self.env['ir.config_parameter'].set_param('mail.catchall.domain', '')
|
||||
self._reset_data()
|
||||
mail.write({'email_from': 'strange@example¢¡.com'})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send(raise_exception=False)
|
||||
self.assertEqual(self._mails[0]['email_from'], 'strange@example¢¡.com')
|
||||
self.assertEqual(mail.failure_reason, "Malformed 'Return-Path' or 'From' address: strange@example¢¡.com - It should contain one valid plain ASCII email")
|
||||
self.assertFalse(mail.failure_type, 'Mail: bugged from (ascii): no failure type, should be updated')
|
||||
self.assertEqual(mail.state, 'exception')
|
||||
self.assertEqual(notification.failure_reason, "Malformed 'Return-Path' or 'From' address: strange@example¢¡.com - It should contain one valid plain ASCII email")
|
||||
self.assertEqual(notification.failure_type, 'unknown', 'Mail: bugged from (ascii): unknown failure type, should be updated')
|
||||
self.assertEqual(notification.notification_status, 'exception')
|
||||
|
||||
# MailServer.send_email(): _prepare_email_message: unexpected ASCII based on catchall domain
|
||||
self.env['ir.config_parameter'].set_param('mail.catchall.domain', 'domain¢¡.com')
|
||||
self._reset_data()
|
||||
mail.write({'email_from': 'test.user@example.com'})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send(raise_exception=False)
|
||||
self.assertEqual(self._mails[0]['email_from'], 'test.user@example.com')
|
||||
self.assertIn("Malformed 'Return-Path' or 'From' address: bounce.test@domain¢¡.com", mail.failure_reason)
|
||||
self.assertFalse(mail.failure_type, 'Mail: bugged catchall domain (ascii): no failure type, should be updated')
|
||||
self.assertEqual(mail.state, 'exception')
|
||||
self.assertEqual(notification.failure_reason, "Malformed 'Return-Path' or 'From' address: bounce.test@domain¢¡.com - It should contain one valid plain ASCII email")
|
||||
self.assertEqual(notification.failure_type, 'unknown', 'Mail: bugged catchall domain (ascii): unknown failure type, should be updated')
|
||||
self.assertEqual(notification.notification_status, 'exception')
|
||||
|
||||
# MailServer.send_email(): _prepare_email_message: Malformed 'Return-Path' or 'From' address
|
||||
self.env['ir.config_parameter'].set_param('mail.catchall.domain', '')
|
||||
self._reset_data()
|
||||
mail.write({'email_from': 'robert'})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send(raise_exception=False)
|
||||
self.assertEqual(self._mails[0]['email_from'], 'robert')
|
||||
self.assertEqual(mail.failure_reason, "Malformed 'Return-Path' or 'From' address: robert - It should contain one valid plain ASCII email")
|
||||
self.assertFalse(mail.failure_type, 'Mail: bugged from (ascii): no failure type, should be updated')
|
||||
self.assertEqual(mail.state, 'exception')
|
||||
self.assertEqual(notification.failure_reason, "Malformed 'Return-Path' or 'From' address: robert - It should contain one valid plain ASCII email")
|
||||
self.assertEqual(notification.failure_type, 'unknown', 'Mail: bugged from (ascii): unknown failure type, should be updated')
|
||||
self.assertEqual(notification.notification_status, 'exception')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_mail_send_exceptions_recipients_emails(self):
|
||||
""" Test various use case with exceptions and errors and see how they are
|
||||
managed and stored at mail and notification level. """
|
||||
mail, notification = self.test_mail, self.test_notification
|
||||
|
||||
self.env['ir.config_parameter'].set_param('mail.catchall.domain', self.alias_domain)
|
||||
self.env['ir.config_parameter'].set_param('mail.default.from', self.default_from)
|
||||
|
||||
# MailServer.send_email(): _prepare_email_message: missing To
|
||||
for email_to in self.emails_falsy:
|
||||
self._reset_data()
|
||||
mail.write({'email_to': email_to})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send(raise_exception=False)
|
||||
self.assertEqual(mail.failure_reason, 'Error without exception. Probably due to sending an email without computed recipients.')
|
||||
self.assertFalse(mail.failure_type, 'Mail: missing email_to: no failure type, should be updated')
|
||||
self.assertEqual(mail.state, 'exception')
|
||||
if email_to == ' ':
|
||||
self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated')
|
||||
self.assertEqual(notification.failure_type, 'mail_email_missing')
|
||||
self.assertEqual(notification.notification_status, 'exception')
|
||||
else:
|
||||
self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated')
|
||||
self.assertEqual(notification.failure_type, False, 'Mail: missing email_to: notification is wrongly set as sent')
|
||||
self.assertEqual(notification.notification_status, 'sent', 'Mail: missing email_to: notification is wrongly set as sent')
|
||||
|
||||
# MailServer.send_email(): _prepare_email_message: invalid To
|
||||
for email_to, failure_type in zip(self.emails_invalid,
|
||||
['mail_email_missing', 'mail_email_missing']):
|
||||
self._reset_data()
|
||||
mail.write({'email_to': email_to})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send(raise_exception=False)
|
||||
self.assertEqual(mail.failure_reason, 'Error without exception. Probably due to sending an email without computed recipients.')
|
||||
self.assertFalse(mail.failure_type, 'Mail: invalid email_to: no failure type, should be updated')
|
||||
self.assertEqual(mail.state, 'exception')
|
||||
self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated')
|
||||
self.assertEqual(notification.failure_type, failure_type, 'Mail: invalid email_to: missing instead of invalid')
|
||||
self.assertEqual(notification.notification_status, 'exception')
|
||||
|
||||
# MailServer.send_email(): _prepare_email_message: invalid To (ascii)
|
||||
for email_to in self.emails_invalid_ascii:
|
||||
self._reset_data()
|
||||
mail.write({'email_to': email_to})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send(raise_exception=False)
|
||||
self.assertEqual(mail.failure_reason, 'Error without exception. Probably due to sending an email without computed recipients.')
|
||||
self.assertFalse(mail.failure_type, 'Mail: invalid (ascii) recipient partner: no failure type, should be updated')
|
||||
self.assertEqual(mail.state, 'exception')
|
||||
self.assertEqual(notification.failure_type, 'mail_email_invalid')
|
||||
self.assertEqual(notification.notification_status, 'exception')
|
||||
|
||||
# MailServer.send_email(): _prepare_email_message: ok To (ascii or just ok)
|
||||
for email_to in self.emails_valid:
|
||||
self._reset_data()
|
||||
mail.write({'email_to': email_to})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send(raise_exception=False)
|
||||
self.assertFalse(mail.failure_reason)
|
||||
self.assertFalse(mail.failure_type)
|
||||
self.assertEqual(mail.state, 'sent')
|
||||
self.assertFalse(notification.failure_reason)
|
||||
self.assertFalse(notification.failure_type)
|
||||
self.assertEqual(notification.notification_status, 'sent')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_mail_send_exceptions_recipients_partners(self):
|
||||
""" Test various use case with exceptions and errors and see how they are
|
||||
managed and stored at mail and notification level. """
|
||||
mail, notification = self.test_mail, self.test_notification
|
||||
|
||||
mail.write({'email_from': 'test.user@test.example.com', 'email_to': False})
|
||||
partners_falsy = self.env['res.partner'].create([
|
||||
{'name': 'Name %s' % email, 'email': email}
|
||||
for email in self.emails_falsy
|
||||
])
|
||||
partners_invalid = self.env['res.partner'].create([
|
||||
{'name': 'Name %s' % email, 'email': email}
|
||||
for email in self.emails_invalid
|
||||
])
|
||||
partners_invalid_ascii = self.env['res.partner'].create([
|
||||
{'name': 'Name %s' % email, 'email': email}
|
||||
for email in self.emails_invalid_ascii
|
||||
])
|
||||
partners_valid = self.env['res.partner'].create([
|
||||
{'name': 'Name %s' % email, 'email': email}
|
||||
for email in self.emails_valid
|
||||
])
|
||||
|
||||
# void values
|
||||
for partner in partners_falsy:
|
||||
self._reset_data()
|
||||
mail.write({'recipient_ids': [(5, 0), (4, partner.id)]})
|
||||
notification.write({'res_partner_id': partner.id})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send(raise_exception=False)
|
||||
self.assertEqual(mail.failure_reason, 'Error without exception. Probably due to sending an email without computed recipients.')
|
||||
self.assertFalse(mail.failure_type, 'Mail: void recipient partner: no failure type, should be updated')
|
||||
self.assertEqual(mail.state, 'exception')
|
||||
self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated')
|
||||
self.assertEqual(notification.failure_type, 'mail_email_invalid', 'Mail: void recipient partner: should be missing, not invalid')
|
||||
self.assertEqual(notification.notification_status, 'exception')
|
||||
|
||||
# wrong values
|
||||
for partner in partners_invalid:
|
||||
self._reset_data()
|
||||
mail.write({'recipient_ids': [(5, 0), (4, partner.id)]})
|
||||
notification.write({'res_partner_id': partner.id})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send(raise_exception=False)
|
||||
self.assertEqual(mail.failure_reason, 'Error without exception. Probably due to sending an email without computed recipients.')
|
||||
self.assertFalse(mail.failure_type, 'Mail: invalid recipient partner: no failure type, should be updated')
|
||||
self.assertEqual(mail.state, 'exception')
|
||||
self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated')
|
||||
self.assertEqual(notification.failure_type, 'mail_email_invalid')
|
||||
self.assertEqual(notification.notification_status, 'exception')
|
||||
|
||||
# ascii ko
|
||||
for partner in partners_invalid_ascii:
|
||||
self._reset_data()
|
||||
mail.write({'recipient_ids': [(5, 0), (4, partner.id)]})
|
||||
notification.write({'res_partner_id': partner.id})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send(raise_exception=False)
|
||||
self.assertEqual(mail.failure_reason, 'Error without exception. Probably due to sending an email without computed recipients.')
|
||||
self.assertFalse(mail.failure_type, 'Mail: invalid (ascii) recipient partner: no failure type, should be updated')
|
||||
self.assertEqual(mail.state, 'exception')
|
||||
self.assertEqual(notification.failure_type, 'mail_email_invalid')
|
||||
self.assertEqual(notification.notification_status, 'exception')
|
||||
|
||||
# ascii ok or just ok
|
||||
for partner in partners_valid:
|
||||
self._reset_data()
|
||||
mail.write({'recipient_ids': [(5, 0), (4, partner.id)]})
|
||||
notification.write({'res_partner_id': partner.id})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send(raise_exception=False)
|
||||
self.assertFalse(mail.failure_reason)
|
||||
self.assertFalse(mail.failure_type)
|
||||
self.assertEqual(mail.state, 'sent')
|
||||
self.assertFalse(notification.failure_reason)
|
||||
self.assertFalse(notification.failure_type)
|
||||
self.assertEqual(notification.notification_status, 'sent')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_mail_send_exceptions_recipients_partners_mixed(self):
|
||||
""" Test various use case with exceptions and errors and see how they are
|
||||
managed and stored at mail and notification level. """
|
||||
mail, notification = self.test_mail, self.test_notification
|
||||
|
||||
mail.write({'email_to': 'test@example.com'})
|
||||
partners_falsy = self.env['res.partner'].create([
|
||||
{'name': 'Name %s' % email, 'email': email}
|
||||
for email in self.emails_falsy
|
||||
])
|
||||
partners_invalid = self.env['res.partner'].create([
|
||||
{'name': 'Name %s' % email, 'email': email}
|
||||
for email in self.emails_invalid
|
||||
])
|
||||
partners_valid = self.env['res.partner'].create([
|
||||
{'name': 'Name %s' % email, 'email': email}
|
||||
for email in self.emails_valid
|
||||
])
|
||||
|
||||
# valid to, missing email for recipient or wrong email for recipient
|
||||
for partner in partners_falsy + partners_invalid:
|
||||
self._reset_data()
|
||||
mail.write({'recipient_ids': [(5, 0), (4, partner.id)]})
|
||||
notification.write({'res_partner_id': partner.id})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send(raise_exception=False)
|
||||
self.assertFalse(mail.failure_reason, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam')
|
||||
self.assertFalse(mail.failure_type, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam')
|
||||
self.assertEqual(mail.state, 'sent', 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam')
|
||||
self.assertFalse(notification.failure_reason, 'Mail: void email considered as invalid')
|
||||
self.assertEqual(notification.failure_type, 'mail_email_invalid', 'Mail: void email considered as invalid')
|
||||
self.assertEqual(notification.notification_status, 'exception')
|
||||
|
||||
# update to have valid partner and invalid partner
|
||||
mail.write({'recipient_ids': [(5, 0), (4, partners_valid[1].id), (4, partners_falsy[0].id)]})
|
||||
notification.write({'res_partner_id': partners_valid[1].id})
|
||||
notification2 = notification.create({
|
||||
'is_read': False,
|
||||
'mail_mail_id': mail.id,
|
||||
'mail_message_id': self.test_message.id,
|
||||
'notification_type': 'email',
|
||||
'res_partner_id': partners_falsy[0].id,
|
||||
})
|
||||
|
||||
# missing to / invalid to
|
||||
for email_to in self.emails_falsy + self.emails_invalid:
|
||||
self._reset_data()
|
||||
notification2.write({'failure_reason': False, 'failure_type': False, 'notification_status': 'ready'})
|
||||
mail.write({'email_to': email_to})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send(raise_exception=False)
|
||||
self.assertFalse(mail.failure_reason, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam')
|
||||
self.assertFalse(mail.failure_type, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam')
|
||||
self.assertEqual(mail.state, 'sent', 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam')
|
||||
self.assertFalse(notification.failure_reason)
|
||||
self.assertFalse(notification.failure_type)
|
||||
self.assertEqual(notification.notification_status, 'sent')
|
||||
self.assertFalse(notification2.failure_reason)
|
||||
self.assertEqual(notification2.failure_type, 'mail_email_invalid')
|
||||
self.assertEqual(notification2.notification_status, 'exception')
|
||||
|
||||
# buggy to (ascii)
|
||||
for email_to in self.emails_invalid_ascii:
|
||||
self._reset_data()
|
||||
notification2.write({'failure_reason': False, 'failure_type': False, 'notification_status': 'ready'})
|
||||
mail.write({'email_to': email_to})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send(raise_exception=False)
|
||||
|
||||
self.assertFalse(mail.failure_type, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam')
|
||||
self.assertEqual(mail.state, 'sent')
|
||||
self.assertFalse(notification.failure_type)
|
||||
self.assertEqual(notification.notification_status, 'sent')
|
||||
self.assertEqual(notification2.failure_type, 'mail_email_invalid')
|
||||
self.assertEqual(notification2.notification_status, 'exception')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_mail_send_exceptions_raise_management(self):
|
||||
""" Test various use case with exceptions and errors and see how they are
|
||||
managed and stored at mail and notification level. """
|
||||
mail, notification = self.test_mail, self.test_notification
|
||||
mail.write({'email_from': 'test.user@test.example.com', 'email_to': 'test@example.com'})
|
||||
|
||||
# SMTP connecting issues
|
||||
with self.mock_mail_gateway():
|
||||
_connect_current = self.connect_mocked.side_effect
|
||||
|
||||
# classic errors that may be raised during sending, just to test their current support
|
||||
for error, msg in [
|
||||
(smtplib.SMTPServerDisconnected('SMTPServerDisconnected'), 'SMTPServerDisconnected'),
|
||||
(smtplib.SMTPResponseException('code', 'SMTPResponseException'), 'code\nSMTPResponseException'),
|
||||
(smtplib.SMTPNotSupportedError('SMTPNotSupportedError'), 'SMTPNotSupportedError'),
|
||||
(smtplib.SMTPException('SMTPException'), 'SMTPException'),
|
||||
(SSLError('SSLError'), 'SSLError'),
|
||||
(gaierror('gaierror'), 'gaierror'),
|
||||
(timeout('timeout'), 'timeout')]:
|
||||
|
||||
def _connect(*args, **kwargs):
|
||||
raise error
|
||||
self.connect_mocked.side_effect = _connect
|
||||
|
||||
mail.send(raise_exception=False)
|
||||
self.assertEqual(mail.failure_reason, msg)
|
||||
self.assertFalse(mail.failure_type)
|
||||
self.assertEqual(mail.state, 'exception')
|
||||
self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated')
|
||||
self.assertEqual(notification.failure_type, 'mail_smtp')
|
||||
self.assertEqual(notification.notification_status, 'exception')
|
||||
self._reset_data()
|
||||
|
||||
self.connect_mocked.side_effect = _connect_current
|
||||
|
||||
# SMTP sending issues
|
||||
with self.mock_mail_gateway():
|
||||
_send_current = self.send_email_mocked.side_effect
|
||||
self._reset_data()
|
||||
mail.write({'email_to': 'test@example.com'})
|
||||
|
||||
# should always raise for those errors, even with raise_exception=False
|
||||
for error, error_class in [
|
||||
(smtplib.SMTPServerDisconnected("Some exception"), smtplib.SMTPServerDisconnected),
|
||||
(MemoryError("Some exception"), MemoryError)]:
|
||||
def _send_email(*args, **kwargs):
|
||||
raise error
|
||||
self.send_email_mocked.side_effect = _send_email
|
||||
|
||||
with self.assertRaises(error_class):
|
||||
mail.send(raise_exception=False)
|
||||
self.assertFalse(mail.failure_reason, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback')
|
||||
self.assertFalse(mail.failure_type, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback')
|
||||
self.assertEqual(mail.state, 'outgoing', 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback')
|
||||
self.assertFalse(notification.failure_reason, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback')
|
||||
self.assertFalse(notification.failure_type, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback')
|
||||
self.assertEqual(notification.notification_status, 'ready', 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback')
|
||||
|
||||
# MailDeliveryException: should be catched; other issues are sub-catched under
|
||||
# a MailDeliveryException and are catched
|
||||
for error, msg in [
|
||||
(MailDeliveryException("Some exception"), 'Some exception'),
|
||||
(ValueError("Unexpected issue"), 'Unexpected issue')]:
|
||||
def _send_email(*args, **kwargs):
|
||||
raise error
|
||||
self.send_email_mocked.side_effect = _send_email
|
||||
|
||||
self._reset_data()
|
||||
mail.send(raise_exception=False)
|
||||
self.assertEqual(mail.failure_reason, msg)
|
||||
self.assertFalse(mail.failure_type, 'Mail: unlogged failure type to fix')
|
||||
self.assertEqual(mail.state, 'exception')
|
||||
self.assertEqual(notification.failure_reason, msg)
|
||||
self.assertEqual(notification.failure_type, 'unknown', 'Mail: generic failure type')
|
||||
self.assertEqual(notification.notification_status, 'exception')
|
||||
|
||||
self.send_email_mocked.side_effect = _send_current
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_mail_send_server(self):
|
||||
"""Test that the mails are send in batch.
|
||||
|
||||
Batch are defined by the mail server and the email from field.
|
||||
"""
|
||||
self.assertEqual(self.env['ir.mail_server']._get_default_from_address(), 'notifications@test.com')
|
||||
|
||||
mail_values = {
|
||||
'body_html': '<p>Test</p>',
|
||||
'email_to': 'user@example.com',
|
||||
}
|
||||
|
||||
# Should be encapsulated in the notification email
|
||||
mails = self.env['mail.mail'].create([{
|
||||
**mail_values,
|
||||
'email_from': 'test@unknown_domain.com',
|
||||
} for _ in range(5)]) | self.env['mail.mail'].create([{
|
||||
**mail_values,
|
||||
'email_from': 'test_2@unknown_domain.com',
|
||||
} for _ in range(5)])
|
||||
|
||||
# Should use the test_2 mail server
|
||||
# Once with "user_1@test_2.com" as login
|
||||
# Once with "user_2@test_2.com" as login
|
||||
mails |= self.env['mail.mail'].create([{
|
||||
**mail_values,
|
||||
'email_from': 'user_1@test_2.com',
|
||||
} for _ in range(5)]) | self.env['mail.mail'].create([{
|
||||
**mail_values,
|
||||
'email_from': 'user_2@test_2.com',
|
||||
} for _ in range(5)])
|
||||
|
||||
# Mail server is forced
|
||||
mails |= self.env['mail.mail'].create([{
|
||||
**mail_values,
|
||||
'email_from': 'user_1@test_2.com',
|
||||
'mail_server_id': self.server_domain.id,
|
||||
} for _ in range(5)])
|
||||
|
||||
with self.mock_smtplib_connection():
|
||||
mails.send()
|
||||
|
||||
self.assertEqual(self.find_mail_server_mocked.call_count, 4, 'Must be called only once per "mail from" when the mail server is not forced')
|
||||
self.assertEqual(len(self.emails), 25)
|
||||
|
||||
# Check call to the connect method to ensure that we authenticate
|
||||
# to the right mail server with the right login
|
||||
self.assertEqual(self.connect_mocked.call_count, 4, 'Must be called once per batch which share the same mail server and the same smtp from')
|
||||
self.connect_mocked.assert_has_calls(
|
||||
calls=[
|
||||
call(smtp_from='notifications@test.com', mail_server_id=self.server_notification.id),
|
||||
call(smtp_from='user_1@test_2.com', mail_server_id=self.server_domain_2.id),
|
||||
call(smtp_from='user_2@test_2.com', mail_server_id=self.server_domain_2.id),
|
||||
call(smtp_from='user_1@test_2.com', mail_server_id=self.server_domain.id),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
|
||||
self.assert_email_sent_smtp(message_from='"test" <notifications@test.com>',
|
||||
emails_count=5, from_filter=self.server_notification.from_filter)
|
||||
self.assert_email_sent_smtp(message_from='"test_2" <notifications@test.com>',
|
||||
emails_count=5, from_filter=self.server_notification.from_filter)
|
||||
self.assert_email_sent_smtp(message_from='user_1@test_2.com', emails_count=5, from_filter=self.server_domain_2.from_filter)
|
||||
self.assert_email_sent_smtp(message_from='user_2@test_2.com', emails_count=5, from_filter=self.server_domain_2.from_filter)
|
||||
self.assert_email_sent_smtp(message_from='user_1@test_2.com', emails_count=5, from_filter=self.server_domain.from_filter)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_mail_values_email_formatted(self):
|
||||
""" Test outgoing email values, with formatting """
|
||||
customer = self.env['res.partner'].create({
|
||||
'name': 'Tony Customer',
|
||||
'email': '"Formatted Emails" <tony.customer@test.example.com>',
|
||||
})
|
||||
mail = self.env['mail.mail'].create({
|
||||
'body_html': '<p>Test</p>',
|
||||
'email_cc': '"Ignasse, le Poilu" <test.cc.1@test.example.com>',
|
||||
'email_to': '"Raoul, le Grand" <test.email.1@test.example.com>, "Micheline, l\'immense" <test.email.2@test.example.com>',
|
||||
'recipient_ids': [(4, self.user_employee.partner_id.id), (4, customer.id)]
|
||||
})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send()
|
||||
self.assertEqual(len(self._mails), 3, 'Mail: sent 3 emails: 1 for email_to, 1 / recipient')
|
||||
self.assertEqual(
|
||||
sorted(sorted(_mail['email_to']) for _mail in self._mails),
|
||||
sorted([sorted(['"Raoul, le Grand" <test.email.1@test.example.com>', '"Micheline, l\'immense" <test.email.2@test.example.com>']),
|
||||
[tools.formataddr((self.user_employee.name, self.user_employee.email_normalized))],
|
||||
[tools.formataddr(("Tony Customer", 'tony.customer@test.example.com'))]
|
||||
]),
|
||||
'Mail: formatting issues should have been removed as much as possible'
|
||||
)
|
||||
# Currently broken: CC are added to ALL emails (spammy)
|
||||
self.assertEqual(
|
||||
[_mail['email_cc'] for _mail in self._mails],
|
||||
[['"Ignasse, le Poilu" <test.cc.1@test.example.com>']] * 3,
|
||||
'Mail: currently always removing formatting in email_cc'
|
||||
)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_mail_values_email_multi(self):
|
||||
""" Test outgoing email values, with email field holding multi emails """
|
||||
# Multi
|
||||
customer = self.env['res.partner'].create({
|
||||
'name': 'Tony Customer',
|
||||
'email': 'tony.customer@test.example.com, norbert.customer@test.example.com',
|
||||
})
|
||||
mail = self.env['mail.mail'].create({
|
||||
'body_html': '<p>Test</p>',
|
||||
'email_cc': 'test.cc.1@test.example.com, test.cc.2@test.example.com',
|
||||
'email_to': 'test.email.1@test.example.com, test.email.2@test.example.com',
|
||||
'recipient_ids': [(4, self.user_employee.partner_id.id), (4, customer.id)]
|
||||
})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send()
|
||||
self.assertEqual(len(self._mails), 3, 'Mail: sent 3 emails: 1 for email_to, 1 / recipient')
|
||||
self.assertEqual(
|
||||
sorted(sorted(_mail['email_to']) for _mail in self._mails),
|
||||
sorted([sorted(['test.email.1@test.example.com', 'test.email.2@test.example.com']),
|
||||
[tools.formataddr((self.user_employee.name, self.user_employee.email_normalized))],
|
||||
sorted([tools.formataddr(("Tony Customer", 'tony.customer@test.example.com')),
|
||||
tools.formataddr(("Tony Customer", 'norbert.customer@test.example.com'))]),
|
||||
]),
|
||||
'Mail: formatting issues should have been removed as much as possible (multi emails in a single address are managed '
|
||||
'like separate emails when sending with recipient_ids'
|
||||
)
|
||||
# Currently broken: CC are added to ALL emails (spammy)
|
||||
self.assertEqual(
|
||||
[_mail['email_cc'] for _mail in self._mails],
|
||||
[['test.cc.1@test.example.com', 'test.cc.2@test.example.com']] * 3,
|
||||
)
|
||||
|
||||
# Multi + formatting
|
||||
customer = self.env['res.partner'].create({
|
||||
'name': 'Tony Customer',
|
||||
'email': 'tony.customer@test.example.com, "Norbert Customer" <norbert.customer@test.example.com>',
|
||||
})
|
||||
mail = self.env['mail.mail'].create({
|
||||
'body_html': '<p>Test</p>',
|
||||
'email_cc': 'test.cc.1@test.example.com, test.cc.2@test.example.com',
|
||||
'email_to': 'test.email.1@test.example.com, test.email.2@test.example.com',
|
||||
'recipient_ids': [(4, self.user_employee.partner_id.id), (4, customer.id)]
|
||||
})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send()
|
||||
self.assertEqual(len(self._mails), 3, 'Mail: sent 3 emails: 1 for email_to, 1 / recipient')
|
||||
self.assertEqual(
|
||||
sorted(sorted(_mail['email_to']) for _mail in self._mails),
|
||||
sorted([sorted(['test.email.1@test.example.com', 'test.email.2@test.example.com']),
|
||||
[tools.formataddr((self.user_employee.name, self.user_employee.email_normalized))],
|
||||
sorted([tools.formataddr(("Tony Customer", 'tony.customer@test.example.com')),
|
||||
tools.formataddr(("Tony Customer", 'norbert.customer@test.example.com'))]),
|
||||
]),
|
||||
'Mail: formatting issues should have been removed as much as possible (multi emails in a single address are managed '
|
||||
'like separate emails when sending with recipient_ids (and partner name is always used as name part)'
|
||||
)
|
||||
# Currently broken: CC are added to ALL emails (spammy)
|
||||
self.assertEqual(
|
||||
[_mail['email_cc'] for _mail in self._mails],
|
||||
[['test.cc.1@test.example.com', 'test.cc.2@test.example.com']] * 3,
|
||||
)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_mail_values_email_unicode(self):
|
||||
""" Unicode should be fine. """
|
||||
mail = self.env['mail.mail'].create({
|
||||
'body_html': '<p>Test</p>',
|
||||
'email_cc': 'test.😊.cc@example.com',
|
||||
'email_to': 'test.😊@example.com',
|
||||
})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send()
|
||||
self.assertEqual(len(self._mails), 1)
|
||||
self.assertEqual(self._mails[0]['email_cc'], ['test.😊.cc@example.com'])
|
||||
self.assertEqual(self._mails[0]['email_to'], ['test.😊@example.com'])
|
||||
|
||||
@users('admin')
|
||||
def test_mail_mail_values_email_uppercase(self):
|
||||
""" Test uppercase support when comparing emails, notably due to
|
||||
'send_validated_to' introduction that checks emails before sending them. """
|
||||
customer = self.env['res.partner'].create({
|
||||
'name': 'Uppercase Partner',
|
||||
'email': 'Uppercase.Partner.youpie@example.gov.uni',
|
||||
})
|
||||
for recipient_values, (exp_to, exp_cc) in zip(
|
||||
[
|
||||
{'email_to': 'Uppercase.Customer.to@example.gov.uni'},
|
||||
{'email_to': '"Formatted Customer" <Uppercase.Customer.to@example.gov.uni>'},
|
||||
{'recipient_ids': [(4, customer.id)], 'email_cc': 'Uppercase.Customer.cc@example.gov.uni'},
|
||||
], [
|
||||
(['uppercase.customer.to@example.gov.uni'], []),
|
||||
(['"Formatted Customer" <uppercase.customer.to@example.gov.uni>'], []),
|
||||
(['"Uppercase Partner" <uppercase.partner.youpie@example.gov.uni>'], ['uppercase.customer.cc@example.gov.uni']),
|
||||
]
|
||||
):
|
||||
with self.subTest(values=recipient_values):
|
||||
mail = self.env['mail.mail'].create({
|
||||
'body_html': '<p>Test</p>',
|
||||
'email_from': '"Forced From" <Forced.From@test.example.com>',
|
||||
**recipient_values,
|
||||
})
|
||||
with self.mock_mail_gateway():
|
||||
mail.send()
|
||||
self.assertSentEmail('"Forced From" <forced.from@test.example.com>', exp_to, email_cc=exp_cc)
|
||||
|
||||
|
||||
@tagged('mail_mail')
|
||||
class TestMailMailRace(common.TransactionCase):
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_bounce_during_send(self):
|
||||
self.partner = self.env['res.partner'].create({
|
||||
'name': 'Ernest Partner',
|
||||
})
|
||||
# we need to simulate a mail sent by the cron task, first create mail, message and notification by hand
|
||||
mail = self.env['mail.mail'].sudo().create({
|
||||
'body_html': '<p>Test</p>',
|
||||
'is_notification': True,
|
||||
'state': 'outgoing',
|
||||
'recipient_ids': [(4, self.partner.id)]
|
||||
})
|
||||
mail_message = mail.mail_message_id
|
||||
|
||||
message = self.env['mail.message'].create({
|
||||
'subject': 'S',
|
||||
'body': 'B',
|
||||
'subtype_id': self.ref('mail.mt_comment'),
|
||||
'notification_ids': [(0, 0, {
|
||||
'res_partner_id': self.partner.id,
|
||||
'mail_mail_id': mail.id,
|
||||
'notification_type': 'email',
|
||||
'is_read': True,
|
||||
'notification_status': 'ready',
|
||||
})],
|
||||
})
|
||||
notif = self.env['mail.notification'].search([('res_partner_id', '=', self.partner.id)])
|
||||
# we need to commit transaction or cr will keep the lock on notif
|
||||
self.cr.commit()
|
||||
|
||||
# patch send_email in order to create a concurent update and check the notif is already locked by _send()
|
||||
this = self # coding in javascript ruinned my life
|
||||
bounce_deferred = []
|
||||
@api.model
|
||||
def send_email(self, message, *args, **kwargs):
|
||||
with this.registry.cursor() as cr, mute_logger('odoo.sql_db'):
|
||||
try:
|
||||
# try ro aquire lock (no wait) on notification (should fail)
|
||||
cr.execute("SELECT notification_status FROM mail_notification WHERE id = %s FOR UPDATE NOWAIT", [notif.id])
|
||||
except psycopg2.OperationalError:
|
||||
# record already locked by send, all good
|
||||
bounce_deferred.append(True)
|
||||
else:
|
||||
# this should trigger psycopg2.extensions.TransactionRollbackError in send().
|
||||
# Only here to simulate the initial use case
|
||||
# If the record is lock, this line would create a deadlock since we are in the same thread
|
||||
# In practice, the update will wait the end of the send() transaction and set the notif as bounce, as expeced
|
||||
cr.execute("UPDATE mail_notification SET notification_status='bounce' WHERE id = %s", [notif.id])
|
||||
return message['Message-Id']
|
||||
self.env['ir.mail_server']._patch_method('send_email', send_email)
|
||||
|
||||
mail.send()
|
||||
|
||||
self.assertTrue(bounce_deferred, "The bounce should have been deferred")
|
||||
self.assertEqual(notif.notification_status, 'sent')
|
||||
|
||||
# some cleaning since we commited the cr
|
||||
self.env['ir.mail_server']._revert_method('send_email')
|
||||
|
||||
notif.unlink()
|
||||
mail.unlink()
|
||||
(mail_message | message).unlink()
|
||||
self.partner.unlink()
|
||||
self.env.cr.commit()
|
||||
|
||||
# because we committed the cursor, the savepoint of the test method is
|
||||
# gone, and this would break TransactionCase cleanups
|
||||
self.cr.execute('SAVEPOINT test_%d' % self._savepoint_id)
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
# -*- 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.tests import tagged
|
||||
|
||||
|
||||
@tagged('mail_management')
|
||||
class TestMailManagement(TestMailCommon, TestRecipients):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailManagement, cls).setUpClass()
|
||||
cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test'})
|
||||
cls._reset_mail_context(cls.test_record)
|
||||
cls.msg = cls.test_record.message_post(body='TEST BODY', author_id=cls.partner_employee.id)
|
||||
cls.notif_p1 = cls.env['mail.notification'].create({
|
||||
'author_id': cls.msg.author_id.id,
|
||||
'mail_message_id': cls.msg.id,
|
||||
'res_partner_id': cls.partner_1.id,
|
||||
'notification_type': 'email',
|
||||
'notification_status': 'exception',
|
||||
'failure_type': 'mail_smtp',
|
||||
})
|
||||
cls.notif_p2 = cls.env['mail.notification'].create({
|
||||
'author_id': cls.msg.author_id.id,
|
||||
'mail_message_id': cls.msg.id,
|
||||
'res_partner_id': cls.partner_2.id,
|
||||
'notification_type': 'email',
|
||||
'notification_status': 'bounce',
|
||||
'failure_type': 'unknown',
|
||||
})
|
||||
cls.partner_3 = cls.env['res.partner'].create({
|
||||
'name': 'Partner3',
|
||||
'email': 'partner3@example.com',
|
||||
})
|
||||
cls.notif_p3 = cls.env['mail.notification'].create({
|
||||
'author_id': cls.msg.author_id.id,
|
||||
'mail_message_id': cls.msg.id,
|
||||
'res_partner_id': cls.partner_3.id,
|
||||
'notification_type': 'email',
|
||||
'notification_status': 'sent',
|
||||
'failure_type': None,
|
||||
})
|
||||
|
||||
def test_mail_notify_cancel(self):
|
||||
self._reset_bus()
|
||||
|
||||
self.test_record.with_user(self.user_employee).notify_cancel_by_type('email')
|
||||
self.assertEqual((self.notif_p1 | self.notif_p2 | self.notif_p3).mapped('notification_status'),
|
||||
['canceled', 'canceled', 'sent'])
|
||||
|
||||
self.assertMessageBusNotifications(self.msg)
|
||||
|
|
@ -0,0 +1,307 @@
|
|||
# -*- 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.exceptions import UserError
|
||||
from odoo.tools import is_html_empty, mute_logger, formataddr
|
||||
from odoo.tests import tagged, users
|
||||
|
||||
|
||||
@tagged('mail_message')
|
||||
class TestMessageValues(TestMailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMessageValues, cls).setUpClass()
|
||||
|
||||
cls.alias_record = cls.env['mail.test.container'].with_context(cls._test_context).create({
|
||||
'name': 'Pigs',
|
||||
'alias_name': 'pigs',
|
||||
'alias_contact': 'followers',
|
||||
})
|
||||
cls.Message = cls.env['mail.message'].with_user(cls.user_employee)
|
||||
|
||||
@users('employee')
|
||||
def test_empty_message(self):
|
||||
""" Test that message is correctly considered as empty (see `_filter_empty()`).
|
||||
Message considered as empty if:
|
||||
- no body or empty body
|
||||
- AND no subtype or no subtype description
|
||||
- AND no tracking values
|
||||
- AND no attachment
|
||||
|
||||
Check _update_content behavior when voiding messages (cleanup side
|
||||
records: stars, notifications).
|
||||
"""
|
||||
note_subtype = self.env.ref('mail.mt_note')
|
||||
_attach_1 = self.env['ir.attachment'].with_user(self.user_employee).create({
|
||||
'name': 'Attach1',
|
||||
'datas': 'bWlncmF0aW9uIHRlc3Q=',
|
||||
'res_id': 0,
|
||||
'res_model': 'mail.compose.message',
|
||||
})
|
||||
record = self.env['mail.test.track'].create({'name': 'EmptyTesting'})
|
||||
self.flush_tracking()
|
||||
record.message_subscribe(partner_ids=self.partner_admin.ids, subtype_ids=note_subtype.ids)
|
||||
message = record.message_post(
|
||||
attachment_ids=_attach_1.ids,
|
||||
body='Test',
|
||||
message_type='comment',
|
||||
subtype_id=note_subtype.id,
|
||||
)
|
||||
message.write({'starred_partner_ids': [(4, self.partner_admin.id)]})
|
||||
|
||||
# check content
|
||||
self.assertEqual(len(message.attachment_ids), 1)
|
||||
self.assertFalse(is_html_empty(message.body))
|
||||
self.assertEqual(len(message.sudo().notification_ids), 1)
|
||||
self.assertEqual(message.notified_partner_ids, self.partner_admin)
|
||||
self.assertEqual(message.starred_partner_ids, self.partner_admin)
|
||||
self.assertFalse(message.sudo().tracking_value_ids)
|
||||
|
||||
# Reset body case
|
||||
record._message_update_content(message, '<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, '', [])
|
||||
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
|
||||
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)
|
||||
|
||||
# test tracking values
|
||||
record.write({'user_id': self.user_admin.id})
|
||||
self.flush_tracking()
|
||||
tracking_message = record.message_ids[0]
|
||||
self.assertFalse(tracking_message.attachment_ids)
|
||||
self.assertTrue(is_html_empty(tracking_message.body))
|
||||
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, '', [])
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_mail_message_format_access(self):
|
||||
"""
|
||||
User that doesn't have access to a record should still be able to fetch
|
||||
the record_name inside message_format.
|
||||
"""
|
||||
company_2 = self.env['res.company'].create({'name': 'Second Test Company'})
|
||||
record1 = self.env['mail.test.multi.company'].create({
|
||||
'name': 'Test1',
|
||||
'company_id': company_2.id,
|
||||
})
|
||||
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.
|
||||
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')
|
||||
|
||||
record1.write({"name": "Test2"})
|
||||
res = message.with_user(self.user_employee).message_format()
|
||||
self.assertEqual(res[0].get('record_name'), 'Test2')
|
||||
|
||||
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)
|
||||
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])
|
||||
)
|
||||
|
||||
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.models')
|
||||
@users('employee')
|
||||
def test_mail_message_values_fromto_long_name(self):
|
||||
""" Long headers may break in python if above 68 chars for certain
|
||||
DKIM verification stacks as folding is not done correctly
|
||||
(see ``_notify_get_reply_to_formatted_email`` docstring
|
||||
+ 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({
|
||||
'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
|
||||
})
|
||||
reply_to_email = f"{test_record.alias_name}@{self.alias_domain}"
|
||||
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,
|
||||
'res_id': test_record.id
|
||||
})
|
||||
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')
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_mail_message_values_fromto_no_document_values(self):
|
||||
msg = self.Message.create({
|
||||
'reply_to': 'test.reply@example.com',
|
||||
'email_from': 'test.from@example.com',
|
||||
})
|
||||
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, 'test.reply@example.com')
|
||||
self.assertEqual(msg.email_from, 'test.from@example.com')
|
||||
|
||||
@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_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()
|
||||
|
||||
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)))
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_mail_message_values_fromto_document_alias(self):
|
||||
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.user.company_id.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)))
|
||||
|
||||
# no alias domain -> author
|
||||
self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.domain')]).unlink()
|
||||
|
||||
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])
|
||||
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()
|
||||
|
||||
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)))
|
||||
|
||||
@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'})
|
||||
|
||||
msg = self.Message.create({
|
||||
'model': 'mail.test.simple',
|
||||
'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_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)))
|
||||
|
||||
@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_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,
|
||||
})
|
||||
|
||||
msg = self.Message.create({
|
||||
'model': 'mail.test.simple',
|
||||
'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_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)))
|
||||
|
||||
def test_mail_message_values_fromto_reply_to_force_new(self):
|
||||
msg = self.Message.create({
|
||||
'model': 'mail.test.container',
|
||||
'res_id': self.alias_record.id,
|
||||
'reply_to_force_new': True,
|
||||
})
|
||||
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])
|
||||
|
|
@ -0,0 +1,778 @@
|
|||
import base64
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
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
|
||||
|
||||
|
||||
class MessageAccessCommon(TestMailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.user_public = mail_new_test_user(
|
||||
cls.env,
|
||||
groups='base.group_public',
|
||||
login='bert',
|
||||
name='Bert Tartignole',
|
||||
)
|
||||
cls.user_portal = mail_new_test_user(
|
||||
cls.env,
|
||||
groups='base.group_portal',
|
||||
login='chell',
|
||||
name='Chell Gladys',
|
||||
)
|
||||
cls.user_portal_2 = mail_new_test_user(
|
||||
cls.env,
|
||||
groups='base.group_portal',
|
||||
login='portal2',
|
||||
name='Chell Gladys',
|
||||
)
|
||||
|
||||
(
|
||||
cls.record_public, cls.record_portal, cls.record_portal_ro,
|
||||
cls.record_followers,
|
||||
cls.record_internal, cls.record_internal_ro,
|
||||
cls.record_admin
|
||||
) = cls.env['mail.test.access'].create([
|
||||
{'access': 'public', 'name': 'Public Record'},
|
||||
{'access': 'logged', 'name': 'Portal Record'},
|
||||
{'access': 'logged_ro', 'name': 'Portal RO Record'},
|
||||
{'access': 'followers', 'name': 'Followers Record'},
|
||||
{'access': 'internal', 'name': 'Internal Record'},
|
||||
{'access': 'internal_ro', 'name': 'Internal Readonly Record'},
|
||||
{'access': 'admin', 'name': 'Admin Record'},
|
||||
])
|
||||
for record in (cls.record_public + cls.record_portal + cls.record_portal_ro + cls.record_followers +
|
||||
cls.record_internal + cls.record_internal_ro + cls.record_admin):
|
||||
record.message_post(
|
||||
body='Test Comment',
|
||||
message_type='comment',
|
||||
subtype_id=cls.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
record.message_post(
|
||||
body='Test Answer',
|
||||
message_type='comment',
|
||||
subtype_id=cls.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
|
||||
|
||||
@tagged('mail_message', 'security', 'post_install', '-at_install')
|
||||
class TestMailMessageAccess(MessageAccessCommon):
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule')
|
||||
def test_assert_initial_values(self):
|
||||
""" Just ensure tests data """
|
||||
for record in (
|
||||
self.record_public + self.record_portal + self.record_portal_ro + self.record_followers +
|
||||
self.record_internal + self.record_internal_ro + self.record_admin):
|
||||
self.assertFalse(record.message_follower_ids)
|
||||
self.assertEqual(len(record.message_ids), 3)
|
||||
|
||||
for index, msg in enumerate(record.message_ids):
|
||||
body = ['<p>Test Answer</p>', '<p>Test Comment</p>', '<p>Mail Access Test created</p>'][index]
|
||||
message_type = ['comment', 'comment', 'notification'][index]
|
||||
subtype_id = [self.env.ref('mail.mt_comment'), self.env.ref('mail.mt_comment'), self.env.ref('mail.mt_note')][index]
|
||||
self.assertEqual(msg.author_id, self.partner_root)
|
||||
self.assertEqual(msg.body, body)
|
||||
self.assertEqual(msg.message_type, message_type)
|
||||
self.assertFalse(msg.notified_partner_ids)
|
||||
self.assertFalse(msg.partner_ids)
|
||||
self.assertEqual(msg.subtype_id, subtype_id)
|
||||
|
||||
# public user access check
|
||||
for allowed in self.record_public:
|
||||
allowed.with_user(self.user_public).read(['name'])
|
||||
for forbidden in self.record_portal + self.record_portal_ro + self.record_followers + self.record_internal + self.record_internal_ro + self.record_admin:
|
||||
with self.assertRaises(AccessError):
|
||||
forbidden.with_user(self.user_public).read(['name'])
|
||||
for forbidden in self.record_public + self.record_portal + self.record_portal_ro + self.record_followers + self.record_internal + self.record_internal_ro + self.record_admin:
|
||||
with self.assertRaises(AccessError):
|
||||
forbidden.with_user(self.user_public).write({'name': 'Update'})
|
||||
|
||||
# portal user access check
|
||||
for allowed in self.record_public + self.record_portal + self.record_portal_ro:
|
||||
allowed.with_user(self.user_portal).read(['name'])
|
||||
for forbidden in self.record_internal + self.record_internal_ro + self.record_admin:
|
||||
with self.assertRaises(AccessError):
|
||||
forbidden.with_user(self.user_portal).read(['name'])
|
||||
for allowed in self.record_portal:
|
||||
allowed.with_user(self.user_portal).write({'name': 'Update'})
|
||||
for forbidden in self.record_public + self.record_portal_ro + self.record_followers + self.record_internal + self.record_internal_ro + self.record_admin:
|
||||
with self.assertRaises(AccessError):
|
||||
forbidden.with_user(self.user_portal).write({'name': 'Update'})
|
||||
self.record_followers.message_subscribe(self.user_portal.partner_id.ids)
|
||||
self.record_followers.with_user(self.user_portal).read(['name'])
|
||||
self.record_followers.with_user(self.user_portal).write({'name': 'Update'})
|
||||
|
||||
# internal user access check
|
||||
for allowed in self.record_public + self.record_portal + self.record_portal_ro + self.record_followers + self.record_internal + self.record_internal_ro:
|
||||
allowed.with_user(self.user_employee).read(['name'])
|
||||
for forbidden in self.record_admin:
|
||||
with self.assertRaises(AccessError):
|
||||
forbidden.with_user(self.user_employee).read(['name'])
|
||||
for allowed in self.record_public + self.record_portal + self.record_portal_ro + self.record_followers + self.record_internal:
|
||||
allowed.with_user(self.user_employee).write({'name': 'Update'})
|
||||
for forbidden in self.record_internal_ro + self.record_admin:
|
||||
with self.assertRaises(AccessError):
|
||||
forbidden.with_user(self.user_employee).write({'name': 'Update'})
|
||||
|
||||
# elevated user access check
|
||||
for allowed in self.record_public + self.record_portal + self.record_portal_ro + self.record_followers + self.record_internal + self.record_internal_ro + self.record_admin:
|
||||
allowed.with_user(self.user_admin).read(['name'])
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# CREATE
|
||||
# - Criterions
|
||||
# - "private message" (no model, no res_id) -> deprecated
|
||||
# - follower of document
|
||||
# - document-based (write or create, using '_get_mail_message_access'
|
||||
# hence '_mail_post_access' by default)
|
||||
# - notified of parent message
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_rule')
|
||||
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.write({'partner_ids': [(4, self.user_employee.partner_id.id)]})
|
||||
|
||||
# prepare 'followers' condition
|
||||
record_admin_fol = self.env['mail.test.access'].create({
|
||||
'access': 'admin',
|
||||
'name': 'Admin Record Follower',
|
||||
})
|
||||
record_admin_fol.message_subscribe(self.user_employee.partner_id.ids)
|
||||
|
||||
for record, msg_vals, should_crash, reason in [
|
||||
# private-like
|
||||
(self.env["mail.test.access"], {}, False, 'Private message like is ok'),
|
||||
# document based
|
||||
(self.record_internal, {}, False, 'W Access on record'),
|
||||
(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, {
|
||||
'reply_to': 'avoid.catchall@my.test.com', # otherwise crashes
|
||||
}, False, 'Followers > no access on record'),
|
||||
# parent based
|
||||
(self.record_admin, { # note: force reply_to normally computed by message_post avoiding ACLs issues
|
||||
'parent_id': admin_msg.id,
|
||||
}, False, 'No access on record but reply to notified parent'),
|
||||
]:
|
||||
with self.subTest(record=record, msg_vals=msg_vals, reason=reason):
|
||||
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,
|
||||
})
|
||||
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,
|
||||
)
|
||||
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,
|
||||
})
|
||||
if record:
|
||||
# TDE note: due to parent_id flattening, doing message_post
|
||||
# with parent_id which should allow posting crashes, as
|
||||
# parent_id is changed to an older message the employee cannot
|
||||
# access. Won't fix that in stable.
|
||||
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,
|
||||
)
|
||||
|
||||
def test_access_create_customized(self):
|
||||
""" Test '_get_mail_message_access' 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'
|
||||
record.write({'is_locked': True})
|
||||
for user in self.user_employee + self.user_portal:
|
||||
with self.assertRaises(AccessError):
|
||||
_message_portal = record.with_user(self.user_portal).message_post(
|
||||
body='Another portal message',
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
|
||||
def test_access_create_mail_post_access(self):
|
||||
""" Test 'mail_post_access' support that allows creating a message with
|
||||
other rights than 'write' access on document """
|
||||
for post_value, should_crash in [
|
||||
('read', False),
|
||||
('write', True),
|
||||
]:
|
||||
with self.subTest(post_value=post_value):
|
||||
with patch.object(MailTestAccess, '_mail_post_access', post_value):
|
||||
if should_crash:
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.message'].with_user(self.user_employee).create({
|
||||
'model': self.record_internal_ro._name,
|
||||
'res_id': self.record_internal_ro.id,
|
||||
'body': 'Test',
|
||||
})
|
||||
else:
|
||||
self.env['mail.message'].with_user(self.user_employee).create({
|
||||
'model': self.record_internal_ro._name,
|
||||
'res_id': self.record_internal_ro.id,
|
||||
'body': 'Test',
|
||||
})
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_rule')
|
||||
def test_access_create_portal(self):
|
||||
""" Test group_portal creation rules """
|
||||
# prepare 'notified of parent' condition
|
||||
admin_msg = self.record_admin.message_ids[-1]
|
||||
admin_msg.write({'partner_ids': [(4, self.user_portal.partner_id.id)]})
|
||||
|
||||
# prepare 'followers' condition
|
||||
record_admin_fol = self.env['mail.test.access'].create({
|
||||
'access': 'admin',
|
||||
'name': 'Admin Record',
|
||||
})
|
||||
record_admin_fol.message_subscribe(self.user_portal.partner_id.ids)
|
||||
|
||||
for record, msg_vals, should_crash, reason in [
|
||||
# private-like
|
||||
(self.env["mail.test.access"], {}, False, 'Private message like is ok'),
|
||||
# document based
|
||||
(self.record_portal, {}, False, 'W Access on record'),
|
||||
(self.record_portal_ro, {}, True, 'No W Access on record'),
|
||||
(self.record_internal, {}, True, 'No R/W Access on record'),
|
||||
(record_admin_fol, {
|
||||
'reply_to': 'avoid.catchall@my.test.com', # otherwise crashes
|
||||
}, False, 'Followers > no access on record'),
|
||||
# parent based
|
||||
(self.record_admin, {
|
||||
'parent_id': admin_msg.id,
|
||||
}, False, 'No access on record but reply to notified parent'),
|
||||
]:
|
||||
with self.subTest(record=record, msg_vals=msg_vals, reason=reason):
|
||||
if should_crash:
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.message'].with_user(self.user_portal).create({
|
||||
'model': record._name if record else False,
|
||||
'res_id': record.id if record else False,
|
||||
'body': 'Test',
|
||||
**msg_vals,
|
||||
})
|
||||
else:
|
||||
_message = self.env['mail.message'].with_user(self.user_portal).create({
|
||||
'model': record._name if record else False,
|
||||
'res_id': record.id if record else False,
|
||||
'body': 'Test',
|
||||
**msg_vals,
|
||||
})
|
||||
|
||||
# check '_mail_post_access', reducing W to R
|
||||
with patch.object(MailTestAccess, '_mail_post_access', 'read'):
|
||||
_message = self.env['mail.message'].with_user(self.user_portal).create({
|
||||
'model': self.record_portal._name,
|
||||
'res_id': self.record_portal.id,
|
||||
'body': 'Test',
|
||||
})
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule')
|
||||
def test_access_create_public(self):
|
||||
""" Public can never create messages """
|
||||
for record in [
|
||||
self.env['mail.test.access'], # old private message: no document
|
||||
self.record_public, # read access
|
||||
self.record_portal, # read access
|
||||
]:
|
||||
with self.subTest(record=record):
|
||||
# can never create message, simple
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.message'].with_user(self.user_public).create({
|
||||
'model': record._name if record else False,
|
||||
'res_id': record.id if record else False,
|
||||
'body': 'Test',
|
||||
})
|
||||
|
||||
@mute_logger('odoo.tests')
|
||||
def test_access_create_wo_parent_access(self):
|
||||
""" Purpose is to test posting a message on a record whose first message / parent
|
||||
is not accessible by current user. This cause issues notably when computing
|
||||
references, checking ancestors message_ids. """
|
||||
test_record = self.env['mail.test.simple'].with_context(self._test_context).create({
|
||||
'email_from': 'ignasse@example.com',
|
||||
'name': 'Test',
|
||||
})
|
||||
partner_1 = self.env['res.partner'].create({
|
||||
'name': 'Not Jitendra Prajapati',
|
||||
'email': 'not.jitendra@mycompany.example.com',
|
||||
})
|
||||
test_record.message_subscribe((partner_1 | self.user_admin.partner_id).ids)
|
||||
|
||||
message = test_record.message_post(
|
||||
body='<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'])
|
||||
|
||||
with patch.object(MailTestSimple, 'check_access_rights', return_value=True):
|
||||
with self.assertRaises(AccessError):
|
||||
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
|
||||
with self.mock_mail_gateway():
|
||||
new_msg = test_record.with_user(self.user_portal).message_post(
|
||||
body='<p>This is Second Message</p>',
|
||||
subject='Subject',
|
||||
parent_id=message.id,
|
||||
mail_auto_delete=False,
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
self.assertEqual(new_msg.sudo().parent_id, message)
|
||||
|
||||
new_mail = self.env['mail.mail'].sudo().search([
|
||||
('mail_message_id', '=', new_msg.id),
|
||||
])
|
||||
self.assertEqual(
|
||||
new_mail.references, f'{message.message_id} {new_msg.message_id}',
|
||||
'References should not include message parent message_id, even if internal note, to help thread formation')
|
||||
self.assertTrue(new_mail)
|
||||
self.assertEqual(new_msg.parent_id, message)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# READ
|
||||
# - Criterions
|
||||
# - author
|
||||
# - recipients / notified
|
||||
# - document-based: read, using '_get_mail_message_access'
|
||||
# - share users: limited to 'not internal' (flag or subtype)
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def test_access_read(self):
|
||||
""" Read access check for internal users. """
|
||||
for msg, msg_vals, should_crash, reason in [
|
||||
# document based
|
||||
(self.record_internal.message_ids[0], {}, False, 'R Access on record'),
|
||||
(self.record_internal_ro.message_ids[0], {}, False, 'R Access on record'),
|
||||
(self.record_admin.message_ids[0], {}, True, 'No access on record'),
|
||||
# author
|
||||
(self.record_admin.message_ids[0], {
|
||||
'author_id': self.user_employee.partner_id.id,
|
||||
}, False, 'Author > no access on record'),
|
||||
# notified
|
||||
(self.record_admin.message_ids[0], {
|
||||
'notification_ids': [(0, 0, {
|
||||
'res_partner_id': self.user_employee.partner_id.id,
|
||||
})],
|
||||
}, False, 'Notified > no access on record'),
|
||||
(self.record_admin.message_ids[0], {
|
||||
'partner_ids': [(4, self.user_employee.partner_id.id)],
|
||||
}, False, 'Recipients > no access on record'),
|
||||
]:
|
||||
original_vals = {
|
||||
'author_id': msg.author_id.id,
|
||||
'notification_ids': [(6, 0, {})],
|
||||
'parent_id': msg.parent_id.id,
|
||||
}
|
||||
with self.subTest(msg=msg, reason=reason):
|
||||
if msg_vals:
|
||||
msg.write(msg_vals)
|
||||
if should_crash:
|
||||
with self.assertRaises(AccessError):
|
||||
msg.with_user(self.user_employee).read(['body'])
|
||||
else:
|
||||
msg.with_user(self.user_employee).read(['body'])
|
||||
if msg_vals:
|
||||
msg.write(original_vals)
|
||||
|
||||
def test_access_read_portal(self):
|
||||
""" Read access check for portal users """
|
||||
for msg, msg_vals, should_crash, reason in [
|
||||
# document based
|
||||
(self.record_portal.message_ids[0], {}, False, 'Access on record'),
|
||||
(self.record_internal.message_ids[0], {}, True, 'No access on record'),
|
||||
# author
|
||||
(self.record_internal.message_ids[0], {
|
||||
'author_id': self.user_portal.partner_id.id,
|
||||
}, False, 'Author > no access on record'),
|
||||
# notified
|
||||
(self.record_admin.message_ids[0], {
|
||||
'notification_ids': [(0, 0, {
|
||||
'res_partner_id': self.user_portal.partner_id.id,
|
||||
})],
|
||||
}, False, 'Notified > no access on record'),
|
||||
# forbidden
|
||||
(self.record_portal.message_ids[0], {
|
||||
'subtype_id': self.env.ref('mail.mt_note').id,
|
||||
}, True, 'Note cannot be read by portal users'),
|
||||
(self.record_portal.message_ids[0], {
|
||||
'is_internal': True,
|
||||
}, True, 'Internal message cannot be read by portal users'),
|
||||
]:
|
||||
original_vals = {
|
||||
'author_id': msg.author_id.id,
|
||||
'is_internal': False,
|
||||
'notification_ids': [(6, 0, {})],
|
||||
'parent_id': msg.parent_id.id,
|
||||
'subtype_id': self.env.ref('mail.mt_comment').id,
|
||||
}
|
||||
with self.subTest(msg=msg, reason=reason):
|
||||
if msg_vals:
|
||||
msg.write(msg_vals)
|
||||
if should_crash:
|
||||
with self.assertRaises(AccessError):
|
||||
msg.with_user(self.user_portal).read(['body'])
|
||||
else:
|
||||
msg.with_user(self.user_portal).read(['body'])
|
||||
if msg_vals:
|
||||
msg.write(original_vals)
|
||||
|
||||
def test_access_read_public(self):
|
||||
""" Read access check for public users """
|
||||
for msg, msg_vals, should_crash, reason in [
|
||||
# document based
|
||||
(self.record_public.message_ids[0], {}, False, 'Access on record'),
|
||||
(self.record_portal.message_ids[0], {}, True, 'No access on record'),
|
||||
# author
|
||||
(self.record_internal.message_ids[0], {
|
||||
'author_id': self.user_public.partner_id.id,
|
||||
}, False, 'Author > no access on record'),
|
||||
# notified
|
||||
(self.record_admin.message_ids[0], {
|
||||
'notification_ids': [(0, 0, {
|
||||
'res_partner_id': self.user_public.partner_id.id,
|
||||
})],
|
||||
}, False, 'Notified > no access on record'),
|
||||
# forbidden
|
||||
(self.record_public.message_ids[0], {
|
||||
'subtype_id': self.env.ref('mail.mt_note').id,
|
||||
}, True, 'Note cannot be read by public users'),
|
||||
(self.record_public.message_ids[0], {
|
||||
'is_internal': True,
|
||||
}, True, 'Internal message cannot be read by public users'),
|
||||
]:
|
||||
original_vals = {
|
||||
'author_id': msg.author_id.id,
|
||||
'is_internal': False,
|
||||
'notification_ids': [(6, 0, {})],
|
||||
'parent_id': msg.parent_id.id,
|
||||
'subtype_id': self.env.ref('mail.mt_comment').id,
|
||||
}
|
||||
with self.subTest(msg=msg, reason=reason):
|
||||
if msg_vals:
|
||||
msg.write(msg_vals)
|
||||
if should_crash:
|
||||
with self.assertRaises(AccessError):
|
||||
msg.with_user(self.user_public).read(['body'])
|
||||
else:
|
||||
msg.with_user(self.user_public).read(['body'])
|
||||
if msg_vals:
|
||||
msg.write(original_vals)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# UNLINK
|
||||
# - Criterion: document-based (write or create), using '_get_mail_message_access'
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def test_access_unlink(self):
|
||||
""" Unlink access check for internal users """
|
||||
for msg, msg_vals, should_crash, reason in [
|
||||
# document based
|
||||
(self.record_portal.message_ids[0], {}, False, 'W Access on record'),
|
||||
(self.record_internal_ro.message_ids[0], {}, True, 'R Access on record'),
|
||||
# notified
|
||||
(self.record_admin.message_ids[0], {
|
||||
'notification_ids': [(0, 0, {
|
||||
'res_partner_id': self.user_portal.partner_id.id,
|
||||
})],
|
||||
}, True, 'Even notified, cannot remove'),
|
||||
]:
|
||||
with self.subTest(msg=msg, reason=reason):
|
||||
if msg_vals:
|
||||
msg.write(msg_vals)
|
||||
if should_crash:
|
||||
with self.assertRaises(AccessError):
|
||||
msg.with_user(self.user_portal).unlink()
|
||||
else:
|
||||
msg.with_user(self.user_portal).unlink()
|
||||
|
||||
def test_access_unlink_portal(self):
|
||||
""" Unlink access check for portal users. """
|
||||
for msg, msg_vals, should_crash, reason in [
|
||||
# document based
|
||||
(self.record_portal.message_ids[0], {}, False, 'W Access on record but unlink limited'),
|
||||
(self.record_portal_ro.message_ids[0], {}, True, 'R Access on record'),
|
||||
# notified
|
||||
(self.record_admin.message_ids[0], {
|
||||
'notification_ids': [(0, 0, {
|
||||
'res_partner_id': self.user_portal.partner_id.id,
|
||||
})],
|
||||
}, True, 'Even notified, cannot remove'),
|
||||
]:
|
||||
with self.subTest(msg=msg, reason=reason):
|
||||
if msg_vals:
|
||||
msg.write(msg_vals)
|
||||
if should_crash:
|
||||
with self.assertRaises(AccessError):
|
||||
msg.with_user(self.user_portal).unlink()
|
||||
else:
|
||||
msg.with_user(self.user_portal).unlink()
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# WRITE
|
||||
# - Criterions
|
||||
# - author
|
||||
# - recipients / notified
|
||||
# - document-based (write or create), using '_get_mail_message_access'
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def test_access_write(self):
|
||||
""" Test updating message content as internal user """
|
||||
for msg, msg_vals, should_crash, reason in [
|
||||
# document based
|
||||
(self.record_internal.message_ids[0], {}, False, 'W Access on record'),
|
||||
(self.record_internal_ro.message_ids[0], {}, True, 'No W Access on record'),
|
||||
(self.record_admin.message_ids[0], {}, True, 'No access on record'),
|
||||
# author
|
||||
(self.record_admin.message_ids[0], {
|
||||
'author_id': self.user_employee.partner_id.id,
|
||||
}, False, 'Author > no access on record'),
|
||||
# notified
|
||||
(self.record_admin.message_ids[0], {
|
||||
'notification_ids': [(0, 0, {
|
||||
'res_partner_id': self.user_employee.partner_id.id,
|
||||
})],
|
||||
}, False, 'Notified > no access on record'),
|
||||
]:
|
||||
original_vals = {
|
||||
'author_id': msg.author_id.id,
|
||||
'notification_ids': [(6, 0, {})],
|
||||
'parent_id': msg.parent_id.id,
|
||||
}
|
||||
with self.subTest(msg=msg, reason=reason):
|
||||
if msg_vals:
|
||||
msg.write(msg_vals)
|
||||
if should_crash:
|
||||
with self.assertRaises(AccessError):
|
||||
msg.with_user(self.user_employee).write({'body': 'Update'})
|
||||
else:
|
||||
msg.with_user(self.user_employee).write({'body': 'Update'})
|
||||
if msg_vals:
|
||||
msg.write(original_vals)
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_rule')
|
||||
def test_access_write_envelope(self):
|
||||
""" 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
|
||||
# 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})
|
||||
# To change in 18+
|
||||
message.write({'notification_ids': [
|
||||
(0, 0, {'res_partner_id': self.user_portal_2.partner_id.id})
|
||||
]})
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_rule')
|
||||
def test_access_write_portal_notification(self):
|
||||
""" Test updating message notification content as portal user """
|
||||
self.record_followers.message_subscribe(self.user_portal.partner_id.ids)
|
||||
test_record = self.record_followers.with_user(self.user_portal)
|
||||
test_record.read(['name'])
|
||||
with self.assertRaises(AccessError):
|
||||
test_record.with_user(self.user_portal_2).read(['name'])
|
||||
message = test_record.message_ids[0].with_user(self.user_portal)
|
||||
message.write({'body': 'Updated'})
|
||||
with self.assertRaises(AccessError):
|
||||
message.with_user(self.user_portal_2).read(['subject'])
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# SEARCH
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def test_search(self):
|
||||
""" Test custom 'search' implemented on 'mail.message' that replicates
|
||||
custom rules defined on 'read' override """
|
||||
base_msg_vals = {
|
||||
'message_type': 'comment',
|
||||
'model': self.record_internal._name,
|
||||
'res_id': self.record_internal.id,
|
||||
'subject': '_ZTest',
|
||||
}
|
||||
|
||||
msgs = self.env['mail.message'].create([
|
||||
dict(base_msg_vals,
|
||||
body='Private Comment (mention portal)',
|
||||
model=False,
|
||||
partner_ids=[(4, self.user_portal.partner_id.id)],
|
||||
res_id=False,
|
||||
subtype_id=self.ref('mail.mt_comment'),
|
||||
),
|
||||
dict(base_msg_vals,
|
||||
body='Internal Log',
|
||||
subtype_id=False,
|
||||
),
|
||||
dict(base_msg_vals,
|
||||
body='Internal Note',
|
||||
subtype_id=self.ref('mail.mt_note'),
|
||||
),
|
||||
dict(base_msg_vals,
|
||||
body='Internal Comment (mention portal)',
|
||||
partner_ids=[(4, self.user_portal.partner_id.id)],
|
||||
subtype_id=self.ref('mail.mt_comment'),
|
||||
),
|
||||
dict(base_msg_vals,
|
||||
body='Internal Comment (mention employee)',
|
||||
partner_ids=[(4, self.user_employee.partner_id.id)],
|
||||
subtype_id=self.ref('mail.mt_comment'),
|
||||
),
|
||||
dict(base_msg_vals,
|
||||
body='Internal Comment',
|
||||
subtype_id=self.ref('mail.mt_comment'),
|
||||
),
|
||||
])
|
||||
msg_record_admin = self.env['mail.message'].create(dict(base_msg_vals,
|
||||
body='Admin Comment',
|
||||
model=self.record_admin._name,
|
||||
res_id=self.record_admin.id,
|
||||
subtype_id=self.ref('mail.mt_comment'),
|
||||
))
|
||||
msg_record_portal = self.env['mail.message'].create(dict(base_msg_vals,
|
||||
body='Portal Comment',
|
||||
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'),
|
||||
))
|
||||
|
||||
for (test_user, add_domain), exp_messages in zip([
|
||||
(self.user_public, []),
|
||||
(self.user_portal, []),
|
||||
(self.user_employee, []),
|
||||
(self.user_employee, [('body', 'ilike', 'Internal')]),
|
||||
(self.user_admin, []),
|
||||
], [
|
||||
msg_record_public,
|
||||
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
|
||||
]):
|
||||
with self.subTest(test_user=test_user.name, add_domain=add_domain):
|
||||
domain = [('subject', 'like', '_ZTest')] + add_domain
|
||||
self.assertEqual(self.env['mail.message'].with_user(test_user).search(domain), exp_messages)
|
||||
|
||||
|
||||
@tagged('mail_message', 'security', 'post_install', '-at_install')
|
||||
class TestMessageSubModelAccess(MessageAccessCommon):
|
||||
|
||||
def test_ir_attachment_read_message_notification(self):
|
||||
message = self.record_admin.message_ids[0]
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'datas': base64.b64encode(b'My attachment'),
|
||||
'name': 'doc.txt',
|
||||
'res_model': message._name,
|
||||
'res_id': message.id})
|
||||
# attach the attachment to the message
|
||||
message.write({'attachment_ids': [(4, attachment.id)]})
|
||||
message.write({'partner_ids': [(4, self.user_employee.partner_id.id)]})
|
||||
message.with_user(self.user_employee).read()
|
||||
# Test: Employee has access to attachment, ok because they can read message
|
||||
attachment.with_user(self.user_employee).read(['name', 'datas'])
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule')
|
||||
def test_mail_follower(self):
|
||||
""" Read access check on sub entities of mail.message """
|
||||
internal_record = self.record_internal.with_user(self.user_employee)
|
||||
internal_record.message_subscribe(
|
||||
partner_ids=self.user_portal.partner_id.ids
|
||||
)
|
||||
|
||||
# employee can access
|
||||
follower = internal_record.message_follower_ids.filtered(
|
||||
lambda f: f.partner_id == self.user_portal.partner_id
|
||||
)
|
||||
self.assertTrue(follower)
|
||||
with self.assertRaises(AccessError):
|
||||
follower.with_user(self.user_portal).read(['partner_id'])
|
||||
|
||||
# employee cannot update
|
||||
with self.assertRaises(AccessError):
|
||||
follower.write({'partner_id': self.user_admin.partner_id.id})
|
||||
follower.with_user(self.user_admin).write({'partner_id': self.user_admin.partner_id.id})
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_rule')
|
||||
def test_mail_notification(self):
|
||||
""" Limit update of notifications for internal users """
|
||||
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_portal.partner_id + self.user_employee.partner_id).ids,
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
notifications = message.with_user(self.user_employee).notification_ids
|
||||
self.assertEqual(len(notifications), 2)
|
||||
self.assertTrue(bool(notifications.read(['is_read'])), 'Internal can read')
|
||||
|
||||
notif_other = notifications.filtered(lambda n: n.res_partner_id == self.user_portal.partner_id)
|
||||
with self.assertRaises(AccessError):
|
||||
notif_other.write({'is_read': True})
|
||||
|
||||
notif_own = notifications.filtered(lambda n: n.res_partner_id == self.user_employee.partner_id)
|
||||
notif_own.write({'is_read': True})
|
||||
# with self.assertRaises(AccessError):
|
||||
# notif_own.write({'author_id': self.user_portal.partner_id.id})
|
||||
with self.assertRaises(AccessError):
|
||||
notif_own.write({'mail_message_id': self.record_internal.message_ids[0]})
|
||||
with self.assertRaises(AccessError):
|
||||
notif_own.write({'res_partner_id': self.user_admin.partner_id.id})
|
||||
|
||||
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))
|
||||
portal_record = self.record_portal.with_user(self.user_portal)
|
||||
message = portal_record.message_post(
|
||||
body='Hello People',
|
||||
message_type='comment',
|
||||
partner_ids=(self.user_portal_2.partner_id + self.user_employee.partner_id).ids,
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
notifications = message.notification_ids
|
||||
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)
|
||||
|
|
@ -0,0 +1,436 @@
|
|||
# -*- 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 unittest.mock import patch
|
||||
from werkzeug.urls import url_parse, url_decode
|
||||
|
||||
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.tests import tagged, users, HttpCase
|
||||
from odoo.tools import formataddr, mute_logger
|
||||
|
||||
|
||||
@tagged('multi_company')
|
||||
class TestMultiCompanySetup(TestMailCommon, TestRecipients):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMultiCompanySetup, cls).setUpClass()
|
||||
cls._activate_multi_company()
|
||||
|
||||
cls.test_model = cls.env['ir.model']._get('mail.test.gateway')
|
||||
cls.email_from = '"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>'
|
||||
|
||||
cls.test_record = cls.env['mail.test.gateway'].with_context(cls._test_context).create({
|
||||
'name': 'Test',
|
||||
'email_from': 'ignasse@example.com',
|
||||
}).with_context({})
|
||||
cls.test_records_mc = cls.env['mail.test.multi.company'].create([
|
||||
{'name': 'Test Company1',
|
||||
'company_id': cls.user_employee.company_id.id},
|
||||
{'name': 'Test Company2',
|
||||
'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,
|
||||
'alias_model_id': cls.test_model.id,
|
||||
'alias_contact': 'everyone'})
|
||||
|
||||
# Set a first message on public group to test update and hierarchy
|
||||
cls.fake_email = cls.env['mail.message'].create({
|
||||
'model': 'mail.test.gateway',
|
||||
'res_id': cls.test_record.id,
|
||||
'subject': 'Public Discussion',
|
||||
'message_type': 'email',
|
||||
'subtype_id': cls.env.ref('mail.mt_comment').id,
|
||||
'author_id': cls.partner_1.id,
|
||||
'message_id': '<123456-openerp-%s-mail.test.gateway@%s>' % (cls.test_record.id, socket.gethostname()),
|
||||
})
|
||||
|
||||
def setUp(self):
|
||||
super(TestMultiCompanySetup, self).setUp()
|
||||
# patch registry to simulate a ready environment
|
||||
self.patch(self.env.registry, 'ready', True)
|
||||
self.flush_tracking()
|
||||
|
||||
@users('employee_c2')
|
||||
@mute_logger('odoo.addons.base.models.ir_rule')
|
||||
def test_post_with_read_access(self):
|
||||
""" Check that with readonly access, a message with attachment can be
|
||||
posted on a model with the attribute _mail_post_access = 'read'. """
|
||||
test_record_c1_su = self.env['mail.test.multi.company.read'].sudo().create([
|
||||
{
|
||||
'company_id': self.user_employee.company_id.id,
|
||||
'name': 'MC Readonly',
|
||||
}
|
||||
])
|
||||
test_record_c1 = test_record_c1_su.with_env(self.env)
|
||||
self.assertFalse(test_record_c1.message_main_attachment_id)
|
||||
|
||||
self.assertEqual(test_record_c1.name, 'MC Readonly')
|
||||
with self.assertRaises(AccessError):
|
||||
test_record_c1.write({'name': 'Cannot Write'})
|
||||
|
||||
message = test_record_c1.message_post(
|
||||
attachments=[('testAttachment', b'Test attachment')],
|
||||
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.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'),
|
||||
'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')],
|
||||
attachment_ids=new_attach.ids,
|
||||
body='My Body',
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(message.attachment_ids.mapped('name')),
|
||||
['TestAttachmentIDS.txt', 'testAttachment'],
|
||||
)
|
||||
self.assertEqual(test_record_c1.message_main_attachment_id, first_attachment)
|
||||
|
||||
@users('employee_c2')
|
||||
@mute_logger('odoo.addons.base.models.ir_rule')
|
||||
def test_post_wo_access(self):
|
||||
test_records_mc_c1, test_records_mc_c2 = self.test_records_mc.with_env(self.env)
|
||||
attachments_data = [
|
||||
('ReportLike1', 'AttContent1'),
|
||||
('ReportLike2', 'AttContent2'),
|
||||
]
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Other company (no access)
|
||||
# ------------------------------------------------------------
|
||||
|
||||
_original_car = Message.check_access_rule
|
||||
with patch.object(Message, 'check_access_rule',
|
||||
autospec=True, side_effect=_original_car) as mock_msg_car:
|
||||
with self.assertRaises(AccessError):
|
||||
test_records_mc_c1.message_post(
|
||||
body='<p>Hello</p>',
|
||||
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')
|
||||
with self.assertRaises(AccessError):
|
||||
_name = test_records_mc_c1.name
|
||||
|
||||
# no access to company1, access to post through being notified of parent
|
||||
with self.assertRaises(AccessError):
|
||||
_subject = test_records_mc_c1.message_ids.subject
|
||||
self.assertEqual(len(self.test_records_mc[0].message_ids), 1)
|
||||
initial_message = self.test_records_mc[0].message_ids
|
||||
|
||||
self.env['mail.notification'].sudo().create({
|
||||
'mail_message_id': initial_message.id,
|
||||
'notification_status': 'sent',
|
||||
'res_partner_id': self.user_employee_c2.partner_id.id,
|
||||
})
|
||||
# additional: works only if in partner_ids, not notified via followers
|
||||
initial_message.write({
|
||||
'partner_ids': [(4, self.user_employee_c2.partner_id.id)],
|
||||
})
|
||||
# now able to post as was notified of parent message
|
||||
test_records_mc_c1.message_post(
|
||||
body='<p>Hello</p>',
|
||||
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,
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# User company (access granted)
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# can effectively link attachments with message to record of writable record
|
||||
attachments = self.env['ir.attachment'].create(
|
||||
self._generate_attachments_data(
|
||||
2, 'mail.compose.message', 0,
|
||||
prefix='Same'
|
||||
)
|
||||
)
|
||||
message = test_records_mc_c2.message_post(
|
||||
attachments=attachments_data,
|
||||
attachment_ids=attachments.ids,
|
||||
body='<p>Hello</p>',
|
||||
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.assertTrue(attachments < message.attachment_ids)
|
||||
self.assertEqual(
|
||||
sorted(message.attachment_ids.mapped('name')),
|
||||
['ReportLike1', 'ReportLike2', 'SameAttFileName_00.txt', 'SameAttFileName_01.txt'],
|
||||
)
|
||||
self.assertEqual(
|
||||
message.attachment_ids.mapped('res_id'),
|
||||
[test_records_mc_c2.id] * 4,
|
||||
)
|
||||
self.assertEqual(
|
||||
message.attachment_ids.mapped('res_model'),
|
||||
[test_records_mc_c2._name] * 4,
|
||||
)
|
||||
|
||||
# cannot link attachments of unreachable records when posting on a document
|
||||
# they can access (aka no access delegation through posting message)
|
||||
attachments = self.env['ir.attachment'].sudo().create(
|
||||
self._generate_attachments_data(
|
||||
1,
|
||||
test_records_mc_c1._name,
|
||||
test_records_mc_c1.id,
|
||||
prefix='NoAccessMC'
|
||||
)
|
||||
)
|
||||
with self.assertRaises(AccessError):
|
||||
message = test_records_mc_c2.message_post(
|
||||
attachments=attachments_data,
|
||||
attachment_ids=attachments.ids,
|
||||
body='<p>Hello</p>',
|
||||
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",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'multi_company', 'mail_controller')
|
||||
class TestMultiCompanyRedirect(TestMailCommon, HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMultiCompanyRedirect, cls).setUpClass()
|
||||
cls._activate_multi_company()
|
||||
|
||||
def test_redirect_to_records(self):
|
||||
""" Test mail/view redirection in MC environment, notably cids being
|
||||
added in redirect if user has access to the record. """
|
||||
mc_record_c1, mc_record_c2 = 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',
|
||||
}
|
||||
])
|
||||
|
||||
for (login, password), mc_record in product(
|
||||
((None, None), # not logged: redirect to web/login
|
||||
('employee', 'employee'), # access only main company
|
||||
('admin', 'admin'), # access both companies
|
||||
),
|
||||
(mc_record_c1, mc_record_c2),
|
||||
):
|
||||
with self.subTest(login=login, mc_record=mc_record):
|
||||
self.authenticate(login, password)
|
||||
response = self.url_open(
|
||||
f'/mail/view?model={mc_record._name}&res_id={mc_record.id}',
|
||||
timeout=15
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
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)
|
||||
else:
|
||||
user = self.env['res.users'].browse(self.session.uid)
|
||||
self.assertEqual(user.login, login)
|
||||
mc_error = login == 'employee' and mc_record == mc_record_c2
|
||||
if mc_error:
|
||||
# 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')
|
||||
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']
|
||||
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}')
|
||||
|
||||
def test_redirect_to_records_nothread(self):
|
||||
""" Test no thread models and redirection """
|
||||
nothreads = self.env['mail.test.nothread'].create([
|
||||
{
|
||||
'company_id': company.id,
|
||||
'name': f'Test with {company.name}',
|
||||
}
|
||||
for company in (self.company_admin, self.company_2, self.env['res.company'])
|
||||
])
|
||||
|
||||
# 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.user_admin.write({'company_id': user_company.id})
|
||||
response = self.url_open(
|
||||
f'/mail/view?model={test_record._name}&res_id={test_record.id}',
|
||||
timeout=15
|
||||
)
|
||||
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))
|
||||
|
||||
# when being not logged, cids should not be added as redirection after
|
||||
# logging will be 'mail/view' again
|
||||
for test_record in nothreads:
|
||||
with self.subTest(record_name=test_record.name):
|
||||
self.authenticate(None, None)
|
||||
response = self.url_open(
|
||||
f'/mail/view?model={test_record._name}&res_id={test_record.id}',
|
||||
timeout=15
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
decoded_fragment = url_decode(url_parse(response.url).fragment)
|
||||
self.assertNotIn('cids', decoded_fragment)
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install", "multi_company", "mail_controller")
|
||||
class TestMultiCompanyThreadData(TestMailCommon, HttpCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls._activate_multi_company()
|
||||
|
||||
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)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# -*- 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.exceptions import AccessError
|
||||
|
||||
|
||||
class TestSubtypeAccess(TestMailCommon):
|
||||
|
||||
def test_subtype_access(self):
|
||||
"""
|
||||
The function aims to formally verify the access restrictions on mail.message.subtype for
|
||||
normal and admin users. It ensures that normal users are unable to modify it,
|
||||
while admin users possess the necessary privileges to modify it successfully.
|
||||
"""
|
||||
|
||||
test_subtype = self.env['mail.message.subtype'].create({
|
||||
'name': 'Test',
|
||||
'description': 'only description',
|
||||
})
|
||||
|
||||
user = mail_new_test_user(self.env, 'Internal user', groups='base.group_user')
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
test_subtype.with_user(user).write({'description': 'changing description'})
|
||||
|
||||
test_subtype.with_user(self.user_admin).write({'description': 'testing'})
|
||||
self.assertEqual(test_subtype.description, 'testing')
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
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.tools import mute_logger, safe_eval
|
||||
|
||||
|
||||
class TestMailTemplateCommon(TestMailCommon, TestRecipients):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailTemplateCommon, cls).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'),
|
||||
'res_model': 'res.partner',
|
||||
'res_id': cls.user_admin.partner_id.id
|
||||
}, {
|
||||
'name': 'second.txt',
|
||||
'datas': base64.b64encode(b'My second attachment'),
|
||||
'res_model': 'res.partner',
|
||||
'res_id': cls.user_admin.partner_id.id
|
||||
}]
|
||||
|
||||
cls.email_1 = 'test1@example.com'
|
||||
cls.email_2 = 'test2@example.com'
|
||||
cls.email_3 = cls.partner_1.email
|
||||
|
||||
# create a complete test template
|
||||
cls.test_template = cls._create_template('mail.test.lang', {
|
||||
'attachment_ids': [(0, 0, cls._attachments[0]), (0, 0, cls._attachments[1])],
|
||||
'body_html': '<p>EnglishBody for <t t-out="object.name"/></p>',
|
||||
'lang': '{{ object.customer_id.lang or object.lang }}',
|
||||
'email_to': '%s, %s' % (cls.email_1, cls.email_2),
|
||||
'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 }}',
|
||||
})
|
||||
|
||||
# activate translations
|
||||
cls._activate_multi_lang(
|
||||
layout_arch_db='<body><t t-out="message.body"/> English Layout for <t t-esc="model_description"/></body>',
|
||||
test_record=cls.test_record, test_template=cls.test_template
|
||||
)
|
||||
|
||||
# admin should receive emails
|
||||
cls.user_admin.write({'notification_type': 'email'})
|
||||
# Force the attachments of the template to be in the natural order.
|
||||
cls.test_template.invalidate_recordset(['attachment_ids'])
|
||||
|
||||
|
||||
@tagged('mail_template')
|
||||
class TestMailTemplate(TestMailTemplateCommon):
|
||||
|
||||
def test_template_add_context_action(self):
|
||||
self.test_template.create_action()
|
||||
|
||||
# check template act_window has been updated
|
||||
self.assertTrue(bool(self.test_template.ref_ir_act_window))
|
||||
|
||||
# check those records
|
||||
action = self.test_template.ref_ir_act_window
|
||||
self.assertEqual(action.name, 'Send Mail (%s)' % self.test_template.name)
|
||||
self.assertEqual(action.binding_model_id.model, 'mail.test.lang')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@users('employee')
|
||||
def test_template_schedule_email(self):
|
||||
""" Test scheduling email sending from template. """
|
||||
now = datetime.datetime(2024, 4, 29, 10, 49, 59)
|
||||
test_template = self.test_template.with_env(self.env)
|
||||
|
||||
# schedule the mail in 3 days -> patch safe_eval.datetime access
|
||||
safe_eval_orig = safe_eval.safe_eval
|
||||
|
||||
def _safe_eval_hacked(*args, **kwargs):
|
||||
""" safe_eval wraps 'datetime' and freeze_time does not mock it;
|
||||
simplest solution found so far is to directly hack safe_eval just
|
||||
for this test """
|
||||
if args[0] == "datetime.datetime.now() + datetime.timedelta(days=3)":
|
||||
return now + datetime.timedelta(days=3)
|
||||
return safe_eval_orig(*args, **kwargs)
|
||||
|
||||
# patch datetime and safe_eval.datetime, as otherwise using standard 'now'
|
||||
# might lead to errors due to test running right before minute switch it
|
||||
# sometimes ends at minute+1 and assert fails - see runbot-54946
|
||||
with patch.object(safe_eval, "safe_eval", autospec=True, side_effect=_safe_eval_hacked):
|
||||
test_template.scheduled_date = '{{datetime.datetime.now() + datetime.timedelta(days=3)}}'
|
||||
with freeze_time(now):
|
||||
mail_id = test_template.send_mail(self.test_record.id)
|
||||
mail = self.env['mail.mail'].sudo().browse(mail_id)
|
||||
self.assertEqual(
|
||||
mail.scheduled_date.replace(second=0, microsecond=0),
|
||||
(now + datetime.timedelta(days=3)).replace(second=0, microsecond=0),
|
||||
)
|
||||
self.assertEqual(mail.state, 'outgoing')
|
||||
|
||||
# check a wrong format
|
||||
test_template.scheduled_date = '{{"test " * 5}}'
|
||||
with freeze_time(now):
|
||||
mail_id = test_template.send_mail(self.test_record.id)
|
||||
mail = self.env['mail.mail'].sudo().browse(mail_id)
|
||||
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):
|
||||
mail_id = self.test_template.send_mail(self.test_record.id)
|
||||
mail = self.env['mail.mail'].sudo().browse(mail_id)
|
||||
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)
|
||||
|
||||
@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_record.write({
|
||||
'lang': 'es_ES',
|
||||
})
|
||||
test_template = self.env['mail.template'].browse(self.test_template.ids)
|
||||
|
||||
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)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
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)
|
||||
|
||||
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)
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 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
|
||||
|
||||
@tagged('mail_template', 'multi_lang')
|
||||
class TestMailTemplateTools(TestMailTemplateCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.test_template_preview = cls.env['mail.template.preview'].create({
|
||||
'mail_template_id': cls.test_template.id,
|
||||
})
|
||||
|
||||
def test_initial_values(self):
|
||||
self.assertTrue(self.test_template.email_to)
|
||||
self.assertTrue(self.test_template.email_cc)
|
||||
self.assertEqual(len(self.test_template.partner_to.split(',')), 2)
|
||||
self.assertTrue(self.test_record.email_from)
|
||||
|
||||
def test_mail_template_preview_force_lang(self):
|
||||
test_record = self.env['mail.test.lang'].browse(self.test_record.ids)
|
||||
test_record.write({
|
||||
'lang': 'es_ES',
|
||||
})
|
||||
test_template = self.env['mail.template'].browse(self.test_template.ids)
|
||||
|
||||
preview = self.env['mail.template.preview'].create({
|
||||
'mail_template_id': test_template.id,
|
||||
'resource_ref': test_record,
|
||||
'lang': 'es_ES',
|
||||
})
|
||||
self.assertEqual(preview.body_html, '<p>SpanishBody for %s</p>' % test_record.name)
|
||||
|
||||
preview.write({'lang': 'en_US'})
|
||||
self.assertEqual(preview.body_html, '<p>EnglishBody for %s</p>' % test_record.name)
|
||||
|
||||
@users('employee')
|
||||
def test_mail_template_preview_recipients(self):
|
||||
form = Form(self.test_template_preview)
|
||||
form.resource_ref = self.test_record
|
||||
|
||||
self.assertEqual(form.email_to, self.test_template.email_to)
|
||||
self.assertEqual(form.email_cc, self.test_template.email_cc)
|
||||
self.assertEqual(set(record.id for record in form.partner_ids),
|
||||
{int(pid) for pid in self.test_template.partner_to.split(',') if pid})
|
||||
|
||||
@users('employee')
|
||||
def test_mail_template_preview_recipients_use_default_to(self):
|
||||
self.test_template.use_default_to = True
|
||||
form = Form(self.test_template_preview)
|
||||
form.resource_ref = self.test_record
|
||||
|
||||
self.assertEqual(form.email_to, self.test_record.email_from)
|
||||
self.assertFalse(form.email_cc)
|
||||
self.assertFalse(form.partner_ids)
|
||||
|
|
@ -0,0 +1,409 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import DEFAULT
|
||||
|
||||
from odoo import exceptions
|
||||
from odoo.addons.test_mail.models.test_mail_models import MailTestSimple
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
|
||||
from odoo.tests.common import tagged, users
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('mail_thread')
|
||||
class TestAPI(TestMailCommon, TestRecipients):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestAPI, cls).setUpClass()
|
||||
cls.ticket_record = cls.env['mail.test.ticket'].with_context(cls._test_context).create({
|
||||
'email_from': '"Paulette Vachette" <paulette@test.example.com>',
|
||||
'name': 'Test',
|
||||
'user_id': cls.user_employee.id,
|
||||
})
|
||||
|
||||
@mute_logger('openerp.addons.mail.models.mail_mail')
|
||||
@users('employee')
|
||||
def test_message_update_content(self):
|
||||
""" Test updating message content. """
|
||||
ticket_record = self.ticket_record.with_env(self.env)
|
||||
attachments = self.env['ir.attachment'].create(
|
||||
self._generate_attachments_data(2, 'mail.compose.message', 0)
|
||||
)
|
||||
|
||||
# post a note
|
||||
message = ticket_record.message_post(
|
||||
attachment_ids=attachments.ids,
|
||||
body="<p>Initial Body</p>",
|
||||
message_type="comment",
|
||||
partner_ids=self.partner_1.ids,
|
||||
)
|
||||
self.assertEqual(message.attachment_ids, attachments)
|
||||
self.assertEqual(set(message.mapped('attachment_ids.res_id')), set(ticket_record.ids))
|
||||
self.assertEqual(set(message.mapped('attachment_ids.res_model')), set([ticket_record._name]))
|
||||
self.assertEqual(message.body, "<p>Initial Body</p>")
|
||||
self.assertEqual(message.subtype_id, self.env.ref('mail.mt_note'))
|
||||
|
||||
# update the content with new attachments
|
||||
new_attachments = self.env['ir.attachment'].create(
|
||||
self._generate_attachments_data(2, 'mail.compose.message', 0)
|
||||
)
|
||||
ticket_record._message_update_content(
|
||||
message, "<p>New Body</p>",
|
||||
attachment_ids=new_attachments.ids
|
||||
)
|
||||
self.assertEqual(message.attachment_ids, attachments + new_attachments)
|
||||
self.assertEqual(set(message.mapped('attachment_ids.res_id')), set(ticket_record.ids))
|
||||
self.assertEqual(set(message.mapped('attachment_ids.res_model')), set([ticket_record._name]))
|
||||
self.assertEqual(message.body, "<p>New Body</p>")
|
||||
|
||||
# void attachments
|
||||
ticket_record._message_update_content(
|
||||
message, "<p>Another Body, void attachments</p>",
|
||||
attachment_ids=[]
|
||||
)
|
||||
self.assertFalse(message.attachment_ids)
|
||||
self.assertFalse((attachments + new_attachments).exists())
|
||||
self.assertEqual(message.body, "<p>Another Body, void attachments</p>")
|
||||
|
||||
@mute_logger('openerp.addons.mail.models.mail_mail')
|
||||
@users('employee')
|
||||
def test_message_update_content_check(self):
|
||||
""" Test cases where updating content should be prevented """
|
||||
ticket_record = self.ticket_record.with_env(self.env)
|
||||
|
||||
# cannot edit user comments (subtype)
|
||||
message = ticket_record.message_post(
|
||||
body="<p>Initial Body</p>",
|
||||
message_type="comment",
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
ticket_record._message_update_content(
|
||||
message, "<p>New Body</p>"
|
||||
)
|
||||
|
||||
message.sudo().write({'subtype_id': self.env.ref('mail.mt_note')})
|
||||
ticket_record._message_update_content(
|
||||
message, "<p>New Body</p>"
|
||||
)
|
||||
|
||||
# cannot edit notifications
|
||||
for message_type in ['notification', 'user_notification', 'email']:
|
||||
message.sudo().write({'message_type': message_type})
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
ticket_record._message_update_content(
|
||||
message, "<p>New Body</p>"
|
||||
)
|
||||
|
||||
|
||||
@tagged('mail_thread')
|
||||
class TestChatterTweaks(TestMailCommon, TestRecipients):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestChatterTweaks, cls).setUpClass()
|
||||
cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
|
||||
|
||||
def test_post_no_subscribe_author(self):
|
||||
original = self.test_record.message_follower_ids
|
||||
self.test_record.with_user(self.user_employee).with_context({'mail_create_nosubscribe': True}).message_post(
|
||||
body='Test Body', message_type='comment', subtype_xmlid='mail.mt_comment')
|
||||
self.assertEqual(self.test_record.message_follower_ids.mapped('partner_id'), original.mapped('partner_id'))
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_post_no_subscribe_recipients(self):
|
||||
original = self.test_record.message_follower_ids
|
||||
self.test_record.with_user(self.user_employee).with_context({'mail_create_nosubscribe': True}).message_post(
|
||||
body='Test Body', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[self.partner_1.id, self.partner_2.id])
|
||||
self.assertEqual(self.test_record.message_follower_ids.mapped('partner_id'), original.mapped('partner_id'))
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_post_subscribe_recipients(self):
|
||||
original = self.test_record.message_follower_ids
|
||||
self.test_record.with_user(self.user_employee).with_context({'mail_create_nosubscribe': True, 'mail_post_autofollow': True}).message_post(
|
||||
body='Test Body', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[self.partner_1.id, self.partner_2.id])
|
||||
self.assertEqual(self.test_record.message_follower_ids.mapped('partner_id'), original.mapped('partner_id') | self.partner_1 | self.partner_2)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_chatter_context_cleaning(self):
|
||||
""" Test default keys are not propagated to message creation as it may
|
||||
induce wrong values for some fields, like parent_id. """
|
||||
parent = self.env['res.partner'].create({'name': 'Parent'})
|
||||
partner = self.env['res.partner'].with_context(default_parent_id=parent.id).create({'name': 'Contact'})
|
||||
self.assertFalse(partner.message_ids[-1].parent_id)
|
||||
|
||||
def test_chatter_mail_create_nolog(self):
|
||||
""" Test disable of automatic chatter message at create """
|
||||
rec = self.env['mail.test.simple'].with_user(self.user_employee).with_context({'mail_create_nolog': True}).create({'name': 'Test'})
|
||||
self.flush_tracking()
|
||||
self.assertEqual(rec.message_ids, self.env['mail.message'])
|
||||
|
||||
rec = self.env['mail.test.simple'].with_user(self.user_employee).with_context({'mail_create_nolog': False}).create({'name': 'Test'})
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(rec.message_ids), 1)
|
||||
|
||||
def test_chatter_mail_notrack(self):
|
||||
""" Test disable of automatic value tracking at create and write """
|
||||
rec = self.env['mail.test.track'].with_user(self.user_employee).create({'name': 'Test', 'user_id': self.user_employee.id})
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(rec.message_ids), 1,
|
||||
"A creation message without tracking values should have been posted")
|
||||
self.assertEqual(len(rec.message_ids.sudo().tracking_value_ids), 0,
|
||||
"A creation message without tracking values should have been posted")
|
||||
|
||||
rec.with_context({'mail_notrack': True}).write({'user_id': self.user_admin.id})
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(rec.message_ids), 1,
|
||||
"No new message should have been posted with mail_notrack key")
|
||||
|
||||
rec.with_context({'mail_notrack': False}).write({'user_id': self.user_employee.id})
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(rec.message_ids), 2,
|
||||
"A tracking message should have been posted")
|
||||
self.assertEqual(len(rec.message_ids.sudo().mapped('tracking_value_ids')), 1,
|
||||
"New tracking message should have tracking values")
|
||||
|
||||
def test_chatter_tracking_disable(self):
|
||||
""" Test disable of all chatter features at create and write """
|
||||
rec = self.env['mail.test.track'].with_user(self.user_employee).with_context({'tracking_disable': True}).create({'name': 'Test', 'user_id': self.user_employee.id})
|
||||
self.flush_tracking()
|
||||
self.assertEqual(rec.sudo().message_ids, self.env['mail.message'])
|
||||
self.assertEqual(rec.sudo().mapped('message_ids.tracking_value_ids'), self.env['mail.tracking.value'])
|
||||
|
||||
rec.write({'user_id': self.user_admin.id})
|
||||
self.flush_tracking()
|
||||
self.assertEqual(rec.sudo().mapped('message_ids.tracking_value_ids'), self.env['mail.tracking.value'])
|
||||
|
||||
rec.with_context({'tracking_disable': False}).write({'user_id': self.user_employee.id})
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(rec.sudo().mapped('message_ids.tracking_value_ids')), 1)
|
||||
|
||||
rec = self.env['mail.test.track'].with_user(self.user_employee).with_context({'tracking_disable': False}).create({'name': 'Test', 'user_id': self.user_employee.id})
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(rec.sudo().message_ids), 1,
|
||||
"Creation message without tracking values should have been posted")
|
||||
self.assertEqual(len(rec.sudo().mapped('message_ids.tracking_value_ids')), 0,
|
||||
"Creation message without tracking values should have been posted")
|
||||
|
||||
def test_cache_invalidation(self):
|
||||
""" Test that creating a mail-thread record does not invalidate the whole cache. """
|
||||
# make a new record in cache
|
||||
record = self.env['res.partner'].new({'name': 'Brave New Partner'})
|
||||
self.assertTrue(record.name)
|
||||
|
||||
# creating a mail-thread record should not invalidate the whole cache
|
||||
self.env['res.partner'].create({'name': 'Actual Partner'})
|
||||
self.assertTrue(record.name)
|
||||
|
||||
|
||||
@tagged('mail_thread')
|
||||
class TestDiscuss(TestMailCommon, TestRecipients):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestDiscuss, cls).setUpClass()
|
||||
cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({
|
||||
'name': 'Test',
|
||||
'email_from': 'ignasse@example.com'
|
||||
})
|
||||
|
||||
@mute_logger('openerp.addons.mail.models.mail_mail')
|
||||
def test_mark_all_as_read(self):
|
||||
def _employee_crash(*args, **kwargs):
|
||||
""" If employee is test employee, consider they have no access on document """
|
||||
recordset = args[0]
|
||||
if recordset.env.uid == self.user_employee.id and not recordset.env.su:
|
||||
if kwargs.get('raise_exception', True):
|
||||
raise exceptions.AccessError('Hop hop hop Ernest, please step back.')
|
||||
return False
|
||||
return DEFAULT
|
||||
|
||||
with patch.object(MailTestSimple, 'check_access_rights', autospec=True, side_effect=_employee_crash):
|
||||
with self.assertRaises(exceptions.AccessError):
|
||||
self.env['mail.test.simple'].with_user(self.user_employee).browse(self.test_record.ids).read(['name'])
|
||||
|
||||
employee_partner = self.env['res.partner'].with_user(self.user_employee).browse(self.partner_employee.ids)
|
||||
|
||||
# mark all as read clear needactions
|
||||
msg1 = self.test_record.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[employee_partner.id])
|
||||
self._reset_bus()
|
||||
with self.assertBus(
|
||||
[(self.cr.dbname, 'res.partner', employee_partner.id)],
|
||||
message_items=[{
|
||||
'type': 'mail.message/mark_as_read',
|
||||
'payload': {
|
||||
'message_ids': [msg1.id],
|
||||
'needaction_inbox_counter': 0,
|
||||
},
|
||||
}]):
|
||||
employee_partner.env['mail.message'].mark_all_as_read(domain=[])
|
||||
na_count = employee_partner._get_needaction_count()
|
||||
self.assertEqual(na_count, 0, "mark all as read should conclude all needactions")
|
||||
|
||||
# mark all as read also clear inaccessible needactions
|
||||
msg2 = self.test_record.message_post(body='Zest', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[employee_partner.id])
|
||||
needaction_accessible = len(employee_partner.env['mail.message'].search([['needaction', '=', True]]))
|
||||
self.assertEqual(needaction_accessible, 1, "a new message to a partner is readable to that partner")
|
||||
|
||||
msg2.sudo().partner_ids = self.env['res.partner']
|
||||
employee_partner.env['mail.message'].search([['needaction', '=', True]])
|
||||
needaction_length = len(employee_partner.env['mail.message'].search([['needaction', '=', True]]))
|
||||
self.assertEqual(needaction_length, 1, "message should still be readable when notified")
|
||||
|
||||
na_count = employee_partner._get_needaction_count()
|
||||
self.assertEqual(na_count, 1, "message not accessible is currently still counted")
|
||||
|
||||
self._reset_bus()
|
||||
with self.assertBus(
|
||||
[(self.cr.dbname, 'res.partner', employee_partner.id)],
|
||||
message_items=[{
|
||||
'type': 'mail.message/mark_as_read',
|
||||
'payload': {
|
||||
'message_ids': [msg2.id],
|
||||
'needaction_inbox_counter': 0,
|
||||
},
|
||||
}]):
|
||||
employee_partner.env['mail.message'].mark_all_as_read(domain=[])
|
||||
na_count = employee_partner._get_needaction_count()
|
||||
self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")
|
||||
|
||||
def test_set_message_done_user(self):
|
||||
with self.assertSinglePostNotifications([{'partner': self.partner_employee, 'type': 'inbox'}], message_info={'content': 'Test'}):
|
||||
message = self.test_record.message_post(
|
||||
body='Test', message_type='comment', subtype_xmlid='mail.mt_comment',
|
||||
partner_ids=[self.user_employee.partner_id.id])
|
||||
message.with_user(self.user_employee).set_message_done()
|
||||
self.assertMailNotifications(message, [{'notif': [{'partner': self.partner_employee, 'type': 'inbox', 'is_read': True}]}])
|
||||
# TDE TODO: it seems bus notifications could be checked
|
||||
|
||||
def test_set_star(self):
|
||||
msg = self.test_record.with_user(self.user_admin).message_post(body='My Body', subject='1')
|
||||
msg_emp = self.env['mail.message'].with_user(self.user_employee).browse(msg.id)
|
||||
|
||||
# Admin set as starred
|
||||
msg.toggle_message_starred()
|
||||
self.assertTrue(msg.starred)
|
||||
|
||||
# Employee set as starred
|
||||
msg_emp.toggle_message_starred()
|
||||
self.assertTrue(msg_emp.starred)
|
||||
|
||||
# Do: Admin unstars msg
|
||||
msg.toggle_message_starred()
|
||||
self.assertFalse(msg.starred)
|
||||
self.assertTrue(msg_emp.starred)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_cc_recipient_suggestion(self):
|
||||
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()[record.id]
|
||||
self.assertEqual(sorted(suggestions), [
|
||||
(False, '"cc3" <cc3@example.com>', None, 'CC Email'),
|
||||
(False, 'cc1@example.com', None, 'CC Email'),
|
||||
(False, 'cc2@example.com', None, 'CC Email'),
|
||||
], 'cc should be in suggestions')
|
||||
|
||||
def test_inbox_message_fetch_needaction(self):
|
||||
user1 = self.env['res.users'].create({'login': 'user1', 'name': 'User 1'})
|
||||
user1.notification_type = 'inbox'
|
||||
user2 = self.env['res.users'].create({'login': 'user2', 'name': 'User 2'})
|
||||
user2.notification_type = 'inbox'
|
||||
message1 = self.test_record.with_user(self.user_admin).message_post(body='Message 1', partner_ids=[user1.partner_id.id, user2.partner_id.id])
|
||||
message2 = self.test_record.with_user(self.user_admin).message_post(body='Message 2', partner_ids=[user1.partner_id.id, user2.partner_id.id])
|
||||
|
||||
# both notified users should have the 2 messages in Inbox initially
|
||||
messages = self.env['mail.message'].with_user(user1)._message_fetch(domain=[['needaction', '=', True]])
|
||||
self.assertEqual(len(messages), 2)
|
||||
messages = self.env['mail.message'].with_user(user2)._message_fetch(domain=[['needaction', '=', True]])
|
||||
self.assertEqual(len(messages), 2)
|
||||
|
||||
# first user is marking one message as done: the other message is still Inbox, while the other user still has the 2 messages in Inbox
|
||||
message1.with_user(user1).set_message_done()
|
||||
messages = self.env['mail.message'].with_user(user1)._message_fetch(domain=[['needaction', '=', True]])
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(messages[0].id, message2.id)
|
||||
messages = self.env['mail.message'].with_user(user2)._message_fetch(domain=[['needaction', '=', True]])
|
||||
self.assertEqual(len(messages), 2)
|
||||
|
||||
def test_notification_has_error_filter(self):
|
||||
"""Ensure message_has_error filter is only returning threads for which
|
||||
the current user is author of a failed message."""
|
||||
message = self.test_record.with_user(self.user_admin).message_post(
|
||||
body='Test', message_type='comment', subtype_xmlid='mail.mt_comment',
|
||||
partner_ids=[self.user_employee.partner_id.id]
|
||||
)
|
||||
self.assertFalse(message.has_error)
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
def _connect(*args, **kwargs):
|
||||
raise Exception("Some exception")
|
||||
self.connect_mocked.side_effect = _connect
|
||||
|
||||
self.user_admin.notification_type = 'email'
|
||||
message2 = self.test_record.with_user(self.user_employee).message_post(
|
||||
body='Test', message_type='comment', subtype_xmlid='mail.mt_comment',
|
||||
partner_ids=[self.user_admin.partner_id.id]
|
||||
)
|
||||
self.assertTrue(message2.has_error)
|
||||
# employee is author of message which has a failure
|
||||
threads_employee = self.test_record.with_user(self.user_employee).search([('message_has_error', '=', True)])
|
||||
self.assertEqual(len(threads_employee), 1)
|
||||
# admin is also author of a message, but it doesn't have a failure
|
||||
# and the failure from employee's message should not be taken into account for admin
|
||||
threads_admin = self.test_record.with_user(self.user_admin).search([('message_has_error', '=', True)])
|
||||
self.assertEqual(len(threads_admin), 0)
|
||||
|
||||
@users("employee")
|
||||
def test_unlink_notification_message(self):
|
||||
channel = self.env['mail.channel'].create({'name': 'testChannel'})
|
||||
notification_msg = channel.with_user(self.user_admin).message_notify(
|
||||
body='test',
|
||||
message_type='user_notification',
|
||||
partner_ids=[self.partner_2.id],
|
||||
)
|
||||
|
||||
with self.assertRaises(exceptions.AccessError):
|
||||
notification_msg.with_env(self.env)._message_format(['id', 'body', 'date', 'author_id', 'email_from'])
|
||||
|
||||
channel_message = self.env['mail.message'].sudo().search([('model', '=', 'mail.channel'), ('res_id', 'in', channel.ids)])
|
||||
self.assertEqual(len(channel_message), 1, "Test message should have been posted")
|
||||
|
||||
channel.sudo().unlink()
|
||||
remaining_message = channel_message.exists()
|
||||
self.assertEqual(len(remaining_message), 0, "Test message should have been deleted")
|
||||
|
||||
|
||||
@tagged('mail_thread')
|
||||
class TestNoThread(TestMailCommon, TestRecipients):
|
||||
""" Specific tests for cross models thread features """
|
||||
|
||||
@users('employee')
|
||||
def test_message_notify(self):
|
||||
test_record = self.env['mail.test.nothread'].create({
|
||||
'customer_id': self.partner_1.id,
|
||||
'name': 'Not A Thread',
|
||||
})
|
||||
with self.assertPostNotifications([{
|
||||
'content': 'Hello Paulo',
|
||||
'email_values': {
|
||||
'reply_to': self.company_admin.catchall_formatted,
|
||||
},
|
||||
'message_type': 'user_notification',
|
||||
'notif': [{
|
||||
'check_send': True,
|
||||
'is_read': True,
|
||||
'partner': self.partner_2,
|
||||
'status': 'sent',
|
||||
'type': 'email',
|
||||
}],
|
||||
'subtype': 'mail.mt_note',
|
||||
}]):
|
||||
_message = self.env['mail.thread'].message_notify(
|
||||
body='<p>Hello Paulo</p>',
|
||||
model=test_record._name,
|
||||
res_id=test_record.id,
|
||||
subject='Test Notify',
|
||||
partner_ids=self.partner_2.ids
|
||||
)
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import exceptions, tools
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('mail_thread', 'mail_blacklist')
|
||||
class TestMailThread(TestMailCommon, TestRecipients):
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_blacklist_mixin_email_normalized(self):
|
||||
""" Test email_normalized and is_blacklisted fields behavior, notably
|
||||
when dealing with encapsulated email fields and multi-email input. """
|
||||
base_email = 'test.email@test.example.com'
|
||||
|
||||
# test data: source email, expected email normalized
|
||||
valid_pairs = [
|
||||
(base_email, base_email),
|
||||
(tools.formataddr(('Another Name', base_email)), base_email),
|
||||
(f'Name That Should Be Escaped <{base_email}>', base_email),
|
||||
('test.😊@example.com', 'test.😊@example.com'),
|
||||
('"Name 😊" <test.😊@example.com>', 'test.😊@example.com'),
|
||||
]
|
||||
void_pairs = [(False, False),
|
||||
('', False),
|
||||
(' ', False)]
|
||||
multi_pairs = [
|
||||
(f'{base_email}, other.email@test.example.com',
|
||||
base_email), # multi supports first found
|
||||
(f'{tools.formataddr(("Another Name", base_email))}, other.email@test.example.com',
|
||||
base_email), # multi supports first found
|
||||
]
|
||||
for email_from, exp_email_normalized in valid_pairs + void_pairs + multi_pairs:
|
||||
with self.subTest(email_from=email_from, exp_email_normalized=exp_email_normalized):
|
||||
new_record = self.env['mail.test.gateway'].create({
|
||||
'email_from': email_from,
|
||||
'name': 'BL Test',
|
||||
})
|
||||
self.assertEqual(new_record.email_normalized, exp_email_normalized)
|
||||
self.assertFalse(new_record.is_blacklisted)
|
||||
|
||||
# blacklist email should fail as void
|
||||
if email_from in [pair[0] for pair in void_pairs]:
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
bl_record = self.env['mail.blacklist']._add(email_from)
|
||||
# blacklist email currently fails but could not
|
||||
elif email_from in [pair[0] for pair in multi_pairs]:
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
bl_record = self.env['mail.blacklist']._add(email_from)
|
||||
# blacklist email ok
|
||||
else:
|
||||
bl_record = self.env['mail.blacklist']._add(email_from)
|
||||
self.assertEqual(bl_record.email, exp_email_normalized)
|
||||
new_record.invalidate_recordset(fnames=['is_blacklisted'])
|
||||
self.assertTrue(new_record.is_blacklisted)
|
||||
|
||||
bl_record.unlink()
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
# -*- 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]}]
|
||||
)
|
||||
1904
odoo-bringout-oca-ocb-test_mail/test_mail/tests/test_message_post.py
Normal file
1904
odoo-bringout-oca-ocb-test_mail/test_mail/tests/test_message_post.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,484 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.tests import Form
|
||||
|
||||
|
||||
@tagged('mail_track')
|
||||
class TestTracking(TestMailCommon):
|
||||
|
||||
def setUp(self):
|
||||
super(TestTracking, self).setUp()
|
||||
|
||||
record = self.env['mail.test.ticket'].with_user(self.user_employee).with_context(self._test_context).create({
|
||||
'name': 'Test',
|
||||
})
|
||||
self.flush_tracking()
|
||||
self.record = record.with_context(mail_notrack=False)
|
||||
|
||||
def test_message_track_message_type(self):
|
||||
"""Check that the right message type is applied for track templates."""
|
||||
self.record.message_subscribe(
|
||||
partner_ids=[self.user_admin.partner_id.id],
|
||||
subtype_ids=[self.env.ref('mail.mt_comment').id]
|
||||
)
|
||||
mail_templates = self.env['mail.template'].create([{
|
||||
'name': f'Template {n}',
|
||||
'subject': f'Template {n}',
|
||||
'model_id': self.env.ref('test_mail.model_mail_test_ticket').id,
|
||||
'body_html': f'<p>Template {n}</p>',
|
||||
} for n in range(2)])
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
return self.env.ref('mail.mt_note')
|
||||
self.patch(self.registry('mail.test.ticket'), '_track_subtype', _track_subtype)
|
||||
|
||||
def _track_template(self, changes):
|
||||
if 'email_from' in changes:
|
||||
return {'email_from': (mail_templates[0], {})}
|
||||
elif 'container_id' in changes:
|
||||
return {'container_id': (mail_templates[1], {'message_type': 'notification'})}
|
||||
return {}
|
||||
self.patch(self.registry('mail.test.ticket'), '_track_template', _track_template)
|
||||
|
||||
container = self.env['mail.test.container'].create({'name': 'Container'})
|
||||
|
||||
# default is auto_comment
|
||||
with self.mock_mail_gateway():
|
||||
self.record.email_from = 'test@test.lan'
|
||||
self.flush_tracking()
|
||||
|
||||
first_message = self.record.message_ids.filtered(lambda message: message.subject == 'Template 0')
|
||||
self.assertEqual(len(self.record.message_ids), 2, 'Should be one change message and one automated template')
|
||||
self.assertEqual(first_message.message_type, 'auto_comment')
|
||||
|
||||
# auto_comment can be overriden by _track_template
|
||||
with self.mock_mail_gateway(mail_unlink_sent=False):
|
||||
self.record.container_id = container
|
||||
self.flush_tracking()
|
||||
|
||||
second_message = self.record.message_ids.filtered(lambda message: message.subject == 'Template 1')
|
||||
self.assertEqual(len(self.record.message_ids), 4, 'Should have added one change message and one automated template')
|
||||
self.assertEqual(second_message.message_type, 'notification')
|
||||
|
||||
def test_message_track_no_tracking(self):
|
||||
""" Update a set of non tracked fields -> no message, no tracking """
|
||||
self.record.write({
|
||||
'name': 'Tracking or not',
|
||||
'count': 32,
|
||||
})
|
||||
self.flush_tracking()
|
||||
self.assertEqual(self.record.message_ids, self.env['mail.message'])
|
||||
|
||||
def test_message_track_no_subtype(self):
|
||||
""" Update some tracked fields not linked to some subtype -> message with onchange """
|
||||
customer = self.env['res.partner'].create({'name': 'Customer', 'email': 'cust@example.com'})
|
||||
with self.mock_mail_gateway():
|
||||
self.record.write({
|
||||
'name': 'Test2',
|
||||
'customer_id': customer.id,
|
||||
})
|
||||
self.flush_tracking()
|
||||
|
||||
# one new message containing tracking; without subtype linked to tracking, a note is generated
|
||||
self.assertEqual(len(self.record.message_ids), 1)
|
||||
self.assertEqual(self.record.message_ids.subtype_id, self.env.ref('mail.mt_note'))
|
||||
|
||||
# no specific recipients except those following notes, no email
|
||||
self.assertEqual(self.record.message_ids.partner_ids, self.env['res.partner'])
|
||||
self.assertEqual(self.record.message_ids.notified_partner_ids, self.env['res.partner'])
|
||||
self.assertNotSentEmail()
|
||||
|
||||
# verify tracked value
|
||||
self.assertTracking(
|
||||
self.record.message_ids,
|
||||
[('customer_id', 'many2one', False, customer) # onchange tracked field
|
||||
])
|
||||
|
||||
def test_message_track_subtype(self):
|
||||
""" Update some tracked fields linked to some subtype -> message with onchange """
|
||||
self.record.message_subscribe(
|
||||
partner_ids=[self.user_admin.partner_id.id],
|
||||
subtype_ids=[self.env.ref('test_mail.st_mail_test_ticket_container_upd').id]
|
||||
)
|
||||
|
||||
container = self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({'name': 'Container'})
|
||||
self.record.write({
|
||||
'name': 'Test2',
|
||||
'email_from': 'noone@example.com',
|
||||
'container_id': container.id,
|
||||
})
|
||||
self.flush_tracking()
|
||||
# one new message containing tracking; subtype linked to tracking
|
||||
self.assertEqual(len(self.record.message_ids), 1)
|
||||
self.assertEqual(self.record.message_ids.subtype_id, self.env.ref('test_mail.st_mail_test_ticket_container_upd'))
|
||||
|
||||
# no specific recipients except those following container
|
||||
self.assertEqual(self.record.message_ids.partner_ids, self.env['res.partner'])
|
||||
self.assertEqual(self.record.message_ids.notified_partner_ids, self.user_admin.partner_id)
|
||||
|
||||
# verify tracked value
|
||||
self.assertTracking(
|
||||
self.record.message_ids,
|
||||
[('container_id', 'many2one', False, container) # onchange tracked field
|
||||
])
|
||||
|
||||
def test_message_track_template(self):
|
||||
""" Update some tracked fields linked to some template -> message with onchange """
|
||||
self.record.write({'mail_template': self.env.ref('test_mail.mail_test_ticket_tracking_tpl').id})
|
||||
self.assertEqual(self.record.message_ids, self.env['mail.message'])
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
self.record.write({
|
||||
'name': 'Test2',
|
||||
'customer_id': self.user_admin.partner_id.id,
|
||||
})
|
||||
self.flush_tracking()
|
||||
|
||||
self.assertEqual(len(self.record.message_ids), 2, 'should have 2 new messages: one for tracking, one for template')
|
||||
|
||||
# one new message containing the template linked to tracking
|
||||
self.assertEqual(self.record.message_ids[0].subject, 'Test Template')
|
||||
self.assertEqual(self.record.message_ids[0].body, '<p>Hello Test2</p>')
|
||||
|
||||
# one email send due to template
|
||||
self.assertSentEmail(self.record.env.user.partner_id, [self.partner_admin], body='<p>Hello Test2</p>')
|
||||
|
||||
# one new message containing tracking; without subtype linked to tracking
|
||||
self.assertEqual(self.record.message_ids[1].subtype_id, self.env.ref('mail.mt_note'))
|
||||
self.assertTracking(
|
||||
self.record.message_ids[1],
|
||||
[('customer_id', 'many2one', False, self.user_admin.partner_id) # onchange tracked field
|
||||
])
|
||||
|
||||
def test_message_track_template_at_create(self):
|
||||
""" Create a record with tracking template on create, template should be sent."""
|
||||
|
||||
Model = self.env['mail.test.ticket'].with_user(self.user_employee).with_context(self._test_context)
|
||||
Model = Model.with_context(mail_notrack=False)
|
||||
with self.mock_mail_gateway():
|
||||
record = Model.create({
|
||||
'name': 'Test',
|
||||
'customer_id': self.user_admin.partner_id.id,
|
||||
'mail_template': self.env.ref('test_mail.mail_test_ticket_tracking_tpl').id,
|
||||
})
|
||||
self.flush_tracking()
|
||||
|
||||
self.assertEqual(len(record.message_ids), 1, 'should have 1 new messages for template')
|
||||
# one new message containing the template linked to tracking
|
||||
self.assertEqual(record.message_ids[0].subject, 'Test Template')
|
||||
self.assertEqual(record.message_ids[0].body, '<p>Hello Test</p>')
|
||||
# one email send due to template
|
||||
self.assertSentEmail(self.record.env.user.partner_id, [self.partner_admin], body='<p>Hello Test</p>')
|
||||
|
||||
def test_create_partner_from_tracking_multicompany(self):
|
||||
company1 = self.env['res.company'].create({'name': 'company1'})
|
||||
self.env.user.write({'company_ids': [(4, company1.id, False)]})
|
||||
self.assertNotEqual(self.env.company, company1)
|
||||
|
||||
email_new_partner = "diamonds@rust.com"
|
||||
Partner = self.env['res.partner']
|
||||
self.assertFalse(Partner.search([('email', '=', email_new_partner)]))
|
||||
|
||||
template = self.env['mail.template'].create({
|
||||
'model_id': self.env['ir.model']._get('mail.test.track').id,
|
||||
'name': 'AutoTemplate',
|
||||
'subject': 'autoresponse',
|
||||
'email_from': self.env.user.email_formatted,
|
||||
'email_to': "{{ object.email_from }}",
|
||||
'body_html': "<div>A nice body</div>",
|
||||
})
|
||||
|
||||
def patched_message_track_post_template(*args, **kwargs):
|
||||
if args[0]._name == "mail.test.track":
|
||||
args[0].message_post_with_template(template.id)
|
||||
return True
|
||||
|
||||
with patch('odoo.addons.mail.models.mail_thread.MailThread._message_track_post_template', patched_message_track_post_template):
|
||||
self.env['mail.test.track'].create({
|
||||
'email_from': email_new_partner,
|
||||
'company_id': company1.id,
|
||||
'user_id': self.env.user.id, # trigger track template
|
||||
})
|
||||
self.flush_tracking()
|
||||
|
||||
new_partner = Partner.search([('email', '=', email_new_partner)])
|
||||
self.assertTrue(new_partner)
|
||||
self.assertEqual(new_partner.company_id, company1)
|
||||
|
||||
def test_track_invalid_selection(self):
|
||||
# Test: Check that initial invalid selection values are allowed when tracking
|
||||
# Create a record with an initially invalid selection value
|
||||
invalid_value = 'I love writing tests!'
|
||||
record = self.env['mail.test.track.selection'].create({
|
||||
'name': 'Test Invalid Selection Values',
|
||||
'selection_type': 'first',
|
||||
})
|
||||
|
||||
self.flush_tracking()
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
UPDATE mail_test_track_selection
|
||||
SET selection_type = %s
|
||||
WHERE id = %s
|
||||
""",
|
||||
[invalid_value, record.id]
|
||||
)
|
||||
|
||||
record.invalidate_recordset()
|
||||
|
||||
self.assertEqual(record.selection_type, invalid_value)
|
||||
|
||||
# Write a valid selection value
|
||||
record.selection_type = "second"
|
||||
|
||||
self.flush_tracking()
|
||||
self.assertTracking(record.message_ids, [
|
||||
('selection_type', 'char', invalid_value, 'Second'),
|
||||
])
|
||||
|
||||
def test_track_template(self):
|
||||
# Test: Check that default_* keys are not taken into account in _message_track_post_template
|
||||
magic_code = 'Up-Up-Down-Down-Left-Right-Left-Right-Square-Triangle'
|
||||
|
||||
mt_name_changed = self.env['mail.message.subtype'].create({
|
||||
'name': 'MAGIC CODE WOOP WOOP',
|
||||
'description': 'SPECIAL CONTENT UNLOCKED'
|
||||
})
|
||||
self.env['ir.model.data'].create({
|
||||
'name': 'mt_name_changed',
|
||||
'model': 'mail.message.subtype',
|
||||
'module': 'mail',
|
||||
'res_id': mt_name_changed.id
|
||||
})
|
||||
mail_template = self.env['mail.template'].create({
|
||||
'name': 'SPECIAL CONTENT UNLOCKED',
|
||||
'subject': 'SPECIAL CONTENT UNLOCKED',
|
||||
'model_id': self.env.ref('test_mail.model_mail_test_container').id,
|
||||
'auto_delete': True,
|
||||
'body_html': '''<div>WOOP WOOP</div>''',
|
||||
})
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
if 'name' in init_values and init_values['name'] == magic_code:
|
||||
return 'mail.mt_name_changed'
|
||||
return False
|
||||
self.registry('mail.test.container')._patch_method('_track_subtype', _track_subtype)
|
||||
|
||||
def _track_template(self, changes):
|
||||
res = {}
|
||||
if 'name' in changes:
|
||||
res['name'] = (mail_template, {'composition_mode': 'mass_mail'})
|
||||
return res
|
||||
self.registry('mail.test.container')._patch_method('_track_template', _track_template)
|
||||
|
||||
cls = type(self.env['mail.test.container'])
|
||||
self.assertFalse(hasattr(getattr(cls, 'name'), 'track_visibility'))
|
||||
getattr(cls, 'name').track_visibility = 'always'
|
||||
|
||||
@self.addCleanup
|
||||
def cleanup():
|
||||
del getattr(cls, 'name').track_visibility
|
||||
|
||||
test_mail_record = self.env['mail.test.container'].create({
|
||||
'name': 'Zizizatestmailname',
|
||||
'description': 'Zizizatestmaildescription',
|
||||
})
|
||||
test_mail_record.with_context(default_parent_id=2147483647).write({'name': magic_code})
|
||||
|
||||
def test_message_track_multiple(self):
|
||||
""" check that multiple updates generate a single tracking message """
|
||||
container = self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({'name': 'Container'})
|
||||
self.record.name = 'Zboub'
|
||||
self.record.customer_id = self.user_admin.partner_id
|
||||
self.record.user_id = self.user_admin
|
||||
self.record.container_id = container
|
||||
self.flush_tracking()
|
||||
|
||||
# should have a single message with all tracked fields
|
||||
self.assertEqual(len(self.record.message_ids), 1, 'should have 1 tracking message')
|
||||
self.assertTracking(self.record.message_ids[0], [
|
||||
('customer_id', 'many2one', False, self.user_admin.partner_id),
|
||||
('user_id', 'many2one', False, self.user_admin),
|
||||
('container_id', 'many2one', False, container),
|
||||
])
|
||||
|
||||
def test_tracked_compute(self):
|
||||
# no tracking at creation
|
||||
record = self.env['mail.test.track.compute'].create({})
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(record.message_ids), 1)
|
||||
self.assertEqual(len(record.message_ids[0].tracking_value_ids), 0)
|
||||
|
||||
# assign partner_id: one tracking message for the modified field and all
|
||||
# the stored and non-stored computed fields on the record
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': 'Foo',
|
||||
'email': 'foo@example.com',
|
||||
'phone': '1234567890',
|
||||
})
|
||||
record.partner_id = partner
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(record.message_ids), 2)
|
||||
self.assertEqual(len(record.message_ids[0].tracking_value_ids), 4)
|
||||
self.assertTracking(record.message_ids[0], [
|
||||
('partner_id', 'many2one', False, partner),
|
||||
('partner_name', 'char', False, 'Foo'),
|
||||
('partner_email', 'char', False, 'foo@example.com'),
|
||||
('partner_phone', 'char', False, '1234567890'),
|
||||
])
|
||||
|
||||
# modify partner: one tracking message for the only recomputed field
|
||||
partner.write({'name': 'Fool'})
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(record.message_ids), 3)
|
||||
self.assertEqual(len(record.message_ids[0].tracking_value_ids), 1)
|
||||
self.assertTracking(record.message_ids[0], [
|
||||
('partner_name', 'char', 'Foo', 'Fool'),
|
||||
])
|
||||
|
||||
# modify partner: one tracking message for both stored computed fields;
|
||||
# the non-stored computed fields have no tracking
|
||||
partner.write({
|
||||
'name': 'Bar',
|
||||
'email': 'bar@example.com',
|
||||
'phone': '0987654321',
|
||||
})
|
||||
# force recomputation of 'partner_phone' to make sure it does not
|
||||
# generate tracking values
|
||||
self.assertEqual(record.partner_phone, '0987654321')
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(record.message_ids), 4)
|
||||
self.assertEqual(len(record.message_ids[0].tracking_value_ids), 2)
|
||||
self.assertTracking(record.message_ids[0], [
|
||||
('partner_name', 'char', 'Fool', 'Bar'),
|
||||
('partner_email', 'char', 'foo@example.com', 'bar@example.com'),
|
||||
])
|
||||
|
||||
@tagged('mail_track')
|
||||
class TestTrackingMonetary(TestMailCommon):
|
||||
|
||||
def setUp(self):
|
||||
super(TestTrackingMonetary, self).setUp()
|
||||
|
||||
self._activate_multi_company()
|
||||
|
||||
record = self.env['mail.test.track.monetary'].with_user(self.user_employee).with_context(self._test_context).create({
|
||||
'company_id': self.user_employee.company_id.id,
|
||||
})
|
||||
self.flush_tracking()
|
||||
self.record = record.with_context(mail_notrack=False)
|
||||
|
||||
def test_message_track_monetary(self):
|
||||
""" Update a record with a tracked monetary field """
|
||||
|
||||
# Check if the tracking value have the correct currency and values
|
||||
self.record.write({
|
||||
'revenue': 100,
|
||||
})
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(self.record.message_ids), 1)
|
||||
|
||||
self.assertTracking(self.record.message_ids[0], [
|
||||
('revenue', 'monetary', 0, 100),
|
||||
])
|
||||
|
||||
# Check if the tracking value have the correct currency and values after changing the value and the company
|
||||
self.record.write({
|
||||
'revenue': 200,
|
||||
'company_id': self.company_2.id,
|
||||
})
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(self.record.message_ids), 2)
|
||||
|
||||
self.assertTracking(self.record.message_ids[0], [
|
||||
('revenue', 'monetary', 100, 200),
|
||||
('company_currency', 'many2one', self.user_employee.company_id.currency_id, self.company_2.currency_id)
|
||||
])
|
||||
|
||||
@tagged('mail_track')
|
||||
class TestTrackingInternals(TestMailCommon):
|
||||
|
||||
def setUp(self):
|
||||
super(TestTrackingInternals, self).setUp()
|
||||
|
||||
record = self.env['mail.test.ticket'].with_user(self.user_employee).with_context(self._test_context).create({
|
||||
'name': 'Test',
|
||||
})
|
||||
self.flush_tracking()
|
||||
self.record = record.with_context(mail_notrack=False)
|
||||
|
||||
def test_track_groups(self):
|
||||
field = self.record._fields['email_from']
|
||||
self.addCleanup(setattr, field, 'groups', field.groups)
|
||||
field.groups = 'base.group_erp_manager'
|
||||
|
||||
self.record.sudo().write({'email_from': 'X'})
|
||||
self.flush_tracking()
|
||||
|
||||
msg_emp = self.record.message_ids.message_format()
|
||||
msg_sudo = self.record.sudo().message_ids.message_format()
|
||||
tracking_values = self.env['mail.tracking.value'].search([('mail_message_id', '=', self.record.message_ids.id)])
|
||||
formattedTrackingValues = [{
|
||||
'changedField': 'Email From',
|
||||
'id': tracking_values[0]['id'],
|
||||
'newValue': {
|
||||
'currencyId': False,
|
||||
'fieldType': 'char',
|
||||
'value': 'X',
|
||||
},
|
||||
'oldValue': {
|
||||
'currencyId': False,
|
||||
'fieldType': 'char',
|
||||
'value': False,
|
||||
},
|
||||
}]
|
||||
self.assertEqual(msg_emp[0].get('trackingValues'), [], "should not have protected tracking values")
|
||||
self.assertEqual(msg_sudo[0].get('trackingValues'), formattedTrackingValues, "should have protected tracking values")
|
||||
|
||||
msg_emp = self.record._notify_by_email_prepare_rendering_context(self.record.message_ids, {})
|
||||
msg_sudo = self.record.sudo()._notify_by_email_prepare_rendering_context(self.record.message_ids, {})
|
||||
self.assertFalse(msg_emp.get('tracking_values'), "should not have protected tracking values")
|
||||
self.assertTrue(msg_sudo.get('tracking_values'), "should have protected tracking values")
|
||||
|
||||
# test editing the record with user not in the group of the field
|
||||
self.env.invalidate_all()
|
||||
self.record.clear_caches()
|
||||
record_form = Form(self.record.with_user(self.user_employee))
|
||||
record_form.name = 'TestDoNoCrash'
|
||||
# the employee user must be able to save the fields on which they can write
|
||||
# if we fetch all the tracked fields, ignoring the group of the current user
|
||||
# it will crash and it shouldn't
|
||||
record = record_form.save()
|
||||
self.assertEqual(record.name, 'TestDoNoCrash')
|
||||
|
||||
def test_track_sequence(self):
|
||||
""" Update some tracked fields and check that the mail.tracking.value are ordered according to their tracking_sequence"""
|
||||
self.record.write({
|
||||
'name': 'Zboub',
|
||||
'customer_id': self.user_admin.partner_id.id,
|
||||
'user_id': self.user_admin.id,
|
||||
'container_id': self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({'name': 'Container'}).id
|
||||
})
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(self.record.message_ids), 1, 'should have 1 tracking message')
|
||||
|
||||
tracking_values = self.env['mail.tracking.value'].search([('mail_message_id', '=', self.record.message_ids.id)])
|
||||
self.assertEqual(tracking_values[0].tracking_sequence, 1)
|
||||
self.assertEqual(tracking_values[1].tracking_sequence, 2)
|
||||
self.assertEqual(tracking_values[2].tracking_sequence, 100)
|
||||
|
||||
def test_unlinked_field(self):
|
||||
record_sudo = self.record.sudo()
|
||||
record_sudo.write({'email_from': 'new_value'}) # create a tracking value
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(record_sudo.message_ids.tracking_value_ids), 1)
|
||||
ir_model_field = self.env['ir.model.fields'].search([
|
||||
('model', '=', 'mail.test.ticket'),
|
||||
('name', '=', 'email_from')])
|
||||
ir_model_field.with_context(_force_unlink=True).unlink()
|
||||
self.assertEqual(len(record_sudo.message_ids.tracking_value_ids), 0)
|
||||
1339
odoo-bringout-oca-ocb-test_mail/test_mail/tests/test_performance.py
Normal file
1339
odoo-bringout-oca-ocb-test_mail/test_mail/tests/test_performance.py
Normal file
File diff suppressed because it is too large
Load diff
21
odoo-bringout-oca-ocb-test_mail/test_mail/tests/test_ui.py
Normal file
21
odoo-bringout-oca-ocb-test_mail/test_mail/tests/test_ui.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# 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