2025-01-06 10:57:38 +07:00
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import json
from ast import literal_eval
from datetime import timedelta
from itertools import chain , product
from unittest . mock import DEFAULT , patch
from odoo import Command
from odoo . addons . base . tests . test_ir_cron import CronMixinCase
from odoo . addons . mail . tests . common import mail_new_test_user , MailCommon
from odoo . addons . mail . wizard . mail_compose_message import MailComposer
from odoo . addons . test_mail . models . test_mail_models import MailTestTicket
from odoo . addons . test_mail . tests . common import TestRecipients
from odoo . fields import Datetime as FieldDatetime
from odoo . exceptions import AccessError , UserError
from odoo . tests import Form , tagged , users
from odoo . tools import email_normalize , mute_logger , formataddr
@tagged ( ' mail_composer ' )
class TestMailComposer ( MailCommon , TestRecipients ) :
""" Test Composer internals """
@classmethod
def setUpClass ( cls ) :
super ( TestMailComposer , cls ) . setUpClass ( )
# force 'now' to ease test about schedulers
cls . reference_now = FieldDatetime . from_string ( ' 2022-12-24 12:00:00 ' )
# ensure employee can create partners, necessary for templates
cls . user_employee . write ( {
' groups_id ' : [ ( 4 , cls . env . ref ( ' base.group_partner_manager ' ) . id ) ] ,
} )
cls . user_employee_2 = mail_new_test_user (
cls . env , login = ' employee2 ' , groups = ' base.group_user ' ,
notification_type = ' email ' , email = ' eglantine@example.com ' ,
name = ' Eglantine Employee ' , signature = ' -- \n Eglantine ' )
cls . partner_employee_2 = cls . user_employee_2 . partner_id
# User without the group "mail.group_mail_template_editor"
cls . user_rendering_restricted = mail_new_test_user (
cls . env , login = ' user_rendering_restricted ' ,
groups = ' base.group_user ' ,
company_id = cls . company_admin . id ,
name = ' Code Template Restricted User ' ,
notification_type = ' inbox ' ,
signature = ' -- \n Ernest '
)
cls . env . ref ( ' mail.group_mail_template_editor ' ) . users - = cls . user_rendering_restricted
with cls . mock_datetime_and_now ( cls , cls . reference_now ) :
cls . test_record = cls . env [ ' mail.test.ticket.mc ' ] . with_context ( cls . _test_context ) . create ( {
' name ' : ' TestRecord ' ,
' customer_id ' : cls . partner_1 . id ,
' user_id ' : cls . user_employee_2 . id ,
} )
cls . test_records , cls . test_partners = cls . _create_records_for_batch (
' mail.test.ticket.mc ' , 2 ,
additional_values = { ' user_id ' : cls . user_employee_2 . id } ,
)
cls . test_report , cls . test_report_2 , cls . test_report_3 = cls . env [ ' ir.actions.report ' ] . create ( [
{
' name ' : ' Test Report on Mail Test Ticket ' ,
' model ' : ' mail.test.ticket.mc ' ,
' print_report_name ' : " ' TestReport for %s ' % o bject.name " ,
' report_type ' : ' qweb-pdf ' ,
' report_name ' : ' test_mail.mail_test_ticket_test_template ' ,
} , {
' name ' : ' Test Report 2 on Mail Test Ticket ' ,
' model ' : ' mail.test.ticket.mc ' ,
' print_report_name ' : " ' TestReport2 for %s ' % o bject.name " ,
' report_type ' : ' qweb-pdf ' ,
' report_name ' : ' test_mail.mail_test_ticket_test_template_2 ' ,
} , {
' name ' : ' Test Report 3 with variable data on Mail Test Ticket ' ,
' model ' : ' mail.test.ticket.mc ' ,
' print_report_name ' : " ' TestReport3 for %s ' % o bject.name " ,
' report_type ' : ' qweb-pdf ' ,
' report_name ' : ' test_mail.mail_test_ticket_test_variable_template ' ,
}
] )
cls . test_from = ' " John Doe " <john.doe@test.example.com> '
cls . template = cls . env [ ' mail.template ' ] . create ( {
' auto_delete ' : True ,
' name ' : ' TestTemplate ' ,
' subject ' : ' TemplateSubject {{ object.name }} ' ,
' body_html ' : ' <p>TemplateBody <t t-esc= " object.name " ></t></p> ' ,
' partner_to ' : ' {{ object.customer_id.id if object.customer_id else " " }} ' ,
' email_to ' : ' {{ (object.email_from if not object.customer_id else " " ) }} ' ,
' email_from ' : ' {{ (object.user_id.email_formatted or user.email_formatted) }} ' ,
' lang ' : ' {{ object.customer_id.lang }} ' ,
' mail_server_id ' : cls . mail_server_domain . id ,
' model_id ' : cls . env [ ' ir.model ' ] . _get ( ' mail.test.ticket.mc ' ) . id ,
' reply_to ' : ' {{ ctx.get( " custom_reply_to " ) or " info@test.example.com " }} ' ,
' scheduled_date ' : ' {{ (object.create_date or datetime.datetime(2022, 12, 26, 18, 0, 0)) + datetime.timedelta(days=2) }} ' ,
} )
# activate translations
cls . _activate_multi_lang (
layout_arch_db = None , # use default mail.test_layout
test_record = cls . test_records ,
test_template = cls . template ,
)
def _get_web_context ( self , records , add_web = True , * * values ) :
""" Helper to generate composer context. Will make tests a bit less
verbose .
: param add_web : add web context , generally making noise especially in
mass mail mode ( active_id / ids both present in context )
"""
base_context = {
' default_model ' : records . _name ,
' default_res_ids ' : records . ids ,
}
if len ( records ) == 1 :
base_context [ ' default_composition_mode ' ] = ' comment '
else :
base_context [ ' default_composition_mode ' ] = ' mass_mail '
if add_web :
base_context [ ' active_model ' ] = records . _name
base_context [ ' active_id ' ] = records [ 0 ] . id
base_context [ ' active_ids ' ] = records . ids
if values :
base_context . update ( * * values )
return base_context
@tagged ( ' mail_composer ' )
class TestComposerForm ( TestMailComposer ) :
@classmethod
def setUpClass ( cls ) :
super ( ) . setUpClass ( )
cls . template . write ( {
' email_layout_xmlid ' : ' mail.test_layout ' ,
} )
def test_assert_initial_data ( self ) :
""" Ensure class initial data to ease understanding """
self . assertTrue ( self . template . auto_delete )
self . assertEqual ( len ( self . test_records ) , 2 )
self . assertEqual ( self . test_records . user_id , self . user_employee_2 )
self . assertEqual ( self . test_records . message_partner_ids , self . partner_employee_2 )
@users ( ' employee ' )
def test_mail_composer_comment ( self ) :
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record , add_web = True )
) )
self . assertTrue ( composer_form . auto_delete , ' MailComposer: comment mode should remove notification emails by default ' )
self . assertFalse ( composer_form . auto_delete_keep_log , ' MailComposer: keep_log makes no sense in comment mode, only auto_delete ' )
self . assertEqual ( composer_form . author_id , self . env . user . partner_id )
self . assertFalse ( composer_form . body )
self . assertFalse ( composer_form . composition_batch )
self . assertEqual ( composer_form . composition_mode , ' comment ' )
self . assertEqual ( composer_form . email_from , self . env . user . email_formatted )
self . assertFalse ( composer_form . email_layout_xmlid )
self . assertTrue ( composer_form . force_send , ' MailComposer: single record post send notifications right away ' )
self . assertFalse ( composer_form . mail_server_id )
self . assertEqual ( composer_form . model , self . test_record . _name )
self . assertFalse ( composer_form . partner_ids )
self . assertEqual ( composer_form . record_alias_domain_id , self . mail_alias_domain )
self . assertEqual ( composer_form . record_company_id , self . env . company )
self . assertEqual ( composer_form . record_name , self . test_record . name , ' MailComposer: comment mode should compute record name ' )
self . assertFalse ( composer_form . reply_to )
self . assertFalse ( composer_form . reply_to_force_new )
self . assertFalse ( composer_form . scheduled_date )
self . assertEqual ( literal_eval ( composer_form . res_ids ) , self . test_record . ids )
self . assertEqual ( composer_form . subject , self . test_record . _message_compute_subject ( ) )
self . assertIn ( f ' Ticket for { self . test_record . name } ' , composer_form . subject ,
' Check effective content ' )
self . assertEqual ( composer_form . subtype_id , self . env . ref ( ' mail.mt_comment ' ) )
self . assertFalse ( composer_form . subtype_is_log )
@users ( ' employee ' )
def test_mail_composer_comment_mc ( self ) :
""" Test values specific to multi-company setup, in comment mode, using
the Form tool . """
# add access to second company to avoid MC rules on ticket model
self . env . user . company_ids = [ ( 4 , self . company_2 . id ) ]
test_record = self . test_record . with_env ( self . env )
source_company = [
self . company_admin ,
self . company_2 ,
self . env [ ' res.company ' ] ,
]
expected = [
( self . company_admin , self . mail_alias_domain ) ,
( self . company_2 , self . mail_alias_domain_c2 ) ,
( self . company_admin , self . mail_alias_domain ) , # env company
]
for company , ( exp_company , exp_alias_domain ) in zip ( source_company , expected ) :
with self . subTest ( cmopany = company ) :
test_record . write ( { ' company_id ' : company . id } )
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record , add_web = True )
) )
self . assertEqual ( composer_form . record_alias_domain_id , exp_alias_domain )
self . assertEqual ( composer_form . record_company_id , exp_company )
@users ( ' employee ' )
def test_mail_composer_comment_attachments ( self ) :
""" Tests that all attachments are added to the composer, static attachments
are not duplicated and while reports are re - generated , and that intermediary
attachments are dropped . """
attachment_data = self . _generate_attachments_data ( 2 , self . template . _name , self . template . id )
template_1 = self . template . copy ( {
' attachment_ids ' : [ ( 0 , 0 , a ) for a in attachment_data ] ,
' report_template_ids ' : [ ( 6 , 0 , ( self . test_report + self . test_report_2 ) . ids ) ] ,
} )
template_1_attachments = template_1 . attachment_ids
self . assertEqual ( len ( template_1_attachments ) , 2 )
template_2 = self . template . copy ( {
' attachment_ids ' : False ,
' report_template_ids ' : [ ( 6 , 0 , self . test_report . ids ) ] ,
} )
# begins without attachments
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record , add_web = True , default_attachment_ids = [ ] )
) )
self . assertEqual ( len ( composer_form . attachment_ids ) , 0 )
# change template: 2 static (attachment_ids) and 2 dynamic (reports)
composer_form . template_id = template_1
self . assertEqual ( len ( composer_form . attachment_ids ) , 4 )
report_attachments = [ att for att in composer_form . attachment_ids if att not in template_1_attachments ]
self . assertEqual ( len ( report_attachments ) , 2 )
tpl_attachments = composer_form . attachment_ids [ : ] - self . env [ ' ir.attachment ' ] . concat ( * report_attachments )
self . assertEqual ( tpl_attachments , template_1_attachments )
# change template: 0 static (attachment_ids) and 1 dynamic (report)
composer_form . template_id = template_2
self . assertEqual ( len ( composer_form . attachment_ids ) , 1 )
report_attachments = [ att for att in composer_form . attachment_ids if att not in template_1_attachments ]
self . assertEqual ( len ( report_attachments ) , 1 )
tpl_attachments = composer_form . attachment_ids [ : ] - self . env [ ' ir.attachment ' ] . concat ( * report_attachments )
self . assertFalse ( tpl_attachments )
# change back to template 1
composer_form . template_id = template_1
self . assertEqual ( len ( composer_form . attachment_ids ) , 4 )
report_attachments = [ att for att in composer_form . attachment_ids if att not in template_1_attachments ]
self . assertEqual ( len ( report_attachments ) , 2 )
tpl_attachments = composer_form . attachment_ids [ : ] - self . env [ ' ir.attachment ' ] . concat ( * report_attachments )
self . assertEqual ( tpl_attachments , template_1_attachments )
# reset template
composer_form . template_id = self . env [ ' mail.template ' ]
self . assertEqual ( len ( composer_form . attachment_ids ) , 0 )
@users ( ' employee ' )
def test_mail_composer_comment_wtpl ( self ) :
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record , add_web = True , default_template_id = self . template . id )
) )
self . assertTrue ( composer_form . auto_delete , ' Should take template value ' )
self . assertFalse ( composer_form . auto_delete_keep_log , ' MailComposer: keep_log makes no sense in comment mode, only auto_delete ' )
self . assertEqual ( composer_form . author_id , self . user_employee_2 . partner_id ,
' MailComposer: author is synchronized with email_from when possible ' )
self . assertEqual ( composer_form . body , f ' <p>TemplateBody { self . test_record . name } </p> ' )
self . assertFalse ( composer_form . composition_batch )
self . assertEqual ( composer_form . composition_mode , ' comment ' )
self . assertEqual ( composer_form . email_from , self . user_employee_2 . email_formatted )
self . assertEqual ( composer_form . email_layout_xmlid , ' mail.test_layout ' )
self . assertTrue ( composer_form . force_send , ' MailComposer: single record post send notifications right away ' )
self . assertEqual ( composer_form . mail_server_id , self . mail_server_domain )
self . assertEqual ( composer_form . model , self . test_record . _name )
self . assertEqual ( composer_form . partner_ids [ : ] , self . partner_1 )
self . assertEqual ( composer_form . record_alias_domain_id , self . mail_alias_domain )
self . assertEqual ( composer_form . record_company_id , self . env . company )
self . assertEqual ( composer_form . record_name , self . test_record . name , ' MailComposer: comment mode should compute record name ' )
self . assertEqual ( composer_form . reply_to , ' info@test.example.com ' )
self . assertFalse ( composer_form . reply_to_force_new )
self . assertEqual ( literal_eval ( composer_form . res_ids ) , self . test_record . ids )
self . assertEqual ( composer_form . scheduled_date , FieldDatetime . to_string ( self . reference_now + timedelta ( days = 2 ) ) )
self . assertEqual ( composer_form . subject , f ' TemplateSubject { self . test_record . name } ' )
self . assertEqual ( composer_form . subtype_id , self . env . ref ( ' mail.mt_comment ' ) )
self . assertFalse ( composer_form . subtype_is_log )
@users ( ' employee ' )
def test_mail_composer_comment_wtpl_batch ( self ) :
""" Batch mode of composer in comment mode. """
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context (
self . test_records ,
add_web = True ,
default_composition_mode = ' comment ' ,
default_template_id = self . template . id ) ,
) )
self . assertTrue ( composer_form . auto_delete , ' Should take composer value ' )
self . assertFalse ( composer_form . auto_delete_keep_log , ' MailComposer: keep_log makes no sense in comment mode, only auto_delete ' )
self . assertEqual ( composer_form . author_id , self . env . user . partner_id )
self . assertEqual ( composer_form . body , self . template . body_html ,
' MailComposer: comment in batch mode should have template raw body if template ' )
self . assertTrue ( composer_form . composition_batch )
self . assertEqual ( composer_form . composition_mode , ' comment ' )
self . assertEqual ( composer_form . email_from , self . template . email_from ,
' MailComposer: comment in batch mode should have template raw email_from if template ' )
self . assertEqual ( composer_form . email_layout_xmlid , ' mail.test_layout ' )
self . assertFalse ( composer_form . force_send , ' MailComposer: batch record post use email queue for notifications ' )
self . assertEqual ( composer_form . mail_server_id , self . mail_server_domain )
self . assertEqual ( composer_form . model , self . test_record . _name )
self . assertFalse ( composer_form . record_alias_domain_id , ' MailComposer: comment in batch mode should have void alias domain ' )
self . assertFalse ( composer_form . record_company_id , ' MailComposer: comment in batch mode should have void company ' )
self . assertFalse ( composer_form . record_name , ' MailComposer: comment in batch mode should have void record name ' )
self . assertEqual ( composer_form . reply_to , self . template . reply_to )
self . assertFalse ( composer_form . reply_to_force_new )
self . assertEqual ( literal_eval ( composer_form . res_ids ) , self . test_records . ids )
self . assertEqual ( composer_form . scheduled_date , self . template . scheduled_date )
self . assertEqual ( composer_form . subject , self . template . subject ,
' MailComposer: comment in batch mode should have template raw subject if template ' )
self . assertEqual ( composer_form . subtype_id , self . env . ref ( ' mail.mt_comment ' ) )
self . assertFalse ( composer_form . subtype_is_log )
@users ( ' employee ' )
def test_mail_composer_comment_wtpl_domain ( self ) :
""" Batch mode of composer in comment mode, using a domain. """
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
default_composition_mode = ' comment ' ,
default_model = self . test_records . _name ,
default_res_domain = [ ( ' id ' , ' in ' , self . test_records . ids ) ] ,
default_template_id = self . template . id ,
) )
self . assertTrue ( composer_form . auto_delete , ' Should take composer value ' )
self . assertFalse ( composer_form . auto_delete_keep_log , ' MailComposer: keep_log makes no sense in comment mode, only auto_delete ' )
self . assertEqual ( composer_form . author_id , self . env . user . partner_id )
self . assertEqual ( composer_form . body , self . template . body_html ,
' MailComposer: comment in batch mode should have template raw body if template ' )
self . assertTrue ( composer_form . composition_batch )
self . assertEqual ( composer_form . composition_mode , ' comment ' )
self . assertEqual ( composer_form . email_from , self . template . email_from ,
' MailComposer: comment in batch mode should have template raw email_from if template ' )
self . assertEqual ( composer_form . email_layout_xmlid , ' mail.test_layout ' )
self . assertFalse ( composer_form . force_send , ' MailComposer: batch record post use email queue for notifications ' )
self . assertEqual ( composer_form . mail_server_id , self . mail_server_domain )
self . assertEqual ( composer_form . model , self . test_record . _name )
self . assertFalse ( composer_form . record_alias_domain_id , ' MailComposer: comment in batch mode should have void alias domain ' )
self . assertFalse ( composer_form . record_company_id , ' MailComposer: comment in batch mode should have void company ' )
self . assertFalse ( composer_form . record_name , ' MailComposer: comment in batch mode should have void record name ' )
self . assertEqual ( composer_form . reply_to , self . template . reply_to )
self . assertFalse ( composer_form . reply_to_force_new )
self . assertFalse ( composer_form . res_ids )
self . assertEqual ( literal_eval ( composer_form . res_domain ) , [ ( ' id ' , ' in ' , self . test_records . ids ) ] )
self . assertEqual ( composer_form . scheduled_date , self . template . scheduled_date )
self . assertEqual ( composer_form . subject , self . template . subject ,
' MailComposer: comment in batch mode should have template raw subject if template ' )
self . assertEqual ( composer_form . subtype_id , self . env . ref ( ' mail.mt_comment ' ) )
self . assertFalse ( composer_form . subtype_is_log )
@users ( ' employee ' )
def test_mail_composer_comment_wtpl_norecords ( self ) :
""" Test specific case when running without records, to see the rendering
when nothing is given as context . """
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
default_composition_mode = ' comment ' ,
default_model = ' mail.test.ticket.mc ' ,
default_template_id = self . template . id ,
) )
self . assertTrue ( composer_form . auto_delete , ' Should take composer value ' )
self . assertFalse ( composer_form . auto_delete_keep_log , ' MailComposer: keep_log makes no sense in comment mode, only auto_delete ' )
self . assertEqual ( composer_form . author_id , self . env . user . partner_id )
self . assertEqual ( composer_form . body , ' <p>TemplateBody </p> ' )
self . assertFalse ( composer_form . composition_batch )
self . assertEqual ( composer_form . composition_mode , ' comment ' )
self . assertEqual ( composer_form . email_from , self . env . user . partner_id . email_formatted )
self . assertEqual ( composer_form . email_layout_xmlid , ' mail.test_layout ' )
self . assertTrue ( composer_form . force_send )
self . assertEqual ( composer_form . mail_server_id , self . mail_server_domain )
self . assertEqual ( composer_form . model , self . test_record . _name )
self . assertFalse ( composer_form . partner_ids [ : ] )
self . assertFalse ( composer_form . record_alias_domain_id )
self . assertFalse ( composer_form . record_company_id )
self . assertFalse ( composer_form . record_name )
self . assertEqual ( composer_form . reply_to , ' info@test.example.com ' )
self . assertFalse ( composer_form . reply_to_force_new )
self . assertFalse ( composer_form . res_ids )
self . assertEqual ( composer_form . scheduled_date ,
' 2022-12-28 18:00:00 ' ,
' No record but rendered, see expression in template ' )
self . assertEqual ( composer_form . subject , ' TemplateSubject ' )
self . assertEqual ( composer_form . subtype_id , self . env . ref ( ' mail.mt_comment ' ) )
self . assertFalse ( composer_form . subtype_is_log )
@users ( ' employee ' )
def test_mail_composer_mass ( self ) :
""" Test composer called in mass mailing mode. """
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_records , add_web = True )
) )
self . assertFalse ( composer_form . auto_delete )
self . assertFalse ( composer_form . auto_delete_keep_log , ' MailComposer: if emails are kept, logs are automatically kept ' )
self . assertEqual ( composer_form . author_id , self . env . user . partner_id )
self . assertFalse ( composer_form . body )
self . assertTrue ( composer_form . composition_batch )
self . assertEqual ( composer_form . composition_mode , ' mass_mail ' )
self . assertEqual ( composer_form . email_from , self . env . user . email_formatted )
self . assertFalse ( composer_form . email_layout_xmlid )
self . assertTrue ( composer_form . force_send , ' MailComposer: mass mode sends emails right away ' )
self . assertFalse ( composer_form . mail_server_id )
self . assertEqual ( composer_form . model , self . test_records . _name )
self . assertFalse ( composer_form . record_alias_domain_id , ' MailComposer: mass mode should have void alias domain ' )
self . assertFalse ( composer_form . record_company_id , ' MailComposer: mass mode should have void company ' )
self . assertFalse ( composer_form . record_name , ' MailComposer: mass mode should have void record name ' )
self . assertFalse ( composer_form . reply_to )
self . assertFalse ( composer_form . reply_to_force_new )
self . assertEqual ( sorted ( literal_eval ( composer_form . res_ids ) ) , sorted ( self . test_records . ids ) )
self . assertFalse ( composer_form . scheduled_date )
self . assertFalse ( composer_form . subject , ' MailComposer: mass mode should have void default subject if no template ' )
self . assertFalse ( composer_form . subtype_id , ' MailComposer: subtype is not used in mail mode ' )
self . assertFalse ( composer_form . subtype_is_log , ' MailComposer: subtype is log has no meaning in mail mode ' )
@users ( ' employee ' )
def test_mail_composer_mass_wtpl ( self ) :
""" Test composer called in mass mailing mode with a template. It globally
takes the template value raw ( aka not rendered ) . """
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_records , add_web = True , default_template_id = self . template . id )
) )
self . assertTrue ( composer_form . auto_delete , ' Should take composer value ' )
self . assertTrue ( composer_form . auto_delete_keep_log )
self . assertEqual ( composer_form . author_id , self . env . user . partner_id )
self . assertEqual ( composer_form . body , self . template . body_html ,
' MailComposer: mass mode should have template raw body if template ' )
self . assertTrue ( composer_form . composition_batch )
self . assertEqual ( composer_form . composition_mode , ' mass_mail ' )
self . assertEqual ( composer_form . email_from , self . template . email_from ,
' MailComposer: mass mode should have template raw email_from if template ' )
self . assertEqual ( composer_form . email_layout_xmlid , ' mail.test_layout ' )
self . assertTrue ( composer_form . force_send , ' MailComposer: mass mode sends emails right away ' )
self . assertEqual ( composer_form . mail_server_id , self . mail_server_domain )
self . assertEqual ( composer_form . model , self . test_records . _name )
self . assertFalse ( composer_form . record_alias_domain_id , ' MailComposer: mass mode should have void alias domain ' )
self . assertFalse ( composer_form . record_company_id , ' MailComposer: mass mode should have void company ' )
self . assertFalse ( composer_form . record_name , ' MailComposer: mass mode should have void record name ' )
self . assertEqual ( composer_form . reply_to , self . template . reply_to )
self . assertFalse ( composer_form . reply_to_force_new )
self . assertEqual ( sorted ( literal_eval ( composer_form . res_ids ) ) , sorted ( self . test_records . ids ) )
self . assertEqual ( composer_form . scheduled_date , self . template . scheduled_date )
self . assertEqual ( composer_form . subject , self . template . subject ,
' MailComposer: mass mode should have template raw subject if template ' )
self . assertFalse ( composer_form . subtype_id , ' MailComposer: subtype is not used in mail mode ' )
self . assertFalse ( composer_form . subtype_is_log , ' MailComposer: subtype is log has no meaning in mail mode ' )
@users ( ' employee ' )
def test_mail_composer_mass_wtpl_domain ( self ) :
""" Same as test_mail_composer_mass_wtpl, but using a domain instead
of res_ids , to check support of domain . """
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
default_composition_mode = ' mass_mail ' ,
default_model = self . test_records . _name ,
default_res_domain = [ ( ' id ' , ' in ' , self . test_records . ids ) ] ,
default_template_id = self . template . id ,
) )
self . assertTrue ( composer_form . auto_delete , ' Should take composer value ' )
self . assertTrue ( composer_form . auto_delete_keep_log )
self . assertEqual ( composer_form . author_id , self . env . user . partner_id )
self . assertEqual ( composer_form . body , self . template . body_html ,
' MailComposer: mass mode should have template raw body if template ' )
self . assertTrue ( composer_form . composition_batch )
self . assertEqual ( composer_form . composition_mode , ' mass_mail ' )
self . assertEqual ( composer_form . email_from , self . template . email_from ,
' MailComposer: mass mode should have template raw email_from if template ' )
self . assertEqual ( composer_form . email_layout_xmlid , ' mail.test_layout ' )
self . assertFalse ( composer_form . force_send , ' MailComposer: mass mode with domain uses email queue ' )
self . assertEqual ( composer_form . mail_server_id , self . mail_server_domain )
self . assertEqual ( composer_form . model , self . test_records . _name )
self . assertFalse ( composer_form . record_alias_domain_id , ' MailComposer: mass mode should have void alias domain ' )
self . assertFalse ( composer_form . record_company_id , ' MailComposer: mass mode should have void company ' )
self . assertFalse ( composer_form . record_name , ' MailComposer: mass mode should have void record name ' )
self . assertEqual ( composer_form . reply_to , self . template . reply_to )
self . assertFalse ( composer_form . reply_to_force_new )
self . assertFalse ( composer_form . res_ids )
self . assertEqual ( literal_eval ( composer_form . res_domain ) , [ ( ' id ' , ' in ' , self . test_records . ids ) ] )
self . assertEqual ( composer_form . scheduled_date , self . template . scheduled_date )
self . assertEqual ( composer_form . subject , self . template . subject ,
' MailComposer: mass mode should have template raw subject if template ' )
self . assertFalse ( composer_form . subtype_id , ' MailComposer: subtype is not used in mail mode ' )
self . assertFalse ( composer_form . subtype_is_log , ' MailComposer: subtype is log has no meaning in mail mode ' )
@users ( ' employee ' )
def test_mail_composer_mass_wtpl_norecords ( self ) :
""" Test specific case when running without records, to see the rendering
when nothing is given as context . """
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
default_composition_mode = ' mass_mail ' ,
default_model = ' mail.test.ticket.mc ' ,
default_template_id = self . template . id ,
) )
self . assertTrue ( composer_form . auto_delete , ' Should take composer value ' )
self . assertTrue ( composer_form . auto_delete_keep_log )
self . assertEqual ( composer_form . author_id , self . env . user . partner_id )
self . assertEqual ( composer_form . body , self . template . body_html ,
' MailComposer: mass mode should have template raw body if template ' )
self . assertFalse ( composer_form . composition_batch )
self . assertEqual ( composer_form . composition_mode , ' mass_mail ' )
self . assertEqual ( composer_form . email_from , self . template . email_from ,
' MailComposer: mass mode should have template raw email_from if template ' )
self . assertEqual ( composer_form . email_layout_xmlid , ' mail.test_layout ' )
self . assertTrue ( composer_form . force_send , ' MailComposer: mass mode sends emails right away ' )
self . assertEqual ( composer_form . mail_server_id , self . mail_server_domain )
self . assertEqual ( composer_form . model , self . test_records . _name )
self . assertFalse ( composer_form . record_alias_domain_id , ' MailComposer: mass mode should have void alias domain ' )
self . assertFalse ( composer_form . record_company_id , ' MailComposer: mass mode should have void company ' )
self . assertFalse ( composer_form . record_name , ' MailComposer: mass mode should have void record name ' )
self . assertEqual ( composer_form . reply_to , self . template . reply_to )
self . assertFalse ( composer_form . reply_to_force_new )
self . assertFalse ( composer_form . res_ids )
self . assertEqual ( composer_form . scheduled_date , self . template . scheduled_date )
self . assertEqual ( composer_form . subject , self . template . subject ,
' MailComposer: mass mode should have template raw subject if template ' )
self . assertFalse ( composer_form . subtype_id , ' MailComposer: subtype is not used in mail mode ' )
self . assertFalse ( composer_form . subtype_is_log , ' MailComposer: subtype is log has no meaning in mail mode ' )
@users ( ' employee ' )
def test_mail_composer_template_switching ( self ) :
""" Ensure that the current user ' s identity serves as the sender,
particularly when transitioning from a template with a designated sender to one lacking such specifications .
Moreover , we verify that switching to a template lacking any subject maintains the existing subject intact . """
# Setup: Prepare Templates
template_complete = self . template . copy ( {
" email_from " : " not_current_user@template_complete.com " ,
" subject " : " subject: template_complete " ,
} )
template_no_sender = template_complete . copy ( { " email_from " : " " } )
template_no_subject = template_no_sender . copy ( { " subject " : " " } )
forms = {
' comment ' : Form ( self . env [ ' mail.compose.message ' ] . with_context ( default_composition_mode = ' comment ' ) ) ,
' mass_mail ' : Form ( self . env [ ' mail.compose.message ' ] . with_context ( default_composition_mode = ' mass_mail ' ) ) ,
}
for composition_mode , form in forms . items ( ) :
with self . subTest ( composition_mode = composition_mode ) :
# Use Template with Sender and Subject
form . template_id = template_complete
self . assertEqual ( form . email_from , template_complete . email_from , " email_from not set correctly to form this test " )
self . assertEqual ( form . subject , template_complete . subject , " subject not set correctly to form this test " )
# Switch to Template without Sender
form . template_id = template_no_sender
self . assertEqual ( form . email_from , self . env . user . email_formatted , " email_from not updated to current user " )
# Switch to Template without Subject
form . template_id = template_no_subject
self . assertEqual ( form . subject , template_complete . subject , " subject should be kept unchanged " )
@tagged ( ' mail_composer ' )
class TestComposerInternals ( TestMailComposer ) :
@users ( ' employee ' )
@mute_logger ( ' odoo.addons.mail.models.mail_mail ' )
def test_mail_composer_attachments ( self ) :
""" Test attachments management in both comment and mass mail mode. """
attachment_data = self . _generate_attachments_data ( 3 , self . template . _name , self . template . id )
self . template . write ( {
' attachment_ids ' : [ ( 0 , 0 , a ) for a in attachment_data ] ,
' report_template_ids ' : [ ( 6 , 0 , ( self . test_report + self . test_report_2 ) . ids ) ] ,
} )
template_void = self . template . copy ( default = {
' attachment_ids ' : False ,
' report_template_ids ' : False ,
} )
attachs = self . env [ ' ir.attachment ' ] . search ( [ ( ' name ' , ' in ' , [ a [ ' name ' ] for a in attachment_data ] ) ] )
self . assertEqual ( len ( attachs ) , 3 )
extra_attach = self . env [ ' ir.attachment ' ] . create ( {
' datas ' : base64 . b64encode ( b ' ExtraData ' ) ,
' mimetype ' : ' text/plain ' ,
' name ' : ' ExtraAttFileName.txt ' ,
' res_model ' : False ,
' res_id ' : False ,
} )
for composition_mode , batch_mode in product ( ( ' comment ' , ' mass_mail ' ) ,
( False , True , ' domain ' ) ) :
with self . subTest ( composition_mode = composition_mode , batch_mode = batch_mode ) :
batch = bool ( batch_mode )
test_records = self . test_records if batch else self . test_record
ctx = {
' default_model ' : test_records . _name ,
' default_composition_mode ' : composition_mode ,
' default_template_id ' : self . template . id ,
}
if batch_mode == ' domain ' :
ctx [ ' default_res_domain ' ] = [ ( ' id ' , ' in ' , test_records . ids ) ]
else :
ctx [ ' default_res_ids ' ] = test_records . ids
composer = self . env [ ' mail.compose.message ' ] . with_context ( ctx ) . create ( {
' body ' : ' <p>Test Body</p> ' ,
} )
# values coming from template: attachment_ids + report in comment
if composition_mode == ' comment ' and not batch :
self . assertEqual ( len ( composer . attachment_ids ) , 5 )
for attach in attachs :
self . assertIn ( attach , composer . attachment_ids )
generated = composer . attachment_ids - attachs
self . assertEqual ( len ( generated ) , 2 , ' MailComposer: should have 2 additional attachments for reports ' )
self . assertEqual (
sorted ( generated . mapped ( ' name ' ) ) ,
sorted ( [ f ' TestReport for { self . test_record . name } .html ' , f ' TestReport2 for { self . test_record . name } .html ' ] ) )
self . assertEqual ( generated . mapped ( ' res_model ' ) , [ ' mail.compose.message ' ] * 2 )
self . assertEqual ( generated . mapped ( ' res_id ' ) , [ 0 ] * 2 )
# values coming from template: attachment_ids only (report is dynamic)
else :
self . assertEqual (
sorted ( composer . attachment_ids . ids ) ,
sorted ( attachs . ids )
)
# manual update
composer . write ( {
' attachment_ids ' : [ ( 4 , extra_attach . id ) ] ,
} )
if composition_mode == ' comment ' and not batch :
self . assertEqual ( composer . attachment_ids , attachs + extra_attach + generated )
else :
self . assertEqual ( composer . attachment_ids , attachs + extra_attach )
# update with template with void values: values are kept, void
# value is not forced in rendering mode as well as when copying
# template values
composer . write ( { ' template_id ' : template_void . id } )
if composition_mode == ' comment ' and not batch :
self . assertEqual ( composer . attachment_ids , attachs + extra_attach + generated )
else :
self . assertEqual ( composer . attachment_ids , attachs + extra_attach )
# reset template: values are reset
composer . write ( { ' template_id ' : False } )
if composition_mode == ' comment ' and not batch :
self . assertFalse ( composer . attachment_ids )
else :
self . assertFalse ( composer . attachment_ids )
@users ( ' employee ' )
@mute_logger ( ' odoo.addons.mail.models.mail_mail ' )
def test_mail_composer_author ( self ) :
""" Test author_id / email_from synchronization, in both comment and mass
mail modes . """
template_void = self . template . copy ( default = {
' email_from ' : False ,
} )
for composition_mode , batch_mode in product ( ( ' comment ' , ' mass_mail ' ) ,
( False , True , ' domain ' ) ) :
with self . subTest ( composition_mode = composition_mode , batch_mode = batch_mode ) :
batch = bool ( batch_mode )
test_records = self . test_records if batch else self . test_record
ctx = {
' default_model ' : test_records . _name ,
' default_composition_mode ' : composition_mode ,
}
if batch_mode == ' domain ' :
ctx [ ' default_res_domain ' ] = [ ( ' id ' , ' in ' , test_records . ids ) ]
else :
ctx [ ' default_res_ids ' ] = test_records . ids
composer = self . env [ ' mail.compose.message ' ] . with_context ( ctx ) . create ( {
' body ' : ' <p>Test Body</p> ' ,
} )
# default values are current user
self . assertEqual ( composer . author_id , self . env . user . partner_id )
self . assertEqual ( composer . composition_mode , composition_mode )
self . assertEqual ( composer . email_from , self . env . user . email_formatted )
# author update should reset email (FIXME: currently not synchronized)
composer . write ( { ' author_id ' : self . partner_1 } )
self . assertEqual ( composer . author_id , self . partner_1 )
self . assertEqual ( composer . email_from , self . env . user . email_formatted ,
' MailComposer: TODO: author / email_from are not synchronized ' )
# self.assertEqual(composer.email_from, self.partner_1.email_formatted)
# changing template should update its email_from
composer . write ( { ' template_id ' : self . template . id } )
if composition_mode == ' comment ' and not batch :
self . assertEqual ( composer . author_id , self . test_record . user_id . partner_id ,
f ' MailComposer: should try to link in rendered mode: { composer . author_id . name } , expected { self . env . user . name } ' )
self . assertEqual ( composer . email_from , self . test_record . user_id . email_formatted ,
' MailComposer: should take email_from rendered from template ' )
else :
self . assertEqual ( composer . author_id , self . env . user . partner_id ,
f ' MailComposer: should reset to current user in raw mode: { composer . author_id . name } , expected { self . env . user . name } ' )
self . assertEqual ( composer . email_from , self . template . email_from ,
' MailComposer: should take email_from raw from template ' )
# manual values are kept over template values; if email does not
# match any author, reset author
composer . write ( { ' email_from ' : self . test_from } )
if composition_mode == ' comment ' and not batch :
self . assertEqual ( composer . author_id , self . test_record . user_id . partner_id ,
' MailComposer: TODO: compute not called ' )
self . assertEqual ( composer . email_from , self . test_from ,
' MailComposer: manual values should be kept ' )
else :
self . assertEqual ( composer . author_id , self . env . user . partner_id ,
' MailComposer: TODO: compute not called ' )
self . assertEqual ( composer . email_from , self . test_from ,
' MailComposer: manual values should be kept ' )
# Update with template with void email_from field, should result in reseting email_from to a default value
# rendering mode as well as when copying template values
composer . write ( { ' template_id ' : template_void . id } )
if composition_mode == ' comment ' and not batch :
self . assertEqual ( composer . author_id , self . env . user . partner_id ,
' MailComposer: TODO: author / email_from are not synchronized ' )
self . assertEqual ( composer . email_from , self . env . user . email_formatted )
else :
self . assertEqual ( composer . author_id , self . env . user . partner_id ,
' MailComposer: TODO: author / email_from are not synchronized ' )
self . assertEqual ( composer . email_from , self . env . user . email_formatted )
# reset template: values are reset due to call to default_get
composer . write ( { ' template_id ' : False } )
if composition_mode == ' comment ' and not batch :
self . assertEqual ( composer . author_id , self . env . user . partner_id ,
' MailComposer: TODO: author / email_from are not synchronized ' )
self . assertEqual ( composer . email_from , self . env . user . email_formatted )
else :
self . assertEqual ( composer . author_id , self . env . user . partner_id ,
' MailComposer: TODO: author / email_from are not synchronized ' )
self . assertEqual ( composer . email_from , self . env . user . email_formatted )
@users ( ' employee ' )
def test_mail_composer_configuration ( self ) :
""" Test content configuration (auto_delete_*, email_*, message_type,
subtype ) in both comment and mass mailing mode . Template update is also
tested when it applies . """
template_falsy = self . template . copy ( default = {
' auto_delete ' : False ,
} )
for composition_mode , batch_mode in product ( ( ' comment ' , ' mass_mail ' ) ,
( False , True , ' domain ' ) ) :
with self . subTest ( composition_mode = composition_mode , batch_mode = batch_mode ) :
batch = bool ( batch_mode )
test_records = self . test_records if batch else self . test_record
ctx = {
' default_model ' : test_records . _name ,
' default_composition_mode ' : composition_mode ,
}
if batch_mode == ' domain ' :
ctx [ ' default_res_domain ' ] = [ ( ' id ' , ' in ' , test_records . ids ) ]
else :
ctx [ ' default_res_ids ' ] = test_records . ids
# 1. check without template (default values) + template update
composer = self . env [ ' mail.compose.message ' ] . with_context ( ctx ) . create ( {
' email_layout_xmlid ' : ' mail.test_layout ' ,
} )
# default creation values
if composition_mode == ' comment ' :
self . assertTrue ( composer . auto_delete , ' By default, remove notification emails ' )
self . assertFalse ( composer . auto_delete_keep_log , ' Not used in comment mode ' )
self . assertTrue ( composer . email_add_signature , ' Default value in comment mode ' )
self . assertEqual ( composer . subtype_id , self . env . ref ( ' mail.mt_comment ' ) )
else :
self . assertFalse ( composer . auto_delete , ' By default, keep mailing emails ' )
self . assertFalse ( composer . auto_delete_keep_log , ' Emails are not unlinked, logs are already kept ' )
self . assertFalse ( composer . email_add_signature , ' Not supported in mass mailing mode ' )
self . assertFalse ( composer . subtype_id )
self . assertEqual ( composer . email_layout_xmlid , ' mail.test_layout ' )
self . assertEqual ( composer . message_type , ' comment ' )
# changing template should update its content
composer . write ( { ' template_id ' : self . template . id } )
# values come from template
if composition_mode == ' comment ' :
self . assertTrue ( composer . auto_delete )
self . assertFalse ( composer . auto_delete_keep_log , ' Not used in comment mode ' )
self . assertFalse ( composer . email_add_signature , ' Template is considered as complete ' )
self . assertEqual ( composer . email_layout_xmlid , ' mail.test_layout ' )
self . assertEqual ( composer . message_type , ' comment ' )
self . assertEqual ( composer . subtype_id , self . env . ref ( ' mail.mt_comment ' ) )
else :
self . assertTrue ( composer . auto_delete )
self . assertTrue ( composer . auto_delete_keep_log )
self . assertFalse ( composer . email_add_signature , ' Template is considered as complete ' )
self . assertEqual ( composer . email_layout_xmlid , ' mail.test_layout ' )
self . assertEqual ( composer . message_type , ' comment ' )
self . assertFalse ( composer . subtype_id )
# manual update
composer . write ( {
' message_type ' : ' notification ' ,
' subtype_id ' : self . env . ref ( ' mail.mt_note ' ) . id ,
} )
self . assertEqual ( composer . message_type , ' notification ' )
self . assertEqual ( composer . subtype_id , self . env . ref ( ' mail.mt_note ' ) )
self . assertTrue ( composer . subtype_is_log )
# update with template with void values: void value is forced for
# booleans, cannot distinguish
composer . write ( { ' template_id ' : template_falsy . id } )
if composition_mode == ' comment ' :
self . assertFalse ( composer . auto_delete )
self . assertEqual ( composer . message_type , ' notification ' )
self . assertEqual ( composer . subtype_id , self . env . ref ( ' mail.mt_note ' ) )
self . assertTrue ( composer . subtype_is_log )
else :
self . assertFalse ( composer . auto_delete )
self . assertEqual ( composer . message_type , ' notification ' )
self . assertEqual ( composer . subtype_id , self . env . ref ( ' mail.mt_note ' ) )
self . assertTrue ( composer . subtype_is_log )
@users ( ' employee ' )
def test_mail_composer_content ( self ) :
""" Test content management (body, mail_server_id, record_name, scheduled_date,
subject ) in both comment and mass mailing mode . Template update is also
tested . """
template_void = self . template . copy ( default = {
' body_html ' : False ,
' mail_server_id ' : False ,
' scheduled_date ' : False ,
' subject ' : False ,
} )
for composition_mode , batch_mode in product ( ( ' comment ' , ' mass_mail ' ) ,
( False , True , ' domain ' ) ) :
with self . subTest ( composition_mode = composition_mode , batch_mode = batch_mode ) :
batch = bool ( batch_mode )
test_records = self . test_records if batch else self . test_record
ctx = {
' default_model ' : test_records . _name ,
' default_composition_mode ' : composition_mode ,
}
if batch_mode == ' domain ' :
ctx [ ' default_res_domain ' ] = [ ( ' id ' , ' in ' , test_records . ids ) ]
else :
ctx [ ' default_res_ids ' ] = test_records . ids
# 1. check without template + template update
composer = self . env [ ' mail.compose.message ' ] . with_context ( ctx ) . create ( {
' body ' : ' <p>Test Body <t t-out= " record.name " /></p> ' ,
' mail_server_id ' : self . mail_server_default . id ,
' scheduled_date ' : ' {{ datetime.datetime(2023, 1, 10, 10, 0, 0) }} ' ,
' subject ' : ' My amazing subject for {{ record.name }} ' ,
} )
# creation values are taken
self . assertEqual ( composer . body , ' <p>Test Body <t t-out= " record.name " /></p> ' )
self . assertEqual ( composer . mail_server_id , self . mail_server_default )
if composition_mode == ' comment ' and not batch :
self . assertEqual ( composer . record_name , self . test_record . name )
else :
self . assertFalse ( composer . record_name )
self . assertEqual ( composer . scheduled_date , ' {{ datetime.datetime(2023, 1, 10, 10, 0, 0) }} ' )
self . assertEqual ( composer . subject , ' My amazing subject for {{ record.name }} ' )
# changing template should update its content
composer . write ( { ' template_id ' : self . template . id } )
# values come from template
if composition_mode == ' comment ' and not batch :
self . assertEqual ( composer . body , f ' <p>TemplateBody { self . test_record . name } </p> ' )
self . assertEqual ( composer . mail_server_id , self . template . mail_server_id )
self . assertEqual ( composer . record_name , self . test_record . name )
self . assertEqual ( FieldDatetime . from_string ( composer . scheduled_date ) ,
self . reference_now + timedelta ( days = 2 ) )
self . assertEqual ( composer . subject , f ' TemplateSubject { self . test_record . name } ' )
else :
self . assertEqual ( composer . body , self . template . body_html )
self . assertEqual ( composer . mail_server_id , self . template . mail_server_id )
self . assertFalse ( composer . record_name )
self . assertEqual ( composer . scheduled_date , self . template . scheduled_date )
self . assertEqual ( composer . subject , self . template . subject )
# manual values is kept over template
composer . write ( {
' body ' : ' <p>Back to my amazing body <t t-out= " record.name " /></p> ' ,
' mail_server_id ' : self . mail_server_default . id ,
' record_name ' : ' Manual update ' ,
' scheduled_date ' : ' {{ datetime.datetime(2023, 1, 10, 10, 0, 0) }} ' ,
' subject ' : ' Back to my amazing subject for {{ record.name }} ' ,
} )
self . assertEqual ( composer . body , ' <p>Back to my amazing body <t t-out= " record.name " /></p> ' )
self . assertEqual ( composer . mail_server_id , self . mail_server_default )
self . assertEqual ( composer . record_name , ' Manual update ' )
self . assertEqual ( composer . scheduled_date , ' {{ datetime.datetime(2023, 1, 10, 10, 0, 0) }} ' )
self . assertEqual ( composer . subject , ' Back to my amazing subject for {{ record.name }} ' )
# update with template with void values: void value is not forced in
# rendering mode as well as in raw mode
composer . write ( { ' template_id ' : template_void . id } )
if composition_mode == ' comment ' and not batch :
self . assertEqual ( composer . body , ' <p>Back to my amazing body <t t-out= " record.name " /></p> ' )
self . assertEqual ( composer . mail_server_id , self . mail_server_default )
self . assertEqual ( composer . record_name , ' Manual update ' )
self . assertEqual ( composer . scheduled_date , ' {{ datetime.datetime(2023, 1, 10, 10, 0, 0) }} ' )
self . assertEqual ( composer . subject , ' Back to my amazing subject for {{ record.name }} ' )
else :
self . assertEqual ( composer . body , ' <p>Back to my amazing body <t t-out= " record.name " /></p> ' )
self . assertEqual ( composer . mail_server_id , self . mail_server_default )
self . assertEqual ( composer . record_name , ' Manual update ' )
self . assertEqual ( composer . scheduled_date , ' {{ datetime.datetime(2023, 1, 10, 10, 0, 0) }} ' )
self . assertEqual ( composer . subject , ' Back to my amazing subject for {{ record.name }} ' )
# reset template should reset values
composer . write ( { ' template_id ' : False } )
# values are reset with compute field
if composition_mode == ' comment ' and not batch :
self . assertFalse ( composer . body )
self . assertFalse ( composer . mail_server_id . id )
self . assertEqual ( composer . record_name , ' Manual update ' ,
' MailComposer: record name does not depend on template ' )
self . assertFalse ( composer . scheduled_date )
self . assertEqual ( composer . subject , self . test_record . _message_compute_subject ( ) )
self . assertIn ( f ' Ticket for { self . test_record . name } ' , composer . subject ,
' Check effective content ' )
else :
self . assertFalse ( composer . body )
self . assertFalse ( composer . mail_server_id . id )
self . assertEqual ( composer . record_name , ' Manual update ' ,
' MailComposer: record name does not depend on template ' )
self . assertFalse ( composer . scheduled_date )
self . assertFalse ( composer . subject )
# 2. check with default
ctx [ ' default_template_id ' ] = self . template . id
composer = self . env [ ' mail.compose.message ' ] . with_context ( ctx ) . create ( {
' template_id ' : self . template . id ,
} )
# values come from template
if composition_mode == ' comment ' and not batch :
self . assertEqual ( composer . body , f ' <p>TemplateBody { self . test_record . name } </p> ' )
self . assertEqual ( composer . mail_server_id , self . template . mail_server_id )
self . assertEqual ( composer . record_name , self . test_record . name )
self . assertEqual ( FieldDatetime . from_string ( composer . scheduled_date ) , self . reference_now + timedelta ( days = 2 ) )
self . assertEqual ( composer . subject , f ' TemplateSubject { self . test_record . name } ' )
else :
self . assertEqual ( composer . body , self . template . body_html )
self . assertEqual ( composer . mail_server_id , self . template . mail_server_id )
self . assertFalse ( composer . record_name )
self . assertEqual ( composer . scheduled_date , self . template . scheduled_date )
self . assertEqual ( composer . subject , self . template . subject )
# 3. check at create
ctx . pop ( ' default_template_id ' )
composer = self . env [ ' mail.compose.message ' ] . with_context ( ctx ) . create ( {
' template_id ' : self . template . id ,
} )
# values come from template
if composition_mode == ' comment ' and not batch :
self . assertEqual ( composer . body , f ' <p>TemplateBody { self . test_record . name } </p> ' )
self . assertEqual ( composer . mail_server_id , self . template . mail_server_id )
self . assertEqual ( composer . record_name , self . test_record . name )
self . assertEqual ( FieldDatetime . from_string ( composer . scheduled_date ) , self . reference_now + timedelta ( days = 2 ) )
self . assertEqual ( composer . subject , f ' TemplateSubject { self . test_record . name } ' )
else :
self . assertEqual ( composer . body , self . template . body_html )
self . assertEqual ( composer . mail_server_id , self . template . mail_server_id )
self . assertFalse ( composer . record_name )
self . assertEqual ( composer . scheduled_date , self . template . scheduled_date )
self . assertEqual ( composer . subject , self . template . subject )
# 4. template + user input
ctx [ ' default_template_id ' ] = self . template . id
composer = self . env [ ' mail.compose.message ' ] . with_context ( ctx ) . create ( {
' body ' : ' <p>Test Body</p> ' ,
' mail_server_id ' : False ,
' record_name ' : ' CustomName ' ,
' scheduled_date ' : ' {{ datetime.datetime(2023, 1, 10, 10, 0, 0) }} ' ,
' subject ' : ' My amazing subject ' ,
} )
# creation values are taken
self . assertEqual ( composer . body , ' <p>Test Body</p> ' )
self . assertEqual ( composer . mail_server_id . id , False )
self . assertEqual ( composer . record_name , ' CustomName ' )
self . assertEqual ( composer . scheduled_date , ' {{ datetime.datetime(2023, 1, 10, 10, 0, 0) }} ' )
self . assertEqual ( composer . subject , ' My amazing subject ' )
@users ( ' employee ' )
@mute_logger ( ' odoo.models.unlink ' )
def test_mail_composer_recipients ( self ) :
""" Test content management (partner_ids, reply_to) in both comment and
mass mailing mode . Template update is also tested . Add some tests for
partner creation based on unknown emails as this is part of the process . """
base_recipients = self . partner_1 + self . partner_2
self . template . write ( {
' email_cc ' : ' test.cc.1@test.example.com, test.cc.2@test.example.com '
} )
template_void = self . template . copy ( default = {
' email_cc ' : False ,
' email_to ' : False ,
' partner_to ' : False ,
' reply_to ' : False ,
} )
for composition_mode , batch_mode in product ( ( ' comment ' , ' mass_mail ' ) ,
( False , True , ' domain ' ) ) :
with self . subTest ( composition_mode = composition_mode , batch_mode = batch_mode ) :
self . assertFalse (
self . env [ ' res.partner ' ] . search ( [
( ' email_normalized ' , ' in ' , [ ' test.cc.1@test.example.com ' ,
' test.cc.2@test.example.com ' ] )
] )
)
batch = bool ( batch_mode )
test_records = self . test_records if batch else self . test_record
ctx = {
' default_model ' : test_records . _name ,
' default_composition_mode ' : composition_mode ,
}
if batch_mode == ' domain ' :
ctx [ ' default_res_domain ' ] = [ ( ' id ' , ' in ' , test_records . ids ) ]
else :
ctx [ ' default_res_ids ' ] = test_records . ids
# 1. check without template + template update
composer = self . env [ ' mail.compose.message ' ] . with_context ( ctx ) . create ( {
' body ' : ' <p>Test Body</p> ' ,
' partner_ids ' : base_recipients . ids ,
' reply_to ' : ' my_reply_to@test.example.com ' ,
' subject ' : ' My amazing subject ' ,
} )
# creation values are taken
self . assertEqual ( composer . partner_ids , base_recipients )
self . assertEqual ( composer . reply_to , ' my_reply_to@test.example.com ' )
self . assertFalse ( composer . reply_to_force_new )
self . assertEqual ( composer . reply_to_mode , ' update ' )
# update with template with void values: void value is not forced in
# rendering mode as well as when copying template values (and recipients
# are not computed until sending in rendering mode)
composer . write ( { ' template_id ' : template_void . id } )
if composition_mode == ' comment ' :
self . assertEqual ( composer . partner_ids , base_recipients )
self . assertEqual ( composer . reply_to , ' my_reply_to@test.example.com ' )
self . assertFalse ( composer . reply_to_force_new )
else :
self . assertEqual ( composer . partner_ids , base_recipients )
self . assertEqual ( composer . reply_to , ' my_reply_to@test.example.com ' )
self . assertFalse ( composer . reply_to_force_new )
# changing template should update its content
composer . write ( { ' template_id ' : self . template . id } )
composer . flush_recordset ( ) # to be able to search for new partners
new_partners = self . env [ ' res.partner ' ] . search (
[ ( ' email_normalized ' , ' in ' , [ ' test.cc.1@test.example.com ' ,
' test.cc.2@test.example.com ' ] )
]
)
# values come from template
if composition_mode == ' comment ' and not batch :
self . assertEqual ( len ( new_partners ) , 2 )
self . assertEqual ( composer . partner_ids , self . partner_1 + new_partners , ' Template took customer_id as set on record ' )
self . assertEqual ( composer . reply_to , ' info@test.example.com ' , ' Template was rendered ' )
self . assertFalse ( composer . reply_to_force_new ) # should not change in comment mode
else :
self . assertEqual ( len ( new_partners ) , 0 )
self . assertEqual ( composer . partner_ids , base_recipients , ' Mass mode: kept original values ' )
self . assertEqual ( composer . reply_to , self . template . reply_to , ' Mass mode: raw template value ' )
self . assertFalse ( composer . reply_to_force_new ) # should probably become True, not supported currently
# manual values is kept over template
composer . write ( { ' partner_ids ' : [ ( 5 , 0 ) , ( 4 , self . partner_admin . id ) ] } )
self . assertEqual ( composer . partner_ids , self . partner_admin )
# reset template should reset values
composer . write ( { ' template_id ' : False } )
# values are reset
if composition_mode == ' comment ' and not batch :
self . assertFalse ( composer . partner_ids )
self . assertFalse ( composer . reply_to )
self . assertFalse ( composer . reply_to_force_new )
else :
self . assertFalse ( composer . partner_ids )
self . assertFalse ( composer . reply_to )
self . assertFalse ( composer . reply_to_force_new )
# 2. check with default
ctx [ ' default_template_id ' ] = self . template . id
composer = self . env [ ' mail.compose.message ' ] . with_context ( ctx ) . create ( {
' template_id ' : self . template . id ,
} )
# values come from template
if composition_mode == ' comment ' and not batch :
self . assertEqual ( composer . partner_ids , self . partner_1 + new_partners )
else :
self . assertFalse ( composer . partner_ids )
if composition_mode == ' comment ' and not batch :
self . assertEqual ( composer . reply_to , " info@test.example.com " )
else :
self . assertEqual ( composer . reply_to , self . template . reply_to )
self . assertFalse ( composer . reply_to_force_new ) # note: this should be updated with reply-to
self . assertEqual ( composer . reply_to_mode , ' update ' ) # note: this should be updated with reply-to
# 3. check at create
ctx . pop ( ' default_template_id ' )
composer = self . env [ ' mail.compose.message ' ] . with_context ( ctx ) . create ( {
' template_id ' : self . template . id ,
} )
# values come from template
if composition_mode == ' comment ' and not batch :
self . assertEqual ( composer . partner_ids , self . partner_1 + new_partners )
else :
self . assertFalse ( composer . partner_ids )
if composition_mode == ' comment ' and not batch :
self . assertEqual ( composer . reply_to , " info@test.example.com " )
else :
self . assertEqual ( composer . reply_to , self . template . reply_to )
self . assertFalse ( composer . reply_to_force_new )
self . assertEqual ( composer . reply_to_mode , ' update ' )
# 4. template + user input
ctx [ ' default_template_id ' ] = self . template . id
composer = self . env [ ' mail.compose.message ' ] . with_context ( ctx ) . create ( {
' body ' : ' <p>Test Body</p> ' ,
' partner_ids ' : base_recipients . ids ,
' subject ' : ' My amazing subject ' ,
' reply_to ' : False ,
} )
# creation values are taken
self . assertEqual ( composer . partner_ids , base_recipients )
self . assertFalse ( composer . reply_to )
self . assertFalse ( composer . reply_to_force_new )
self . assertEqual ( composer . reply_to_mode , ' update ' )
self . env [ ' res.partner ' ] . search ( [
( ' email_normalized ' , ' in ' , [ ' test.cc.1@test.example.com ' ,
' test.cc.2@test.example.com ' ] )
] ) . unlink ( )
@users ( ' employee ' )
@mute_logger ( ' odoo.tests ' , ' odoo.addons.mail.models.mail_mail ' , ' odoo.models.unlink ' )
def test_mail_composer_parent ( self ) :
""" Test specific management in comment mode when having parent_id set:
record_name , subject , parent ' s partners. " " "
parent_subject = " Parent Subject "
parent = self . test_record . message_post (
body = ' Test ' ,
partner_ids = ( self . partner_1 + self . partner_2 ) . ids ,
subject = parent_subject ,
)
composer = self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record , add_web = False , default_parent_id = parent . id )
) . create ( {
' body ' : ' <p>Test Body</p> ' ,
} )
# creation values taken from parent
self . assertEqual ( composer . body , ' <p>Test Body</p> ' )
self . assertEqual ( composer . parent_id , parent )
self . assertEqual ( composer . partner_ids , self . partner_1 + self . partner_2 )
self . assertEqual ( composer . record_name , self . test_record . name )
self . assertEqual ( composer . subject , parent_subject )
@users ( ' user_rendering_restricted ' )
@mute_logger ( ' odoo.tests ' , ' odoo.addons.base.models.ir_rule ' , ' odoo.addons.mail.models.mail_mail ' , ' odoo.models.unlink ' )
def test_mail_composer_rights_attachments ( self ) :
""" Ensure a user without write access to a template can send an email """
template_1 = self . template . copy ( {
' report_template_ids ' : [ ( 6 , 0 , self . test_report . ids ) ] ,
} )
attachment_data = self . _generate_attachments_data ( 2 , self . template . _name , self . template . id )
template_1 . write ( {
' attachment_ids ' : [ ( 0 , 0 , dict ( a , res_model = " mail.template " , res_id = template_1 . id ) ) for a in attachment_data ]
} )
with self . assertRaises ( AccessError ) :
# ensure user_rendering_restricted has no write access
template_1 . with_user ( self . env . user ) . write ( { ' name ' : ' New Name ' } )
template_1_attachments = template_1 . attachment_ids
self . assertEqual ( len ( template_1_attachments ) , 2 )
template_1_attachment_name = list ( template_1_attachments . mapped ( ' name ' ) ) + [ f " TestReport for { self . test_record . name } .html " ]
composer = self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record )
) . create ( {
' body ' : ' <p>Template Body</p> ' ,
' template_id ' : template_1 . id ,
} )
composer . _action_send_mail ( )
self . assertEqual (
self . test_record . message_ids [ 0 ] . subject ,
f ' TemplateSubject { self . test_record . name } ' )
self . assertEqual (
sorted ( self . test_record . message_ids [ 0 ] . attachment_ids . mapped ( ' name ' ) ) ,
sorted ( template_1_attachment_name ) )
@mute_logger ( ' odoo.tests ' , ' odoo.addons.mail.models.mail_mail ' , ' odoo.models.unlink ' )
def test_mail_composer_rights_portal ( self ) :
portal_user = self . _create_portal_user ( )
# give read access to the record to portal (for check access rule)
self . test_record . message_subscribe ( partner_ids = portal_user . partner_id . ids )
# patch check access rights for write access, required to post a message by default
with patch . object ( MailTestTicket , ' _check_access ' , return_value = None ) :
with self . assertRaises ( AccessError ) :
# ensure portal can not send messages
self . env [ ' mail.compose.message ' ] . with_user ( portal_user ) . with_context (
self . _get_web_context ( self . test_record )
) . create ( {
' subject ' : ' Subject ' ,
' body ' : ' <p>Body text</p> ' ,
' partner_ids ' : [ ]
} ) . _action_send_mail ( )
@users ( ' employee ' )
def test_mail_composer_save_template ( self ) :
self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record , add_web = False )
) . create ( {
' template_name ' : ' My Template ' ,
' subject ' : ' Template Subject ' ,
' body ' : ' <p>Template Body</p> ' ,
} ) . create_mail_template ( )
# Test: email_template subject, body_html, model
template = self . env [ ' mail.template ' ] . search ( [
( ' model ' , ' = ' , self . test_record . _name ) ,
( ' subject ' , ' = ' , ' Template Subject ' )
] , limit = 1 )
self . assertEqual ( template . name , ' My Template ' )
self . assertEqual ( template . subject , ' Template Subject ' )
self . assertEqual ( template . body_html , ' <p>Template Body</p> ' , ' email_template incorrect body_html ' )
@users ( ' employee ' )
def test_mail_composer_schedule_message ( self ) :
""" Test scheduling of a message from the composer """
# cannot schedule a message in mass_mail mode
composer = self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record )
) . create ( { ' body ' : ' Test ' , ' composition_mode ' : ' mass_mail ' } )
with self . assertRaises ( UserError ) :
composer . action_schedule_message ( FieldDatetime . to_string ( self . reference_now + timedelta ( hours = 2 ) ) )
# schedule a message in mono-comment mode
attachment_data = self . _generate_attachments_data ( 1 , ' mail.compose.message ' , 0 ) [ 0 ]
composer = self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record )
) . create ( {
' body ' : ' <p>Test Body</p> ' ,
' subject ' : ' Test Subject ' ,
' attachment_ids ' : [ ( 0 , 0 , attachment_data ) ] ,
' partner_ids ' : [ ( 4 , self . test_record . customer_id . id ) ] ,
} )
composer_attachment = composer . attachment_ids
with self . mock_datetime_and_now ( self . reference_now ) :
composer . action_schedule_message ( FieldDatetime . to_string ( self . reference_now + timedelta ( hours = 2 ) ) )
# should have created a scheduled message with correct parameters
scheduled_message = self . env [ ' mail.scheduled.message ' ] . search ( [
[ ' model ' , ' = ' , self . test_record . _name ] ,
[ ' res_id ' , ' = ' , self . test_record . id ] ,
] )
self . assertEqual ( scheduled_message . body , ' <p>Test Body</p> ' )
self . assertEqual ( scheduled_message . subject , ' Test Subject ' )
self . assertEqual ( scheduled_message . scheduled_date , self . reference_now + timedelta ( hours = 2 ) )
self . assertEqual ( scheduled_message . attachment_ids , composer_attachment )
self . assertEqual ( scheduled_message . model , self . test_record . _name )
self . assertEqual ( scheduled_message . res_id , self . test_record . id )
self . assertEqual ( scheduled_message . author_id , self . partner_employee )
self . assertEqual ( scheduled_message . partner_ids , self . test_record . customer_id )
self . assertFalse ( scheduled_message . is_note )
# attachment transfer
self . assertEqual ( composer_attachment . res_model , scheduled_message . _name )
self . assertEqual ( composer_attachment . res_id , scheduled_message . id )
@users ( ' erp_manager ' )
@mute_logger ( ' odoo.addons.mail.models.mail_mail ' , ' odoo.models.unlink ' )
def test_mail_composer_wtpl_populate_new_recipients_mc ( self ) :
""" Test auto-populate of auto created partner with related record
values when sending a mail with a template , in multi - company environment .
"""
companies = self . company_admin + self . company_2
test_records = self . env [ ' mail.test.ticket.mc ' ] . create ( [
{
' email_from ' : f ' newpartner { idx } @example.com ' ,
' company_id ' : companies [ idx ] . id ,
' customer_id ' : False ,
' mobile_number ' : f ' +3319900 { idx : 02d } { idx : 02d } ' ,
' name ' : f ' TestRecord { idx } ' ,
' phone_number ' : False ,
' user_id ' : False ,
} for idx in range ( 2 )
] )
manual_recipients = test_records . mapped ( ' email_from ' )
template = self . env [ ' mail.template ' ] . create ( {
' email_to ' : ' {{ object.email_from }} ' ,
' model_id ' : self . env [ ' ir.model ' ] . _get_id ( test_records . _name ) ,
' partner_to ' : False ,
} )
for composition_mode , batch_mode in product (
( ' comment ' , ' mass_mail ' ) ,
( True , False )
) :
with self . subTest ( composition_mode = composition_mode , batch_mode = batch_mode ) :
test_records = test_records if batch_mode else test_records [ 0 ]
self . assertFalse (
self . env [ ' res.partner ' ] . search (
[ ( ' email_normalized ' , ' in ' , manual_recipients ) ]
)
)
ctx = {
' default_composition_mode ' : composition_mode ,
' default_model ' : test_records . _name ,
' default_res_ids ' : test_records . ids ,
}
composer = self . env [ ' mail.compose.message ' ] . with_context ( ctx ) . create ( {
' template_id ' : template . id ,
} )
with self . mock_mail_gateway ( ) :
composer . _action_send_mail ( )
new_partners = self . env [ ' res.partner ' ] . search ( [ ( ' email_normalized ' , ' in ' , manual_recipients ) ] ,
order = ' email ' )
try :
self . assertEqual (
len ( new_partners ) , len ( test_records )
)
self . assertEqual (
new_partners . mapped ( ' company_id ' ) ,
test_records . mapped ( ' company_id ' )
)
self . assertEqual (
new_partners . mapped ( ' mobile ' ) ,
test_records . mapped ( ' mobile_number ' )
)
finally :
new_partners . unlink ( )
@users ( ' employee ' )
def test_mail_composer_wtpl_schedule_message ( self ) :
""" Test scheduling message using a template. Should use the scheduled date of the
template if not date is passed . """
composer = self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record )
) . create ( { ' template_id ' : self . template . id } )
# scheduling the message
with self . mock_datetime_and_now ( self . reference_now ) :
composer . action_schedule_message ( )
# should have created the scheduled message with the correct parameters
scheduled_message = self . env [ ' mail.scheduled.message ' ] . search ( [
[ ' model ' , ' = ' , self . test_record . _name ] ,
[ ' res_id ' , ' = ' , self . test_record . id ] ,
] )
self . assertEqual ( scheduled_message . body , ' <p>TemplateBody TestRecord</p> ' )
self . assertEqual ( scheduled_message . subject , ' TemplateSubject TestRecord ' )
self . assertEqual ( scheduled_message . scheduled_date , self . test_record . create_date + timedelta ( days = 2 ) )
self . assertEqual ( scheduled_message . model , self . test_record . _name )
self . assertEqual ( scheduled_message . res_id , self . test_record . id )
self . assertEqual ( scheduled_message . author_id , self . test_record . user_id . partner_id )
self . assertEqual ( scheduled_message . partner_ids , self . test_record . customer_id )
self . assertFalse ( scheduled_message . is_note )
notification_parameters = json . loads ( scheduled_message . notification_parameters )
self . assertEqual ( notification_parameters [ ' email_from ' ] , self . test_record . user_id . email_formatted )
self . assertEqual ( notification_parameters [ ' force_email_lang ' ] , self . test_record . customer_id . lang )
self . assertEqual ( notification_parameters [ ' mail_server_id ' ] , self . mail_server_domain . id )
self . assertEqual ( notification_parameters [ ' mail_auto_delete ' ] , True )
self . assertEqual ( notification_parameters [ ' message_type ' ] , ' comment ' )
@tagged ( ' mail_composer ' , ' multi_lang ' , ' multi_company ' )
class TestComposerResultsComment ( TestMailComposer , CronMixinCase ) :
""" Test global output of composer used in comment mode. Test notably
notification and emails generated during this process . """
def test_assert_initial_data ( self ) :
""" Ensure class initial data to ease understanding """
self . assertTrue ( self . template . auto_delete )
self . assertEqual ( len ( self . test_record ) , 1 )
self . assertEqual ( self . test_record . user_id , self . user_employee_2 )
self . assertEqual ( self . test_record . message_partner_ids , self . partner_employee_2 )
self . assertEqual ( self . test_record [ 0 ] . customer_id . lang , ' en_US ' )
self . assertEqual ( self . test_record . company_id , self . company_admin )
self . assertEqual ( len ( self . test_records ) , 2 )
self . assertEqual ( self . test_records . user_id , self . user_employee_2 )
self . assertEqual ( self . test_records . message_partner_ids , self . partner_employee_2 )
self . assertEqual ( self . test_records [ 0 ] . customer_id . lang , ' en_US ' )
self . assertEqual ( self . test_records [ 1 ] . customer_id . lang , ' en_US ' )
self . assertEqual ( self . test_records . company_id , self . company_admin )
self . assertEqual ( len ( self . test_partners ) , 2 )
self . assertEqual ( self . user_employee . lang , ' en_US ' )
self . assertEqual ( self . user_employee_2 . lang , ' en_US ' )
@users ( ' employee ' )
def test_mail_composer_default_subject ( self ) :
""" Make sure the default subject is applied in the composer. """
simple_record = self . env [ ' mail.test.simple ' ] . create ( { ' name ' : ' TestA ' } )
ticket_record = self . env [ ' mail.test.ticket ' ] . create ( { ' name ' : ' Test1 ' } )
nonthread_record = self . env [ ' mail.test.nothread ' ] . create ( { ' name ' : ' TestNoThread ' } )
# default behavior: use the record name
ctx = self . _get_web_context ( simple_record , add_web = False , default_composition_mode = ' comment ' )
_ , message = self . env [ ' mail.compose.message ' ] . with_context ( ctx ) . create ( {
' body ' : ' <p>Test Body</p> ' ,
} ) . _action_send_mail ( )
self . assertEqual ( message . subject , simple_record . name )
# default behavior without thread: use record name
ctx = self . _get_web_context ( nonthread_record , add_web = False , default_composition_mode = ' comment ' )
_ , message = self . env [ ' mail.compose.message ' ] . with_context ( ctx ) . create ( {
' body ' : ' <p>Test Body</p> ' ,
' partner_ids ' : self . env . user . partner_id . ids ,
} ) . _action_send_mail ( )
self . assertEqual ( message . record_name , nonthread_record . name )
self . assertEqual ( message . subject , nonthread_record . name )
# custom subject
ctx = self . _get_web_context ( ticket_record , add_web = False , default_composition_mode = ' comment ' )
_ , message = self . env [ ' mail.compose.message ' ] . with_context ( ctx ) . create ( {
' body ' : ' <p>Test Body</p> ' ,
} ) . _action_send_mail ( )
self . assertEqual ( message . subject , ticket_record . _message_compute_subject ( ) )
self . assertEqual ( message . subject , f ' Ticket for { ticket_record . name } on { ticket_record . datetime . strftime ( " % m/ %d / % Y, % H: % M: % S " ) } ' )
# forced value
ctx = self . _get_web_context ( ticket_record , add_web = False , default_composition_mode = ' comment ' )
_ , message = self . env [ ' mail.compose.message ' ] . with_context ( ctx ) . create ( {
' body ' : ' <p>Test Body</p> ' ,
' subject ' : ' Forced Subject ' ,
} ) . _action_send_mail ( )
self . assertEqual ( message . subject , ' Forced Subject ' )
@users ( ' employee ' )
@mute_logger ( ' odoo.tests ' , ' odoo.addons.mail.models.mail_mail ' , ' odoo.models.unlink ' )
def test_mail_composer_notifications_delete ( self ) :
""" Notifications are correctly deleted once sent """
composer = self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record )
) . create ( {
' body ' : ' <p>Test Body</p> ' ,
' partner_ids ' : [ ( 4 , self . partner_1 . id ) , ( 4 , self . partner_2 . id ) ]
} )
self . assertTrue ( composer . auto_delete , ' Comment mode removes notification emails by default ' )
self . assertFalse ( composer . auto_delete_keep_log , ' Not used in comment mode ' )
with self . mock_mail_gateway ( mail_unlink_sent = True ) :
composer . _action_send_mail ( )
# notifications
message = self . test_record . message_ids [ 0 ]
self . assertEqual ( message . notified_partner_ids , self . partner_employee_2 + self . partner_1 + self . partner_2 )
# global outgoing
self . assertEqual ( len ( self . _mails ) , 3 , ' Should have sent an email each recipient ' )
self . assertEqual ( len ( self . _new_mails ) , 2 , ' Should have created 2 mail.mail (1 for users, 1 for customers) ' )
self . assertFalse ( self . _new_mails . exists ( ) , ' Should have deleted mail.mail records ' )
# Check ``auto_delete`` field usage (note: currently not correctly managed)
composer = self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record ) ,
) . create ( {
' auto_delete ' : False ,
' body ' : ' <p>Test Body</p> ' ,
' partner_ids ' : [ ( 4 , self . partner_1 . id ) , ( 4 , self . partner_2 . id ) ]
} )
self . assertFalse ( composer . auto_delete )
self . assertFalse ( composer . auto_delete_keep_log , ' Not used in comment mode ' )
with self . mock_mail_gateway ( mail_unlink_sent = True ) :
composer . _action_send_mail ( )
# notifications
message = self . test_record . message_ids [ 0 ]
self . assertEqual ( message . notified_partner_ids , self . partner_employee_2 + self . partner_1 + self . partner_2 )
# global outgoing
self . assertEqual ( len ( self . _mails ) , 3 , ' Should have sent an email each recipient ' )
self . assertEqual ( len ( self . _new_mails ) , 2 , ' Should have created 2 mail.mail (1 for users, 1 for customers) ' )
self . assertEqual ( len ( self . _new_mails . exists ( ) ) , 2 , ' Should not have deleted mail.mail records ' )
@users ( ' employee ' )
@mute_logger ( ' odoo.tests ' , ' odoo.addons.mail.models.mail_mail ' , ' odoo.models.unlink ' )
def test_mail_composer_post_parameters ( self ) :
""" Test various fields and tweaks in comment mode used for message_post
parameters and process . . """
# default behavior
composer = self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record )
) . create ( {
' body ' : ' <p>Test Body</p> ' ,
} )
_mail , message = composer . _action_send_mail ( )
self . assertEqual ( message . body , ' <p>Test Body</p> ' )
self . assertTrue ( message . email_add_signature )
self . assertFalse ( message . email_layout_xmlid )
self . assertEqual ( message . message_type , ' comment ' , ' Mail: default message type with composer is user comment ' )
self . assertEqual ( message . record_name , self . test_record . name )
self . assertEqual ( message . subtype_id , self . env . ref ( ' mail.mt_comment ' , ' Mail: default subtype is comment ' ) )
# tweaks
composer = self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record )
) . create ( {
' body ' : ' <p>Test Body 2</p> ' ,
' email_add_signature ' : False ,
' email_layout_xmlid ' : ' mail.mail_notification_light ' ,
' message_type ' : ' notification ' ,
' subtype_id ' : self . env . ref ( ' mail.mt_note ' ) . id ,
' partner_ids ' : [ ( 4 , self . partner_1 . id ) , ( 4 , self . partner_2 . id ) ] ,
' record_name ' : ' Custom record name ' ,
} )
_mail , message = composer . _action_send_mail ( )
self . assertEqual ( message . body , ' <p>Test Body 2</p> ' )
self . assertFalse ( message . email_add_signature )
self . assertEqual ( message . email_layout_xmlid , ' mail.mail_notification_light ' )
self . assertEqual ( message . message_type , ' notification ' )
self . assertEqual ( message . record_name , ' Custom record name ' )
self . assertEqual ( message . subtype_id , self . env . ref ( ' mail.mt_note ' ) )
# subtype through xml id
composer = self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record ) ,
default_subtype_xmlid = ' mail.mt_note ' ,
) . create ( {
' body ' : ' <p>Default subtype through xml id</p> ' ,
} )
_mail , message = composer . _action_send_mail ( )
self . assertEqual ( message . subtype_id , self . env . ref ( ' mail.mt_note ' ) )
# Check that the message created from the mail composer gets the values
# we passed to the composer context. When we provide a custom `body`
# and `email_add_signature` flag, the message should keep those values
# and should not add any signature to the message body.
composer = self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record ) ,
default_body = ' <p>Hello world</p> ' ,
default_email_add_signature = False
) . create ( { } )
_mail , message = composer . _action_send_mail ( )
self . assertFalse ( message . email_add_signature )
self . assertEqual ( message . body , ' <p>Hello world</p> ' )
composer = self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record ) ,
default_body = ' <p>Hi there</p> ' ,
default_email_add_signature = True
) . create ( { } )
_mail , message = composer . _action_send_mail ( )
self . assertTrue ( message . email_add_signature )
self . assertEqual ( message . body , ' <p>Hi there</p> ' )
@users ( ' employee ' )
@mute_logger ( ' odoo.tests ' , ' odoo.addons.mail.models.mail_mail ' , ' odoo.models.unlink ' )
def test_mail_composer_recipients ( self ) :
""" Test partner_ids given to composer are given to the final message. """
composer = self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record )
) . create ( {
' body ' : ' <p>Test Body</p> ' ,
' partner_ids ' : [ ( 4 , self . partner_1 . id ) , ( 4 , self . partner_2 . id ) ]
} )
composer . _action_send_mail ( )
message = self . test_record . message_ids [ 0 ]
self . assertEqual ( message . author_id , self . user_employee . partner_id )
self . assertEqual ( message . body , ' <p>Test Body</p> ' )
self . assertEqual ( message . subject , self . test_record . _message_compute_subject ( ) )
self . assertEqual ( message . subtype_id , self . env . ref ( ' mail.mt_comment ' ) )
self . assertEqual ( message . partner_ids , self . partner_1 | self . partner_2 )
@users ( ' employee ' )
@mute_logger ( ' odoo.addons.mail.models.mail_mail ' )
def test_mail_composer_server_config ( self ) :
""" Test various configuration to check behavior of outgoing mail
servers , notifications , . . . . """
# add access to second company to avoid MC rules on ticket model
self . env . user . company_ids = [ ( 4 , self . company_2 . id ) ]
# initial data
self . assertEqual ( self . env . company , self . company_admin )
self . assertEqual ( self . user_admin . company_id , self . company_admin )
# update test configuration
test_records = self . test_records
test_companies = self . company_admin + self . company_2
self . company_2 . alias_domain_id = self . mail_alias_domain
for company , record in zip ( test_companies , test_records ) :
record . company_id = company . id
# various from / servers configuration
self . template . write ( { ' mail_server_id ' : False } ) # allow server archive
server_other = self . env [ ' ir.mail_server ' ] . sudo ( ) . create ( {
' name ' : ' Server Other ' ,
' from_filter ' : ' test.othercompany.com ' ,
' sequence ' : 4 ,
' smtp_encryption ' : ' none ' ,
' smtp_host ' : ' smtp_host ' ,
} )
servers_all = self . mail_servers + server_other
for ( emails_from , servers_active , mail_config ) , ( exp_smtp_from_lst , exp_msg_from_lst , exp_from_filter ) in zip (
[
(
[ f ' user.from@ { self . alias_domain } ' , ' user@other.domain.com ' ] ,
self . env [ ' ir.mail_server ' ] ,
{ ' default_from ' : f ' notifications@ { self . alias_domain } ' , ' from_filter ' : self . alias_domain }
) , # odoo-style configuration
] , [
(
[ f ' { self . alias_bounce } @ { self . alias_domain } ' , f ' { self . alias_bounce } @ { self . alias_domain } ' ] ,
[ f ' " { self . env . user . name } " <user.from@ { self . alias_domain } > ' , f ' " { self . env . user . name } " <notifications@ { self . alias_domain } > ' ] ,
self . alias_domain
) , # no spoof
] ,
) :
with self . subTest ( emails_from = emails_from ,
servers_active = servers_active ) :
# update servers
servers_all . active = False
if servers_active :
servers_active . active = True
# update mail config
default_from = mail_config . get ( ' default_from ' , self . default_from )
from_filter = mail_config . get ( ' from_filter ' , self . default_from_filter )
self . mail_alias_domain . default_from = default_from
self . env [ ' ir.config_parameter ' ] . sudo ( ) . set_param ( ' mail.default.from_filter ' , from_filter )
for email_from , exp_smtp_from , exp_msg_from in zip ( emails_from , exp_smtp_from_lst , exp_msg_from_lst ) :
self . env . user . email = email_from
# open a composer and run it in comment mode
composer = Form ( self . env [ ' mail.compose.message ' ] . with_context (
default_composition_mode = ' comment ' ,
default_force_send = True , # force sending emails directly to check SMTP
default_model = test_records . _name ,
default_res_ids = test_records . ids ,
# avoid successive tests issues with followers
mail_create_nosubscribe = True ,
) )
composer . body = ' Hello {{ object.name }} '
composer . subject = ' My Subject '
composer = composer . save ( )
with self . mock_mail_gateway ( mail_unlink_sent = False ) , \
self . mock_mail_app ( ) :
composer . _action_send_mail ( )
self . assertSMTPEmailsSent (
smtp_from = exp_smtp_from ,
smtp_to_list = [ self . partner_employee_2 . email_normalized ] ,
emails_count = 2 , # same on both records
message_from = exp_msg_from ,
from_filter = exp_from_filter ,
)
@users ( ' employee ' )
@mute_logger ( ' odoo.models.unlink ' , ' odoo.addons.mail.models.mail_mail ' , ' odoo.addons.mail.models.mail_message_schedule ' )
def test_mail_composer_wtpl_complete ( self ) :
""" Test a posting process using a complex template, holding several
additional recipients and attachments . It is done in monorecord and
in batch since this is now supported .
This tests notifies : 2 new email_to ( + 1 duplicated ) , 1 email_cc ,
test_records followers and partner_admin added in partner_to .
Global notification
* monorecord : send notifications right away ( force_send = True )
* multirecord : delay notification sending ( force_send = False )
Use cases
* scheduled_date : creates mail . message . schedule ( no email sent ) , then
scheduling send notifications with notification parameters kept
* otherwise : global behavior
Test with and without notification layout specified .
Test with and without languages .
Setup with batch and langs
* record1 : customer lang = es_ES
follower partner_employee_2 lang = en_US
3 new partners ( en_US ) created by template
* record2 : customer lang = en_US
follower partner_employee_2 lang = en_US
3 new partners ( en_US ) created by template
"""
attachment_data = self . _generate_attachments_data ( 2 , self . template . _name , self . template . id )
email_to_1 = ' test.to.1@test.example.com '
email_to_2 = ' test.to.2@test.example.com '
email_to_3 = ' test.to.1@test.example.com ' # duplicate: should not sent twice the email
email_cc_1 = ' test.cc.1@test.example.com '
self . template . write ( {
' auto_delete ' : False , # keep sent emails to check content
' attachment_ids ' : [ ( 0 , 0 , a ) for a in attachment_data ] ,
' email_to ' : ' %s , %s , %s ' % ( email_to_1 , email_to_2 , email_to_3 ) ,
' email_cc ' : email_cc_1 ,
' partner_to ' : ' %s , {{ object.customer_id.id if object.customer_id else " " }} ' % self . partner_admin . id ,
' report_template_ids ' : [ ( 6 , 0 , ( self . test_report + self . test_report_2 ) . ids ) ] ,
} )
attachs = self . env [ ' ir.attachment ' ] . search ( [ ( ' name ' , ' in ' , [ a [ ' name ' ] for a in attachment_data ] ) ] )
self . assertEqual ( len ( attachs ) , 2 )
for batch_mode , scheduled_date , email_layout_xmlid , use_lang in product (
( False , True , ' domain ' ) ,
( False , ' {{ (object.create_date or datetime.datetime(2022, 12, 26, 18, 0, 0)) + datetime.timedelta(days=2) }} ' ) ,
( False , ' mail.test_layout ' ) ,
( False , True ) ,
) :
with self . subTest ( batch_mode = batch_mode ,
scheduled_date = scheduled_date ,
email_layout_xmlid = email_layout_xmlid ,
use_lang = use_lang ) :
# update test configuration
batch = bool ( batch_mode )
self . template . write ( {
' scheduled_date ' : scheduled_date ,
' email_layout_xmlid ' : email_layout_xmlid ,
} )
if use_lang :
if batch :
langs = ( ' es_ES ' , ' en_US ' )
self . test_partners [ 0 ] . lang = langs [ 0 ]
self . test_partners [ 1 ] . lang = langs [ 1 ]
else :
langs = ( ' es_ES ' , )
self . partner_1 . lang = langs [ 0 ]
if not use_lang :
if batch :
langs = ( False , False )
self . test_partners . lang = False
else :
langs = ( False , )
self . partner_1 . lang = False
test_records = self . test_records if batch else self . test_record
# ensure initial data
self . assertEqual ( len ( test_records . customer_id ) , len ( test_records ) )
self . assertEqual ( test_records . user_id , self . user_employee_2 )
self . assertEqual ( test_records . message_partner_ids , self . partner_employee_2 )
ctx = {
' default_model ' : test_records . _name ,
' default_composition_mode ' : ' comment ' ,
' default_template_id ' : self . template . id ,
# avoid successive tests issues with followers
' mail_create_nosubscribe ' : True ,
}
if batch_mode == ' domain ' :
ctx [ ' default_res_domain ' ] = [ ( ' id ' , ' in ' , test_records . ids ) ]
else :
ctx [ ' default_res_ids ' ] = test_records . ids
# open a composer and run it in comment mode
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context ( ctx ) )
composer = composer_form . save ( )
self . assertEqual ( composer . email_layout_xmlid , email_layout_xmlid )
# ensure some parameters used afterwards
if batch :
author = self . env . user . partner_id
self . assertEqual ( composer . author_id , author ,
' Author cannot be synchronized with a raw email_from ' )
self . assertEqual ( composer . email_from , self . template . email_from )
else :
author = self . partner_employee_2
self . assertEqual ( composer . author_id , author ,
' Author is synchronized with rendered email_from ' )
self . assertEqual ( composer . email_from , self . partner_employee_2 . email_formatted )
self . assertFalse ( composer . reply_to_force_new , ' Mail: thread-enabled models should use auto thread by default ' )
# due to scheduled_date, cron for sending notification will be used
schedule_cron_id = self . env . ref ( ' mail.ir_cron_send_scheduled_message ' ) . id
with self . mock_mail_gateway ( mail_unlink_sent = False ) , \
self . mock_mail_app ( ) , \
self . mock_datetime_and_now ( self . reference_now ) , \
self . capture_triggers ( schedule_cron_id ) as capt :
composer . _action_send_mail ( )
# notification process should not have been sent
if scheduled_date :
self . assertFalse ( self . _new_mails )
self . assertFalse ( self . _mails )
# monorecord: force_send notifications
elif not batch :
# as there are recipients with different langs: we have
# 3 outgoing mails: partner_employee2 (user) then customers
# in two langs
if use_lang :
self . assertEqual (
len ( self . _new_mails ) , 3 ,
' Should have created 1 mail for user, then 2 for customers that belong to 2 langs ' )
# without lang, recipients are grouped by main usage aka user and customer
else :
self . assertEqual (
len ( self . _new_mails ) , 2 ,
' Should have created 1 mail for user, then 1 for customers ' )
self . assertEqual ( self . _new_mails . mapped ( ' state ' ) , [ ' sent ' ] * len ( self . _new_mails ) )
self . assertEqual ( len ( self . _mails ) , 5 , ' Should have sent 5 emails, one per recipient per record ' )
# multirecord: use email queue
else :
# see not-batch comment, then add 2 mails for the second
# record as all customers have same language
if use_lang :
self . assertEqual (
len ( self . _new_mails ) , 5 ,
' Should have created 3 mails for first record, then 2 for second ' )
else :
self . assertEqual (
len ( self . _new_mails ) , 4 ,
' Should have created 2 mails / record (one for user, one for customers) ' )
self . assertEqual ( self . _new_mails . mapped ( ' state ' ) , [ ' outgoing ' ] * len ( self . _new_mails ) )
self . assertEqual ( len ( self . _mails ) , 0 , ' Should have put emails in queue and not sent any emails ' )
# simulate cron sending emails
self . env [ ' mail.mail ' ] . sudo ( ) . process_email_queue ( )
# notification process should not have been sent
if scheduled_date :
self . assertEqual (
capt . records . mapped ( ' call_at ' ) , [ self . reference_now + timedelta ( days = 2 ) ] * len ( test_records ) ,
msg = ' Should have created a cron trigger for the scheduled sending '
)
else :
self . assertFalse ( capt . records )
# check new partners have been created based on emails given
new_partners = self . env [ ' res.partner ' ] . search ( [
( ' email ' , ' in ' , [ email_to_1 , email_to_2 , email_to_3 , email_cc_1 ] )
] )
self . assertEqual ( len ( new_partners ) , 3 )
self . assertEqual (
set ( new_partners . mapped ( ' email ' ) ) ,
{ ' test.to.1@test.example.com ' , ' test.to.2@test.example.com ' , ' test.cc.1@test.example.com ' } ,
)
self . assertEqual (
set ( new_partners . mapped ( ' lang ' ) ) ,
{ ' en_US ' } ,
)
# if scheduled_date is set: simulate cron for sending notifications
if scheduled_date :
# Send the scheduled message from the CRON
with self . mock_mail_gateway ( mail_unlink_sent = False ) , \
self . mock_mail_app ( ) , \
self . mock_datetime_and_now ( self . reference_now + timedelta ( days = 3 ) ) :
self . env [ ' mail.message.schedule ' ] . sudo ( ) . _send_notifications_cron ( )
# monorecord: force_send notifications
if not batch :
# as there are recipients with different langs: we have
# 3 outgoing mails: partner_employee2 (user) then customers
# in two langs
if use_lang :
self . assertEqual (
len ( self . _new_mails ) , 3 ,
' Should have created 1 mail for user, then 2 for customers that belong to 2 langs ' )
# without lang, recipients are grouped by main usage aka user and customer
else :
self . assertEqual (
len ( self . _new_mails ) , 2 ,
' Should have created 1 mail for user, then 1 for customers ' )
self . assertEqual ( self . _new_mails . mapped ( ' state ' ) , [ ' sent ' ] * len ( self . _new_mails ) )
self . assertEqual ( len ( self . _mails ) , 5 , ' Should have sent 5 emails, one per recipient per record ' )
# multirecord: use email queue
else :
# see not-batch comment, then add 2 mails for the second
# record as all customers have same language
if use_lang :
self . assertEqual (
len ( self . _new_mails ) , 5 ,
' Should have created 3 mails for first record, then 2 for second ' )
else :
self . assertEqual (
len ( self . _new_mails ) , 4 ,
' Should have created 2 mails / record (one for user, one for customers) ' )
self . assertEqual ( self . _new_mails . mapped ( ' state ' ) , [ ' outgoing ' ] * len ( self . _new_mails ) )
self . assertEqual ( len ( self . _mails ) , 0 , ' Should have put emails in queue and not sent any emails ' )
# simulate cron sending emails
self . env [ ' mail.mail ' ] . sudo ( ) . process_email_queue ( )
# template is sent only to partners (email_to are transformed)
for test_record , exp_lang in zip ( test_records , langs ) :
message = test_record . message_ids [ 0 ]
# check created mail.mail and outgoing emails. In comment
# 2 or 3 mails are generated (due to group-based layouting):
# - one for recipient that is a user
# - one / two for recipients that are customers, one / lang
# Then each recipient receives its own outgoing email. See
# 'assertMailMail' for more details.
# user email (one user, one email)
if exp_lang == ' es_ES ' :
exp_body = f ' SpanishBody for { test_record . name } '
exp_subject = f ' SpanishSubject for { test_record . name } '
else :
exp_body = f ' TemplateBody { test_record . name } '
exp_subject = f ' TemplateSubject { test_record . name } '
self . assertMailMail ( self . partner_employee_2 , ' sent ' ,
mail_message = message ,
author = author , # author is different in batch and monorecord mode (raw or rendered email_from)
email_values = {
' body_content ' : exp_body ,
' email_from ' : test_record . user_id . email_formatted , # set by template
' subject ' : exp_subject ,
' attachments_info ' : [
{ ' name ' : ' AttFileName_00.txt ' , ' raw ' : b ' AttContent_00 ' , ' type ' : ' text/plain ' } ,
{ ' name ' : ' AttFileName_01.txt ' , ' raw ' : b ' AttContent_01 ' , ' type ' : ' text/plain ' } ,
{ ' name ' : f ' TestReport for { test_record . name } .html ' , ' type ' : ' text/plain ' } ,
{ ' name ' : f ' TestReport2 for { test_record . name } .html ' , ' type ' : ' text/plain ' } ,
]
} ,
fields_values = {
' mail_server_id ' : self . mail_server_domain ,
} ,
)
# customers emails (several customers, one or two emails depending
# on multi-lang testing environment)
if use_lang and test_record == test_records [ 0 ] :
# in this case, we are in a multi-lang customers testing
emails_recipients = [
test_record . customer_id , # es_ES
new_partners # en_US (default lang of new customers)
]
else :
# all recipients have same language, one email
emails_recipients = [ test_record . customer_id + new_partners ]
for recipients in emails_recipients :
self . assertMailMail ( recipients , ' sent ' ,
mail_message = message ,
author = author , # author is different in batch and monorecord mode (raw or rendered email_from)
email_values = {
' body_content ' : exp_body ,
' email_from ' : test_record . user_id . email_formatted , # set by template
' subject ' : exp_subject ,
' attachments_info ' : [
{ ' name ' : ' AttFileName_00.txt ' , ' raw ' : b ' AttContent_00 ' , ' type ' : ' text/plain ' } ,
{ ' name ' : ' AttFileName_01.txt ' , ' raw ' : b ' AttContent_01 ' , ' type ' : ' text/plain ' } ,
{ ' name ' : f ' TestReport for { test_record . name } .html ' , ' type ' : ' text/plain ' } ,
{ ' name ' : f ' TestReport2 for { test_record . name } .html ' , ' type ' : ' text/plain ' } ,
]
} ,
fields_values = {
' mail_server_id ' : self . mail_server_domain ,
} ,
)
# Specifically for the language-specific recipient, perform
# low-level checks on outgoing email for the recipient to
# check layouting and language. Note that standard layout
# is not tested against translations, only the custom one
# to ease translations checks.
# We could do the check for other layouts but it would be
# mainly noisy / duplicated check
email = self . _find_sent_email ( test_record . user_id . email_formatted , [ test_record . customer_id . email_formatted ] )
self . assertTrue ( bool ( email ) , ' Email not found, check recipients ' )
exp_layout_content_en = ' English Layout for Ticket-like model '
exp_layout_content_es = ' Spanish Layout para Spanish Model Description '
exp_button_en = ' View Ticket-like model '
exp_button_es = ' SpanishView Spanish Model Description '
if email_layout_xmlid :
if exp_lang == ' es_ES ' :
self . assertIn ( exp_layout_content_es , email [ ' body ' ] )
self . assertIn ( exp_button_es , email [ ' body ' ] )
else :
self . assertIn ( exp_layout_content_en , email [ ' body ' ] )
self . assertIn ( exp_button_en , email [ ' body ' ] )
else :
# check default layouting applies
if exp_lang == ' es_ES ' :
self . assertIn ( ' html lang= " es_ES " ' , email [ ' body ' ] )
else :
self . assertIn ( ' html lang= " en_US " ' , email [ ' body ' ] )
# message is posted and notified admin
self . assertEqual ( message . subtype_id , self . env . ref ( ' mail.mt_comment ' ) )
self . assertNotified ( message , [ { ' partner ' : self . partner_admin , ' is_read ' : False , ' type ' : ' inbox ' } ] )
# attachments are copied on message and linked to document
self . assertEqual (
set ( message . attachment_ids . mapped ( ' name ' ) ) ,
set ( [ ' AttFileName_00.txt ' , ' AttFileName_01.txt ' ,
f ' TestReport for { test_record . name } .html ' ,
f ' TestReport2 for { test_record . name } .html ' ] )
)
self . assertEqual ( set ( message . attachment_ids . mapped ( ' res_model ' ) ) , set ( [ test_record . _name ] ) )
self . assertEqual ( set ( message . attachment_ids . mapped ( ' res_id ' ) ) , set ( test_record . ids ) )
self . assertTrue ( all ( attach not in message . attachment_ids for attach in attachs ) , ' Should have copied attachments ' )
@users ( ' employee ' )
@mute_logger ( ' odoo.addons.mail.models.mail_mail ' )
def test_mail_composer_wtpl_mc ( self ) :
""" Test specific to multi-company environment, notably company propagation
or aliases . """
# add access to second company to avoid MC rules on ticket model
self . env . user . company_ids = [ ( 4 , self . company_2 . id ) ]
# initial data
self . assertEqual ( self . env . company , self . company_admin )
self . assertEqual ( self . user_admin . company_id , self . company_admin )
attachment_data = self . _generate_attachments_data ( 2 , self . template . _name , self . template . id )
email_to_1 = ' test.to.1@test.example.com '
self . template . write ( {
' auto_delete ' : False , # keep sent emails to check content
' attachment_ids ' : [ ( 0 , 0 , a ) for a in attachment_data ] ,
' email_from ' : False , # use current user as author
' email_layout_xmlid ' : ' mail.test_layout ' ,
' email_to ' : email_to_1 ,
' mail_server_id ' : False , # let it find a server
' partner_to ' : ' %s , {{ object.customer_id.id if object.customer_id else " " }} ' % self . partner_admin . id ,
} )
attachs = self . env [ ' ir.attachment ' ] . search ( [ ( ' name ' , ' in ' , [ a [ ' name ' ] for a in attachment_data ] ) ] )
self . assertEqual ( len ( attachs ) , 2 )
for batch , companies , expected_companies , expected_alias_domains in [
( False , self . company_admin , self . company_admin , self . mail_alias_domain ) ,
(
True , self . company_admin + self . company_2 , self . company_admin + self . company_2 ,
self . mail_alias_domain + self . mail_alias_domain_c2 ,
) ,
] :
with self . subTest ( batch = batch ,
companies = companies ) :
# update test configuration
test_records = self . test_records if batch else self . test_record
for company , record in zip ( companies , test_records ) :
record . company_id = company . id
# open a composer and run it in comment mode
composer = Form ( self . env [ ' mail.compose.message ' ] . with_context (
default_composition_mode = ' comment ' ,
default_force_send = True , # force sending emails directly to check SMTP
default_model = test_records . _name ,
default_res_ids = test_records . ids ,
default_template_id = self . template . id ,
# avoid successive tests issues with followers
mail_create_nosubscribe = True ,
) ) . save ( )
with self . mock_mail_gateway ( mail_unlink_sent = False ) , \
self . mock_mail_app ( ) :
composer . _action_send_mail ( )
new_partner = self . env [ ' res.partner ' ] . search ( [ ( ' email_normalized ' , ' = ' , ' test.to.1@test.example.com ' ) ] )
self . assertEqual ( len ( new_partner ) , 1 )
# check output, company-specific values mainly for this test
for record , exp_company , exp_alias_domain in zip (
test_records , expected_companies , expected_alias_domains
) :
message = record . message_ids [ 0 ]
for recipient in [ self . partner_employee_2 , new_partner , record . customer_id ] :
self . assertMailMail (
recipient ,
' sent ' ,
author = self . partner_employee ,
mail_message = message ,
email_values = {
' headers ' : {
' Return-Path ' : f ' { exp_alias_domain . bounce_email } ' ,
' X-Odoo-Objects ' : f ' { record . _name } - { record . id } ' ,
} ,
' subject ' : f ' TemplateSubject { record . name } ' ,
} ,
fields_values = {
' headers ' : {
' Return-Path ' : f ' { exp_alias_domain . bounce_email } ' ,
' X-Odoo-Objects ' : f ' { record . _name } - { record . id } ' ,
} ,
' mail_server_id ' : self . env [ ' ir.mail_server ' ] ,
' record_alias_domain_id ' : exp_alias_domain ,
' record_company_id ' : exp_company ,
' subject ' : f ' TemplateSubject { record . name } ' ,
} ,
)
smtp_to_list = [ recipient . email_normalized ]
if exp_alias_domain == self . mail_alias_domain :
self . assertSMTPEmailsSent (
smtp_from = f ' { self . default_from } @ { self . alias_domain } ' ,
smtp_to_list = smtp_to_list ,
mail_server = self . mail_server_notification ,
emails_count = 1 ,
)
else :
self . assertSMTPEmailsSent (
smtp_from = exp_alias_domain . bounce_email ,
smtp_to_list = smtp_to_list ,
emails_count = 1 ,
)
@users ( ' employee ' )
@mute_logger ( ' odoo.addons.mail.models.mail_mail ' )
def test_mail_composer_wtpl_recipients_email_fields ( self ) :
""" Test various combinations of corner case / not standard filling of
email fields : multi email , formatted emails , . . . on template , used to
post a message using the composer . """
existing_partners = self . env [ ' res.partner ' ] . search ( [ ] )
partner_format_tofind , partner_multi_tofind , partner_at_tofind = self . env [ ' res.partner ' ] . create ( [
{
' email ' : ' " FindMe Format " <find.me.format@test.example.com> ' ,
' name ' : ' FindMe Format ' ,
} , {
' email ' : ' find.me.multi.1@test.example.com, " FindMe Multi " <find.me.multi.2@test.example.com> ' ,
' name ' : ' FindMe Multi ' ,
} , {
' email ' : ' " Bike@Home " <find.me.at@test.example.com> ' ,
' name ' : ' NotBike@Home ' ,
}
] )
email_ccs = [ ' " Raoul " <test.cc.1@example.com> ' , ' " Raoulette " <test.cc.2@example.com> ' , ' test.cc.2.2@example.com> ' , ' invalid ' , ' ' ]
email_tos = [ ' " Micheline, l \' Immense " <test.to.1@example.com> ' , ' test.to.2@example.com ' , ' wrong ' , ' ' ]
self . template . write ( {
' email_cc ' : ' , ' . join ( email_ccs ) ,
' email_from ' : ' {{ user.email_formatted }} ' ,
' email_to ' : ' , ' . join ( email_tos + ( partner_format_tofind + partner_multi_tofind + partner_at_tofind ) . mapped ( ' email ' ) ) ,
' partner_to ' : f ' { self . partner_1 . id } , { self . partner_2 . id } ,0,test ' ,
} )
self . user_employee . write ( { ' email ' : ' email.from.1@test.mycompany.com, email.from.2@test.mycompany.com ' } )
self . partner_1 . write ( { ' email ' : ' " Valid Formatted " <valid.lelitre@agrolait.com> ' } )
self . partner_2 . write ( { ' email ' : ' valid.other.1@agrolait.com, valid.other.cc@agrolait.com ' } )
# ensure values used afterwards for testing
self . assertEqual (
self . partner_employee . email_formatted ,
' " Ernest Employee " <email.from.1@test.mycompany.com,email.from.2@test.mycompany.com> ' ,
' Formatting: wrong formatting due to multi-email ' )
self . assertEqual (
self . partner_1 . email_formatted ,
' " Valid Lelitre " <valid.lelitre@agrolait.com> ' ,
' Formatting: avoid wrong double encapsulation ' )
self . assertEqual (
self . partner_2 . email_formatted ,
' " Valid Poilvache " <valid.other.1@agrolait.com,valid.other.cc@agrolait.com> ' ,
' Formatting: wrong formatting due to multi-email ' )
# instantiate composer, post message
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context (
self . test_record ,
add_web = True ,
default_template_id = self . template . id ,
)
) )
composer = composer_form . save ( )
with self . mock_mail_gateway ( mail_unlink_sent = False ) , self . mock_mail_app ( ) :
# don't want to test duplicates, more email management
composer . with_context ( mailing_document_based = True ) . action_send_mail ( )
# find partners created during sending (as emails are transformed into partners)
# FIXME: currently email finding based on formatted / multi emails does
# not work
new_partners = self . env [ ' res.partner ' ] . search ( [ ] ) . search ( [ ( ' id ' , ' not in ' , existing_partners . ids ) ] )
self . assertEqual ( len ( new_partners ) , 9 ,
' Mail (FIXME): multiple partner creation due to formatted / multi emails: 1 extra partner ' )
self . assertIn ( partner_format_tofind , new_partners )
self . assertIn ( partner_multi_tofind , new_partners )
self . assertIn ( partner_at_tofind , new_partners )
self . assertEqual ( new_partners [ 0 : 3 ] . ids , ( partner_format_tofind + partner_multi_tofind + partner_at_tofind ) . ids )
self . assertEqual (
sorted ( new_partners . mapped ( ' email ' ) ) ,
sorted ( [ ' " Bike@Home " <find.me.at@test.example.com> ' ,
' " FindMe Format " <find.me.format@test.example.com> ' ,
' find.me.multi.1@test.example.com, " FindMe Multi " <find.me.multi.2@test.example.com> ' ,
' find.me.multi.2@test.example.com ' ,
' test.cc.1@example.com ' ,
' test.cc.2@example.com ' ,
' test.cc.2.2@example.com ' ,
' test.to.1@example.com ' ,
' test.to.2@example.com ' ] ) ,
' Mail: created partners for valid emails (wrong / invalid not taken into account) + did not find corner cases (FIXME) '
)
self . assertEqual (
sorted ( new_partners . mapped ( ' email_formatted ' ) ) ,
sorted ( [ ' " NotBike@Home " <find.me.at@test.example.com> ' ,
' " FindMe Format " <find.me.format@test.example.com> ' ,
' " FindMe Multi " <find.me.multi.1@test.example.com,find.me.multi.2@test.example.com> ' ,
' " find.me.multi.2@test.example.com " <find.me.multi.2@test.example.com> ' ,
' " test.cc.1@example.com " <test.cc.1@example.com> ' ,
' " test.cc.2@example.com " <test.cc.2@example.com> ' ,
' " test.cc.2.2@example.com " <test.cc.2.2@example.com> ' ,
' " test.to.1@example.com " <test.to.1@example.com> ' ,
' " test.to.2@example.com " <test.to.2@example.com> ' ] ) ,
)
self . assertEqual (
sorted ( new_partners . mapped ( ' name ' ) ) ,
sorted ( [ ' NotBike@Home ' ,
' FindMe Format ' ,
' FindMe Multi ' ,
' find.me.multi.2@test.example.com ' ,
' test.cc.1@example.com ' ,
' test.to.1@example.com ' ,
' test.to.2@example.com ' ,
' test.cc.2@example.com ' ,
' test.cc.2.2@example.com ' ] ) ,
' Mail: currently setting name = email, not taking into account formatted emails '
)
# global outgoing: two mail.mail (all customer recipients, then all employee recipients)
# and 11 emails, and 1 inbox notification (admin)
# FIXME template is sent only to partners (email_to are transformed) ->
# wrong / weird emails (see email_formatted of partners) is kept
# FIXME: more partners created than real emails (see above) -> due to
# transformation from email -> partner in template 'generate_recipients'
# there are more partners than email to notify;
self . assertEqual ( len ( self . _new_mails ) , 2 , ' Should have created 2 mail.mail ' )
self . assertEqual (
len ( self . _mails ) , len ( new_partners ) + 3 ,
f ' Should have sent { len ( new_partners ) + 3 } emails, one / recipient ( { len ( new_partners ) } mailed partners + partner_1 + partner_2 + partner_employee) ' )
self . assertMailMail (
self . partner_employee_2 , ' sent ' ,
author = self . partner_employee ,
email_values = {
' body_content ' : f ' TemplateBody { self . test_record . name } ' ,
# single email event if email field is multi-email
' email_from ' : formataddr ( ( self . user_employee . name , ' email.from.1@test.mycompany.com ' ) ) ,
' subject ' : f ' TemplateSubject { self . test_record . name } ' ,
} ,
fields_values = {
# currently holding multi-email 'email_from'
' email_from ' : formataddr ( ( self . user_employee . name , ' email.from.1@test.mycompany.com,email.from.2@test.mycompany.com ' ) ) ,
} ,
mail_message = self . test_record . message_ids [ 0 ] ,
)
recipients = self . partner_1 + self . partner_2 + new_partners
self . assertMailMail (
recipients ,
' sent ' ,
author = self . partner_employee ,
email_to_recipients = [
[ self . partner_1 . email_formatted ] ,
[ f ' " { self . partner_2 . name } " <valid.other.1@agrolait.com> ' , f ' " { self . partner_2 . name } " <valid.other.cc@agrolait.com> ' ] ,
] + [ [ new_partners [ 0 ] [ ' email_formatted ' ] ] ,
[ ' " FindMe Multi " <find.me.multi.1@test.example.com> ' , ' " FindMe Multi " <find.me.multi.2@test.example.com> ' ]
] + [ [ email ] for email in new_partners [ 2 : ] . mapped ( ' email_formatted ' ) ] ,
email_values = {
' body_content ' : f ' TemplateBody { self . test_record . name } ' ,
# single email event if email field is multi-email
' email_from ' : formataddr ( ( self . user_employee . name , ' email.from.1@test.mycompany.com ' ) ) ,
' subject ' : f ' TemplateSubject { self . test_record . name } ' ,
} ,
fields_values = {
# currently holding multi-email 'email_from'
' email_from ' : formataddr ( ( self . user_employee . name , ' email.from.1@test.mycompany.com,email.from.2@test.mycompany.com ' ) ) ,
} ,
mail_message = self . test_record . message_ids [ 0 ] ,
)
# actual emails sent through smtp
for recipient in recipients :
# multi emails -> send multiple emails (smart)
if recipient == self . partner_2 :
smtp_to_list = [ ' valid.other.1@agrolait.com ' , ' valid.other.cc@agrolait.com ' ]
# find.me.format
elif recipient == new_partners [ 0 ] :
self . assertEqual ( recipient , partner_format_tofind )
smtp_to_list = [ ' find.me.format@test.example.com ' ]
# find.me.multi was split into two partners
elif recipient == new_partners [ 1 ] :
self . assertEqual ( recipient , partner_multi_tofind )
smtp_to_list = [ ' find.me.multi.1@test.example.com ' , ' find.me.multi.2@test.example.com ' ]
elif recipient == new_partners [ 3 ] :
smtp_to_list = [ ' find.me.multi.2@test.example.com ' ]
# bike@home: name is not recognized as email anymore
elif recipient == new_partners [ 2 ] :
self . assertEqual ( recipient , partner_at_tofind )
smtp_to_list = [ ' find.me.at@test.example.com ' ]
else :
smtp_to_list = [ recipient . email_normalized ]
self . assertSMTPEmailsSent (
smtp_from = f ' { self . alias_bounce } @ { self . alias_domain } ' ,
smtp_to_list = smtp_to_list ,
mail_server = self . mail_server_domain ,
# msg_from takes only first found normalized email to make a valid email_from
message_from = formataddr (
( self . user_employee . name ,
' email.from.1@test.mycompany.com ' ,
) ) ,
# similar envelope, assertSMTPEmailsSent cannot distinguish
# records (would have to dive into content, too complicated)
emails_count = 1 ,
)
@tagged ( ' mail_composer ' , ' mail_blacklist ' )
class TestComposerResultsCommentStatus ( TestMailComposer ) :
""" Test cases involving blacklist, opt-out, state management, ... specific
class to avoid bloating the base comment - based composer tests . """
@classmethod
def setUpClass ( cls ) :
""" Test data: 4 records with a customer set, then some additional
records based on emails , duplicates , . . .
Record0 : partner is blacklisted
Record1 : Record4 has the same email ( but no customer set )
Record5 and Record6 have same email ( notlinked to any customer )
"""
super ( TestComposerResultsCommentStatus , 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 ) ] ,
} )
# add 2 new records with customers
cls . test_records , cls . test_partners = cls . _create_records_for_batch (
' mail.test.ticket.el ' , 4 ,
additional_values = { ' user_id ' : cls . user_employee_2 . id } ,
prefix = ' el_ '
)
# create bl / optout / duplicates, see docstring
cls . env [ ' mail.blacklist ' ] . _add (
cls . test_partners [ 0 ] . email_formatted
)
cls . test_records + = cls . env [ cls . test_records . _name ] . create ( [
{
' email_from ' : cls . test_records [ 1 ] . email_from ,
' name ' : ' Email of Record2 ' ,
' user_id ' : cls . user_employee_2 . id ,
} ,
{
' email_from ' : ' test.duplicate@test.example.com ' ,
' name ' : ' Dupe email (first) ' ,
' user_id ' : cls . user_employee_2 . id ,
} ,
{
' email_from ' : ' test.duplicate@test.example.com ' ,
' name ' : ' Dupe email (second) ' ,
' user_id ' : cls . user_employee_2 . id ,
} ,
] )
cls . template . write ( {
' auto_delete ' : False ,
' model_id ' : cls . env [ ' ir.model ' ] . _get_id ( cls . test_records . _name ) ,
' scheduled_date ' : False ,
} )
def test_assert_initial_data ( self ) :
""" Ensure class initial data to ease understanding """
self . assertFalse ( self . template . auto_delete )
self . assertEqual ( len ( self . test_records ) , 7 )
self . assertEqual ( self . test_records . user_id , self . user_employee_2 )
self . assertEqual ( self . test_records . message_partner_ids , self . partner_employee_2 )
self . assertEqual ( self . test_records [ 1 ] . email_from , self . test_records [ 4 ] . email_from )
self . assertEqual ( self . test_records [ 5 ] . email_from , self . test_records [ 6 ] . email_from )
self . assertEqual ( len ( self . test_partners ) , 4 )
self . assertTrue ( self . test_partners [ 0 ] . is_blacklisted )
@users ( ' employee ' )
@mute_logger ( ' odoo.tests ' , ' odoo.addons.mail.models.mail_mail ' , ' odoo.models.unlink ' )
def test_comment_blacklist ( self ) :
""" Tests a document-based comment with the excluded emails. It is
currently bypassed , as we consider posting bypasses the exclusion list .
"""
test_record = self . test_records [ 0 ] . with_env ( self . env )
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( test_record , add_web = True ,
default_template_id = self . template . id )
) )
composer = composer_form . save ( )
with self . mock_mail_gateway ( mail_unlink_sent = False ) , self . mock_mail_app ( ) :
composer . _action_send_mail ( )
# one mail to the customer, one mail to the follower
message = test_record . message_ids [ 0 ]
for recipient in test_record . customer_id + self . partner_employee_2 :
with self . subTest ( recipient = recipient ) :
self . assertMailMail (
recipient , ' sent ' ,
mail_message = message ,
author = self . partner_employee_2 , # author synchronized with email_from
email_values = {
' email_from ' : self . user_employee_2 . email_formatted , # set by template
} ,
)
self . assertEqual ( len ( self . _mails ) , 2 , ' Should have sent 2 emails, skipping the exclusion list ' )
@tagged ( ' mail_composer ' , ' multi_lang ' )
class TestComposerResultsMass ( TestMailComposer ) :
@classmethod
def setUpClass ( cls ) :
super ( TestComposerResultsMass , 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 . template . write ( {
" scheduled_date " : False ,
} )
@users ( ' employee ' )
@mute_logger ( ' odoo.tests ' , ' odoo.addons.mail.models.mail_mail ' , ' odoo.models.unlink ' )
def test_mail_composer_delete ( self ) :
""" Check mail / msg delete support """
# ensure initial data
self . assertTrue ( self . template . auto_delete )
self . assertEqual ( self . test_records . user_id , self . user_employee_2 )
self . assertEqual ( self . test_records . message_partner_ids , self . partner_employee_2 )
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_records , add_web = True ,
default_template_id = self . template . id )
) )
composer = composer_form . save ( )
self . assertTrue ( composer . auto_delete , ' Should take composer value ' )
self . assertTrue ( composer . auto_delete_keep_log )
with self . mock_mail_gateway ( mail_unlink_sent = True ) , self . mock_mail_app ( ) :
composer . _action_send_mail ( )
self . assertEqual ( len ( self . _mails ) , 2 , ' Should have sent 1 email per record ' )
self . assertEqual ( len ( self . _new_mails ) , 2 , ' Should have created 1 mail.mail per record ' )
self . assertFalse ( self . _new_mails . exists ( ) , ' Should have deleted mail.mail records ' )
self . assertEqual ( len ( self . _new_msgs ) , 2 , ' Should have created 1 mail.mail per record ' )
self . assertEqual ( self . _new_msgs . exists ( ) , self . _new_msgs , ' Should not have deleted mail.message records ' )
# force composer auto_delete field
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_records , add_web = True ,
default_template_id = self . template . id )
) )
composer = composer_form . save ( )
composer . auto_delete = False
with self . mock_mail_gateway ( mail_unlink_sent = True ) , self . mock_mail_app ( ) :
composer . _action_send_mail ( )
self . assertEqual ( len ( self . _mails ) , 2 , ' Should have sent 1 email per record ' )
self . assertEqual ( len ( self . _new_mails ) , 2 , ' Should have created 1 mail.mail per record ' )
self . assertEqual ( self . _new_mails . exists ( ) , self . _new_mails , ' Should not have deleted mail.mail records ' )
self . assertEqual ( len ( self . _new_msgs ) , 2 , ' Should have created 1 mail.mail per record ' )
self . assertEqual ( self . _new_msgs . exists ( ) , self . _new_msgs , ' Should not have deleted mail.message records ' )
# check composer auto_delete_keep_log
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_records , add_web = True ,
default_template_id = self . template . id )
) )
composer = composer_form . save ( )
composer . auto_delete_keep_log = False
with self . mock_mail_gateway ( mail_unlink_sent = True ) , self . mock_mail_app ( ) :
composer . _action_send_mail ( )
# global outgoing
self . assertEqual ( len ( self . _mails ) , 2 , ' Should have sent 1 email per record ' )
self . assertEqual ( len ( self . _new_mails ) , 2 , ' Should have created 1 mail.mail per record ' )
self . assertFalse ( self . _new_mails . exists ( ) , ' Should have deleted mail.mail records ' )
self . assertEqual ( len ( self . _new_msgs ) , 2 , ' Should have created 1 mail.mail per record ' )
self . assertFalse ( self . _new_msgs . exists ( ) , ' Should have deleted mail.message records ' )
@users ( ' employee ' )
@mute_logger ( ' odoo.tests ' , ' odoo.addons.mail.models.mail_mail ' , ' odoo.models.unlink ' )
def test_mail_composer_duplicates ( self ) :
""" Ensures emails sent to the same recipient multiple times
are only sent when they are not duplicates
"""
self . template . write ( {
' auto_delete ' : False ,
' body_html ' : ' <p>Common Body</p> ' ,
' email_to ' : ' ' ,
' partner_to ' : ' {{ object.customer_id.id if object.customer_id else " " }} ' ,
' subject ' : ' Common Subject ' ,
} )
# Guarantee points of variation for test_report_3
self . test_records [ 0 ] . write ( { ' count ' : 1 , ' name ' : ' A ' } )
self . test_records [ 1 ] . write ( { ' count ' : 2 , ' name ' : ' B ' } )
template_attachment , composer_attachment = self . env [ ' ir.attachment ' ] . create ( [ {
' datas ' : base64 . b64encode ( b ' ExtraData ' ) ,
' mimetype ' : ' text/plain ' ,
' name ' : f ' { record . _name } _Common_Attachment.txt ' ,
' res_id ' : record . id if record else False ,
' res_model ' : record . _name ,
} for record in ( self . template , self . env [ ' mail.compose.message ' ] ) ] )
customer_ids = self . partners [ : 2 ]
customer_ids . write ( {
' email ' : ' test@test.lan '
} )
def _instanciate_composer ( composer_attachments = False ) :
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_records [ : 2 ] , add_web = True ,
default_template_id = self . template . id )
) )
composer = composer_form . save ( )
if composer_attachments :
composer_attachments . res_id = composer . id
composer . attachment_ids + = composer_attachments
return composer
base_customer_values = [ { ' email ' : ' test@test.lan ' } ]
# different recipients should always receive emails
customer_diff_emails = [ { ' email ' : f ' difftest { n } @test.lan ' } for n in range ( 2 ) ]
base_template_values = {
' attachment_ids ' : [ Command . clear ( ) ] ,
' body_html ' : ' <p>Common Body</p> ' ,
' report_template_ids ' : [ Command . clear ( ) ] ,
' subject ' : ' Common Subject ' ,
}
same_attachments = { ' attachment_ids ' : template_attachment . ids }
diff_body = { ' body_html ' : ' <p><t t-esc= " object.name " ></t></p> ' }
# regardless of whether they have different bodies or not, they are considered duplicates
diff_attachment_same_content = { ' report_template_ids ' : [ self . test_report_2 . id ] }
diff_attachment_diff_content = { ' report_template_ids ' : [ self . test_report_3 . id ] }
diff_subject = { ' subject ' : ' {{ object.name }} ' }
# all template variations using same recipients
diff_combinations = product (
[ [ diff_body ] , [ diff_subject ] ,
[ diff_attachment_same_content ] , [ diff_attachment_diff_content ] ,
[ diff_attachment_same_content , same_attachments ] ,
[ diff_body , diff_attachment_diff_content , diff_subject ] ] ,
[ base_customer_values ] )
# no template variations using different recipients
diff_combinations = chain ( diff_combinations , [ [ [ ] , customer_diff_emails ] ] )
# expect all sent
for template_changes , customer_changes in diff_combinations :
test_template_values = dict ( base_template_values )
for change in template_changes :
test_template_values . update ( change )
self . template . write ( test_template_values )
for customer , base_vals , update_vals in zip ( customer_ids , base_customer_values , customer_changes ) :
customer . write ( { * * base_vals , * * update_vals } )
with self . subTest ( template_values = test_template_values , customer_changes = customer_changes ) :
composer = _instanciate_composer ( )
with self . mock_mail_gateway ( mail_unlink_sent = False ) , self . mock_mail_app ( ) :
composer . _action_send_mail ( )
self . assertEqual ( len ( self . _new_mails ) , 2 )
# check emails
for record in self . test_records [ : 2 ] :
# email_to will be normalized and formatted, even if already formatted
expected_subject = base_template_values [ ' subject ' ]
if test_template_values [ ' subject ' ] != base_template_values [ ' subject ' ] :
expected_subject = record . name
expected_body = base_template_values [ ' body_html ' ]
if test_template_values [ ' body_html ' ] != base_template_values [ ' body_html ' ] :
expected_body = f ' <p> { record . name } </p> '
expected_attachment_info = [ ]
if self . template . attachment_ids :
expected_attachment_info . append ( { ' name ' : ' mail.template_Common_Attachment.txt ' , ' type ' : ' text/plain ' } )
if self . template . report_template_ids :
test_report_name = ' TestReport2 ' if self . template . report_template_ids == self . test_report_2 else ' TestReport3 '
expected_attachment_info . append (
{ ' name ' : f ' { test_report_name } for { record . name } .html ' , ' type ' : ' text/plain ' } ,
)
self . assertMailMailWEmails (
[ record . customer_id . email ] ,
' sent ' ,
author = self . env . user . partner_id ,
content = expected_body ,
mail_message = record . message_ids [ 0 ] ,
email_to_recipients = [ [ record . customer_id . email_formatted ] ] ,
email_values = {
' attachments_info ' : expected_attachment_info ,
' body ' : expected_body ,
' email_from ' : record . user_id . email_formatted ,
' subject ' : expected_subject ,
} ,
)
# expect duplicates
for composer_attachment , template_changes in zip ( [ False , composer_attachment , [ ] , composer_attachment ] , [ [ ] , [ ] , [ same_attachments ] , [ same_attachments ] ] ) :
test_template_values = dict ( base_template_values )
for change in template_changes :
test_template_values . update ( change )
self . template . write ( test_template_values )
# reset customers to have the same email
for customer , base_vals in zip ( customer_ids , base_customer_values ) :
customer . write ( base_vals )
with self . subTest ( composer_attachments = composer_attachment , template_values = test_template_values ) :
composer = _instanciate_composer ( composer_attachments = composer_attachment )
with self . mock_mail_gateway ( mail_unlink_sent = False ) , self . mock_mail_app ( ) :
composer . _action_send_mail ( )
self . assertEqual ( len ( self . _new_mails ) , 2 )
self . assertEqual ( len ( self . _mails ) , 1 )
# check email
expected_attachment_info = [ ]
if template_changes :
expected_attachment_info . append ( {
' name ' : f ' { self . template . _name } _Common_Attachment.txt ' ,
' raw ' : b ' ExtraData ' ,
' type ' : ' text/plain ' ,
} )
if composer_attachment :
expected_attachment_info . append ( {
' name ' : f ' { composer . _name } _Common_Attachment.txt ' ,
' raw ' : b ' ExtraData ' ,
' type ' : ' text/plain ' ,
} )
self . assertMailMailWRecord (
self . test_records [ 0 ] ,
[ self . test_records [ 0 ] . customer_id ] ,
' sent ' ,
author = self . env . user . partner_id ,
content = ' Common Body ' ,
email_values = {
' attachments_info ' : expected_attachment_info ,
' body_content ' : ' Common Body ' ,
' email_from ' : record . user_id . email_formatted ,
' subject ' : ' Common Subject ' ,
} ,
)
self . assertMailMailWRecord (
self . test_records [ 1 ] ,
[ self . test_records [ 1 ] . customer_id ] ,
' cancel ' ,
author = self . env . user . partner_id ,
content = ' Common Body ' ,
email_values = {
' attachments_info ' : expected_attachment_info ,
' body_content ' : ' Common Body ' ,
' email_from ' : record . user_id . email_formatted ,
' subject ' : ' Common Subject ' ,
} ,
)
@users ( ' employee ' )
def test_mail_composer_scalability ( self ) :
""" Test scalability (big batch of emails) and related configuration """
batch_records , _partners = self . _create_records_for_batch (
' mail.test.ticket.mc ' , 10 ,
)
for ( batch_size , send_limit ) , ( exp_mail_create_count , exp_force_send , exp_state ) in zip (
[
( False , False ) , # unset
( 8 , 0 ) , # 0 = always use queue
( 8 , False ) , # send limit defaults to 100, so force_send is set
( 0 , 8 ) , # render: defaults to 500 hence 1 iteration in test
] ,
[
( 1 , True , " sent " ) ,
( 2 , False , " outgoing " ) ,
( 2 , True , " sent " ) ,
( 1 , False , " outgoing " ) ,
]
) :
with self . subTest ( batch_size = batch_size , send_limit = send_limit ) :
self . env [ ' ir.config_parameter ' ] . sudo ( ) . set_param (
" mail.batch_size " , batch_size
)
self . env [ ' ir.config_parameter ' ] . sudo ( ) . set_param (
" mail.mail.force.send.limit " , send_limit
)
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( batch_records , add_web = True ,
default_auto_delete = False ,
default_template_id = self . template . id )
) )
composer = composer_form . save ( )
self . assertFalse ( composer . auto_delete )
self . assertEqual ( composer . force_send , exp_force_send )
with self . mock_mail_gateway ( mail_unlink_sent = True ) :
composer . _action_send_mail ( )
self . assertEqual ( self . mail_mail_create_mocked . call_count , exp_mail_create_count )
self . assertTrue (
all ( mail . state == exp_state for mail in self . _new_mails )
)
@users ( ' employee ' )
@mute_logger ( ' odoo.models.unlink ' , ' odoo.addons.mail.models.mail_mail ' )
def test_mail_composer_wtpl ( self ) :
self . template . auto_delete = False # keep sent emails to check content
# ensure initial data
self . assertEqual ( self . test_records . user_id , self . user_employee_2 )
self . assertEqual ( self . test_records . message_partner_ids , self . partner_employee_2 )
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_records , add_web = True ,
default_template_id = self . template . id )
) )
composer = composer_form . save ( )
self . assertFalse ( composer . reply_to_force_new , ' Mail: thread-enabled models should use auto thread by default ' )
with self . mock_mail_gateway ( mail_unlink_sent = True ) :
composer . _action_send_mail ( )
# global outgoing
self . assertEqual ( len ( self . _new_mails ) , 2 , ' Should have created 1 mail.mail per record ' )
self . assertEqual ( len ( self . _mails ) , 2 , ' Should have sent 1 email per record ' )
for record in self . test_records :
# message copy is kept
message = record . message_ids [ 0 ]
# template is sent directly using customer field, meaning we have recipients
self . assertMailMail ( record . customer_id , ' sent ' ,
mail_message = message ,
author = self . partner_employee ,
email_values = {
' email_from ' : self . partner_employee_2 . email_formatted ,
} )
# message content
self . assertEqual ( message . subject , ' TemplateSubject %s ' % record . name )
self . assertEqual ( message . body , ' <p>TemplateBody %s </p> ' % record . name )
self . assertEqual ( message . author_id , self . user_employee . partner_id )
# post-related fields are void
self . assertFalse ( message . subtype_id )
self . assertFalse ( message . partner_ids )
@users ( ' employee ' )
@mute_logger ( ' odoo.models.unlink ' , ' odoo.addons.mail.models.mail_mail ' )
def test_mail_composer_wtpl_complete ( self ) :
""" Test a composer in mass mode with a quite complete template, containing
notably email - based recipients and attachments .
Translations and email layout supported are also tested .
"""
# as we use the email queue, don't have failing tests due to other outgoing emails
self . env [ ' mail.mail ' ] . sudo ( ) . search ( [ ] ) . unlink ( )
attachment_data = self . _generate_attachments_data ( 2 , self . template . _name , self . template . id )
email_to_1 = ' test.to.1@test.example.com '
email_to_2 = ' test.to.2@test.example.com '
email_to_3 = ' test.to.1@test.example.com ' # duplicate: should not sent twice the email
email_cc_1 = ' test.cc.1@test.example.com '
self . template . write ( {
' auto_delete ' : False , # keep sent emails to check content
' attachment_ids ' : [ ( 0 , 0 , a ) for a in attachment_data ] ,
' email_to ' : ' %s , %s , %s ' % ( email_to_1 , email_to_2 , email_to_3 ) ,
' email_cc ' : email_cc_1 ,
' partner_to ' : ' %s , {{ object.customer_id.id if object.customer_id else " " }} ' % self . partner_admin . id ,
' report_template_ids ' : [ ( 6 , 0 , self . test_report . ids ) ] ,
} )
attachs = self . env [ ' ir.attachment ' ] . search ( [ ( ' name ' , ' in ' , [ a [ ' name ' ] for a in attachment_data ] ) ] )
self . assertEqual ( len ( attachs ) , 2 )
# ensure initial data
self . assertEqual ( self . test_records . user_id , self . user_employee_2 )
self . assertEqual ( self . test_records . message_partner_ids , self . partner_employee_2 )
for use_domain , scheduled_date , email_layout_xmlid , use_lang in product (
( False , True ) ,
( False , ' {{ (object.create_date or datetime.datetime(2022, 12, 26, 18, 0, 0)) + datetime.timedelta(days=2) }} ' ) ,
( False , ' mail.test_layout ' ) ,
( False , True ) ,
) :
with self . subTest ( use_domain = use_domain ,
scheduled_date = scheduled_date ,
email_layout_xmlid = email_layout_xmlid ,
use_lang = use_lang ) :
# update test configuration
self . template . write ( {
' scheduled_date ' : scheduled_date ,
} )
if use_lang :
langs = ( ' es_ES ' , ' en_US ' )
self . test_partners [ 0 ] . lang = langs [ 0 ]
self . test_partners [ 1 ] . lang = langs [ 1 ]
else :
langs = ( False , False )
self . test_partners . lang = False
ctx = {
' default_model ' : self . test_records . _name ,
' default_composition_mode ' : ' mass_mail ' ,
' default_template_id ' : self . template . id ,
}
if use_domain :
ctx [ ' default_res_domain ' ] = [ ( ' id ' , ' in ' , self . test_records . ids ) ]
ctx [ ' default_force_send ' ] = True # otherwise domain = email queue
else :
ctx [ ' default_res_ids ' ] = self . test_records . ids
if email_layout_xmlid :
ctx [ ' default_email_layout_xmlid ' ] = email_layout_xmlid
# launch composer in mass mode
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context ( ctx ) )
composer = composer_form . save ( )
# ensure some parameters used afterwards
author = self . env . user . partner_id
self . assertEqual ( composer . author_id , author ,
' Author is not synchronized, as template email_from does not match existing partner ' )
self . assertEqual ( composer . email_from , self . template . email_from )
with self . mock_mail_gateway ( mail_unlink_sent = False ) , \
self . mock_datetime_and_now ( self . reference_now ) :
composer . _action_send_mail ( )
# partners created from raw emails
new_partners = self . env [ ' res.partner ' ] . search ( [
( ' email ' , ' in ' , [ email_to_1 , email_to_2 , email_to_3 , email_cc_1 ] )
] )
self . assertEqual ( len ( new_partners ) , 3 )
self . assertEqual ( new_partners . mapped ( ' lang ' ) , [ ' en_US ' ] * 3 ,
' New partners lang is always the default DB one, whatever the context ' )
# check global outgoing
self . assertEqual ( len ( self . _new_mails ) , 2 , ' Should have created 1 mail.mail per record ' )
if not scheduled_date :
# emails sent directly
self . assertEqual ( len ( self . _mails ) , 10 , ' Should have sent emails ' )
self . assertEqual ( self . _new_mails . mapped ( ' scheduled_date ' ) ,
[ False ] * 2 )
else :
# emails not sent due to scheduled_date
self . assertEqual ( len ( self . _mails ) , 0 , ' Should not send emails, scheduled in the future ' )
self . assertEqual ( self . _new_mails . mapped ( ' scheduled_date ' ) ,
[ self . reference_now + timedelta ( days = 2 ) ] * 2 )
# simulate cron queue at right time for sending
with self . mock_datetime_and_now ( self . reference_now + timedelta ( days = 2 ) ) :
self . env [ ' mail.mail ' ] . sudo ( ) . process_email_queue ( )
# everything should be sent now
self . assertEqual ( len ( self . _mails ) , 10 , ' Should have sent 5 emails per record ' )
# check email content
for record , exp_lang in zip ( self . test_records , langs ) :
# message copy is kept
message = record . message_ids [ 0 ]
if exp_lang == ' es_ES ' :
exp_body = f ' SpanishBody for { record . name } '
exp_subject = f ' SpanishSubject for { record . name } '
else :
exp_body = f ' TemplateBody { record . name } '
exp_subject = f ' TemplateSubject { record . name } '
# template is sent only to partners (email_to are transformed)
self . assertMailMail ( record . customer_id + new_partners + self . partner_admin ,
' sent ' ,
mail_message = message ,
author = author ,
email_values = {
' attachments_info ' : [
{ ' name ' : ' AttFileName_00.txt ' , ' raw ' : b ' AttContent_00 ' , ' type ' : ' text/plain ' } ,
{ ' name ' : ' AttFileName_01.txt ' , ' raw ' : b ' AttContent_01 ' , ' type ' : ' text/plain ' } ,
{ ' name ' : ' TestReport for %s .html ' % record . name , ' type ' : ' text/plain ' } ,
] ,
' body_content ' : exp_body ,
' email_from ' : self . partner_employee_2 . email_formatted ,
2025-03-04 12:23:19 +07:00
# profit from this test to check references are set to message_id in mailing emails
' references_message_id_check ' : True ,
2025-01-06 10:57:38 +07:00
' subject ' : exp_subject ,
} ,
fields_values = {
' email_from ' : self . partner_employee_2 . email_formatted ,
' mail_server_id ' : self . mail_server_domain ,
' reply_to ' : formataddr ( (
f ' { self . env . user . company_id . name } { record . name } ' ,
f ' { self . alias_catchall } @ { self . alias_domain } '
) ) ,
' subject ' : exp_subject ,
} ,
)
# Low-level checks on outgoing email for the recipient to
# check layouting and language. Note that standard layout
# is not tested against translations, only the custom one
# to ease translations checks.
sent_mail = self . _find_sent_email (
self . partner_employee_2 . email_formatted ,
[ formataddr ( ( record . customer_id . name , email_normalize ( record . customer_id . email , strict = False ) ) ) ]
)
debug_info = ' '
if not sent_mail :
debug_info = ' - ' . join ( ' From: %s -To: %s ' % ( mail [ ' email_from ' ] , mail [ ' email_to ' ] ) for mail in self . _mails )
self . assertTrue (
bool ( sent_mail ) ,
f ' Expected mail from { self . partner_employee_2 . email_formatted } to { formataddr ( ( record . customer_id . name , record . customer_id . email ) ) } not found in { debug_info } '
)
if record == self . test_records [ 0 ] :
self . assertEqual ( sent_mail [ ' email_to ' ] , [ ' " Partner_0 " <test_partner_0@example.com> ' ] ,
' Should take email normalized in to ' )
else :
self . assertEqual ( sent_mail [ ' email_to ' ] , [ ' " Partner_1 " <test_partner_1@example.com> ' ] ,
' Should take email normalized in to ' )
if not email_layout_xmlid :
self . assertEqual (
sent_mail [ ' body ' ] ,
f ' <p> { exp_body } </p> '
)
else :
exp_layout_content_en = ' English Layout for Ticket-like model '
exp_layout_content_es = ' Spanish Layout para Spanish Model Description '
exp_button_en = ' View Ticket-like model '
exp_button_es = ' Spanish Layout para Spanish Model Description '
if exp_lang == ' es_ES ' :
self . assertIn ( exp_layout_content_es , sent_mail [ ' body ' ] )
self . assertIn ( exp_button_es , sent_mail [ ' body ' ] )
else :
self . assertIn ( exp_layout_content_en , sent_mail [ ' body ' ] )
# self.assertIn(exp_button_es, sent_mail['body'])
self . assertIn ( exp_button_en , sent_mail [ ' body ' ] )
@users ( ' employee ' )
@mute_logger ( ' odoo.addons.mail.models.mail_mail ' )
def test_mail_composer_wtpl_mc ( self ) :
""" Test specific to multi-company environment, notably company propagation
or aliases . """
# add access to second company to avoid MC rules on ticket model
self . env . user . company_ids = [ ( 4 , self . company_2 . id ) ]
# initial data
self . assertEqual ( self . env . company , self . company_admin )
self . assertEqual ( self . user_admin . company_id , self . company_admin )
attachment_data = self . _generate_attachments_data ( 2 , self . template . _name , self . template . id )
email_to_1 = ' test.to.1@test.example.com '
self . template . write ( {
' auto_delete ' : False , # keep sent emails to check content
' attachment_ids ' : [ ( 0 , 0 , a ) for a in attachment_data ] ,
' email_from ' : False , # use current user as author
' email_layout_xmlid ' : ' mail.test_layout ' ,
' email_to ' : email_to_1 ,
' mail_server_id ' : False , # let it find a server
' partner_to ' : ' %s , {{ object.customer_id.id if object.customer_id else " " }} ' % self . partner_admin . id ,
} )
attachs = self . env [ ' ir.attachment ' ] . search ( [ ( ' name ' , ' in ' , [ a [ ' name ' ] for a in attachment_data ] ) ] )
self . assertEqual ( len ( attachs ) , 2 )
for companies , expected_companies , expected_alias_domains in [
(
self . company_admin + self . company_2 ,
self . company_admin + self . company_2 ,
self . mail_alias_domain + self . mail_alias_domain_c2 ,
) ,
] :
with self . subTest ( companies = companies ) :
# update test configuration
test_records = self . test_records
for company , record in zip ( companies , test_records ) :
record . company_id = company . id
# open a composer and run it in comment mode
composer = Form ( self . env [ ' mail.compose.message ' ] . with_context (
default_composition_mode = ' mass_mail ' ,
default_force_send = True , # force sending emails directly to check SMTP
default_model = test_records . _name ,
default_res_ids = test_records . ids ,
default_template_id = self . template . id ,
# avoid successive tests issues with followers
mail_create_nosubscribe = True ,
) ) . save ( )
with self . mock_mail_gateway ( mail_unlink_sent = False ) , \
self . mock_mail_app ( ) :
composer . _action_send_mail ( )
new_partner = self . env [ ' res.partner ' ] . search ( [ ( ' email_normalized ' , ' = ' , ' test.to.1@test.example.com ' ) ] )
self . assertEqual ( len ( new_partner ) , 1 )
# check output, company-specific values mainly for this test
for record , exp_company , exp_alias_domain in zip (
test_records , expected_companies , expected_alias_domains
) :
# message copy is kept
message = record . message_ids [ 0 ]
recipients = record . customer_id + new_partner + self . partner_admin
self . assertMailMail (
record . customer_id + new_partner + self . partner_admin ,
' sent ' ,
author = self . partner_employee ,
mail_message = message ,
email_values = {
' headers ' : {
' Return-Path ' : f ' { exp_alias_domain . bounce_email } ' ,
' X-Odoo-Objects ' : f ' { record . _name } - { record . id } ' ,
} ,
' subject ' : f ' TemplateSubject { record . name } ' ,
} ,
fields_values = {
' headers ' : {
' Return-Path ' : f ' { exp_alias_domain . bounce_email } ' ,
' X-Odoo-Objects ' : f ' { record . _name } - { record . id } ' ,
} ,
' mail_server_id ' : self . env [ ' ir.mail_server ' ] ,
' record_alias_domain_id ' : exp_alias_domain ,
' record_company_id ' : exp_company ,
' subject ' : f ' TemplateSubject { record . name } ' ,
} ,
)
for recipient in recipients :
smtp_to_list = [ recipient . email_normalized ]
if exp_alias_domain == self . mail_alias_domain :
self . assertSMTPEmailsSent (
smtp_from = f ' { self . default_from } @ { self . alias_domain } ' ,
smtp_to_list = smtp_to_list ,
mail_server = self . mail_server_notification ,
emails_count = 1 ,
)
else :
self . assertSMTPEmailsSent (
smtp_from = exp_alias_domain . bounce_email ,
smtp_to_list = smtp_to_list ,
emails_count = 1 ,
)
@users ( ' employee ' )
@mute_logger ( ' odoo.models.unlink ' , ' odoo.addons.mail.models.mail_mail ' )
def test_mail_composer_wtpl_recipients ( self ) :
""" Test various combinations of recipients: res_domain, active_id,
active_ids , . . . to ensure fallback behavior are working . """
# 1: active ids
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
active_ids = self . test_records . ids ,
default_composition_mode = ' mass_mail ' ,
default_model = self . test_records . _name ,
default_template_id = self . template . id ,
) )
composer = composer_form . save ( )
self . assertEqual ( sorted ( literal_eval ( composer . res_ids ) ) , sorted ( self . test_records . ids ) )
with self . mock_mail_gateway ( mail_unlink_sent = True ) :
composer . _action_send_mail ( )
# should create emails in a single batch
self . assertEqual ( self . build_email_mocked . call_count , 2 , ' One build email per outgoing email ' )
self . assertEqual ( self . mail_mail_create_mocked . call_count , 1 , ' Emails are created in batch ' )
# global outgoing
self . assertEqual ( len ( self . _new_mails ) , 2 , ' Should have created 1 mail.mail per record based on active_ids ' )
self . assertEqual ( len ( self . _mails ) , 2 , ' Should have sent 1 email per record based on active_ids ' )
for record in self . test_records :
# template is sent directly using customer field, even if author is partner_employee
self . assertSentEmail ( self . partner_employee_2 . email_formatted ,
record . customer_id )
# 2: default_res_ids + active_ids -> res_ids takes lead
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_record , add_web = False ,
default_composition_mode = ' mass_mail ' ,
default_res_ids = self . test_record . ids ,
default_template_id = self . template . id ,
active_ids = self . test_records . ids ,
)
) )
composer = composer_form . save ( )
self . assertEqual ( literal_eval ( composer . res_ids ) , self . test_record . ids )
with self . mock_mail_gateway ( mail_unlink_sent = True ) :
composer . _action_send_mail ( )
# global outgoing
self . assertEqual ( len ( self . _new_mails ) , 1 , ' Should have taken default_res_ids (1 record) ' )
self . assertEqual ( len ( self . _mails ) , 1 , ' Should have taken default_res_ids (1 record) ' )
# template is sent directly using customer field, even if author is partner_employee
self . assertSentEmail ( self . partner_employee_2 . email_formatted ,
self . test_record . customer_id )
# 3: fallback on active_id if not active_ids
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
active_id = self . test_record . id ,
default_composition_mode = ' mass_mail ' ,
default_model = self . test_records . _name ,
default_template_id = self . template . id ,
) )
composer = composer_form . save ( )
self . assertEqual ( literal_eval ( composer . res_ids ) , self . test_record . ids )
with self . mock_mail_gateway ( mail_unlink_sent = False ) :
composer . _action_send_mail ( )
# global outgoing
self . assertEqual ( len ( self . _new_mails ) , 1 , ' Should have created 1 mail.mail per record ' )
self . assertEqual ( len ( self . _mails ) , 1 , ' Should have sent 1 email per record ' )
# 4: _batch_size limit for active_ids
with patch . object ( MailComposer , ' _batch_size ' , new = 1 ) :
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
active_ids = self . test_records . ids ,
default_composition_mode = ' mass_mail ' ,
default_model = self . test_records . _name ,
default_template_id = self . template . id ,
) )
composer = composer_form . save ( )
self . assertTrue ( composer . composition_batch )
self . assertEqual ( composer . composition_mode , ' mass_mail ' )
self . assertEqual ( sorted ( literal_eval ( composer . res_ids ) ) , sorted ( self . test_records . ids ) )
with self . mock_mail_gateway ( mail_unlink_sent = True ) :
composer . _action_send_mail ( )
# should create emails in 2 batches of 1
self . assertEqual ( self . build_email_mocked . call_count , 2 )
self . assertEqual ( self . mail_mail_create_mocked . call_count , 2 )
# global outgoing
self . assertEqual ( len ( self . _new_mails ) , 2 , ' Should have created 1 mail.mail per record based on active_ids ' )
self . assertEqual ( len ( self . _mails ) , 2 , ' Should have sent 1 email per record based on on active_ids ' )
# 5: mail.batch_size config parameter support, for sending only
self . env [ ' ir.config_parameter ' ] . sudo ( ) . set_param ( ' mail.batch_size ' , 1 )
with patch . object ( MailComposer , ' _batch_size ' , new = 50 ) :
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
active_ids = self . test_records . ids ,
default_composition_mode = ' mass_mail ' ,
default_model = self . test_records . _name ,
default_template_id = self . template . id ,
) )
composer = composer_form . save ( )
self . assertTrue ( composer . composition_batch )
self . assertEqual ( composer . composition_mode , ' mass_mail ' )
self . assertEqual ( sorted ( literal_eval ( composer . res_ids ) ) , sorted ( self . test_records . ids ) )
with self . mock_mail_gateway ( mail_unlink_sent = True ) :
composer . _action_send_mail ( )
# should create emails in 2 batches of 1
self . assertEqual ( self . build_email_mocked . call_count , 2 )
self . assertEqual ( self . mail_mail_create_mocked . call_count , 2 )
# global outgoing
self . assertEqual ( len ( self . _new_mails ) , 2 , ' Should have created 1 mail.mail per record based on active_ids ' )
self . assertEqual ( len ( self . _mails ) , 2 , ' Should have sent 1 email per record based on on active_ids ' )
# 6: void is void: raise in comment mode, just don't send anything in mass mail mode
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
default_model = ' mail.test.ticket.mc ' ,
default_template_id = self . template . id
) )
composer = composer_form . save ( )
self . assertEqual ( composer . composition_mode , ' comment ' )
with self . mock_mail_gateway ( mail_unlink_sent = False ) , self . assertRaises ( ValueError ) :
composer . _action_send_mail ( )
self . assertNotSentEmail ( )
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
default_composition_mode = ' mass_mail ' ,
default_model = ' mail.test.ticket.mc ' ,
default_template_id = self . template . id
) )
composer = composer_form . save ( )
self . assertEqual ( composer . composition_mode , ' mass_mail ' )
with self . mock_mail_gateway ( mail_unlink_sent = False ) :
composer . _action_send_mail ( )
self . assertNotSentEmail ( )
@users ( ' employee ' )
@mute_logger ( ' odoo.addons.mail.models.mail_mail ' )
def test_mail_composer_wtpl_recipients_email_fields ( self ) :
""" Test various combinations of corner case / not standard filling of
email fields : multi email , formatted emails , . . . """
existing_partners = self . env [ ' res.partner ' ] . search ( [ ] )
partner_format_tofind , partner_multi_tofind , partner_at_tofind = self . env [ ' res.partner ' ] . create ( [
{
' email ' : ' " FindMe Format " <find.me.format@test.example.com> ' ,
' name ' : ' FindMe Format ' ,
} , {
' email ' : ' find.me.multi.1@test.example.com, " FindMe Multi " <find.me.multi.2@test.example.com> ' ,
' name ' : ' FindMe Multi ' ,
} , {
' email ' : ' " Bike@Home " <find.me.at@test.example.com> ' ,
' name ' : ' NotBike@Home ' ,
}
] )
email_ccs = [ ' " Raoul " <test.cc.1@example.com> ' , ' " Raoulette " <test.cc.2@example.com> ' , ' test.cc.2.2@example.com> ' , ' invalid ' , ' ' ]
email_tos = [ ' " Micheline, l \' Immense " <test.to.1@example.com> ' , ' test.to.2@example.com ' , ' wrong ' , ' ' ]
self . template . write ( {
' email_cc ' : ' , ' . join ( email_ccs ) ,
' email_from ' : ' {{ user.email_formatted }} ' ,
' email_to ' : ' , ' . join ( email_tos + ( partner_format_tofind + partner_multi_tofind + partner_at_tofind ) . mapped ( ' email ' ) ) ,
' partner_to ' : f ' { self . partner_1 . id } , { self . partner_2 . id } ,0,test ' ,
} )
self . user_employee . write ( { ' email ' : ' email.from.1@test.mycompany.com, email.from.2@test.mycompany.com ' } )
self . partner_1 . write ( { ' email ' : ' " Valid Formatted " <valid.lelitre@agrolait.com> ' } )
self . partner_2 . write ( { ' email ' : ' valid.other.1@agrolait.com, valid.other.cc@agrolait.com ' } )
# ensure values used afterwards for testing
self . assertEqual (
self . partner_employee . email_formatted ,
' " Ernest Employee " <email.from.1@test.mycompany.com,email.from.2@test.mycompany.com> ' ,
' Formatting: wrong formatting due to multi-email ' )
self . assertEqual (
self . partner_1 . email_formatted ,
' " Valid Lelitre " <valid.lelitre@agrolait.com> ' ,
' Formatting: avoid wrong double encapsulation ' )
self . assertEqual (
self . partner_2 . email_formatted ,
' " Valid Poilvache " <valid.other.1@agrolait.com,valid.other.cc@agrolait.com> ' ,
' Formatting: wrong formatting due to multi-email ' )
# instantiate composer, send mailing
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context (
self . test_records ,
add_web = True ,
default_template_id = self . template . id ,
)
) )
composer = composer_form . save ( )
with self . mock_mail_gateway ( mail_unlink_sent = False ) , self . mock_mail_app ( ) :
composer . action_send_mail ( )
# find partners created during sending (as emails are transformed into partners)
# FIXME: currently email finding based on formatted / multi emails does
# not work
new_partners = self . env [ ' res.partner ' ] . search ( [ ] ) . search ( [ ( ' id ' , ' not in ' , existing_partners . ids ) ] )
self . assertEqual ( len ( new_partners ) , 9 ,
' Mail (FIXME): did not find existing partners for formatted / multi emails: 1 extra partner ' )
self . assertIn ( partner_format_tofind , new_partners )
self . assertIn ( partner_multi_tofind , new_partners )
self . assertIn ( partner_at_tofind , new_partners )
self . assertEqual ( new_partners [ 0 : 3 ] . ids , ( partner_format_tofind + partner_multi_tofind + partner_at_tofind ) . ids )
self . assertEqual (
sorted ( new_partners . mapped ( ' email ' ) ) ,
sorted ( [ ' " Bike@Home " <find.me.at@test.example.com> ' ,
' " FindMe Format " <find.me.format@test.example.com> ' ,
' find.me.multi.1@test.example.com, " FindMe Multi " <find.me.multi.2@test.example.com> ' ,
' find.me.multi.2@test.example.com ' ,
' test.cc.1@example.com ' , ' test.cc.2@example.com ' , ' test.cc.2.2@example.com ' ,
' test.to.1@example.com ' , ' test.to.2@example.com ' ] ) ,
' Mail: created partners for valid emails (wrong / invalid not taken into account) + did not find corner cases (FIXME) '
)
self . assertEqual (
sorted ( new_partners . mapped ( ' email_formatted ' ) ) ,
sorted ( [ ' " NotBike@Home " <find.me.at@test.example.com> ' ,
' " FindMe Format " <find.me.format@test.example.com> ' ,
' " FindMe Multi " <find.me.multi.1@test.example.com,find.me.multi.2@test.example.com> ' ,
' " find.me.multi.2@test.example.com " <find.me.multi.2@test.example.com> ' ,
' " test.cc.1@example.com " <test.cc.1@example.com> ' ,
' " test.cc.2@example.com " <test.cc.2@example.com> ' ,
' " test.cc.2.2@example.com " <test.cc.2.2@example.com> ' ,
' " test.to.1@example.com " <test.to.1@example.com> ' ,
' " test.to.2@example.com " <test.to.2@example.com> ' ] ) ,
)
self . assertEqual (
sorted ( new_partners . mapped ( ' name ' ) ) ,
sorted ( [ ' NotBike@Home ' ,
' FindMe Format ' ,
' FindMe Multi ' ,
' find.me.multi.2@test.example.com ' ,
' test.cc.1@example.com ' ,
' test.to.1@example.com ' ,
' test.to.2@example.com ' ,
' test.cc.2@example.com ' ,
' test.cc.2.2@example.com ' ] ) ,
' Mail: when possible, find name in formatted emails, otherwise fallback on email '
)
# global outgoing: one mail.mail (all customer recipients), * 2 records
# Note that employee is not mailed here compared to 'comment' mode as he
# is not in the template recipients, only a follower
# FIXME template is sent only to partners (email_to are transformed) ->
# wrong / weird emails (see email_formatted of partners) is kept
# FIXME: more partners created than real emails (see above) -> due to
# transformation from email -> partner in template 'generate_recipients'
# there are more partners than email to notify;
self . assertEqual ( len ( self . _new_mails ) , 2 , ' Should have created 2 mail.mail ' )
self . assertEqual (
len ( self . _mails ) , ( len ( new_partners ) + 2 ) * 2 ,
f ' Should have sent { ( len ( new_partners ) + 2 ) * 2 } emails, one / recipient ( { len ( new_partners ) } mailed partners + partner_1 + partner_2) * 2 records ' )
for record in self . test_records :
recipients = self . partner_1 + self . partner_2 + new_partners
self . assertMailMail (
recipients ,
' sent ' ,
author = self . partner_employee ,
email_to_recipients = [
[ self . partner_1 . email_formatted ] ,
[ f ' " { self . partner_2 . name } " <valid.other.1@agrolait.com> ' , f ' " { self . partner_2 . name } " <valid.other.cc@agrolait.com> ' ] ,
] + [ [ new_partners [ 0 ] [ ' email_formatted ' ] ] ,
[ ' " FindMe Multi " <find.me.multi.1@test.example.com> ' , ' " FindMe Multi " <find.me.multi.2@test.example.com> ' ]
] + [ [ email ] for email in new_partners [ 2 : ] . mapped ( ' email_formatted ' ) ] ,
email_values = {
' body_content ' : f ' TemplateBody { record . name } ' ,
# single email event if email field is multi-email
' email_from ' : formataddr ( ( self . user_employee . name , ' email.from.1@test.mycompany.com ' ) ) ,
' reply_to ' : formataddr ( (
f ' { self . env . user . company_id . name } { record . name } ' ,
f ' { self . alias_catchall } @ { self . alias_domain } '
) ) ,
' subject ' : f ' TemplateSubject { record . name } ' ,
} ,
fields_values = {
# currently holding multi-email 'email_from'
' email_from ' : self . partner_employee . email_formatted ,
' reply_to ' : formataddr ( (
f ' { self . env . user . company_id . name } { record . name } ' ,
f ' { self . alias_catchall } @ { self . alias_domain } '
) ) ,
} ,
mail_message = record . message_ids [ 0 ] , # message copy is kept
)
# actual emails sent through smtp
for recipient in recipients :
# multi emails -> send multiple emails (smart)
if recipient == self . partner_2 :
smtp_to_list = [ ' valid.other.1@agrolait.com ' , ' valid.other.cc@agrolait.com ' ]
# find.me.format
elif recipient == new_partners [ 0 ] :
self . assertEqual ( recipient , partner_format_tofind )
smtp_to_list = [ ' find.me.format@test.example.com ' ]
# find.me.multi was split into two partners
elif recipient == new_partners [ 1 ] :
self . assertEqual ( recipient , partner_multi_tofind )
smtp_to_list = [ ' find.me.multi.1@test.example.com ' , ' find.me.multi.2@test.example.com ' ]
elif recipient == new_partners [ 3 ] :
smtp_to_list = [ ' find.me.multi.2@test.example.com ' ]
# bike@home: name is not recognized as email anymore
elif recipient == new_partners [ 2 ] :
self . assertEqual ( recipient , partner_at_tofind )
smtp_to_list = [ ' find.me.at@test.example.com ' ]
else :
smtp_to_list = [ recipient . email_normalized ]
self . assertSMTPEmailsSent (
smtp_from = f ' { self . alias_bounce } @ { self . alias_domain } ' ,
smtp_to_list = smtp_to_list ,
mail_server = self . mail_server_domain ,
# msg_from takes only first found normalized email to make a valid email_from
message_from = formataddr (
( self . user_employee . name ,
' email.from.1@test.mycompany.com ' ,
) ) ,
# similar envelope, assertSMTPEmailsSent cannot distinguish
# records (would have to dive into content, too complicated)
emails_count = 2 ,
)
@users ( ' employee ' )
@mute_logger ( ' odoo.models.unlink ' , ' odoo.addons.mail.models.mail_mail ' )
def test_mail_composer_wtpl_reply_to ( self ) :
# test without catchall filling reply-to
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_records , add_web = True ,
default_template_id = self . template . id )
) )
composer = composer_form . save ( )
# remove alias so that _notify_get_reply_to will return the default value instead of alias
self . company_admin . write ( {
' alias_domain_id ' : False ,
} )
with self . mock_mail_gateway ( mail_unlink_sent = False ) :
composer . action_send_mail ( )
for record in self . test_records :
# template is sent only to partners (email_to are transformed)
self . assertMailMail ( record . customer_id ,
' sent ' ,
mail_message = record . message_ids [ 0 ] ,
author = self . partner_employee ,
email_values = {
' email_from ' : self . partner_employee_2 . email_formatted ,
' reply_to ' : self . partner_employee_2 . email_formatted ,
} ,
fields_values = {
' email_from ' : self . partner_employee_2 . email_formatted ,
' reply_to ' : self . partner_employee_2 . email_formatted ,
} ,
)
@users ( ' employee ' )
@mute_logger ( ' odoo.models.unlink ' , ' odoo.addons.mail.models.mail_mail ' )
def test_mail_composer_wtpl_reply_to_force_new ( self ) :
""" Test no auto thread behavior, notably with reply-to. """
# launch composer in mass mode
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( self . test_records , add_web = True ,
default_template_id = self . template . id )
) )
composer_form . reply_to_mode = ' new '
composer_form . reply_to = " {{ ' \" ' + object.name + ' \" < %s > ' % ' dynamic.reply.to@test.mycompany.com ' }} "
composer = composer_form . save ( )
self . assertTrue ( composer . reply_to_force_new )
with self . mock_mail_gateway ( mail_unlink_sent = False ) :
composer . action_send_mail ( )
for record in self . test_records :
self . assertMailMail ( record . customer_id ,
' sent ' ,
mail_message = record . message_ids [ 0 ] ,
author = self . partner_employee ,
email_values = {
' body_content ' : ' TemplateBody %s ' % record . name ,
' email_from ' : self . partner_employee_2 . email_formatted ,
' reply_to ' : formataddr ( (
f ' { record . name } ' ,
' dynamic.reply.to@test.mycompany.com '
) ) ,
' subject ' : ' TemplateSubject %s ' % record . name ,
} ,
fields_values = {
' email_from ' : self . partner_employee_2 . email_formatted ,
' reply_to ' : formataddr ( (
f ' { record . name } ' ,
' dynamic.reply.to@test.mycompany.com '
) ) ,
} ,
)
@tagged ( ' mail_composer ' , ' mail_blacklist ' )
class TestComposerResultsMassStatus ( TestMailComposer ) :
""" Test cases involving blacklist, opt-out, state management, ... specific
class to avoid bloating the base mailing - based composer tests . """
@classmethod
def setUpClass ( cls ) :
""" Test data: 4 records with a customer set, then some additional
records based on emails , duplicates , . . .
Record0 : partner is blacklisted
Record1 : Record4 has the same email ( but no customer set )
Record5 and Record6 have same email ( notlinked to any customer )
"""
super ( TestComposerResultsMassStatus , 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 ) ] ,
} )
# add 2 new records with customers
cls . test_records , cls . test_partners = cls . _create_records_for_batch (
' mail.test.ticket.el ' , 4 ,
additional_values = { ' user_id ' : cls . user_employee_2 . id } ,
prefix = ' el_ '
)
# create bl / optout / duplicates, see docstring
cls . env [ ' mail.blacklist ' ] . _add (
cls . test_partners [ 0 ] . email_formatted
)
cls . test_records + = cls . env [ cls . test_records . _name ] . create ( [
{
' email_from ' : cls . test_records [ 1 ] . email_from ,
' name ' : ' Email of Record2 ' ,
' user_id ' : cls . user_employee_2 . id ,
} ,
{
' email_from ' : ' test.duplicate@test.example.com ' ,
' name ' : ' Dupe email (first) ' ,
' user_id ' : cls . user_employee_2 . id ,
} ,
{
' email_from ' : ' test.duplicate@test.example.com ' ,
' name ' : ' Dupe email (second) ' ,
' user_id ' : cls . user_employee_2 . id ,
} ,
] )
cls . template . write ( {
' model_id ' : cls . env [ ' ir.model ' ] . _get_id ( cls . test_records . _name ) ,
' scheduled_date ' : False ,
} )
def test_assert_initial_data ( self ) :
""" Ensure class initial data to ease understanding """
self . assertTrue ( self . template . auto_delete )
self . assertEqual ( len ( self . test_records ) , 7 )
self . assertEqual ( self . test_records . user_id , self . user_employee_2 )
self . assertEqual ( self . test_records . message_partner_ids , self . partner_employee_2 )
self . assertEqual ( self . test_records [ 1 ] . email_from , self . test_records [ 4 ] . email_from )
self . assertEqual ( self . test_records [ 5 ] . email_from , self . test_records [ 6 ] . email_from )
self . assertEqual ( len ( self . test_partners ) , 4 )
self . assertTrue ( self . test_partners [ 0 ] . is_blacklisted )
@users ( ' employee ' )
@mute_logger ( ' odoo.tests ' , ' odoo.addons.mail.models.mail_mail ' , ' odoo.models.unlink ' )
def test_mailing_blacklist_mixin ( self ) :
""" Tests a document-based mass mailing with excluded emails. Their emails
are canceled if the model inherits from the blacklist mixin . """
test_records = self . test_records [ : 2 ] . with_env ( self . env )
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( test_records , add_web = True ,
default_template_id = self . template . id )
) )
composer = composer_form . save ( )
with self . mock_mail_gateway ( mail_unlink_sent = False ) , self . mock_mail_app ( ) :
composer . _action_send_mail ( )
for record , expected_state , expected_ft in zip (
test_records ,
[ ' cancel ' , ' sent ' ] ,
[ ' mail_bl ' , False ]
) :
with self . subTest ( record = record , expected_state = expected_state , expected_ft = expected_ft ) :
self . assertMailMail (
record . customer_id , expected_state ,
# author is current user, email_from is coming from template (user_id of record)
author = self . user_employee . partner_id ,
fields_values = {
' email_from ' : self . user_employee_2 . email_formatted ,
' failure_reason ' : False ,
' failure_type ' : expected_ft ,
} ,
email_values = {
' email_from ' : self . user_employee_2 . email_formatted ,
}
)
self . assertEqual ( len ( self . _mails ) , 1 , ' Should have sent 1 email, and skipped an excluded email. ' )
# test exclusion list bypass
composer_form = Form ( self . env [ ' mail.compose.message ' ] . with_context (
self . _get_web_context ( test_records , add_web = True ,
default_template_id = self . template . id ,
default_use_exclusion_list = False )
) )
composer = composer_form . save ( )
with self . mock_mail_gateway ( mail_unlink_sent = False ) , self . mock_mail_app ( ) :
composer . _action_send_mail ( )
for record , expected_state , expected_ft in zip (
test_records ,
[ ' sent ' , ' sent ' ] ,
[ False , False ]
) :
with self . subTest ( record = record , expected_state = expected_state , expected_ft = expected_ft ) :
self . assertMailMail (
record . customer_id , expected_state ,
# author is current user, email_from is coming from template (user_id of record)
author = self . user_employee . partner_id ,
fields_values = {
' email_from ' : self . user_employee_2 . email_formatted ,
' failure_reason ' : False ,
' failure_type ' : expected_ft ,
} ,
email_values = {
' email_from ' : self . user_employee_2 . email_formatted ,
}
)
self . assertEqual ( len ( self . _mails ) , 2 , ' Should have sent 2 emails, even to excluded email. ' )