[MERGE] mail, various: improve post and composer usage, coding and performances
Purpose of this task is to cleanup usage of main mail.thread post methods and
helpers as well as composer usage and code. Achieve this by removing or
cleaning deprecated code, simplifying flow / API, improving composer support
of templates, rewrite composer code, ease batch support.
In this task we notably
* cleanup input / output as well as default values for message_post as well
as 'with view' and 'with template' helpers. Those are now cleaned and
two main helpers are introduced: post with source (view or template)
and mail with source (view or template), depending on main notification
style.
* rewrite and improve template code generating values from the template
definition. It is now cleaner, delegated into sub methods and made
more efficient in batch mode, notably when computing or creating
recipients;
* rewrite and improve composer code generating values for either posting
either sending a mass mailing. It is now better separated into a static
rendering and a dynamic rendering;
* supports a batch post on documents, instead of being limited to a single
res_id and improve overall code to be batch-enabled;
* support scheduled_date from template;
* support multi reports templates;
* perform various code cleaning, rewriting in order to achieve both a more
readable code, easier to override and understand as well as better tested
and documented;
* cleanup configuration of composer. Use fields, better define and update
their value. Don't add them in UI but allow more fine-grain usage, notably
when invoked in code;
METHODOLOGY NOTE
Several points are linked to different tasks. A big PR containing a cleaning
and some improvements in the composer behavior, usage and definition allowed
to ease testing and review.
This PR is build on previously merged content that originated form this PR.
See notably (but not limited to)
* account, mail, sale: properly locate code
Task-2792146 - odoo/odoo#106658
* various: cleanup alias usage and empty list help overrides
Task-3074358 - odoo/odoo#105992
* clean parameters propagation in post methods
Task-2726501 - odoo/odoo#82167
* rating send request api cleanup
odoo/odoo#106024
* empty list help cleanup
Task-3074357 - odoo/odoo#105992
* rendering mixin options and usage cleanup
odoo/odoo#106072
* improve exclusion list mail logging
odoo/odoo#106568
* linting and cleaning
odoo/odoo#106025
Next step will be to improve composer fields to be editable computed fields
and remove the onchange and hard-to-predict code. Translation support will
also be improved, as well as some performance bits.
LINKS
Task-2710804 (Mail: Clean MailThread API)
Task-2993872 (Mail: Support scheduled date in all composer flows)
Task-3035101 (Mail: Support batch-posting from composer)
Task-3034875 (Mail: Speedup and batch partners find or create with templates)
Task-2868153 (Mail: Allow multi-reports mail templates)
Task-3132710 (Mail: Configurable composer)
Prepares Task-2088884 (Mail: Use editable computed stored fields in composer)
Prepares Task-36879 (Mail: Support MultiCompany Aliases)
closes odoo/odoo#99482
Related: odoo/upgrade#3856
Related: odoo/enterprise#31017
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -54,8 +54,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
<field name="report_template" ref="account_invoices"/>
|
||||
<field name="report_name">Invoice_{{ (object.name or '').replace('/','_') }}{{ object.state == 'draft' and '_draft' or '' }}</field>
|
||||
<field name="report_template_ids" eval="[(4, ref('account.account_invoices'))]"/>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
@@ -84,8 +83,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
<field name="report_template" ref="account.action_report_payment_receipt"/>
|
||||
<field name="report_name">{{ (object.name or '').replace('/','-') }}</field>
|
||||
<field name="report_template_ids" eval="[(4, ref('account.action_report_payment_receipt'))]"/>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
@@ -129,8 +127,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
<field name="report_template" ref="account_invoices"/>
|
||||
<field name="report_name">Credit_note_{{ (object.name or '').replace('/','_') }}{{ object.state == 'draft' and '_draft' or '' }}</field>
|
||||
<field name="report_template_ids" eval="[(4, ref('account.account_invoices'))]"/>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
@@ -3509,12 +3509,7 @@ class AccountMove(models.Model):
|
||||
compose_form = self.env.ref('account.account_invoice_send_wizard_form', raise_if_not_found=False)
|
||||
ctx = dict(
|
||||
default_model='account.move',
|
||||
default_res_id=self.id,
|
||||
# For the sake of consistency we need a default_res_model if
|
||||
# default_res_id is set. Not renaming default_model as it can
|
||||
# create many side-effects.
|
||||
default_res_model='account.move',
|
||||
default_use_template=bool(template),
|
||||
default_res_ids=self.ids,
|
||||
default_template_id=template and template.id or False,
|
||||
default_composition_mode='comment',
|
||||
mark_invoice_as_sent=True,
|
||||
|
||||
@@ -430,7 +430,6 @@
|
||||
<field name="context" eval="{
|
||||
'mail_post_autofollow': True,
|
||||
'default_composition_mode': 'comment',
|
||||
'default_use_template': True,
|
||||
'default_template_id': ref('account.mail_template_data_payment_receipt'),
|
||||
'default_email_layout_xmlid': 'mail.mail_notification_light',
|
||||
}"/>
|
||||
@@ -447,7 +446,6 @@
|
||||
<field name="context" eval="{
|
||||
'mail_post_autofollow': True,
|
||||
'default_composition_mode': 'mass_mail',
|
||||
'default_use_template': True,
|
||||
'default_template_id': ref('account.mail_template_data_payment_receipt'),
|
||||
'default_email_layout_xmlid': 'mail.mail_notification_light',
|
||||
}"/>
|
||||
|
||||
@@ -41,6 +41,7 @@ class AccountInvoiceSend(models.TransientModel):
|
||||
|
||||
composer = self.env['mail.compose.message'].create({
|
||||
'composition_mode': 'comment' if len(res_ids) == 1 else 'mass_mail',
|
||||
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment'),
|
||||
})
|
||||
res.update({
|
||||
'invoice_ids': res_ids,
|
||||
@@ -86,7 +87,8 @@ class AccountInvoiceSend(models.TransientModel):
|
||||
if not self.composer_id:
|
||||
self.composer_id = self.env['mail.compose.message'].create({
|
||||
'composition_mode': 'comment' if len(res_ids) == 1 else 'mass_mail',
|
||||
'template_id': self.template_id.id
|
||||
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment'),
|
||||
'template_id': self.template_id.id,
|
||||
})
|
||||
else:
|
||||
self.composer_id.composition_mode = 'comment' if len(res_ids) == 1 else 'mass_mail'
|
||||
|
||||
@@ -183,7 +183,6 @@ class AlarmManager(models.AbstractModel):
|
||||
for alarm in alarms:
|
||||
alarm_attendees = attendees.filtered(lambda attendee: attendee.event_id.id in events_by_alarm[alarm.id])
|
||||
alarm_attendees.with_context(
|
||||
mail_notify_force_send=True,
|
||||
calendar_template_ignore_recurrence=True
|
||||
)._send_mail_to_attendees(
|
||||
alarm.mail_template_id,
|
||||
|
||||
@@ -750,12 +750,11 @@ class Meeting(models.Model):
|
||||
raise UserError(_("There are no attendees on these events"))
|
||||
template_id = self.env['ir.model.data']._xmlid_to_res_id('calendar.calendar_template_meeting_update', raise_if_not_found=False)
|
||||
# The mail is sent with datetime corresponding to the sending user TZ
|
||||
composition_mode = self.env.context.get('composition_mode', 'comment')
|
||||
default_composition_mode = self.env.context.get('default_composition_mode', self.env.context.get('composition_mode', 'comment'))
|
||||
compose_ctx = dict(
|
||||
default_composition_mode=composition_mode,
|
||||
default_composition_mode=default_composition_mode,
|
||||
default_model='calendar.event',
|
||||
default_res_ids=self.ids,
|
||||
default_use_template=bool(template_id),
|
||||
default_template_id=template_id,
|
||||
default_partner_ids=self.partner_ids.ids,
|
||||
mail_tz=self.env.user.tz,
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Meetings" sample="1" multi_edit="1">
|
||||
<header>
|
||||
<button name="action_open_composer" type="object" context="{'composition_mode':'mass_mail'}"
|
||||
<button name="action_open_composer" type="object" context="{'default_composition_mode':'mass_mail'}"
|
||||
string="Send Mail"/>
|
||||
</header>
|
||||
<field name="name" string="Subject" decoration-bf="1" attrs="{'readonly':[('recurrency','=',True)]}"/>
|
||||
|
||||
@@ -1561,14 +1561,14 @@ class Lead(models.Model):
|
||||
def _merge_log_summary(self, merged_followers, opportunities_tail):
|
||||
"""Log the merge message on the lead."""
|
||||
self.ensure_one()
|
||||
self.message_post_with_view(
|
||||
self.message_post_with_source(
|
||||
"crm.crm_lead_merge_summary",
|
||||
values={
|
||||
render_values={
|
||||
"merged_followers": merged_followers,
|
||||
"opportunities": opportunities_tail,
|
||||
"is_html_empty": is_html_empty,
|
||||
},
|
||||
subtype_id=self.env.ref('mail.mt_note').id,
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
def _format_properties(self):
|
||||
|
||||
@@ -24,7 +24,7 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
|
||||
with self.assertQueryCount(user_sales_manager=0):
|
||||
test_leads = self.env['crm.lead'].browse(test_leads.ids)
|
||||
|
||||
with self.assertQueryCount(user_sales_manager=543): # crm 537 / com 543 / ent 537
|
||||
with self.assertQueryCount(user_sales_manager=543): # crm 537 / com 541 / ent 536
|
||||
test_leads._handle_salesmen_assignment(user_ids=user_ids, team_id=False)
|
||||
|
||||
self.assertEqual(test_leads.team_id, self.sales_team_convert | self.sales_team_1)
|
||||
@@ -42,7 +42,7 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
|
||||
with self.assertQueryCount(user_sales_manager=0):
|
||||
test_leads = self.env['crm.lead'].browse(test_leads.ids)
|
||||
|
||||
with self.assertQueryCount(user_sales_manager=524): # crm 521 / com 524
|
||||
with self.assertQueryCount(user_sales_manager=524): # crm 521 / com 516 / ent 516
|
||||
test_leads._handle_salesmen_assignment(user_ids=user_ids, team_id=team_id)
|
||||
|
||||
self.assertEqual(test_leads.team_id, self.sales_team_convert)
|
||||
@@ -167,7 +167,7 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
|
||||
user_ids = self.assign_users.ids
|
||||
|
||||
# randomness: at least 1 query
|
||||
with self.assertQueryCount(user_sales_manager=1704): # crm 1410 / com 1697
|
||||
with self.assertQueryCount(user_sales_manager=1704): # crm 1410 / com 1677 / ent 1685
|
||||
mass_convert = self.env['crm.lead2opportunity.partner.mass'].with_context({
|
||||
'active_model': 'crm.lead',
|
||||
'active_ids': test_leads.ids,
|
||||
|
||||
@@ -178,7 +178,7 @@ class TestLeadAssignPerf(TestLeadAssignCommon):
|
||||
|
||||
# randomness
|
||||
with self.with_user('user_sales_manager'):
|
||||
with self.assertQueryCount(user_sales_manager=6280): # crm 6226 / com 6276 / ent 6278
|
||||
with self.assertQueryCount(user_sales_manager=6280): # crm 6226 / com 6212 / ent 6214
|
||||
self.env['crm.team'].browse(sales_teams.ids)._action_assign_leads(work_days=30)
|
||||
|
||||
# teams assign
|
||||
|
||||
@@ -735,7 +735,6 @@
|
||||
<field name="target">new</field>
|
||||
<field name="context" eval="{
|
||||
'default_composition_mode': 'comment',
|
||||
'default_use_template': False,
|
||||
}"/>
|
||||
<field name="binding_model_id" ref="model_crm_lead"/>
|
||||
<field name="binding_view_types">form</field>
|
||||
@@ -748,7 +747,6 @@
|
||||
<field name="target">new</field>
|
||||
<field name="context" eval="{
|
||||
'default_composition_mode': 'mass_mail',
|
||||
'default_use_template': False,
|
||||
}"/>
|
||||
<field name="binding_model_id" ref="model_crm_lead"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
|
||||
@@ -68,18 +68,20 @@ class Lead(models.Model):
|
||||
|
||||
normalized_email = tools.email_normalize(lead.email_from)
|
||||
if not normalized_email:
|
||||
lead.message_post_with_view(
|
||||
lead.message_post_with_source(
|
||||
'crm_iap_enrich.mail_message_lead_enrich_no_email',
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
continue
|
||||
|
||||
email_domain = normalized_email.split('@')[1]
|
||||
# Discard domains of generic email providers as it won't return relevant information
|
||||
if email_domain in iap_tools._MAIL_DOMAIN_BLACKLIST:
|
||||
lead.write({'iap_enrich_done': True})
|
||||
lead.message_post_with_view(
|
||||
lead.message_post_with_source(
|
||||
'crm_iap_enrich.mail_message_lead_enrich_notfound',
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
else:
|
||||
lead_emails[lead.id] = email_domain
|
||||
|
||||
@@ -126,7 +128,10 @@ class Lead(models.Model):
|
||||
iap_data = iap_response.get(str(lead.id))
|
||||
if not iap_data:
|
||||
lead.write({'iap_enrich_done': True})
|
||||
lead.message_post_with_view('crm_iap_enrich.mail_message_lead_enrich_notfound', subtype_id=self.env.ref('mail.mt_note').id)
|
||||
lead.message_post_with_source(
|
||||
'crm_iap_enrich.mail_message_lead_enrich_notfound',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
continue
|
||||
|
||||
values = {'iap_enrich_done': True}
|
||||
@@ -156,10 +161,10 @@ class Lead(models.Model):
|
||||
|
||||
template_values = iap_data
|
||||
template_values['flavor_text'] = _("Lead enriched based on email address")
|
||||
lead.message_post_with_view(
|
||||
lead.message_post_with_source(
|
||||
'iap_mail.enrich_company',
|
||||
values=template_values,
|
||||
subtype_id=self.env.ref('mail.mt_note').id
|
||||
render_values=template_values,
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
def _merge_get_fields_specific(self):
|
||||
|
||||
@@ -278,7 +278,11 @@ class CRMLeadMiningRequest(models.Model):
|
||||
leads = self.env['crm.lead'].create(lead_vals_list)
|
||||
for lead in leads:
|
||||
if messages_to_post.get(lead.reveal_id):
|
||||
lead.message_post_with_view('iap_mail.enrich_company', values=messages_to_post[lead.reveal_id], subtype_id=self.env.ref('mail.mt_note').id)
|
||||
lead.message_post_with_source(
|
||||
'iap_mail.enrich_company',
|
||||
render_values=messages_to_post[lead.reveal_id],
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
# Methods responsible for format response data into valid odoo lead data
|
||||
@api.model
|
||||
|
||||
@@ -172,20 +172,24 @@ class DataRecycleModel(models.Model):
|
||||
('recycle_model_id', '=', self.id),
|
||||
('create_date', '>=', last_date)
|
||||
])
|
||||
|
||||
if records_count:
|
||||
partner_ids = self.notify_user_ids.partner_id.ids
|
||||
partner_ids = self.notify_user_ids.partner_id.ids if records_count else []
|
||||
if partner_ids:
|
||||
menu_id = self.env.ref('data_recycle.menu_data_cleaning_root').id
|
||||
kwargs = {
|
||||
'body': self.env['ir.qweb']._render('data_recycle.notification', {
|
||||
'records_count': records_count,
|
||||
'res_model_label': self.res_model_id.name,
|
||||
'recycle_model_id': self.id,
|
||||
'menu_id': menu_id
|
||||
}),
|
||||
'partner_ids': partner_ids,
|
||||
}
|
||||
self.env['mail.thread'].with_context(mail_notify_author=True).message_notify(**kwargs)
|
||||
self.env['mail.thread'].with_context(mail_notify_author=True).message_notify(
|
||||
body=self.env['ir.qweb']._render(
|
||||
'data_recycle.notification',
|
||||
{
|
||||
'records_count': records_count,
|
||||
'res_model_label': self.res_model_id.name,
|
||||
'recycle_model_id': self.id,
|
||||
'menu_id': menu_id
|
||||
}
|
||||
),
|
||||
model=self._name,
|
||||
partner_ids=partner_ids,
|
||||
res_id=self.id,
|
||||
subject=_('Data to Recycle'),
|
||||
)
|
||||
|
||||
def write(self, vals):
|
||||
if 'active' in vals and not vals['active']:
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
<t t-out="object.event_id.user_id.signature or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
</div></field>
|
||||
<field name="report_template" ref="action_report_event_registration_foldable_badge"/>
|
||||
<field name="report_name">Foldable Badge - {{ (object.event_id.name or 'Event').replace('/','_') }}</field>
|
||||
<field name="report_template_ids" eval="[(4, ref('event.action_report_event_registration_foldable_badge'))]"/>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
@@ -254,8 +253,7 @@
|
||||
</td></tr>
|
||||
</table>
|
||||
</field>
|
||||
<field name="report_template" ref="action_report_event_registration_full_page_ticket"/>
|
||||
<field name="report_name">Full Page Ticket - {{ (object.event_id.name or 'Event').replace('/','') }}</field>
|
||||
<field name="report_template_ids" eval="[(4, ref('event.action_report_event_registration_full_page_ticket'))]"/>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -195,9 +195,8 @@ class EventRegistration(models.Model):
|
||||
compose_form = self.env.ref('mail.email_compose_message_wizard_form')
|
||||
ctx = dict(
|
||||
default_model='event.registration',
|
||||
default_res_id=self.id,
|
||||
default_use_template=bool(template),
|
||||
default_template_id=template and template.id,
|
||||
default_res_ids=self.ids,
|
||||
default_template_id=template.id if template else False,
|
||||
default_composition_mode='comment',
|
||||
default_email_layout_xmlid="mail.mail_notification_light",
|
||||
)
|
||||
|
||||
@@ -100,12 +100,12 @@ class EventBooth(models.Model):
|
||||
|
||||
def _post_confirmation_message(self):
|
||||
for booth in self:
|
||||
booth.event_id.message_post_with_view(
|
||||
booth.event_id.message_post_with_source(
|
||||
'event_booth.event_booth_booked_template',
|
||||
values={
|
||||
render_values={
|
||||
'booth': booth,
|
||||
},
|
||||
subtype_id=self.env.ref('event_booth.mt_event_booth_booked').id,
|
||||
subtype_xmlid='event_booth.mt_event_booth_booked',
|
||||
)
|
||||
|
||||
def action_confirm(self, additional_values=None):
|
||||
|
||||
@@ -77,10 +77,11 @@ class EventRegistration(models.Model):
|
||||
registrations = super(EventRegistration, self).create(vals_list)
|
||||
for registration in registrations:
|
||||
if registration.sale_order_id:
|
||||
registration.message_post_with_view(
|
||||
registration.message_post_with_source(
|
||||
'mail.message_origin_link',
|
||||
values={'self': registration, 'origin': registration.sale_order_id},
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
render_values={'self': registration, 'origin': registration.sale_order_id},
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return registrations
|
||||
|
||||
def write(self, vals):
|
||||
|
||||
@@ -71,7 +71,7 @@ class TestSyncOdoo2Google(TestSyncGoogle):
|
||||
})
|
||||
partner_model = self.env.ref('base.model_res_partner')
|
||||
partner = self.env['res.partner'].search([], limit=1)
|
||||
with self.assertQueryCount(__system__=616):
|
||||
with self.assertQueryCount(__system__=615):
|
||||
events = self.env['calendar.event'].create([{
|
||||
'name': "Event %s" % (i),
|
||||
'start': datetime(2020, 1, 15, 8, 0),
|
||||
@@ -86,7 +86,7 @@ class TestSyncOdoo2Google(TestSyncGoogle):
|
||||
|
||||
events._sync_odoo2google(self.google_service)
|
||||
|
||||
with self.assertQueryCount(__system__=130):
|
||||
with self.assertQueryCount(__system__=126):
|
||||
events.unlink()
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ class TestSyncOdoo2Google(TestSyncGoogle):
|
||||
})
|
||||
partner_model = self.env.ref('base.model_res_partner')
|
||||
partner = self.env['res.partner'].search([], limit=1)
|
||||
with self.assertQueryCount(__system__=72):
|
||||
with self.assertQueryCount(__system__=56):
|
||||
event = self.env['calendar.event'].create({
|
||||
'name': "Event",
|
||||
'start': datetime(2020, 1, 15, 8, 0),
|
||||
@@ -120,7 +120,7 @@ class TestSyncOdoo2Google(TestSyncGoogle):
|
||||
'res_id': partner.id,
|
||||
})
|
||||
|
||||
with self.assertQueryCount(__system__=35):
|
||||
with self.assertQueryCount(__system__=31):
|
||||
event.unlink()
|
||||
|
||||
def test_event_without_user(self):
|
||||
|
||||
@@ -636,8 +636,12 @@ class HrExpense(models.Model):
|
||||
def refuse_expense(self, reason):
|
||||
self.write({'is_refused': True})
|
||||
self.sheet_id.write({'state': 'cancel'})
|
||||
self.sheet_id.message_post_with_view('hr_expense.hr_expense_template_refuse_reason',
|
||||
values={'reason': reason, 'is_sheet': False, 'name': self.name})
|
||||
for expense in self:
|
||||
expense.sheet_id.message_post_with_source(
|
||||
'hr_expense.hr_expense_template_refuse_reason',
|
||||
render_values={'reason': reason, 'is_sheet': False, 'name': expense.name},
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
|
||||
@api.model
|
||||
def get_expense_dashboard(self):
|
||||
@@ -798,21 +802,21 @@ class HrExpense(models.Model):
|
||||
# TDE TODO: seems louche, check to use notify
|
||||
if expense.employee_id.user_id.partner_id:
|
||||
expense.message_post(
|
||||
body=body,
|
||||
email_layout_xmlid='mail.mail_notification_light',
|
||||
partner_ids=expense.employee_id.user_id.partner_id.ids,
|
||||
subject='Re: %s' % msg_dict.get('subject', ''),
|
||||
body=body,
|
||||
subtype_id=self.env.ref('mail.mt_note').id,
|
||||
email_layout_xmlid='mail.mail_notification_light',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
else:
|
||||
self.env['mail.mail'].sudo().create({
|
||||
'email_from': self.env.user.email_formatted,
|
||||
'author_id': self.env.user.partner_id.id,
|
||||
'body_html': body,
|
||||
'subject': 'Re: %s' % msg_dict.get('subject', ''),
|
||||
'email_to': msg_dict.get('email_from', False),
|
||||
'auto_delete': True,
|
||||
'body_html': body,
|
||||
'email_from': self.env.user.email_formatted,
|
||||
'email_to': msg_dict.get('email_from', False),
|
||||
'references': msg_dict.get('message_id'),
|
||||
'subject': 'Re: %s' % msg_dict.get('subject', ''),
|
||||
}).send()
|
||||
|
||||
|
||||
@@ -1213,8 +1217,13 @@ class HrExpenseSheet(models.Model):
|
||||
raise UserError(_("You can only refuse your department expenses"))
|
||||
|
||||
self.write({'state': 'cancel'})
|
||||
subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
|
||||
for sheet in self:
|
||||
sheet.message_post_with_view('hr_expense.hr_expense_template_refuse_reason', values={'reason': reason, 'is_sheet': True, 'name': sheet.name})
|
||||
sheet.message_post_with_source(
|
||||
'hr_expense.hr_expense_template_refuse_reason',
|
||||
subtype_id=subtype_id,
|
||||
render_values={'reason': reason, 'is_sheet': True, 'name': sheet.name},
|
||||
)
|
||||
self.activity_update()
|
||||
|
||||
def reset_expense_sheets(self):
|
||||
|
||||
@@ -1624,7 +1624,8 @@ class HolidaysRequest(models.Model):
|
||||
if recipient:
|
||||
self.env['mail.thread'].sudo().message_notify(
|
||||
body=message,
|
||||
partner_ids=[recipient]
|
||||
partner_ids=[recipient],
|
||||
subject=_('Your Time Off'),
|
||||
)
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
|
||||
@@ -323,7 +323,7 @@ class TestCompanyLeave(TransactionCase):
|
||||
})
|
||||
company_leave._compute_date_from_to()
|
||||
|
||||
with self.assertQueryCount(__system__=774, admin=867): # 770 community
|
||||
with self.assertQueryCount(__system__=605, admin=867): # 770 community
|
||||
# Original query count: 1987
|
||||
# Without tracking/activity context keys: 5154
|
||||
company_leave.action_validate()
|
||||
|
||||
@@ -181,12 +181,11 @@ Do not hesitate to contact your manager or the human resource department.""")
|
||||
compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
|
||||
ctx = dict(
|
||||
default_model="hr.employee",
|
||||
default_res_id=self.id,
|
||||
default_use_template=bool(template),
|
||||
default_res_ids=self.ids,
|
||||
default_template_id=template.id,
|
||||
default_composition_mode='comment',
|
||||
default_is_log=True,
|
||||
default_email_layout_xmlid='mail.mail_notification_light',
|
||||
default_subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
)
|
||||
return {
|
||||
'name': _('Compose Email'),
|
||||
|
||||
@@ -450,7 +450,7 @@ class Applicant(models.Model):
|
||||
applicant = self[0]
|
||||
if 'stage_id' in changes and applicant.stage_id.template_id:
|
||||
res['stage_id'] = (applicant.stage_id.template_id, {
|
||||
'auto_delete_message': True,
|
||||
'auto_delete_keep_log': False,
|
||||
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
'email_layout_xmlid': 'mail.mail_notification_light'
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.translate import _
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
@@ -28,8 +27,9 @@ class HrEmployee(models.Model):
|
||||
employees = super().create(vals_list)
|
||||
for employee in employees:
|
||||
if employee.applicant_id:
|
||||
employee.applicant_id.message_post_with_view(
|
||||
employee.applicant_id.message_post_with_source(
|
||||
'hr_recruitment.applicant_hired_template',
|
||||
values={'applicant': employee.applicant_id},
|
||||
subtype_id=self.env.ref("hr_recruitment.mt_applicant_hired").id)
|
||||
render_values={'applicant': employee.applicant_id},
|
||||
subtype_xmlid='hr_recruitment.mt_applicant_hired',
|
||||
)
|
||||
return employees
|
||||
|
||||
@@ -45,8 +45,19 @@ class ApplicantGetRefuseReason(models.TransientModel):
|
||||
self.applicant_ids.write({'refuse_reason_id': self.refuse_reason_id.id, 'active': False})
|
||||
if self.send_mail:
|
||||
applicants = self.applicant_ids.filtered(lambda x: x.email_from or x.partner_id.email)
|
||||
applicants.with_context(active_test=True).message_post_with_template(self.template_id.id, **{
|
||||
'auto_delete_message': True,
|
||||
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
'email_layout_xmlid': 'mail.mail_notification_light'
|
||||
})
|
||||
# TDE note: keeping 16.0 behavior, clean me please
|
||||
message_values = {
|
||||
'email_layout_xmlid' : 'mail.mail_notification_light',
|
||||
}
|
||||
if len(applicants) > 1:
|
||||
applicants.with_context(active_test=True).message_mail_with_source(
|
||||
self.template_id,
|
||||
auto_delete_keep_log=False,
|
||||
**message_values
|
||||
)
|
||||
else:
|
||||
applicants.with_context(active_test=True).message_post_with_source(
|
||||
self.template_id,
|
||||
subtype_xmlid='mail.mt_note',
|
||||
**message_values
|
||||
)
|
||||
|
||||
@@ -41,8 +41,8 @@ class ApplicantSendMail(models.TransientModel):
|
||||
})
|
||||
|
||||
applicant.message_post(
|
||||
author_id=self.author_id.id,
|
||||
body=self.body,
|
||||
email_from=self.author_id.email,
|
||||
email_layout_xmlid='mail.mail_notification_light',
|
||||
message_type='comment',
|
||||
partner_ids=applicant.partner_id.ids,
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<field name="author_id" invisible="1"/>
|
||||
<field name="lang" invisible="1"/>
|
||||
<field name="render_model" invisible="1"/>
|
||||
<field name="template_id" invisible="1"/>
|
||||
<group>
|
||||
<field name="subject" required="1"/>
|
||||
<field name="applicant_ids" widget="many2many_tags" context="{'show_partner_name': 1}"/>
|
||||
|
||||
@@ -134,9 +134,9 @@ class HrContract(models.Model):
|
||||
).create(all_new_leave_vals)
|
||||
new_leaves.filtered(lambda l: l.state in 'validate')._validate_leave_request()
|
||||
for index, new_leave in enumerate(new_leaves):
|
||||
subtype_note = self.env.ref('mail.mt_note')
|
||||
new_leave.message_post_with_view(
|
||||
new_leave.message_post_with_source(
|
||||
'mail.message_origin_link',
|
||||
values={'self': new_leave, 'origin': all_new_leave_origin[index]},
|
||||
subtype_id=subtype_note.id)
|
||||
render_values={'self': new_leave, 'origin': all_new_leave_origin[index]},
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return super(HrContract, self - specific_contracts).write(vals)
|
||||
|
||||
@@ -32,7 +32,7 @@ class TestWorkEntryHolidaysPerformance(TestWorkEntryHolidaysBase):
|
||||
self.richard_emp.generate_work_entries(date(2018, 1, 1), date(2018, 1, 2))
|
||||
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
|
||||
|
||||
with self.assertQueryCount(__system__=93, admin=94):
|
||||
with self.assertQueryCount(__system__=79, admin=81):
|
||||
leave.action_validate()
|
||||
leave.action_refuse()
|
||||
|
||||
@@ -41,14 +41,14 @@ class TestWorkEntryHolidaysPerformance(TestWorkEntryHolidaysBase):
|
||||
def test_performance_leave_write(self):
|
||||
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
|
||||
|
||||
with self.assertQueryCount(__system__=21, admin=30):
|
||||
with self.assertQueryCount(__system__=19, admin=27):
|
||||
leave.date_to = datetime(2018, 1, 1, 19, 0)
|
||||
leave.action_refuse()
|
||||
|
||||
@users('__system__', 'admin')
|
||||
@warmup
|
||||
def test_performance_leave_create(self):
|
||||
with self.assertQueryCount(__system__=31, admin=32): # 25/26 com
|
||||
with self.assertQueryCount(__system__=29, admin=30):
|
||||
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
|
||||
leave.action_refuse()
|
||||
|
||||
@@ -57,7 +57,7 @@ class TestWorkEntryHolidaysPerformance(TestWorkEntryHolidaysBase):
|
||||
def test_performance_leave_confirm(self):
|
||||
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
|
||||
leave.action_draft()
|
||||
with self.assertQueryCount(__system__=28, admin=29):
|
||||
with self.assertQueryCount(__system__=25, admin=26):
|
||||
leave.action_confirm()
|
||||
leave.state = 'refuse'
|
||||
|
||||
@@ -112,7 +112,7 @@ class TestWorkEntryHolidaysPerformancesBigData(TestWorkEntryHolidaysBase):
|
||||
def test_work_entries_generation_perf(self):
|
||||
# Test Case 7: Try to generate work entries for
|
||||
# a hundred employees over a month
|
||||
with self.assertQueryCount(__system__=2607, admin=2807):
|
||||
with self.assertQueryCount(__system__=500, admin=2807):
|
||||
work_entries = self.contracts._generate_work_entries(date(2020, 7, 1), date(2020, 8, 31))
|
||||
|
||||
# Original work entries to generate when we don't adapt date_generated_from and
|
||||
|
||||
@@ -34,8 +34,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
<field name="report_template" ref="loyalty.report_gift_card"/>
|
||||
<field name="report_name">Your Gift Card</field>
|
||||
<field name="report_template_ids" eval="[(4, ref('loyalty.report_gift_card'))]"/>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
@@ -96,8 +95,7 @@
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</field>
|
||||
<field name="report_template" ref="loyalty.report_loyalty_card"/>
|
||||
<field name="report_name">Your Coupon Code</field>
|
||||
<field name="report_template_ids" eval="[(4, ref('loyalty.report_loyalty_card'))]"/>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -81,8 +81,7 @@ class LoyaltyCard(models.Model):
|
||||
compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
|
||||
ctx = dict(
|
||||
default_model='loyalty.card',
|
||||
default_res_id=self.id,
|
||||
default_use_template=bool(default_template),
|
||||
default_res_ids=self.ids,
|
||||
default_template_id=default_template and default_template.id,
|
||||
default_composition_mode='comment',
|
||||
default_email_layout_xmlid='mail.mail_notification_light',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import pytz
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.osv import expression
|
||||
|
||||
from .lunch_supplier import float_to_time
|
||||
@@ -194,5 +194,6 @@ class LunchAlert(models.Model):
|
||||
if partners:
|
||||
self.env['mail.thread'].message_notify(
|
||||
body=self.message,
|
||||
partner_ids=partners.ids
|
||||
partner_ids=partners.ids,
|
||||
subject=_('Your Lunch Order'),
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
{
|
||||
'name': 'Discuss',
|
||||
'version': '1.11',
|
||||
'version': '1.12',
|
||||
'category': 'Productivity/Discuss',
|
||||
'sequence': 145,
|
||||
'summary': 'Chat, mail gateway and private channels',
|
||||
|
||||
@@ -207,12 +207,10 @@ class ServerActions(models.Model):
|
||||
subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
|
||||
else:
|
||||
subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
|
||||
for record in records:
|
||||
record.message_post_with_template(
|
||||
self.template_id.id,
|
||||
composition_mode='comment',
|
||||
subtype_id=subtype_id,
|
||||
)
|
||||
records.message_post_with_source(
|
||||
self.template_id,
|
||||
subtype_id=subtype_id,
|
||||
)
|
||||
else:
|
||||
template = self.template_id.with_context(cleaned_ctx)
|
||||
for res_id in res_ids:
|
||||
|
||||
@@ -574,16 +574,16 @@ class MailActivity(models.Model):
|
||||
next_activities_values.append(vals)
|
||||
|
||||
# post message on activity, before deleting it
|
||||
activity_message = record.message_post_with_view(
|
||||
activity_message = record.message_post_with_source(
|
||||
'mail.message_activity_done',
|
||||
values={
|
||||
attachment_ids=attachment_ids,
|
||||
render_values={
|
||||
'activity': activity,
|
||||
'feedback': feedback,
|
||||
'display_assignee': activity.user_id != self.env.user
|
||||
},
|
||||
subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_activities'),
|
||||
mail_activity_type_id=activity.activity_type_id.id,
|
||||
attachment_ids=[Command.link(attachment_id) for attachment_id in attachment_ids] if attachment_ids else [],
|
||||
subtype_xmlid='mail.mt_activities',
|
||||
)
|
||||
|
||||
# Moving the attachments in the message
|
||||
|
||||
@@ -342,11 +342,10 @@ class MailActivityMixin(models.AbstractModel):
|
||||
template = self.env['mail.template'].browse(template_id).exists()
|
||||
if not template:
|
||||
return False
|
||||
for record in self:
|
||||
record.message_post_with_template(
|
||||
template_id,
|
||||
composition_mode='comment'
|
||||
)
|
||||
self.message_post_with_source(
|
||||
template,
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
return True
|
||||
|
||||
def activity_search(self, act_type_xmlids='', user_id=None, additional_domain=None):
|
||||
|
||||
@@ -81,7 +81,7 @@ class MailBlackList(models.Model):
|
||||
if message:
|
||||
record.with_context(mail_create_nosubscribe=True).message_post(
|
||||
body=message,
|
||||
subtype_id=self.env.ref('mail.mt_note').id,
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return record
|
||||
|
||||
@@ -97,7 +97,7 @@ class MailBlackList(models.Model):
|
||||
if message:
|
||||
record.with_context(mail_create_nosubscribe=True).message_post(
|
||||
body=message,
|
||||
subtype_id=self.env.ref('mail.mt_note').id,
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return record
|
||||
|
||||
|
||||
@@ -617,7 +617,7 @@ class Channel(models.Model):
|
||||
Automatically set the message posted by the current user as seen for themselves.
|
||||
"""
|
||||
self._set_last_seen_message(message)
|
||||
return super()._message_post_after_hook(message=message, msg_vals=msg_vals)
|
||||
return super()._message_post_after_hook(message, msg_vals)
|
||||
|
||||
def _check_can_update_message_content(self, message):
|
||||
""" We don't call super in this override as we want to ignore the
|
||||
|
||||
+124
-63
@@ -309,6 +309,7 @@ class MailMail(models.Model):
|
||||
except (ValueError, TypeError):
|
||||
parsed_datetime = False
|
||||
if parsed_datetime:
|
||||
parsed_datetime = parsed_datetime.replace(microsecond=0)
|
||||
if not parsed_datetime.tzinfo:
|
||||
parsed_datetime = pytz.utc.localize(parsed_datetime)
|
||||
else:
|
||||
@@ -322,31 +323,122 @@ class MailMail(models.Model):
|
||||
# mail_mail formatting, tools and send mechanism
|
||||
# ------------------------------------------------------
|
||||
|
||||
def _send_prepare_body(self):
|
||||
def _prepare_outgoing_body(self):
|
||||
"""Return a specific ir_email body. The main purpose of this method
|
||||
is to be inherited to add custom content depending on some module."""
|
||||
self.ensure_one()
|
||||
return self.body_html or ''
|
||||
if tools.is_html_empty(self.body_html):
|
||||
return ''
|
||||
return self.env['mail.render.mixin']._replace_local_links(self.body_html)
|
||||
|
||||
def _send_prepare_values(self, partner=None):
|
||||
"""Return a dictionary for specific email values, depending on a
|
||||
partner, or generic to the whole recipients given by mail.email_to.
|
||||
def _prepare_outgoing_list(self):
|
||||
""" Return a list of emails to send based on current mail.mail. Each
|
||||
is a dictionary for specific email values, depending on a partner, or
|
||||
generic to the whole recipients given by mail.email_to.
|
||||
|
||||
:param Model partner: specific recipient partner
|
||||
:return list: list of dicts used in IrMailServer.build_email()
|
||||
"""
|
||||
self.ensure_one()
|
||||
body = self._send_prepare_body()
|
||||
body = self._prepare_outgoing_body()
|
||||
body_alternative = tools.html2plaintext(body)
|
||||
if partner:
|
||||
email_to = [tools.formataddr((partner.name or 'False', partner.email or 'False'))]
|
||||
else:
|
||||
|
||||
# headers
|
||||
headers = {}
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
bounce_alias = ICP.get_param("mail.bounce.alias")
|
||||
catchall_domain = ICP.get_param("mail.catchall.domain")
|
||||
if bounce_alias and catchall_domain:
|
||||
headers['Return-Path'] = f'{bounce_alias}@{catchall_domain}'
|
||||
if self.headers:
|
||||
try:
|
||||
headers.update(ast.literal_eval(self.headers))
|
||||
except (ValueError, TypeError) as e:
|
||||
_logger.warning(
|
||||
'Evaluation error when evaluating mail headers (received %r): %s',
|
||||
self.headers, e,
|
||||
)
|
||||
# global except as we don't want to crash the queue just due to a malformed
|
||||
# headers value
|
||||
except Exception as e:
|
||||
_logger.warning(
|
||||
'Unknown error when evaluating mail headers (received %r): %s',
|
||||
self.headers, e,
|
||||
)
|
||||
|
||||
# prepare recipients: use email_to if defined then check recipient_ids
|
||||
# that receive a specific email, notably due to link shortening / redirect
|
||||
# that is recipients-dependent. Keep original email/partner as this is
|
||||
# used in post-processing to know failures, like missing recipients
|
||||
email_list = []
|
||||
if self.email_to:
|
||||
email_to = tools.email_split_and_format(self.email_to)
|
||||
res = {
|
||||
'body': body,
|
||||
'body_alternative': body_alternative,
|
||||
'email_to': email_to,
|
||||
}
|
||||
return res
|
||||
email_list.append({
|
||||
'email_cc': [],
|
||||
'email_to': email_to,
|
||||
# keep raw initial value for incoming pre processing of outgoing emails
|
||||
'email_to_raw': self.email_to or '',
|
||||
'partner_id': False,
|
||||
})
|
||||
# add all cc once, either to the first "To", either as a single entry (do not mix
|
||||
# with partner-specific sending)
|
||||
if self.email_cc:
|
||||
if email_list:
|
||||
email_list[0]['email_cc'] = tools.email_split(self.email_cc)
|
||||
else:
|
||||
email_list.append({
|
||||
'email_cc': tools.email_split(self.email_cc),
|
||||
'email_to': [],
|
||||
'email_to_raw': False,
|
||||
'partner_id': False,
|
||||
})
|
||||
# specific behavior to customize the send email for notified partners
|
||||
for partner in self.recipient_ids:
|
||||
# check partner email content
|
||||
email_to = [tools.formataddr((partner.name or '', partner.email or 'False'))]
|
||||
email_list.append({
|
||||
'email_cc': [],
|
||||
'email_to': email_to,
|
||||
# keep raw initial value for incoming pre processing of outgoing emails
|
||||
'email_to_raw': partner.email or '',
|
||||
'partner_id': partner,
|
||||
})
|
||||
|
||||
# prepare attachments: remove attachments if user send the link with the
|
||||
# access_token.
|
||||
attachments = self.attachment_ids
|
||||
if attachments:
|
||||
if body:
|
||||
link_ids = {int(link) for link in re.findall(r'/web/(?:content|image)/([0-9]+)', body)}
|
||||
if link_ids:
|
||||
attachments = attachments - self.env['ir.attachment'].browse(list(link_ids))
|
||||
# load attachment binary data with a separate read(), as prefetching all
|
||||
# `datas` (binary field) could bloat the browse cache, triggering
|
||||
# soft/hard mem limits with temporary data.
|
||||
email_attachments = [
|
||||
(a['name'], base64.b64decode(a['datas']), a['mimetype'])
|
||||
for a in attachments.sudo().read(['name', 'datas', 'mimetype']) if a['datas'] is not False
|
||||
]
|
||||
else:
|
||||
email_attachments = []
|
||||
|
||||
return [
|
||||
{
|
||||
'attachments': email_attachments,
|
||||
'body': body,
|
||||
'body_alternative': body_alternative,
|
||||
'email_cc': email_values['email_cc'],
|
||||
'email_from': self.email_from,
|
||||
'email_to': email_values['email_to'],
|
||||
'email_to_raw': email_values['email_to_raw'],
|
||||
'headers': headers,
|
||||
'message_id': self.message_id,
|
||||
'object_id': f'{self.res_id}-{self.model}' if self.res_id else '',
|
||||
'partner_id': email_values['partner_id'],
|
||||
'references': self.references,
|
||||
'reply_to': self.reply_to,
|
||||
'subject': self.subject,
|
||||
} for email_values in email_list
|
||||
]
|
||||
|
||||
def _split_by_mail_configuration(self):
|
||||
"""Group the <mail.mail> based on their "email_from" and their "mail_server_id".
|
||||
@@ -432,7 +524,6 @@ class MailMail(models.Model):
|
||||
|
||||
def _send(self, auto_commit=False, raise_exception=False, smtp_session=None):
|
||||
IrMailServer = self.env['ir.mail_server']
|
||||
IrAttachment = self.env['ir.attachment']
|
||||
for mail_id in self.ids:
|
||||
success_pids = []
|
||||
failure_type = None
|
||||
@@ -443,40 +534,6 @@ class MailMail(models.Model):
|
||||
if mail.state != 'outgoing':
|
||||
continue
|
||||
|
||||
# remove attachments if user send the link with the access_token
|
||||
body = mail.body_html or ''
|
||||
attachments = mail.attachment_ids
|
||||
for link in re.findall(r'/web/(?:content|image)/([0-9]+)', body):
|
||||
attachments = attachments - IrAttachment.browse(int(link))
|
||||
|
||||
# load attachment binary data with a separate read(), as prefetching all
|
||||
# `datas` (binary field) could bloat the browse cache, triggerring
|
||||
# soft/hard mem limits with temporary data.
|
||||
attachments = [(a['name'], base64.b64decode(a['datas']), a['mimetype'])
|
||||
for a in attachments.sudo().read(['name', 'datas', 'mimetype']) if a['datas'] is not False]
|
||||
|
||||
# specific behavior to customize the send email for notified partners
|
||||
email_list = []
|
||||
if mail.email_to:
|
||||
email_list.append(mail._send_prepare_values())
|
||||
for partner in mail.recipient_ids:
|
||||
values = mail._send_prepare_values(partner=partner)
|
||||
values['partner_id'] = partner
|
||||
email_list.append(values)
|
||||
|
||||
# headers
|
||||
headers = {}
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
bounce_alias = ICP.get_param("mail.bounce.alias")
|
||||
catchall_domain = ICP.get_param("mail.catchall.domain")
|
||||
if bounce_alias and catchall_domain:
|
||||
headers['Return-Path'] = '%s@%s' % (bounce_alias, catchall_domain)
|
||||
if mail.headers:
|
||||
try:
|
||||
headers.update(ast.literal_eval(mail.headers))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Writing on the mail object may fail (e.g. lock on user) which
|
||||
# would trigger a rollback *after* actually sending the email.
|
||||
# To avoid sending twice the same email, provoke the failure earlier
|
||||
@@ -507,22 +564,26 @@ class MailMail(models.Model):
|
||||
res = None
|
||||
# TDE note: could be great to pre-detect missing to/cc and skip sending it
|
||||
# to go directly to failed state update
|
||||
email_list = mail._prepare_outgoing_list()
|
||||
|
||||
# send each sub-email
|
||||
for email in email_list:
|
||||
msg = IrMailServer.build_email(
|
||||
email_from=mail.email_from,
|
||||
email_to=email.get('email_to'),
|
||||
subject=mail.subject,
|
||||
body=email.get('body'),
|
||||
body_alternative=email.get('body_alternative'),
|
||||
email_cc=tools.email_split(mail.email_cc),
|
||||
reply_to=mail.reply_to,
|
||||
attachments=attachments,
|
||||
message_id=mail.message_id,
|
||||
references=mail.references,
|
||||
object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
|
||||
email_from=email['email_from'],
|
||||
email_to=email['email_to'],
|
||||
subject=email['subject'],
|
||||
body=email['body'],
|
||||
body_alternative=email['body_alternative'],
|
||||
email_cc=email['email_cc'],
|
||||
reply_to=email['reply_to'],
|
||||
attachments=email['attachments'],
|
||||
message_id=email['message_id'],
|
||||
references=email['references'],
|
||||
object_id=email['object_id'],
|
||||
subtype='html',
|
||||
subtype_alternative='plain',
|
||||
headers=headers)
|
||||
headers=email['headers'],
|
||||
)
|
||||
processing_pid = email.pop("partner_id", None)
|
||||
try:
|
||||
res = IrMailServer.send_email(
|
||||
|
||||
@@ -571,7 +571,7 @@ class Message(models.Model):
|
||||
if 'record_name' not in values and 'default_record_name' not in self.env.context:
|
||||
values['record_name'] = self._get_record_name(values)
|
||||
|
||||
if 'attachment_ids' not in values:
|
||||
if not values.get('attachment_ids'):
|
||||
values['attachment_ids'] = []
|
||||
# extract base64 images
|
||||
if 'body' in values:
|
||||
@@ -604,9 +604,9 @@ class Message(models.Model):
|
||||
messages = super(Message, self).create(values_list)
|
||||
|
||||
check_attachment_access = []
|
||||
if all(isinstance(command, int) or command[0] in (4, 6) for values in values_list for command in values.get('attachment_ids')):
|
||||
if all(isinstance(command, int) or command[0] in (4, 6) for values in values_list for command in values['attachment_ids']):
|
||||
for values in values_list:
|
||||
for command in values.get('attachment_ids'):
|
||||
for command in values['attachment_ids']:
|
||||
if isinstance(command, int):
|
||||
check_attachment_access += [command]
|
||||
elif command[0] == 6:
|
||||
|
||||
@@ -12,6 +12,7 @@ from werkzeug import urls
|
||||
|
||||
from odoo import _, api, fields, models, tools
|
||||
from odoo.addons.base.models.ir_qweb import QWebException
|
||||
from odoo.addons.http_routing.models.ir_http import slug
|
||||
from odoo.exceptions import UserError, AccessError
|
||||
from odoo.tools import is_html_empty
|
||||
from odoo.tools.rendering_tools import convert_inline_template_to_qweb, parse_inline_template, render_inline_template, template_env_globals
|
||||
@@ -241,7 +242,10 @@ class MailRenderMixin(models.AbstractModel):
|
||||
def _check_access_right_dynamic_template(self):
|
||||
if not self.env.su and not self.env.user.has_group('mail.group_mail_template_editor') and self._is_dynamic():
|
||||
group = self.env.ref('mail.group_mail_template_editor')
|
||||
raise AccessError(_('Only users belonging to the "%s" group can modify dynamic templates.', group.name))
|
||||
raise AccessError(
|
||||
_('Only users belonging to the "%(group_name)s" group can modify dynamic templates.',
|
||||
group_name=group.name)
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# RENDERING
|
||||
@@ -256,14 +260,15 @@ class MailRenderMixin(models.AbstractModel):
|
||||
* various formatting tools;
|
||||
"""
|
||||
render_context = {
|
||||
'ctx': self._context,
|
||||
'format_date': lambda date, date_format=False, lang_code=False: format_date(self.env, date, date_format, lang_code),
|
||||
'format_datetime': lambda dt, tz=False, dt_format=False, lang_code=False: format_datetime(self.env, dt, tz, dt_format, lang_code),
|
||||
'format_time': lambda time, tz=False, time_format=False, lang_code=False: format_time(self.env, time, tz, time_format, lang_code),
|
||||
'format_amount': lambda amount, currency, lang_code=False: tools.format_amount(self.env, amount, currency, lang_code),
|
||||
'format_duration': lambda value: tools.format_duration(value),
|
||||
'user': self.env.user,
|
||||
'ctx': self._context,
|
||||
'is_html_empty': is_html_empty,
|
||||
'slug': slug,
|
||||
'user': self.env.user,
|
||||
}
|
||||
render_context.update(copy.copy(template_env_globals))
|
||||
return render_context
|
||||
@@ -314,15 +319,21 @@ class MailRenderMixin(models.AbstractModel):
|
||||
except Exception as e:
|
||||
if isinstance(e, QWebException) and isinstance(e.__cause__, PermissionError):
|
||||
group = self.env.ref('mail.group_mail_template_editor')
|
||||
raise AccessError(_('Only users belonging to the "%s" group can modify dynamic templates.', group.name)) from e
|
||||
raise AccessError(
|
||||
_('Only users belonging to the "%(group_name)s" group can modify dynamic templates.',
|
||||
group_name=group.name)
|
||||
) from e
|
||||
_logger.info("Failed to render template : %s", template_src, exc_info=True)
|
||||
raise UserError(_("Failed to render QWeb template : %s)", template_src)) from e
|
||||
raise UserError(
|
||||
_("Failed to render QWeb template : %(template_src)s)",
|
||||
template_src=template_src)
|
||||
) from e
|
||||
results[record.id] = render_result
|
||||
|
||||
return results
|
||||
|
||||
@api.model
|
||||
def _render_template_qweb_view(self, view_xmlid, model, res_ids,
|
||||
def _render_template_qweb_view(self, view_ref, model, res_ids,
|
||||
add_context=None, options=None):
|
||||
""" Render a QWeb template based on an ir.ui.view content.
|
||||
|
||||
@@ -330,8 +341,9 @@ class MailRenderMixin(models.AbstractModel):
|
||||
variables are added:
|
||||
* ``object``: record based on which the template is rendered;
|
||||
|
||||
:param str view_xmlid: source QWeb template. It should be a string
|
||||
XmlID allowing to fetch an ``ir.ui.view``;
|
||||
:param str/int/record view_ref: source QWeb template. It should be an
|
||||
XmlID allowing to fetch an ``ir.ui.view``, or an ID of a view or
|
||||
an ``ir.ui.view`` record;
|
||||
:param str model: see ``MailRenderMixin._render_template()``;
|
||||
:param list res_ids: see ``MailRenderMixin._render_template()``;
|
||||
|
||||
@@ -352,14 +364,23 @@ class MailRenderMixin(models.AbstractModel):
|
||||
if add_context:
|
||||
variables.update(**add_context)
|
||||
|
||||
view_ref = view_ref.id if isinstance(view_ref, models.BaseModel) else view_ref
|
||||
for record in self.env[model].browse(res_ids):
|
||||
variables['object'] = record
|
||||
try:
|
||||
render_result = self.env['ir.qweb']._render(view_xmlid, variables, minimal_qcontext=True, raise_if_not_found=False, **(options or {}))
|
||||
render_result = self.env['ir.qweb']._render(
|
||||
view_ref,
|
||||
variables,
|
||||
minimal_qcontext=True,
|
||||
raise_if_not_found=False,
|
||||
**(options or {})
|
||||
)
|
||||
results[record.id] = render_result
|
||||
except Exception as e:
|
||||
_logger.info("Failed to render template : %s", view_xmlid, exc_info=True)
|
||||
raise UserError(_("Failed to render template : %s") % view_xmlid)
|
||||
_logger.info("Failed to render template : %s", view_ref, exc_info=True)
|
||||
raise UserError(
|
||||
_("Failed to render template : %(view_ref)s", view_ref=view_ref)
|
||||
) from e
|
||||
|
||||
return results
|
||||
|
||||
@@ -395,7 +416,10 @@ class MailRenderMixin(models.AbstractModel):
|
||||
if (not self._unrestricted_rendering and is_dynamic and not self.env.is_admin() and
|
||||
not self.env.user.has_group('mail.group_mail_template_editor')):
|
||||
group = self.env.ref('mail.group_mail_template_editor')
|
||||
raise AccessError(_('Only users belonging to the "%s" group can modify dynamic templates.', group.name))
|
||||
raise AccessError(
|
||||
_('Only users belonging to the "%(group_name)s" group can modify dynamic templates.',
|
||||
group_name=group.name)
|
||||
)
|
||||
|
||||
if not is_dynamic:
|
||||
# Either the content is a raw text without placeholders, either we fail to
|
||||
@@ -414,10 +438,16 @@ class MailRenderMixin(models.AbstractModel):
|
||||
variables['object'] = record
|
||||
|
||||
try:
|
||||
results[record.id] = render_inline_template(template_instructions, variables)
|
||||
results[record.id] = render_inline_template(
|
||||
template_instructions,
|
||||
variables
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.info("Failed to render inline_template: \n%s", str(template_txt), exc_info=True)
|
||||
raise UserError(_("Failed to render inline_template template : %s)", e))
|
||||
raise UserError(
|
||||
_("Failed to render inline_template template : %(template_txt)s)",
|
||||
template_txt=template_txt)
|
||||
) from e
|
||||
|
||||
return results
|
||||
|
||||
@@ -435,6 +465,15 @@ class MailRenderMixin(models.AbstractModel):
|
||||
rendered[res_id] = self._replace_local_links(rendered_html)
|
||||
return rendered
|
||||
|
||||
@api.model
|
||||
def _process_scheduled_date(self, scheduled_date):
|
||||
if scheduled_date:
|
||||
# parse scheduled_date to make it timezone agnostic UTC as expected
|
||||
# by the ORM
|
||||
parsed_datetime = self.env['mail.mail']._parse_scheduled_datetime(scheduled_date)
|
||||
scheduled_date = parsed_datetime.replace(tzinfo=None) if parsed_datetime else False
|
||||
return scheduled_date
|
||||
|
||||
@api.model
|
||||
def _render_template(self, template_src, model, res_ids, engine='inline_template',
|
||||
add_context=None, options=None):
|
||||
@@ -467,9 +506,15 @@ class MailRenderMixin(models.AbstractModel):
|
||||
options = {}
|
||||
|
||||
if not isinstance(res_ids, (list, tuple)):
|
||||
raise ValueError(_('Template rendering should be called only using on a list of IDs.'))
|
||||
raise ValueError(
|
||||
_('Template rendering should be called only using on a list of IDs; received %(res_ids)r instead.',
|
||||
res_ids=res_ids)
|
||||
)
|
||||
if engine not in ('inline_template', 'qweb', 'qweb_view'):
|
||||
raise ValueError(_('Template rendering supports only inline_template, qweb, or qweb_view (view or raw).'))
|
||||
raise ValueError(
|
||||
_('Template rendering supports only inline_template, qweb, or qweb_view (view or raw); received %(engine)s instead.',
|
||||
engine=engine)
|
||||
)
|
||||
|
||||
if engine == 'qweb_view':
|
||||
rendered = self._render_template_qweb_view(template_src, model, res_ids,
|
||||
@@ -498,8 +543,6 @@ class MailRenderMixin(models.AbstractModel):
|
||||
:return dict: {res_id: lang code (i.e. en_US)}
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not isinstance(res_ids, (list, tuple)):
|
||||
raise ValueError(_('Template rendering for language should be called with a list of IDs.'))
|
||||
|
||||
rendered_langs = self._render_template(self.lang, self.render_model, res_ids, engine=engine)
|
||||
return dict(
|
||||
|
||||
+313
-115
@@ -7,6 +7,7 @@ import logging
|
||||
from odoo import _, api, fields, models, tools, Command
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import is_html_empty
|
||||
from odoo.tools.safe_eval import safe_eval, time
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -63,10 +64,12 @@ class MailTemplate(models.Model):
|
||||
'attachment_id', 'Attachments',
|
||||
help="You may attach files to this template, to be added to all "
|
||||
"emails created from this template")
|
||||
report_name = fields.Char('Report Filename', translate=True, prefetch=True,
|
||||
help="Name to use for the generated report file (may contain placeholders)\n"
|
||||
"The extension can be omitted and will then come from the report type.")
|
||||
report_template = fields.Many2one('ir.actions.report', 'Optional report to print and attach')
|
||||
report_template_ids = fields.Many2many(
|
||||
'ir.actions.report', relation='mail_template_ir_actions_report_rel',
|
||||
column1='mail_template_id',
|
||||
column2='ir_actions_report_id',
|
||||
string='Reports to print and attach',
|
||||
domain="[('model', '=', model)]")
|
||||
# options
|
||||
mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing Mail Server', readonly=False,
|
||||
help="Optional preferred server for outgoing mails. If not set, the highest "
|
||||
@@ -170,14 +173,18 @@ class MailTemplate(models.Model):
|
||||
def create_action(self):
|
||||
ActWindow = self.env['ir.actions.act_window']
|
||||
view = self.env.ref('mail.email_compose_message_wizard_form')
|
||||
|
||||
for template in self:
|
||||
context = {
|
||||
'default_composition_mode': 'mass_mail',
|
||||
'default_model': template.model,
|
||||
'default_template_id' : template.id,
|
||||
}
|
||||
button_name = _('Send Mail (%s)', template.name)
|
||||
action = ActWindow.create({
|
||||
'name': button_name,
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'mail.compose.message',
|
||||
'context': "{'default_composition_mode': 'mass_mail', 'default_template_id' : %d, 'default_use_template': True}" % (template.id),
|
||||
'context': repr(context),
|
||||
'view_mode': 'form,tree',
|
||||
'view_id': view.id,
|
||||
'target': 'new',
|
||||
@@ -191,126 +198,312 @@ class MailTemplate(models.Model):
|
||||
# MESSAGE/EMAIL VALUES GENERATION
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def generate_recipients(self, results, res_ids):
|
||||
"""Generates the recipients of the template. Default values can ben generated
|
||||
instead of the template values if requested by template or context.
|
||||
Emails (email_to, email_cc) can be transformed into partners if requested
|
||||
in the context. """
|
||||
self.ensure_one()
|
||||
def _generate_template_attachments(self, res_ids, render_fields,
|
||||
render_results=None):
|
||||
""" Render attachments of template 'self', returning values for records
|
||||
given by 'res_ids'. Note that ``report_template_ids`` returns values for
|
||||
'attachments', as we have a list of tuple (report_name, base64 value)
|
||||
for those reports. It is considered as being the job of callers to
|
||||
transform those attachments into valid ``ir.attachment`` records.
|
||||
|
||||
if self.use_default_to or self._context.get('tpl_force_default_to'):
|
||||
records = self.env[self.model].browse(res_ids).sudo()
|
||||
default_recipients = records._message_get_default_recipients()
|
||||
for res_id, recipients in default_recipients.items():
|
||||
results[res_id].pop('partner_to', None)
|
||||
results[res_id].update(recipients)
|
||||
:param list res_ids: list of record IDs on which template is rendered;
|
||||
:param list render_fields: list of fields to render on template which
|
||||
are specific to attachments, e.g. attachment_ids or report_template_ids;
|
||||
:param dict render_results: res_ids-based dictionary of render values.
|
||||
For each res_id, a dict of values based on render_fields is given
|
||||
|
||||
records_company = None
|
||||
if self._context.get('tpl_partners_only') and self.model and results and 'company_id' in self.env[self.model]._fields:
|
||||
records = self.env[self.model].browse(results.keys()).read(['company_id'])
|
||||
records_company = {rec['id']: (rec['company_id'][0] if rec['company_id'] else None) for rec in records}
|
||||
|
||||
for res_id, values in results.items():
|
||||
partner_ids = values.get('partner_ids', list())
|
||||
if self._context.get('tpl_partners_only'):
|
||||
mails = tools.email_split(values.pop('email_to', '')) + tools.email_split(values.pop('email_cc', ''))
|
||||
Partner = self.env['res.partner']
|
||||
if records_company:
|
||||
Partner = Partner.with_context(default_company_id=records_company[res_id])
|
||||
for mail in mails:
|
||||
partner = Partner.find_or_create(mail)
|
||||
partner_ids.append(partner.id)
|
||||
partner_to = values.pop('partner_to', '')
|
||||
if partner_to:
|
||||
# placeholders could generate '', 3, 2 due to some empty field values
|
||||
tpl_partner_ids = [int(pid) for pid in partner_to.split(',') if pid]
|
||||
partner_ids += self.env['res.partner'].sudo().browse(tpl_partner_ids).exists().ids
|
||||
results[res_id]['partner_ids'] = partner_ids
|
||||
return results
|
||||
|
||||
def generate_email(self, res_ids, render_fields):
|
||||
"""Generates an email from the template for given the given model based on
|
||||
records given by res_ids.
|
||||
|
||||
:param res_id: id of the record to use for rendering the template (model
|
||||
is taken from template definition)
|
||||
:returns: a dict containing all relevant fields for creating a new
|
||||
mail.mail entry, with one extra key ``attachments``, in the
|
||||
format [(report_name, data)] where data is base64 encoded.
|
||||
:return: updated (or new) render_results;
|
||||
"""
|
||||
self.ensure_one()
|
||||
multi_mode = True
|
||||
if isinstance(res_ids, int):
|
||||
res_ids = [res_ids]
|
||||
multi_mode = False
|
||||
if render_results is None:
|
||||
render_results = {}
|
||||
|
||||
results = dict()
|
||||
for lang, (template, template_res_ids) in self._classify_per_lang(res_ids).items():
|
||||
for field in render_fields:
|
||||
# generating reports is done on a per-record basis, better ensure cache
|
||||
# is filled up to avoid rendering and browsing in a loop
|
||||
if res_ids and 'report_template_ids' in render_fields and self.report_template_ids:
|
||||
self.env[self.model].browse(res_ids)
|
||||
|
||||
for res_id in res_ids:
|
||||
values = render_results.setdefault(res_id, {})
|
||||
|
||||
# link template attachments directly
|
||||
if 'attachment_ids' in render_fields:
|
||||
values['attachment_ids'] = self.attachment_ids.ids
|
||||
|
||||
# generate attachments (reports)
|
||||
if 'report_template_ids' in render_fields and self.report_template_ids:
|
||||
for report in self.report_template_ids:
|
||||
# generate content
|
||||
if report.report_type in ['qweb-html', 'qweb-pdf']:
|
||||
report_content, report_format = self.env['ir.actions.report']._render_qweb_pdf(report, [res_id])
|
||||
else:
|
||||
render_res = self.env['ir.actions.report']._render(report, [res_id])
|
||||
if not render_res:
|
||||
raise UserError(_('Unsupported report type %s found.', report.report_type))
|
||||
report_content, report_format = render_res
|
||||
report_content = base64.b64encode(report_content)
|
||||
# generate name
|
||||
if report.print_report_name:
|
||||
report_name = safe_eval(
|
||||
report.print_report_name,
|
||||
{
|
||||
'object': self.env[self.model].browse(res_id),
|
||||
'time': time,
|
||||
}
|
||||
)
|
||||
else:
|
||||
report_name = _('Report')
|
||||
extension = "." + report_format
|
||||
if not report_name.endswith(extension):
|
||||
report_name += extension
|
||||
values.setdefault('attachments', []).append((report_name, report_content))
|
||||
elif 'report_template_ids' in render_fields:
|
||||
values['attachments'] = []
|
||||
|
||||
# hook for attachments-specific computation, used currently only for accounting
|
||||
if hasattr(self.env[self.model], '_process_attachments_for_template_post'):
|
||||
records_attachments = self.env[self.model].browse(res_ids)._process_attachments_for_template_post(self)
|
||||
for res_id, additional_attachments in records_attachments.items():
|
||||
if not additional_attachments:
|
||||
continue
|
||||
if additional_attachments.get('attachment_ids'):
|
||||
render_results[res_id].setdefault('attachment_ids', []).extend(additional_attachments['attachment_ids'])
|
||||
if additional_attachments.get('attachments'):
|
||||
render_results[res_id].setdefault('attachments', []).extend(additional_attachments['attachments'])
|
||||
|
||||
return render_results
|
||||
|
||||
def _generate_template_recipients(self, res_ids, render_fields,
|
||||
find_or_create_partners=False,
|
||||
render_results=None):
|
||||
""" Render recipients of the template 'self', returning values for records
|
||||
given by 'res_ids'. Default values can be generated instead of the template
|
||||
values if requested by template (see 'use_default_to' field). Email fields
|
||||
('email_cc', 'email_to') are transformed into partners if requested
|
||||
(finding or creating partners). 'partner_to' field is transformed into
|
||||
'partner_ids' field.
|
||||
|
||||
:param list res_ids: list of record IDs on which template is rendered;
|
||||
:param list render_fields: list of fields to render on template which
|
||||
are specific to recipients, e.g. email_cc, email_to, partner_to);
|
||||
:param boolean find_or_create_partners: transform emails into partners
|
||||
(calling ``find_or_create`` on partner model);
|
||||
:param dict render_results: res_ids-based dictionary of render values.
|
||||
For each res_id, a dict of values based on render_fields is given;
|
||||
|
||||
:return: updated (or new) render_results. It holds a 'partner_ids' key
|
||||
holding partners given by ``_message_get_default_recipients`` and/or
|
||||
generated based on 'partner_to'. If ``find_or_create_partners`` is
|
||||
False emails are present, otherwise they are included as partners
|
||||
contained in ``partner_ids``.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if render_results is None:
|
||||
render_results = {}
|
||||
records_sudo = None
|
||||
|
||||
# if using default recipients -> ``_message_get_default_recipients`` gives
|
||||
# values for email_to, email_cc and partner_ids
|
||||
if self.use_default_to and self.model:
|
||||
records_sudo = self.env[self.model].browse(res_ids).sudo()
|
||||
default_recipients = records_sudo._message_get_default_recipients()
|
||||
for res_id, recipients in default_recipients.items():
|
||||
render_results.setdefault(res_id, {}).update(recipients)
|
||||
# render fields dynamically which generates recipients
|
||||
else:
|
||||
for field in set(render_fields) & {'email_cc', 'email_to', 'partner_to'}:
|
||||
generated_field_values = self._render_field(field, res_ids)
|
||||
for res_id in res_ids:
|
||||
render_results.setdefault(res_id, {})[field] = generated_field_values[res_id]
|
||||
|
||||
# classify records per company for partner fetch/creation
|
||||
if find_or_create_partners and self.model and 'company_id' in self.env[self.model]._fields:
|
||||
if not records_sudo:
|
||||
records_sudo = self.env[self.model].browse(res_ids).sudo()
|
||||
records_per_company = {}
|
||||
for read_record in records_sudo.read(['company_id']):
|
||||
company_id = read_record['company_id'][0] if read_record['company_id'] else False
|
||||
records_per_company.setdefault(company_id, []).append(read_record['id'])
|
||||
else:
|
||||
records_per_company = {False: res_ids}
|
||||
|
||||
# create partners from emails if asked to
|
||||
if find_or_create_partners:
|
||||
for company_id, record_ids in records_per_company.items():
|
||||
all_emails = []
|
||||
email_to_res_ids = {}
|
||||
for res_id in record_ids:
|
||||
record_values = render_results.setdefault(res_id, {})
|
||||
mails = tools.email_split(record_values.pop('email_to', '')) + \
|
||||
tools.email_split(record_values.pop('email_cc', ''))
|
||||
all_emails += mails
|
||||
for mail in mails:
|
||||
email_to_res_ids.setdefault(mail, []).append(res_id)
|
||||
|
||||
if not all_emails:
|
||||
continue
|
||||
additional_values = {}
|
||||
if company_id:
|
||||
additional_values['company_id'] = company_id
|
||||
partners = self.env['res.partner']._find_or_create_from_emails(
|
||||
all_emails,
|
||||
additional_values=additional_values
|
||||
)
|
||||
for original_email, partner in zip(all_emails, partners):
|
||||
if not partner:
|
||||
continue
|
||||
for res_id in email_to_res_ids[original_email]:
|
||||
render_results[res_id].setdefault('partner_ids', []).append(partner.id)
|
||||
|
||||
# update 'partner_to' rendered value to 'partner_ids'
|
||||
all_partner_to = {
|
||||
int(pid)
|
||||
for record_values in render_results.values()
|
||||
for pid in record_values.get('partner_to', '').split(',')
|
||||
if pid and pid.strip()
|
||||
}
|
||||
existing_pids = set()
|
||||
if all_partner_to:
|
||||
existing_pids = set(self.env['res.partner'].sudo().browse(list(all_partner_to)).exists().ids)
|
||||
for res_id, record_values in render_results.items():
|
||||
partner_to = record_values.pop('partner_to', '')
|
||||
if partner_to:
|
||||
tpl_partner_ids = set(int(pid) for pid in partner_to.split(',') if pid) & existing_pids
|
||||
record_values.setdefault('partner_ids', []).extend(tpl_partner_ids)
|
||||
|
||||
return render_results
|
||||
|
||||
def _generate_template_scheduled_date(self, res_ids, render_results=None):
|
||||
""" Render scheduled date based on template 'self'. Specific parsing is
|
||||
done to ensure value matches ORM expected value: UTC but without
|
||||
timezone set in value.
|
||||
|
||||
:param list res_ids: list of record IDs on which template is rendered;
|
||||
:param dict render_results: res_ids-based dictionary of render values.
|
||||
For each res_id, a dict of values based on render_fields is given;
|
||||
|
||||
:return: updated (or new) render_results;
|
||||
"""
|
||||
self.ensure_one()
|
||||
if render_results is None:
|
||||
render_results = {}
|
||||
|
||||
scheduled_dates = self._render_field('scheduled_date', res_ids)
|
||||
for res_id in res_ids:
|
||||
scheduled_date = self._process_scheduled_date(scheduled_dates.get(res_id))
|
||||
render_results.setdefault(res_id, {})['scheduled_date'] = scheduled_date
|
||||
|
||||
return render_results
|
||||
|
||||
def _generate_template_static_values(self, res_ids, render_fields, render_results=None):
|
||||
""" Return values based on template 'self'. Those are not rendered nor
|
||||
dynamic, just static values used for configuration of emails.
|
||||
|
||||
:param list res_ids: list of record IDs on which template is rendered;
|
||||
:param list render_fields: list of fields to render, currently limited
|
||||
to a subset (i.e. auto_delete, mail_server_id, model, res_id);
|
||||
:param dict render_results: res_ids-based dictionary of render values.
|
||||
For each res_id, a dict of values based on render_fields is given;
|
||||
|
||||
:return: updated (or new) render_results;
|
||||
"""
|
||||
self.ensure_one()
|
||||
if render_results is None:
|
||||
render_results = {}
|
||||
|
||||
for res_id in res_ids:
|
||||
values = render_results.setdefault(res_id, {})
|
||||
|
||||
# technical settings
|
||||
if 'auto_delete' in render_fields:
|
||||
values['auto_delete'] = self.auto_delete
|
||||
if 'mail_server_id' in render_fields:
|
||||
values['mail_server_id'] = self.mail_server_id.id
|
||||
if 'model' in render_fields:
|
||||
values['model'] = self.model
|
||||
if 'res_id' in render_fields:
|
||||
values['res_id'] = res_id or False
|
||||
|
||||
return render_results
|
||||
|
||||
def _generate_template(self, res_ids, render_fields,
|
||||
find_or_create_partners=False):
|
||||
""" Render values from template 'self' on records given by 'res_ids'.
|
||||
Those values are generally used to create a mail.mail or a mail.message.
|
||||
Model of records is the one defined on template.
|
||||
|
||||
:param list res_ids: list of record IDs on which template is rendered;
|
||||
:param list render_fields: list of fields to render on template;
|
||||
:param boolean find_or_create_partners: transform emails into partners
|
||||
(see ``_generate_template_recipients``);
|
||||
|
||||
:returns: a dict of (res_ids, values) where values contains all rendered
|
||||
fields asked in ``render_fields``. Asking for attachments adds an
|
||||
'attachments' key using the format [(report_name, data)] where data
|
||||
is base64 encoded. Asking for recipients adds a 'partner_ids' key.
|
||||
Note that 2many fields contain a list of IDs, not commands.
|
||||
"""
|
||||
self.ensure_one()
|
||||
render_fields_set = set(render_fields)
|
||||
fields_specific = {
|
||||
'attachment_ids', # attachments
|
||||
'email_cc', # recipients
|
||||
'email_to', # recipients
|
||||
'partner_to', # recipients
|
||||
'report_template_ids', # attachments
|
||||
'scheduled_date', # specific
|
||||
# not rendered (static)
|
||||
'auto_delete',
|
||||
'mail_server_id',
|
||||
'model',
|
||||
'res_id',
|
||||
}
|
||||
|
||||
render_results = {}
|
||||
for _lang, (template, template_res_ids) in self._classify_per_lang(res_ids).items():
|
||||
# render fields not rendered by sub methods
|
||||
fields_torender = {
|
||||
field for field in render_fields_set
|
||||
if field not in fields_specific
|
||||
}
|
||||
for field in fields_torender:
|
||||
generated_field_values = template._render_field(
|
||||
field, template_res_ids
|
||||
)
|
||||
for res_id, field_value in generated_field_values.items():
|
||||
results.setdefault(res_id, dict())[field] = field_value
|
||||
# compute recipients
|
||||
if any(field in render_fields for field in ['email_to', 'partner_to', 'email_cc']):
|
||||
results = template.generate_recipients(results, template_res_ids)
|
||||
# update values for all res_ids
|
||||
for res_id in template_res_ids:
|
||||
values = results[res_id]
|
||||
if values.get('body_html'):
|
||||
values['body'] = tools.html_sanitize(values['body_html'])
|
||||
# if asked in fields to return, parse generated date into tz agnostic UTC as expected by ORM
|
||||
scheduled_date = values.pop('scheduled_date', None)
|
||||
if 'scheduled_date' in render_fields and scheduled_date:
|
||||
parsed_datetime = self.env['mail.mail']._parse_scheduled_datetime(scheduled_date)
|
||||
values['scheduled_date'] = parsed_datetime.replace(tzinfo=None) if parsed_datetime else False
|
||||
render_results.setdefault(res_id, {})[field] = field_value
|
||||
|
||||
# technical settings
|
||||
values.update(
|
||||
mail_server_id=template.mail_server_id.id or False,
|
||||
auto_delete=template.auto_delete,
|
||||
model=template.model,
|
||||
res_id=res_id or False,
|
||||
attachment_ids=[attach.id for attach in template.attachment_ids],
|
||||
# render recipients
|
||||
if render_fields_set & {'email_cc', 'email_to', 'partner_to'}:
|
||||
template._generate_template_recipients(
|
||||
template_res_ids, render_fields_set,
|
||||
render_results=render_results,
|
||||
find_or_create_partners=find_or_create_partners
|
||||
)
|
||||
|
||||
# Add report in attachments: generate once for all template_res_ids
|
||||
if template.report_template:
|
||||
for res_id in template_res_ids:
|
||||
attachments = []
|
||||
report_name = template._render_field('report_name', [res_id])[res_id]
|
||||
report = template.report_template
|
||||
report_service = report.report_name
|
||||
# render scheduled_date
|
||||
if 'scheduled_date' in render_fields_set:
|
||||
template._generate_template_scheduled_date(
|
||||
template_res_ids,
|
||||
render_results=render_results
|
||||
)
|
||||
|
||||
if report.report_type in ['qweb-html', 'qweb-pdf']:
|
||||
result, report_format = self.env['ir.actions.report']._render_qweb_pdf(report, [res_id])
|
||||
else:
|
||||
res = self.env['ir.actions.report']._render(report, [res_id])
|
||||
if not res:
|
||||
raise UserError(_('Unsupported report type %s found.', report.report_type))
|
||||
result, report_format = res
|
||||
# add values static for all res_ids
|
||||
template._generate_template_static_values(
|
||||
template_res_ids,
|
||||
render_fields_set,
|
||||
render_results=render_results
|
||||
)
|
||||
|
||||
# TODO in trunk, change return format to binary to match message_post expected format
|
||||
result = base64.b64encode(result)
|
||||
if not report_name:
|
||||
report_name = 'report.' + report_service
|
||||
ext = "." + report_format
|
||||
if not report_name.endswith(ext):
|
||||
report_name += ext
|
||||
attachments.append((report_name, result))
|
||||
results[res_id]['attachments'] = attachments
|
||||
# generate attachments if requested
|
||||
if render_fields_set & {'attachment_ids', 'report_template_ids'}:
|
||||
template._generate_template_attachments(
|
||||
template_res_ids,
|
||||
render_fields_set,
|
||||
render_results=render_results
|
||||
)
|
||||
|
||||
# hook for attachments-specific computation, used currently only for accounting
|
||||
if 'attachments' in render_fields or 'attachment_ids' in render_fields and hasattr(self.env[self.model], '_process_attachments_for_template_post'):
|
||||
records_attachments = self.env[self.model].browse(template_res_ids)._process_attachments_for_template_post(template)
|
||||
for res_id, additional_attachments in records_attachments.items():
|
||||
if not additional_attachments:
|
||||
continue
|
||||
results[res_id]['attachment_ids'] += additional_attachments.get('attachment_ids', [])
|
||||
results[res_id]['attachments'] += additional_attachments.get('attachments', [])
|
||||
|
||||
return multi_mode and results or results[res_ids[0]]
|
||||
return render_results
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# EMAIL
|
||||
@@ -342,19 +535,24 @@ class MailTemplate(models.Model):
|
||||
Attachment = self.env['ir.attachment'] # TDE FIXME: should remove default_type from context
|
||||
|
||||
# create a mail_mail based on values, without attachments
|
||||
values = self.generate_email(
|
||||
res_id,
|
||||
('auto_delete',
|
||||
values = self._generate_template(
|
||||
[res_id],
|
||||
('attachment_ids',
|
||||
'auto_delete',
|
||||
'body_html',
|
||||
'email_cc',
|
||||
'email_from',
|
||||
'email_to',
|
||||
'mail_server_id',
|
||||
'model',
|
||||
'partner_to',
|
||||
'reply_to',
|
||||
'report_template_ids',
|
||||
'res_id',
|
||||
'scheduled_date',
|
||||
'subject',
|
||||
)
|
||||
)
|
||||
)[res_id]
|
||||
values['recipient_ids'] = [Command.link(pid) for pid in values.get('partner_ids', list())]
|
||||
values['attachment_ids'] = [Command.link(aid) for aid in values.get('attachment_ids', list())]
|
||||
values.update(email_values or {})
|
||||
|
||||
+533
-180
@@ -635,7 +635,7 @@ class MailThread(models.AbstractModel):
|
||||
:param dict changes: mapping {record_id: (changed_field_names, tracking_value_ids)}
|
||||
containing existing records only
|
||||
"""
|
||||
if not changes:
|
||||
if not self or not changes:
|
||||
return True
|
||||
# Clean the context to get rid of residual default_* keys
|
||||
# that could cause issues afterward during the mail.message
|
||||
@@ -644,15 +644,18 @@ class MailThread(models.AbstractModel):
|
||||
# its creation, but could refer to wrong parent message id,
|
||||
# leading to a traceback in case the related message_id
|
||||
# doesn't exist
|
||||
self = self.with_context(clean_context(self._context))
|
||||
cleaned_self = self.with_context(clean_context(self._context))._fallback_lang()
|
||||
templates = self._track_template(changes)
|
||||
default_composition_mode = 'mass_mail' if len(self) != 1 else 'comment'
|
||||
for _field_name, (template, post_kwargs) in templates.items():
|
||||
if not template:
|
||||
continue
|
||||
if isinstance(template, str):
|
||||
self._fallback_lang().message_post_with_view(template, **post_kwargs)
|
||||
|
||||
composition_mode = post_kwargs.pop('composition_mode', default_composition_mode)
|
||||
if composition_mode == 'mass_mail':
|
||||
cleaned_self.message_mail_with_source(template, **post_kwargs)
|
||||
else:
|
||||
self._fallback_lang().message_post_with_template(template.id, **post_kwargs)
|
||||
cleaned_self.message_post_with_source(template, **post_kwargs)
|
||||
return True
|
||||
|
||||
def _track_template(self, changes):
|
||||
@@ -1813,6 +1816,8 @@ class MailThread(models.AbstractModel):
|
||||
:param int author_id: optional ID of partner record being the author. See
|
||||
``_message_compute_author`` that uses it to make email_from / author_id coherent;
|
||||
:param int parent_id: handle thread formation
|
||||
:param str subtype_xmlid: optional xml id of a mail.message.subtype to
|
||||
fetch, will force value of subtype_id;
|
||||
:param int subtype_id: subtype_id of the message, used mainly for followers
|
||||
notification mechanism;
|
||||
:param list(int) partner_ids: partner_ids to notify in addition to partners
|
||||
@@ -1821,36 +1826,61 @@ class MailThread(models.AbstractModel):
|
||||
tuples in the form ``(name,content)`` or ``(name,content, info)`` where content
|
||||
is NOT base64 encoded;
|
||||
:param list attachment_ids: list of existing attachments to link to this message
|
||||
-Should only be set by chatter
|
||||
-Attachment object attached to mail.compose.message(0) will be attached
|
||||
to the related document.
|
||||
Should not be a list of commands. Attachment records attached to mail
|
||||
composer will be attached to the related document.
|
||||
|
||||
Extra keyword arguments will be used either
|
||||
* as default column values for the new mail.message record if they match
|
||||
mail.message fields;
|
||||
* propagated to notification methods;
|
||||
* propagated to notification methods if not;
|
||||
|
||||
:return record: newly create mail.message
|
||||
"""
|
||||
self.ensure_one() # should always be posted on a record, use message_notify if no record
|
||||
# split message additional values from notify additional values
|
||||
msg_kwargs = dict((key, val) for key, val in kwargs.items() if key in self.env['mail.message']._fields)
|
||||
notif_kwargs = dict((key, val) for key, val in kwargs.items() if key not in msg_kwargs)
|
||||
|
||||
# preliminary value safety check
|
||||
partner_ids = set(partner_ids or [])
|
||||
if self._name == 'mail.thread' or not self.id or message_type == 'user_notification':
|
||||
raise ValueError(_('Posting a message should be done on a business document. Use message_notify to send a notification to an user.'))
|
||||
if 'channel_ids' in kwargs:
|
||||
raise ValueError(_("Posting a message with channels as listeners is not supported since Odoo 14.3+. Please update code accordingly."))
|
||||
if 'model' in msg_kwargs or 'res_id' in msg_kwargs:
|
||||
raise ValueError(_("message_post does not support model and res_id parameters anymore. Please call message_post on record."))
|
||||
if 'subtype' in kwargs:
|
||||
raise ValueError(_("message_post does not support subtype parameter anymore. Please give a valid subtype_id or subtype_xmlid value instead."))
|
||||
if any(not isinstance(pc_id, int) for pc_id in partner_ids):
|
||||
raise ValueError(_('message_post partner_ids and must be integer list, not commands.'))
|
||||
self._raise_for_invalid_parameters(
|
||||
set(kwargs.keys()),
|
||||
forbidden_names={'model', 'res_id', 'subtype'}
|
||||
)
|
||||
if self._name == 'mail.thread' or not self.id:
|
||||
raise ValueError(_("Posting a message should be done on a business document. Use message_notify to send a notification to an user."))
|
||||
if message_type == 'user_notification':
|
||||
raise ValueError(_("Use message_notify to send a notification to an user."))
|
||||
if attachments:
|
||||
# attachments should be a list (or tuples) of 3-elements list (or tuple)
|
||||
format_error = not tools.is_list_of(attachments, list) and not tools.is_list_of(attachments, tuple)
|
||||
if not format_error:
|
||||
format_error = not all(len(attachment) in {2, 3} for attachment in attachments)
|
||||
if format_error:
|
||||
raise ValueError(
|
||||
_('Posting a message should receive attachments as a list of list or tuples (received %(aids)s)',
|
||||
aids=repr(attachment_ids),
|
||||
)
|
||||
)
|
||||
if attachment_ids and not tools.is_list_of(attachment_ids, int):
|
||||
raise ValueError(
|
||||
_('Posting a message should receive attachments records as a list of IDs (received %(aids)s)',
|
||||
aids=repr(attachment_ids),
|
||||
)
|
||||
)
|
||||
attachment_ids = list(attachment_ids or [])
|
||||
if partner_ids and not tools.is_list_of(partner_ids, int):
|
||||
raise ValueError(
|
||||
_('Posting a message should receive partners as a list of IDs (received %(pids)s)',
|
||||
pids=repr(partner_ids),
|
||||
)
|
||||
)
|
||||
partner_ids = list(partner_ids or [])
|
||||
|
||||
self = self._fallback_lang() # add lang to context immediately since it will be useful in various flows latter.
|
||||
# split message additional values from notify additional values
|
||||
msg_kwargs = {key: val for key, val in kwargs.items()
|
||||
if key in self.env['mail.message']._fields}
|
||||
notif_kwargs = {key: val for key, val in kwargs.items()
|
||||
if key not in msg_kwargs}
|
||||
|
||||
# Add lang to context immediately since it will be useful in various flows later
|
||||
self = self._fallback_lang()
|
||||
|
||||
# Find the message's author
|
||||
if self.env.user._is_public() and 'guest' in self.env.context:
|
||||
@@ -1884,23 +1914,18 @@ class MailThread(models.AbstractModel):
|
||||
'res_id': self.id,
|
||||
# content
|
||||
'body': body,
|
||||
'subject': subject or False,
|
||||
'message_type': message_type,
|
||||
'parent_id': self._message_compute_parent_id(parent_id),
|
||||
'subject': subject or False,
|
||||
'subtype_id': subtype_id,
|
||||
# recipients
|
||||
'partner_ids': partner_ids,
|
||||
})
|
||||
|
||||
attachments = attachments or []
|
||||
attachment_ids = attachment_ids or []
|
||||
attachement_values = self._process_attachments_for_post(attachments, attachment_ids, msg_values)
|
||||
msg_values.update(attachement_values) # attachement_ids, [body]
|
||||
|
||||
new_message = self._message_create(msg_values)
|
||||
|
||||
# Set main attachment field if necessary
|
||||
self._message_set_main_attachment_id(msg_values['attachment_ids'])
|
||||
msg_values.update(
|
||||
self._process_attachments_for_post(attachments, attachment_ids, msg_values)
|
||||
) # attachement_ids, body
|
||||
new_message = self._message_create([msg_values])
|
||||
|
||||
if msg_values['author_id'] and msg_values['message_type'] != 'notification' and not self._context.get('mail_create_nosubscribe'):
|
||||
if self.env['res.partner'].browse(msg_values['author_id']).active: # we dont want to add odoobot/inactive as a follower
|
||||
@@ -1910,19 +1935,15 @@ class MailThread(models.AbstractModel):
|
||||
self._notify_thread(new_message, msg_values, **notif_kwargs)
|
||||
return new_message
|
||||
|
||||
def _message_set_main_attachment_id(self, attachment_ids): # todo move this out of mail.thread
|
||||
if not self._abstract and attachment_ids and not self.message_main_attachment_id:
|
||||
all_attachments = self.env['ir.attachment'].browse([attachment_tuple[1] for attachment_tuple in attachment_ids])
|
||||
prioritary_attachments = all_attachments.filtered(lambda x: x.mimetype.endswith('pdf')) \
|
||||
or all_attachments.filtered(lambda x: x.mimetype.startswith('image')) \
|
||||
or all_attachments
|
||||
self.sudo().with_context(tracking_disable=True).write({'message_main_attachment_id': prioritary_attachments[0].id})
|
||||
|
||||
def _message_post_after_hook(self, message, msg_vals):
|
||||
def _message_post_after_hook(self, message, msg_values):
|
||||
""" Hook to add custom behavior after having posted the message. Both
|
||||
message and computed value are given, to try to lessen query count by
|
||||
using already-computed values instead of having to rebrowse things. """
|
||||
return
|
||||
# Set main attachment field if necessary
|
||||
self._message_set_main_attachment_id([
|
||||
attachment_command[1]
|
||||
for attachment_command in (msg_values['attachment_ids'] or [])
|
||||
])
|
||||
|
||||
def _message_mail_after_hook(self, mails):
|
||||
""" Hook to add custom behavior after having sent an mass mailing.
|
||||
@@ -1930,6 +1951,16 @@ class MailThread(models.AbstractModel):
|
||||
:param mail.mail mails: mail.mail records about to be sent"""
|
||||
return
|
||||
|
||||
def _message_set_main_attachment_id(self, attachment_ids):
|
||||
if self._name == 'mail.thread':
|
||||
return
|
||||
if attachment_ids and not self.message_main_attachment_id:
|
||||
all_attachments = self.env['ir.attachment'].browse(attachment_ids)
|
||||
prioritary_attachments = all_attachments.filtered(lambda x: x.mimetype.endswith('pdf')) \
|
||||
or all_attachments.filtered(lambda x: x.mimetype.startswith('image')) \
|
||||
or all_attachments
|
||||
self.sudo().with_context(tracking_disable=True).message_main_attachment_id = prioritary_attachments[0].id
|
||||
|
||||
def _process_attachments_for_post(self, attachments, attachment_ids, message_values):
|
||||
""" Preprocess attachments for MailTread.message_post() or MailMail.create().
|
||||
Purpose is to
|
||||
@@ -2071,95 +2102,257 @@ class MailThread(models.AbstractModel):
|
||||
# MESSAGE POST API / WRAPPERS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _message_compose_with_view(self, views_or_xmlid, message_log=False, **kwargs):
|
||||
""" Helper method to send a mail / post a message / log a note using
|
||||
a view_id to render using the ir.qweb engine. This method is stand
|
||||
alone, because there is nothing in template and composer that allows
|
||||
to handle views in batch. This method should probably disappear when
|
||||
templates handle ir ui views. """
|
||||
values = kwargs.pop('values', None) or dict()
|
||||
try:
|
||||
from odoo.addons.http_routing.models.ir_http import slug
|
||||
values['slug'] = slug
|
||||
except ImportError:
|
||||
values['slug'] = lambda self: self.id
|
||||
view_ref = views_or_xmlid.id if isinstance(views_or_xmlid, models.BaseModel) else views_or_xmlid
|
||||
def message_mail_with_source(self, source_ref, render_values=None,
|
||||
message_type='notification',
|
||||
auto_commit=False,
|
||||
**kwargs):
|
||||
""" Send a mass mail on self, using an external source to render part
|
||||
of the content. It can be either a 'mail.template', either a view used
|
||||
to render the body using QWeb.
|
||||
|
||||
messages_as_sudo = self.env['mail.message']
|
||||
for record in self:
|
||||
values['object'] = record
|
||||
rendered_template = self.env['ir.qweb']._render(view_ref, values, minimal_qcontext=True, raise_if_not_found=False)
|
||||
if not rendered_template:
|
||||
continue
|
||||
if message_log:
|
||||
messages_as_sudo += record._message_log(body=rendered_template, **kwargs)
|
||||
else:
|
||||
kwargs['body'] = rendered_template
|
||||
# a bit complicated to handle, to be improved soon in master
|
||||
_mails_as_sudo, _messages_as_sudo = record.message_post_with_template(False, **kwargs)
|
||||
if _messages_as_sudo:
|
||||
messages_as_sudo += _messages_as_sudo
|
||||
SPOILER: this method currently calls a composer in a loop when using
|
||||
a view even if it is suboptimal. This is due to current composer
|
||||
implementation.. This will be cleaned soon to optimize mass mailing
|
||||
through mail.thread and lessen usage of composer itself.
|
||||
|
||||
return messages_as_sudo
|
||||
Default values
|
||||
* subtype_id: will be False, forced by composer in mass mode;
|
||||
|
||||
def message_post_with_view(self, views_or_xmlid, **kwargs):
|
||||
""" Helper method to send a mail / post a message using a view_id """
|
||||
return self._message_compose_with_view(views_or_xmlid, **kwargs)
|
||||
:param record/str source_ref: reference to a source for rendering.
|
||||
It can be one of
|
||||
* a MailTemplate record. It will be used to render the various
|
||||
message values (body, subject, recipients, ...). It should behave
|
||||
like using the mail composer with a template;
|
||||
* an IrUIView record. It will be used to render the content
|
||||
(body). Other fields are left to the caller and/or default values
|
||||
computation;
|
||||
* an XmlID of a MailTemplate or of an IrUiView: see above;
|
||||
:param dict render_values: additional rendering values for qweb context;
|
||||
|
||||
def message_post_with_template(self, template_id, email_layout_xmlid=None, auto_commit=False, **kwargs):
|
||||
""" Helper method to send a mail with a template
|
||||
:param template_id : the id of the template to render to create the body of the message
|
||||
:param **kwargs : parameter to create a mail.compose.message woaerd (which inherit from mail.message)
|
||||
:param str message_type: one of 'notification' or 'comment';
|
||||
:param bool auto_commit: auto commit after each batch of emails sent
|
||||
(see ``MailComposer._action_send_mail()``);
|
||||
:param dict kwargs: additional values given to the 'mail.compose.message'
|
||||
creation;
|
||||
|
||||
:return: created mail.mail records, as sudo
|
||||
"""
|
||||
# Get composition mode, or force it according to the number of record in self
|
||||
if not kwargs.get('composition_mode'):
|
||||
kwargs['composition_mode'] = 'comment' if len(self.ids) == 1 else 'mass_mail'
|
||||
if not kwargs.get('message_type'):
|
||||
kwargs['message_type'] = 'notification'
|
||||
res_id = kwargs.get('res_id', self.ids and self.ids[0] or 0)
|
||||
res_ids = kwargs.get('res_id') and [kwargs['res_id']] or self.ids
|
||||
template, view = self._get_source_from_ref(source_ref)
|
||||
|
||||
# Create the composer
|
||||
composer = self.env['mail.compose.message'].with_context(
|
||||
active_id=res_id,
|
||||
active_ids=res_ids,
|
||||
active_model=kwargs.get('model', self._name),
|
||||
default_composition_mode=kwargs['composition_mode'],
|
||||
default_email_layout_xmlid=email_layout_xmlid,
|
||||
default_model=kwargs.get('model', self._name),
|
||||
default_res_id=res_id,
|
||||
default_template_id=template_id,
|
||||
).create(kwargs)
|
||||
# Simulate the onchange (like trigger in form the view) only
|
||||
# when having a template in single-email mode
|
||||
if template_id:
|
||||
update_values = composer._onchange_template_id(template_id, kwargs['composition_mode'], self._name, res_id)['value']
|
||||
composer.write(update_values)
|
||||
return composer._action_send_mail(auto_commit=auto_commit)
|
||||
# preliminary value safety check
|
||||
self._raise_for_invalid_parameters(
|
||||
set(kwargs.keys()),
|
||||
forbidden_names={'body', 'composition_mode', 'model', 'res_id', 'values'}
|
||||
)
|
||||
|
||||
# with a view, render bodies in batch (template is managed by composer)
|
||||
bodies = self.env['mail.render.mixin']._render_template_qweb_view(
|
||||
view,
|
||||
self._name,
|
||||
self.ids,
|
||||
add_context=render_values,
|
||||
) if view else {}
|
||||
|
||||
# Prepare composer values for creation
|
||||
composer_values = {
|
||||
'composition_mode': 'mass_mail',
|
||||
'message_type': message_type,
|
||||
# subtype is not really used in mass mail mode as it is used mainly
|
||||
# when posting, but keep it in case it is used in post send
|
||||
'subtype_id': kwargs.pop('subtype_id', False) or self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
**kwargs,
|
||||
}
|
||||
composer_ctx = {
|
||||
'default_composition_mode': 'mass_mail',
|
||||
'default_model': self._name,
|
||||
'default_template_id': template.id if template else False,
|
||||
}
|
||||
|
||||
mails_su = self.env['mail.mail'].sudo()
|
||||
for subset in [self] if template else self:
|
||||
composer_ctx['default_res_ids'] = subset.ids
|
||||
if not template:
|
||||
composer_values['body'] = bodies[subset.id]
|
||||
|
||||
composer = self.env['mail.compose.message'].with_context(
|
||||
**composer_ctx
|
||||
).create(composer_values)
|
||||
# Simulate the onchange (like trigger in form the view) only
|
||||
# when having a template in single-email mode
|
||||
if template:
|
||||
update_values = composer._onchange_template_id(
|
||||
template.id,
|
||||
'mass_mail',
|
||||
self._name,
|
||||
subset.ids,
|
||||
)['value']
|
||||
composer.write(update_values)
|
||||
|
||||
mails_as_sudo, _messages_as_sudo = composer._action_send_mail(auto_commit=auto_commit)
|
||||
mails_su += mails_as_sudo
|
||||
return mails_su
|
||||
|
||||
def message_post_with_source(self, source_ref, render_values=None,
|
||||
message_type='notification',
|
||||
subtype_xmlid=False, subtype_id=False,
|
||||
**kwargs):
|
||||
""" Post a message on each record of self, using a view to render the
|
||||
body using QWeb.
|
||||
|
||||
Default values
|
||||
* subtype_id: if not given, fallback on ``note`` to be consistent
|
||||
with what message_post does;
|
||||
|
||||
:param record/str source_ref: reference to a source for rendering.
|
||||
It can be one of
|
||||
* a MailTemplate record. It will be used to render the various
|
||||
message values (body, subject, recipients, ...). It should behave
|
||||
like using the mail composer with a template;
|
||||
* an IrUIView record. It will be used to render the content
|
||||
(body). Other fields are left to the caller and/or default values
|
||||
computation;
|
||||
* an XmlID of a MailTemplate or of an IrUiView: see above
|
||||
:param dict render_values: additional rendering values for qweb context;
|
||||
|
||||
:param str message_type: one of 'notification' or 'comment';
|
||||
:param str subtype_xmlid: optional xml id of a mail.message.subtype to
|
||||
fetch, will force value of subtype_id;
|
||||
:param int subtype_id: subtype_id of the message, used mainly for followers
|
||||
notification mechanism;
|
||||
:param dict kwargs: additional values given to the 'mail.compose.message'
|
||||
creation;
|
||||
|
||||
:return: posted mail.message records
|
||||
"""
|
||||
template, view = self._get_source_from_ref(source_ref)
|
||||
|
||||
# preliminary value safety check
|
||||
self._raise_for_invalid_parameters(
|
||||
set(kwargs.keys()),
|
||||
forbidden_names={'body', 'composition_mode', 'model', 'res_id', 'values'}
|
||||
)
|
||||
|
||||
# with a view, render bodies in batch (template is managed by composer)
|
||||
bodies = self.env['mail.render.mixin']._render_template_qweb_view(
|
||||
view,
|
||||
self._name,
|
||||
self.ids,
|
||||
add_context=render_values,
|
||||
) if view else {}
|
||||
|
||||
# Prepare composer values for creation
|
||||
if subtype_xmlid:
|
||||
subtype_id = self.env['ir.model.data']._xmlid_to_res_id(subtype_xmlid)
|
||||
if not subtype_id:
|
||||
subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
|
||||
|
||||
messages = self.env['mail.message']
|
||||
for record in self:
|
||||
if template:
|
||||
composer = self.env['mail.compose.message'].with_context(
|
||||
default_composition_mode='comment',
|
||||
default_model=self._name,
|
||||
default_res_ids=record.ids,
|
||||
default_template_id=template.id,
|
||||
).create({
|
||||
'message_type': message_type,
|
||||
'subtype_id': subtype_id,
|
||||
**kwargs,
|
||||
})
|
||||
|
||||
# Simulate the onchange (like trigger in form the view) to do the
|
||||
# rendering in mono-record mode
|
||||
update_values = composer._onchange_template_id(
|
||||
template.id,
|
||||
'comment',
|
||||
self._name,
|
||||
record.ids,
|
||||
)['value']
|
||||
composer.write(update_values)
|
||||
_mails_as_sudo, messages_as_sudo = composer._action_send_mail()
|
||||
messages += messages_as_sudo
|
||||
else:
|
||||
messages += record.message_post(
|
||||
body=bodies[record.id],
|
||||
message_type=message_type,
|
||||
subtype_id=subtype_id,
|
||||
**kwargs
|
||||
)
|
||||
return messages
|
||||
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_notify(self, *,
|
||||
partner_ids=False, parent_id=False, model=False, res_id=False,
|
||||
author_id=None, email_from=None, body='', subject=False, **kwargs):
|
||||
""" Shortcut allowing to notify partners of messages that shouldn't be
|
||||
displayed on a document. It pushes notifications on inbox or by email depending
|
||||
on the user configuration, like other notifications. """
|
||||
body='', subject=False,
|
||||
author_id=None, email_from=None,
|
||||
model=False, res_id=False,
|
||||
subtype_xmlid=None, subtype_id=False, partner_ids=False,
|
||||
**kwargs):
|
||||
""" Shortcut allowing to notify partners of messages that should not be
|
||||
displayed on a document. It pushes notifications on inbox or by email
|
||||
depending on the user configuration, like other notifications.
|
||||
|
||||
Default values
|
||||
* subtype_id: if not given, fallback on ``note`` to be consistent
|
||||
with what message_post does;
|
||||
|
||||
:param str body: body of the message, usually raw HTML that will
|
||||
be sanitized
|
||||
:param str subject: subject of the message
|
||||
:param int author_id: optional ID of partner record being the author. See
|
||||
``_message_compute_author`` that uses it to make email_from / author_id coherent;
|
||||
:param str email_from: from address of the author. See ``_message_compute_author``
|
||||
that uses it to make email_from / author_id coherent;
|
||||
:param str model: when invoked on MailThread directly, this method
|
||||
allows to push a notification on a given record (allows to notify
|
||||
on not thread-enabled records);
|
||||
:param int res_id: defines the record in combination with model;
|
||||
:param str subtype_xmlid: optional xml id of a mail.message.subtype to
|
||||
fetch, will force value of subtype_id;
|
||||
:param int subtype_id: subtype_id of the message, used mainly for followers
|
||||
notification mechanism;
|
||||
:param list(int) partner_ids: partner_ids to notify in addition to partners
|
||||
computed based on subtype / followers matching;
|
||||
|
||||
Extra keyword arguments will be used either
|
||||
* as default column values for the new mail.message record if they match
|
||||
mail.message fields;
|
||||
* propagated to notification methods if not;
|
||||
|
||||
:return: posted mail.message records
|
||||
"""
|
||||
if self:
|
||||
self.ensure_one()
|
||||
# split message additional values from notify additional values
|
||||
msg_kwargs = dict((key, val) for key, val in kwargs.items() if key in self.env['mail.message']._fields)
|
||||
notif_kwargs = dict((key, val) for key, val in kwargs.items() if key not in msg_kwargs)
|
||||
|
||||
author_id, email_from = self._message_compute_author(author_id, email_from, raise_on_email=True)
|
||||
|
||||
if not partner_ids:
|
||||
_logger.warning('Message notify called without recipient_ids, skipping')
|
||||
return self.env['mail.message']
|
||||
|
||||
# preliminary value safety check
|
||||
self._raise_for_invalid_parameters(
|
||||
set(kwargs.keys()),
|
||||
forbidden_names={'message_id', 'message_type', 'parent_id'}
|
||||
)
|
||||
if not tools.is_list_of(partner_ids, int):
|
||||
raise ValueError(
|
||||
_('Notification should receive partners given as a list of IDs (received %(pids)s)',
|
||||
pids=repr(partner_ids),
|
||||
)
|
||||
)
|
||||
|
||||
# split message additional values from notify additional values
|
||||
msg_kwargs = {key: val for key, val in kwargs.items() if key in self.env['mail.message']._fields}
|
||||
notif_kwargs = {key: val for key, val in kwargs.items() if key not in msg_kwargs}
|
||||
|
||||
author_id, email_from = self._message_compute_author(author_id, email_from, raise_on_email=True)
|
||||
|
||||
# allow to link a notification to a document that does not inherit from
|
||||
# MailThread by supporting model / res_id
|
||||
if not (model and res_id): # both value should be set or none should be set (record)
|
||||
model = False
|
||||
res_id = False
|
||||
# MailThread by supporting model / res_id, but then both value should be set
|
||||
if not model or not res_id:
|
||||
model, res_id = False, False
|
||||
|
||||
if subtype_xmlid:
|
||||
subtype_id = self.env['ir.model.data']._xmlid_to_res_id(subtype_xmlid)
|
||||
if not subtype_id:
|
||||
subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
|
||||
|
||||
msg_values = {
|
||||
# author
|
||||
@@ -2173,68 +2366,91 @@ class MailThread(models.AbstractModel):
|
||||
'body': body,
|
||||
'is_internal': True,
|
||||
'message_type': 'user_notification',
|
||||
'parent_id': parent_id,
|
||||
'subject': subject,
|
||||
'subtype_id': subtype_id,
|
||||
# recipients
|
||||
'message_id': tools.generate_tracking_message_id('message-notify'),
|
||||
'partner_ids': partner_ids,
|
||||
# notification
|
||||
'email_add_signature': True,
|
||||
}
|
||||
msg_values.update(msg_kwargs)
|
||||
# add default-like values afterwards, to avoid useless queries
|
||||
if 'subtype_id' not in msg_values:
|
||||
msg_values['subtype_id'] = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
|
||||
if 'reply_to' not in msg_values:
|
||||
msg_values['reply_to'] = self._notify_get_reply_to(default=email_from)[self.id if self else False]
|
||||
if 'email_add_signature' not in msg_values:
|
||||
msg_values['email_add_signature'] = True
|
||||
|
||||
new_message = self._message_create(msg_values)
|
||||
self._notify_thread(new_message, msg_values, **notif_kwargs)
|
||||
new_message = self._message_create([msg_values])
|
||||
self._fallback_lang()._notify_thread(new_message, msg_values, **notif_kwargs)
|
||||
return new_message
|
||||
|
||||
def _message_log_with_view(self, views_or_xmlid, **kwargs):
|
||||
""" Helper method to log a note using a view_id without notifying followers. """
|
||||
return self._message_compose_with_view(views_or_xmlid, message_log=True, **kwargs)
|
||||
def _message_log_with_view(self, view_ref, render_values=None,
|
||||
message_type='notification', **kwargs):
|
||||
""" Log a message on each record of self, using a view to render the
|
||||
body using QWeb.
|
||||
|
||||
def _message_log(self, *, body='', author_id=None, email_from=None, subject=False, message_type='notification', **kwargs):
|
||||
""" Shortcut allowing to post note on a document. It does not perform
|
||||
any notification and pre-computes some values to have a short code
|
||||
:param str/int/record view_ref: source QWeb template. It should be an
|
||||
XmlID allowing to fetch an ``ir.ui.view``, or an ID of a view or
|
||||
an ``ir.ui.view`` record;
|
||||
:param dict render_values: additional rendering values for qweb context;
|
||||
:param str message_type: one of 'notification' or 'comment';
|
||||
:param kwargs: additional values propagated to ``_message_log``;
|
||||
|
||||
:return: posted mail.message records (as sudo)
|
||||
"""
|
||||
self._raise_for_invalid_parameters(
|
||||
set(kwargs.keys()),
|
||||
forbidden_names={'body', 'bodies'}
|
||||
)
|
||||
|
||||
# with a view, render bodies in batch (template is managed by composer)
|
||||
bodies = self.env['mail.render.mixin']._render_template_qweb_view(
|
||||
view_ref,
|
||||
self._name,
|
||||
self.ids,
|
||||
add_context=render_values,
|
||||
)
|
||||
|
||||
return self._message_log_batch(
|
||||
bodies=bodies,
|
||||
message_type=message_type,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def _message_log(self, *,
|
||||
body='', subject=False,
|
||||
author_id=None, email_from=None,
|
||||
message_type='notification',
|
||||
attachment_ids=False, tracking_value_ids=False):
|
||||
""" Shortcut allowing to post note on a document. See ``_message_log_batch``
|
||||
for more details. """
|
||||
self.ensure_one()
|
||||
|
||||
return self._message_log_batch(
|
||||
{self.id: body}, subject=subject,
|
||||
author_id=author_id, email_from=email_from,
|
||||
message_type=message_type,
|
||||
attachment_ids=attachment_ids, tracking_value_ids=tracking_value_ids
|
||||
)
|
||||
|
||||
def _message_log_batch(self, bodies, subject=False,
|
||||
author_id=None, email_from=None,
|
||||
message_type='notification',
|
||||
attachment_ids=False, tracking_value_ids=False):
|
||||
""" Shortcut allowing to post notes on a batch of documents. It does not
|
||||
perform any notification and pre-computes some values to have a short code
|
||||
as optimized as possible. This method is private as it does not check
|
||||
access rights and perform the message creation as sudo to speedup
|
||||
the log process. This method should be called within methods where
|
||||
access rights are already granted to avoid privilege escalation. """
|
||||
self.ensure_one()
|
||||
author_id, email_from = self._message_compute_author(author_id, email_from, raise_on_email=False)
|
||||
access rights are already granted to avoid privilege escalation.
|
||||
|
||||
msg_values = {
|
||||
# author
|
||||
'author_id': author_id,
|
||||
'email_from': email_from,
|
||||
# document
|
||||
'model': kwargs.get('model', self._name),
|
||||
'record_name': False,
|
||||
'res_id': self.ids[0] if self.ids else False,
|
||||
# content
|
||||
'body': body,
|
||||
'is_internal': True,
|
||||
'message_type': message_type,
|
||||
'subject': subject,
|
||||
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
# recipients
|
||||
'email_add_signature': False, # False as no notification -> no need to compute signature
|
||||
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
|
||||
'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False],
|
||||
}
|
||||
msg_values.update(kwargs)
|
||||
:param bodies: dict {record_id: body}
|
||||
|
||||
return self.sudo()._message_create(msg_values)
|
||||
|
||||
def _message_log_batch(self, bodies, author_id=None, email_from=None, subject=False, message_type='notification'):
|
||||
""" Shortcut allowing to post notes on a batch of documents. It achieve the
|
||||
same purpose as _message_log, done in batch to speedup quick note log.
|
||||
|
||||
:param bodies: dict {record_id: body}
|
||||
:return: created messages (as sudo)
|
||||
"""
|
||||
# protect against side-effect prone usage
|
||||
if len(self) > 1 and (attachment_ids or tracking_value_ids):
|
||||
raise ValueError(_('Batch log cannot support attachments or tracking values on more than 1 document'))
|
||||
|
||||
author_id, email_from = self._message_compute_author(author_id, email_from, raise_on_email=False)
|
||||
|
||||
base_message_values = {
|
||||
@@ -2245,15 +2461,18 @@ class MailThread(models.AbstractModel):
|
||||
'model': self._name,
|
||||
'record_name': False,
|
||||
# content
|
||||
'attachment_ids': attachment_ids,
|
||||
'message_type': message_type,
|
||||
'is_internal': True,
|
||||
'subject': subject,
|
||||
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
'tracking_value_ids': tracking_value_ids,
|
||||
# recipients
|
||||
'email_add_signature': False,
|
||||
'email_add_signature': False, # False as no notification -> no need to compute signature
|
||||
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
|
||||
'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False],
|
||||
}
|
||||
|
||||
values_list = [dict(base_message_values,
|
||||
res_id=record.id,
|
||||
body=bodies.get(record.id, ''))
|
||||
@@ -2321,17 +2540,24 @@ class MailThread(models.AbstractModel):
|
||||
return parent_id
|
||||
|
||||
def _message_compute_subject(self):
|
||||
"""Overridable method to get the default subject for a message related to this record.
|
||||
""" Get the default subject for a message posted in this record's
|
||||
discussion thread.
|
||||
|
||||
:return str:
|
||||
"""
|
||||
:return str: default subject """
|
||||
self.ensure_one()
|
||||
return self.name_get()[0][1]
|
||||
|
||||
def _message_create(self, values_list):
|
||||
if not isinstance(values_list, (list)):
|
||||
values_list = [values_list]
|
||||
""" Low-level helper to create mail.message records. It is mainly used
|
||||
to hide the cleanup of given values, for mail gateway or helpers."""
|
||||
create_values_list = []
|
||||
|
||||
# preliminary value safety check
|
||||
self._raise_for_invalid_parameters(
|
||||
{key for values in values_list for key in values.keys()},
|
||||
restricting_names=self._get_message_create_valid_field_names()
|
||||
)
|
||||
|
||||
for values in values_list:
|
||||
create_values = dict(values)
|
||||
# Avoid warnings about non-existing fields
|
||||
@@ -2346,6 +2572,131 @@ class MailThread(models.AbstractModel):
|
||||
clean_context(self.env.context)
|
||||
).create(create_values_list)
|
||||
|
||||
def _get_message_create_valid_field_names(self):
|
||||
""" Some fields should not be given when creating a mail.message from
|
||||
mail.thread main API methods (in addition to some API specific check).
|
||||
Those fields are generally used through UI or dedicated methods. We
|
||||
therefore give an allowed field names list. """
|
||||
return {
|
||||
'attachment_ids',
|
||||
'author_guest_id',
|
||||
'author_id',
|
||||
'body',
|
||||
'create_date', # anyway limited to admins
|
||||
'date',
|
||||
'email_add_signature',
|
||||
'email_from',
|
||||
'email_layout_xmlid',
|
||||
'is_internal',
|
||||
'mail_activity_type_id',
|
||||
'mail_server_id',
|
||||
'message_id',
|
||||
'message_type',
|
||||
'model',
|
||||
'parent_id',
|
||||
'partner_ids',
|
||||
'record_name',
|
||||
'reply_to',
|
||||
'reply_to_force_new',
|
||||
'res_id',
|
||||
'subject',
|
||||
'subtype_id',
|
||||
'tracking_value_ids',
|
||||
}
|
||||
|
||||
def _get_source_from_ref(self, source_ref):
|
||||
""" From a source_reference, return either a mail template, either
|
||||
an ir ui view.
|
||||
|
||||
:return tuple(template, view): one is a recordset (may be void if
|
||||
source_ref is a void recordset, or a singleton), the other one is
|
||||
False. Always only one is set, as source is either a template,
|
||||
either a view.
|
||||
"""
|
||||
template, view = False, False
|
||||
if isinstance(source_ref, models.BaseModel):
|
||||
if source_ref._name == 'mail.template':
|
||||
template = source_ref
|
||||
elif source_ref._name == 'ir.ui.view':
|
||||
view = source_ref
|
||||
else:
|
||||
raise ValueError(
|
||||
_('Invalid template or view source record %(svalue)s, is %(model)s instead',
|
||||
svalue=source_ref,
|
||||
model=source_ref._name,
|
||||
))
|
||||
if not template and not view:
|
||||
raise ValueError(
|
||||
_('Mailing or posting with a source should not be called with an empty %(source_type)s',
|
||||
source_type=_('template') if template is not False else _('view'))
|
||||
)
|
||||
elif isinstance(source_ref, str):
|
||||
try:
|
||||
res_model, res_id = self.env['ir.model.data']._xmlid_to_res_model_res_id(
|
||||
source_ref,
|
||||
raise_if_not_found=True
|
||||
)
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
_('Invalid template or view source Xml ID %(source_ref)s does not exist anymore',
|
||||
source_ref=source_ref)
|
||||
) from e
|
||||
if res_model == 'mail.template':
|
||||
template = self.env['mail.template'].browse(res_id)
|
||||
elif res_model == 'ir.ui.view':
|
||||
view = self.env['ir.ui.view'].browse(res_id)
|
||||
else:
|
||||
raise ValueError(
|
||||
_('Invalid template or view source reference %(svalue)s, is %(model)s instead',
|
||||
svalue=source_ref,
|
||||
model=res_model,
|
||||
))
|
||||
else:
|
||||
raise ValueError(
|
||||
_('Invalid template or view source %(svalue)s (type %(stype)s), should be a record or an XMLID',
|
||||
svalue=source_ref,
|
||||
stype=type(source_ref),
|
||||
))
|
||||
return template, view
|
||||
|
||||
def _get_notify_valid_parameters(self):
|
||||
""" Several parameters exist for notification methods as business
|
||||
flows often want to customize the standard notification experience.
|
||||
In order to ease coding kwargs are frequently used. This method
|
||||
acts like a filter, allowing to spot parameters that are not
|
||||
supported. """
|
||||
return {
|
||||
'force_email_company',
|
||||
'force_email_lang',
|
||||
'force_send',
|
||||
'mail_auto_delete',
|
||||
'model_description',
|
||||
'resend_existing',
|
||||
'scheduled_date',
|
||||
'send_after_commit',
|
||||
'skip_existing',
|
||||
'subtitles',
|
||||
}
|
||||
|
||||
def _raise_for_invalid_parameters(self, parameter_names, forbidden_names=None, restricting_names=None):
|
||||
""" Helper to warn about invalid parameters (or fields).
|
||||
|
||||
:param set parameter_names: a set of parameter names;
|
||||
:param set forbidden_names: set of parameter name that should not be
|
||||
present in parameter_names;
|
||||
:param set restricting_names: set of parameters restricting given
|
||||
parameter_names, parameters not belonging to this list are rejected;
|
||||
"""
|
||||
if forbidden_names:
|
||||
conflicting_names = parameter_names & forbidden_names
|
||||
elif restricting_names:
|
||||
conflicting_names = parameter_names - restricting_names
|
||||
if conflicting_names:
|
||||
raise ValueError(
|
||||
_('Those values are not supported when posting or notifying: %(param_names)s',
|
||||
param_names=', '.join(conflicting_names))
|
||||
)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# NOTIFICATION API
|
||||
# ------------------------------------------------------
|
||||
@@ -2418,6 +2769,10 @@ class MailThread(models.AbstractModel):
|
||||
"""
|
||||
# add lang to context immediately since it will be useful in various rendering later
|
||||
self = self._fallback_lang()
|
||||
self._raise_for_invalid_parameters(
|
||||
set(kwargs.keys()),
|
||||
restricting_names=self._get_notify_valid_parameters()
|
||||
)
|
||||
|
||||
msg_vals = msg_vals if msg_vals else {}
|
||||
recipients_data = self._notify_get_recipients(message, msg_vals, **kwargs)
|
||||
@@ -2690,11 +3045,12 @@ class MailThread(models.AbstractModel):
|
||||
lang = False
|
||||
if force_email_lang:
|
||||
lang = force_email_lang
|
||||
elif {'default_template_id', 'default_model', 'default_res_id'} <= self.env.context.keys():
|
||||
elif {'default_template_id', 'default_model', 'default_res_ids'} <= self.env.context.keys():
|
||||
# TDE FIXME: this whole brol should be cleaned !
|
||||
res_ids = self.env['mail.compose.message']._parse_res_ids(self.env.context['default_res_ids'])
|
||||
template = self.env['mail.template'].browse(self.env.context['default_template_id'])
|
||||
if template and template.lang:
|
||||
lang = template._render_lang([self.env.context['default_res_id']])[self.env.context['default_res_id']]
|
||||
if res_ids and template and template.lang:
|
||||
lang = template._render_lang(res_ids)[res_ids[0]]
|
||||
if not lang:
|
||||
lang = self.env.context.get('lang')
|
||||
|
||||
@@ -2748,17 +3104,14 @@ class MailThread(models.AbstractModel):
|
||||
to notify (mail_message_id and its fields), server, references, subject.
|
||||
"""
|
||||
mail_subject = message.subject
|
||||
if not message.subject:
|
||||
thread = self
|
||||
if not thread and message.model:
|
||||
thread = self.env[message.model].browse(message.res_id)
|
||||
if not thread:
|
||||
mail_subject = message.record_name
|
||||
else:
|
||||
mail_subject = thread._message_compute_subject()
|
||||
if not mail_subject and self:
|
||||
mail_subject = self._message_compute_subject()
|
||||
if not mail_subject:
|
||||
mail_subject = message.record_name
|
||||
if mail_subject:
|
||||
# replace new lines by spaces to conform to email headers requirements
|
||||
mail_subject = ' '.join(mail_subject.splitlines())
|
||||
|
||||
# Replace new lines by spaces to conform to email headers requirements
|
||||
mail_subject = ' '.join((mail_subject or '').splitlines())
|
||||
# compute references: set references to the parent and add current message just to
|
||||
# have a fallback in case replies mess with Messsage-Id in the In-Reply-To (e.g. amazon
|
||||
# SES SMTP may replace Message-Id and In-Reply-To refers an internal ID not stored in Odoo)
|
||||
|
||||
@@ -102,6 +102,97 @@ class Partner(models.Model):
|
||||
create_values['email'] = parsed_email
|
||||
return self.create(create_values)
|
||||
|
||||
@api.model
|
||||
def _find_or_create_from_emails(self, emails, additional_values=None):
|
||||
""" Based on a list of emails, find or create partners. Additional values
|
||||
can be given to newly created partners. If an email is not unique (e.g.
|
||||
multi-email input), only the first found email is considered.
|
||||
|
||||
If no valid email is found for a given item, the given value is used to
|
||||
find partners with same invalid email or create a new one with the wrong
|
||||
value. It allows updating it afterwards. Notably with notifications
|
||||
resend it is possible to update emails, if only a typo prevents from
|
||||
having a real email for example.
|
||||
|
||||
:param list emails: list of emails that may be formatted (each input
|
||||
will be parsed and normalized);
|
||||
:param dict additional_values: additional values given to create if
|
||||
the partner is not found. Typically used to propagate a company_id;
|
||||
|
||||
:return: res.partner records in a list, following order of emails. It
|
||||
is not a recordset, to keep Falsy values.
|
||||
"""
|
||||
partners, tocreate_vals_list = self.env['res.partner'], []
|
||||
name_emails = [self._parse_partner_name(email) for email in emails]
|
||||
|
||||
# find valid emails_normalized, filtering out false / void values, and search
|
||||
# for existing partners based on those emails
|
||||
emails_normalized = {email_normalized
|
||||
for _name, email_normalized in name_emails
|
||||
if email_normalized}
|
||||
# find partners for invalid (but not void) emails, aka either invalid email
|
||||
# either no email and a name that will be used as email
|
||||
names = {
|
||||
name.strip()
|
||||
for name, email_normalized in name_emails
|
||||
if not email_normalized and name.strip()
|
||||
}
|
||||
if emails_normalized or names:
|
||||
domains = []
|
||||
if emails_normalized:
|
||||
domains.append([('email_normalized', 'in', list(emails_normalized))])
|
||||
if names:
|
||||
domains.append([('email', 'in', list(names))])
|
||||
partners += self.search(expression.OR(domains))
|
||||
|
||||
# create partners for valid email without any existing partner. Keep
|
||||
# only first found occurrence of each normalized email, aka: ('Norbert',
|
||||
# 'norbert@gmail.com'), ('Norbert With Surname', 'norbert@gmail.com')'
|
||||
# -> a single partner is created for email 'norbert@gmail.com'
|
||||
seen = set()
|
||||
notfound_emails = (emails_normalized - set(partners.mapped('email_normalized'))) if partners else emails_normalized
|
||||
notfound_name_emails = [
|
||||
name_email
|
||||
for name_email in name_emails
|
||||
if name_email[1] in notfound_emails and name_email[1] not in seen
|
||||
and not seen.add(name_email[1])
|
||||
]
|
||||
tocreate_vals_list += [
|
||||
{
|
||||
self._rec_name: name or email_normalized,
|
||||
'email': email_normalized,
|
||||
**(additional_values or {})
|
||||
}
|
||||
for name, email_normalized in notfound_name_emails
|
||||
]
|
||||
|
||||
# create partners for invalid emails (aka name and not email_normalized)
|
||||
# without any existing partner
|
||||
tocreate_vals_list += [
|
||||
{
|
||||
self._rec_name: name,
|
||||
'email': name,
|
||||
**(additional_values or {})
|
||||
}
|
||||
for name in names if name not in partners.mapped('email')
|
||||
]
|
||||
|
||||
# create partners once
|
||||
if tocreate_vals_list:
|
||||
partners += self.create(tocreate_vals_list)
|
||||
|
||||
return [
|
||||
next(
|
||||
(partner for partner in partners
|
||||
if (email_normalized and partner.email_normalized == email_normalized)
|
||||
or (not email_normalized and email and partner.email == email)
|
||||
or (not email_normalized and name and partner.name == name)
|
||||
),
|
||||
self.env['res.partner']
|
||||
)
|
||||
for (name, email_normalized), email in zip(name_emails, emails)
|
||||
]
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# DISCUSS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@@ -341,9 +341,8 @@ const KanbanActivity = AbstractField.extend({
|
||||
views: [[false, "form"]],
|
||||
target: "new",
|
||||
context: {
|
||||
default_res_id: this.res_id,
|
||||
default_res_ids: [this.res_id],
|
||||
default_model: this.model,
|
||||
default_use_template: true,
|
||||
default_template_id: templateID,
|
||||
force_email: true,
|
||||
},
|
||||
|
||||
@@ -547,10 +547,10 @@ Model({
|
||||
const context = {
|
||||
default_attachment_ids: attachmentIds,
|
||||
default_body: escapeAndCompactTextContent(this.composer.textInputContent),
|
||||
default_is_log: this.composer.isLog,
|
||||
default_model: this.composer.activeThread.model,
|
||||
default_partner_ids: this.composer.recipients.map((partner) => partner.id),
|
||||
default_res_id: this.composer.activeThread.id,
|
||||
default_res_ids: [this.composer.activeThread.id],
|
||||
default_subtype_xmlid: this.composer.isLog ? 'mail.mt_note' : 'mail.mt_comment',
|
||||
mail_post_autofollow: this.composer.activeThread.hasWriteAccess,
|
||||
};
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ Model({
|
||||
views: [[false, "form"]],
|
||||
target: "new",
|
||||
context: {
|
||||
default_res_id: activity.thread.id,
|
||||
default_res_ids: [activity.thread.id],
|
||||
default_model: activity.thread.model,
|
||||
default_use_template: true,
|
||||
default_subtype_xmlid: 'mail.mt_comment',
|
||||
default_template_id: this.id,
|
||||
force_email: true,
|
||||
},
|
||||
|
||||
@@ -619,7 +619,7 @@ QUnit.module("mail", {}, function () {
|
||||
});
|
||||
|
||||
QUnit.test("activity with mail template: preview mail", async function (assert) {
|
||||
assert.expect(10);
|
||||
assert.expect(9);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const resPartnerId1 = pyEnv["res.partner"].create({});
|
||||
@@ -642,20 +642,16 @@ QUnit.module("mail", {}, function () {
|
||||
patchWithCleanup(env.services.action, {
|
||||
doAction(action) {
|
||||
assert.step("do_action");
|
||||
assert.strictEqual(
|
||||
action.context.default_res_id,
|
||||
resPartnerId1,
|
||||
"Action should have the activity res id as default res id in context"
|
||||
assert.deepEqual(
|
||||
action.context.default_res_ids,
|
||||
[resPartnerId1],
|
||||
"Action should have the activity res id as default res ids in context"
|
||||
);
|
||||
assert.strictEqual(
|
||||
action.context.default_model,
|
||||
"res.partner",
|
||||
"Action should have the activity res model as default model in context"
|
||||
);
|
||||
assert.ok(
|
||||
action.context.default_use_template,
|
||||
"Action should have true as default use_template in context"
|
||||
);
|
||||
assert.strictEqual(
|
||||
action.context.default_template_id,
|
||||
mailTemplateId1,
|
||||
|
||||
+97
-63
@@ -69,10 +69,11 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
|
||||
with self.mock_smtplib_connection(), \
|
||||
patch.object(IrMailServer, 'build_email', autospec=True, wraps=IrMailServer, side_effect=_ir_mail_server_build_email) as build_email_mocked, \
|
||||
patch.object(IrMailServer, 'send_email', autospec=True, wraps=IrMailServer, side_effect=send_email_origin) as send_email_mocked, \
|
||||
patch.object(MailMail, 'create', autospec=True, wraps=MailMail, side_effect=_mail_mail_create), \
|
||||
patch.object(MailMail, 'create', autospec=True, wraps=MailMail, side_effect=_mail_mail_create) as mail_mail_create_mocked, \
|
||||
patch.object(MailMail, 'unlink', autospec=True, wraps=MailMail, side_effect=_mail_mail_unlink):
|
||||
self.build_email_mocked = build_email_mocked
|
||||
self.send_email_mocked = send_email_mocked
|
||||
self.mail_mail_create_mocked = mail_mail_create_mocked
|
||||
yield
|
||||
|
||||
if mail_unlink_sent:
|
||||
@@ -329,18 +330,19 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
|
||||
# GATEWAY ASSERTS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def assertMailMail(self, recipients, status,
|
||||
mail_message=None, author=None,
|
||||
content=None, fields_values=None, email_values=None):
|
||||
""" Assert mail.mail records are created and maybe sent as emails. Allow
|
||||
def _assertMailMail(self, mail, recipients_list,
|
||||
status, author=None,
|
||||
content=None, fields_values=None, email_values=None):
|
||||
""" Assert mail.mail record values and maybe related emails. Allow
|
||||
asserting their content. Records to check are the one generated when
|
||||
using mock (mail.mail and outgoing emails). This method takes partners
|
||||
as source of record fetch and assert.
|
||||
using mock (mail.mail and outgoing emails).
|
||||
|
||||
:param recipients: a ``res.partner`` recordset. See ``_find_mail_mail_wpartners``;
|
||||
:param mail: a ``mail.mail`` record;
|
||||
:param recipients_list: an ``res.partner`` recordset or a list of
|
||||
emails (both are supported, see ``_find_mail_mail_wpartners`` and
|
||||
``_find_mail_mail_wemail``);
|
||||
:param status: mail.mail state used to filter mails. If ``sent`` this method
|
||||
also check that emails have been sent trough gateway;
|
||||
:param mail_message: see ``_find_mail_mail_wpartners``;
|
||||
:param author: see ``_find_mail_mail_wpartners``;
|
||||
:param content: if given, check it is contained within mail html body;
|
||||
:param fields_values: if given, should be a dictionary of field names /
|
||||
@@ -350,52 +352,89 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
|
||||
allowing to check sent email additional values (if any).
|
||||
See ``assertSentEmail``;
|
||||
"""
|
||||
self.assertTrue(bool(mail))
|
||||
if content:
|
||||
self.assertIn(content, mail.body_html)
|
||||
for fname, fvalue in (fields_values or {}).items():
|
||||
with self.subTest(fname=fname, fvalue=fvalue):
|
||||
self.assertEqual(
|
||||
mail[fname], fvalue,
|
||||
'Mail: expected %s for %s, got %s' % (fvalue, fname, mail[fname])
|
||||
)
|
||||
if status == 'sent':
|
||||
for recipient in recipients_list:
|
||||
with self.subTest(recipient=recipient):
|
||||
self.assertSentEmail(
|
||||
email_values['email_from'] if email_values and email_values.get('email_from') else author,
|
||||
[recipient],
|
||||
**(email_values or {})
|
||||
)
|
||||
|
||||
def assertMailMail(self, recipients, status,
|
||||
mail_message=None, author=None,
|
||||
content=None, fields_values=None, email_values=None):
|
||||
""" Assert mail.mail records are created and maybe sent as emails. This
|
||||
method takes partners as source to find mails and check their content.
|
||||
See '_assertMailMail' for more details.
|
||||
|
||||
:param recipients: a ``res.partner`` recordset. See
|
||||
``_find_mail_mail_wpartners``;
|
||||
:param mail_message: used to find the related email;
|
||||
|
||||
See '_assertMailMail' for more details about other parameters.
|
||||
"""
|
||||
found_mail = self._find_mail_mail_wpartners(recipients, status, mail_message=mail_message, author=author)
|
||||
self.assertTrue(bool(found_mail))
|
||||
if content:
|
||||
self.assertIn(content, found_mail.body_html)
|
||||
for fname, fvalue in (fields_values or {}).items():
|
||||
self.assertEqual(
|
||||
found_mail[fname], fvalue,
|
||||
'Mail: expected %s for %s, got %s' % (fvalue, fname, found_mail[fname]))
|
||||
if status == 'sent':
|
||||
for recipient in recipients:
|
||||
self.assertSentEmail(email_values['email_from'] if email_values and email_values.get('email_from') else author, [recipient], **(email_values or {}))
|
||||
self._assertMailMail(
|
||||
found_mail, recipients,
|
||||
status, author=author,
|
||||
content=content, fields_values=fields_values, email_values=email_values,
|
||||
)
|
||||
|
||||
def assertMailMailWEmails(self, emails, status,
|
||||
mail_message=None, author=None,
|
||||
content=None, fields_values=None, email_values=None):
|
||||
""" Assert mail.mail records are created and maybe sent as emails. Allow
|
||||
asserting their content. Records to check are the one generated when
|
||||
using mock (mail.mail and outgoing emails). This method takes emails
|
||||
as source of record fetch and assert.
|
||||
""" Assert mail.mail records are created and maybe sent as emails. This
|
||||
method takes emails as source to find mails and check their content.
|
||||
See '_assertMailMail' for more details.
|
||||
|
||||
:param emails: a list of emails. See ``_find_mail_mail_wemail``;
|
||||
:param status: mail.mail state used to filter mails. If ``sent`` this method
|
||||
also check that emails have been sent trough gateway;
|
||||
:param mail_message: see ``_find_mail_mail_wemail``;
|
||||
:param author: see ``_find_mail_mail_wemail``;;
|
||||
:param content: if given, check it is contained within mail html body;
|
||||
:param fields_values: if given, should be a dictionary of field names /
|
||||
values allowing to check ``mail.mail`` additional values (subject,
|
||||
reply_to, ...);
|
||||
:param email_values: if given, should be a dictionary of keys / values
|
||||
allowing to check sent email additional values (if any).
|
||||
See ``assertSentEmail``;
|
||||
:param mail_message: used to find the related email;
|
||||
|
||||
See '_assertMailMail' for more details about other parameters.
|
||||
"""
|
||||
for email_to in emails:
|
||||
found_mail = self._find_mail_mail_wemail(email_to, status, mail_message=mail_message, author=author)
|
||||
if content:
|
||||
self.assertIn(content, found_mail.body_html)
|
||||
for fname, fvalue in (fields_values or {}).items():
|
||||
self.assertEqual(
|
||||
found_mail[fname], fvalue,
|
||||
'Mail: expected %s for %s, got %s' % (fvalue, fname, found_mail[fname]))
|
||||
if status == 'sent':
|
||||
for email_to in emails:
|
||||
self.assertSentEmail(email_values['email_from'] if email_values and email_values.get('email_from') else author, [email_to], **(email_values or {}))
|
||||
self.assertTrue(bool(found_mail))
|
||||
self._assertMailMail(
|
||||
found_mail, [email_to],
|
||||
status, author=author,
|
||||
content=content, fields_values=fields_values, email_values=email_values,
|
||||
)
|
||||
|
||||
def assertMailMailWRecord(self, record, recipients, status,
|
||||
mail_message=None, author=None,
|
||||
content=None, fields_values=None, email_values=None):
|
||||
""" Assert mail.mail records are created and maybe sent as emails. This
|
||||
method takes a record as source to find mails and check their content
|
||||
using model / res_id. See '_assertMailMail' for more details.
|
||||
|
||||
:param record: a record used to find emails sent related on it.
|
||||
See ``_find_mail_mail_wrecord``;
|
||||
:param mail_message: used to find the related email;
|
||||
|
||||
See '_assertMailMail' for more details about other parameters.
|
||||
"""
|
||||
found_mail = self._find_mail_mail_wrecord(record, mail_message=mail_message, author=author)
|
||||
self.assertTrue(bool(found_mail))
|
||||
self._assertMailMail(
|
||||
found_mail, recipients,
|
||||
status, author=author,
|
||||
content=content, fields_values=fields_values, email_values=email_values,
|
||||
)
|
||||
|
||||
def assertMailMailWId(self, mail_id, status,
|
||||
author=None,
|
||||
content=None, fields_values=None):
|
||||
""" Assert mail.mail records are created and maybe sent as emails. Allow
|
||||
asserting their content. Records to check are the one generated when
|
||||
@@ -403,32 +442,27 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
|
||||
as source of record fetch and assert.
|
||||
|
||||
:param mail_id: a ``mail.mail`` DB ID. See ``_find_mail_mail_wid``;
|
||||
:param status: mail.mail state to check upon found mail;
|
||||
:param content: if given, check it is contained within mail html body;
|
||||
:param fields_values: if given, should be a dictionary of field names /
|
||||
values allowing to check ``mail.mail`` additional values (subject,
|
||||
reply_to, ...);
|
||||
|
||||
For other parameters, see ``_assertMailMail``.
|
||||
"""
|
||||
found_mail = self._find_mail_mail_wid(mail_id)
|
||||
self.assertTrue(bool(found_mail))
|
||||
if status:
|
||||
self.assertEqual(found_mail.state, status)
|
||||
if content:
|
||||
self.assertIn(content, found_mail.body_html)
|
||||
for fname, fvalue in (fields_values or {}).items():
|
||||
self.assertEqual(
|
||||
found_mail[fname], fvalue,
|
||||
'Mail: expected %s for %s, got %s' % (fvalue, fname, found_mail[fname]))
|
||||
self._assertMailMail(
|
||||
found_mail, [], # generally used when recipients are Falsy
|
||||
status, author=author,
|
||||
content=content, fields_values=fields_values,
|
||||
)
|
||||
|
||||
def assertMessageFields(self, message, fields_values):
|
||||
""" Just a quick helper to check a mail.message content by giving directly
|
||||
a dict for fields. Allows to hide a lot of assertEqual under a simple
|
||||
call with a dictionary of expected values. """
|
||||
for fname, fvalue in fields_values.items():
|
||||
self.assertEqual(
|
||||
message[fname], fvalue,
|
||||
'Message: expected %s for %s, got %s' % (fvalue, fname, message[fname])
|
||||
)
|
||||
with self.subTest(fname=fname, fvalue=fvalue):
|
||||
self.assertEqual(
|
||||
message[fname], fvalue,
|
||||
f'Message: expected {fvalue} for {fname}, got {message[fname]}',
|
||||
)
|
||||
|
||||
def assertNoMail(self, recipients, mail_message=None, author=None):
|
||||
""" Check no mail.mail and email was generated during gateway mock. """
|
||||
@@ -1030,22 +1064,22 @@ class MailCommon(common.TransactionCase, MailCase):
|
||||
return cls.user_portal
|
||||
|
||||
@classmethod
|
||||
def _create_records_for_batch(cls, model, count, additional_values=None):
|
||||
def _create_records_for_batch(cls, model, count, additional_values=None, prefix=None):
|
||||
additional_values = additional_values or {}
|
||||
records = cls.env[model]
|
||||
partners = cls.env['res.partner']
|
||||
country_id = cls.env.ref('base.be').id
|
||||
|
||||
base_values = [
|
||||
{'name': 'Test_%s' % idx,
|
||||
{'name': f'{prefix}Test_{idx}',
|
||||
**additional_values,
|
||||
} for idx in range(count)
|
||||
]
|
||||
|
||||
if 'customer_id' in cls.env[model]:
|
||||
partners = cls.env['res.partner'].with_context(**cls._test_context).create([{
|
||||
'name': 'Partner_%s' % idx,
|
||||
'email': '_test_partner_%s@example.com' % idx,
|
||||
'name': f'Partner_{idx}',
|
||||
'email': f'{prefix}test_partner_{idx}@example.com',
|
||||
'country_id': country_id,
|
||||
'mobile': '047500%02d%02d' % (idx, idx)
|
||||
} for idx in range(count)])
|
||||
|
||||
@@ -84,7 +84,7 @@ class TestMailComposerForm(TestMailComposer):
|
||||
form = Form(self.env['mail.compose.message'].with_context({
|
||||
'default_partner_ids': partner_classic.ids,
|
||||
'default_model': test_record._name,
|
||||
'default_res_id': test_record.id,
|
||||
'default_res_ids': test_record.ids,
|
||||
}))
|
||||
form.body = '<p>Hello</p>'
|
||||
self.assertEqual(
|
||||
@@ -116,7 +116,7 @@ class TestMailComposerForm(TestMailComposer):
|
||||
form = Form(self.env['mail.compose.message'].with_context({
|
||||
'default_partner_ids': (partner_private + partner_classic).ids,
|
||||
'default_model': test_record._name,
|
||||
'default_res_id': test_record.id,
|
||||
'default_res_ids': test_record.ids,
|
||||
}))
|
||||
form.body = '<p>Hello</p>'
|
||||
self.assertEqual(
|
||||
@@ -156,7 +156,7 @@ class TestMailComposerForm(TestMailComposer):
|
||||
_form = Form(self.env['mail.compose.message'].with_context({
|
||||
'default_partner_ids': (self.partner_private + partner_classic).ids,
|
||||
'default_model': test_record._name,
|
||||
'default_res_id': test_record.id,
|
||||
'default_res_ids': test_record.ids,
|
||||
}))
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@@ -177,7 +177,7 @@ class TestMailComposerForm(TestMailComposer):
|
||||
|
||||
form = Form(self.env['mail.compose.message'].with_context({
|
||||
'default_model': test_record._name,
|
||||
'default_res_id': test_record.id,
|
||||
'default_res_ids': test_record.ids,
|
||||
'default_template_id': template.id,
|
||||
}))
|
||||
|
||||
@@ -229,7 +229,7 @@ class TestMailComposerRendering(TestMailComposer):
|
||||
'subject': 'MSO FTW',
|
||||
})
|
||||
|
||||
values = mail_compose_message.get_mail_values(self.partner_employee.ids)
|
||||
values = mail_compose_message._prepare_mail_values(self.partner_employee.ids)
|
||||
|
||||
self.assertIn(
|
||||
self.body_html,
|
||||
@@ -254,7 +254,7 @@ class TestMailComposerRendering(TestMailComposer):
|
||||
with self.mock_mail_gateway(mail_unlink_sent=True):
|
||||
composer._action_send_mail()
|
||||
|
||||
values = composer.get_mail_values(self.partner_employee.ids)
|
||||
values = composer._prepare_mail_values(self.partner_employee.ids)
|
||||
|
||||
self.assertIn(
|
||||
self.body_html,
|
||||
|
||||
@@ -45,7 +45,7 @@ class TestMailTemplate(MailCommon):
|
||||
'subject': '{{ 1 + 5 }}',
|
||||
})
|
||||
|
||||
values = mail_compose_message.get_mail_values(self.partner_employee.ids)
|
||||
values = mail_compose_message._prepare_mail_values(self.partner_employee.ids)
|
||||
|
||||
self.assertEqual(values[self.partner_employee.id]['subject'], '6', 'We must trust mail template values')
|
||||
self.assertIn('13', values[self.partner_employee.id]['body_html'], 'We must trust mail template values')
|
||||
|
||||
@@ -1,29 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import patch
|
||||
from uuid import uuid4
|
||||
|
||||
from odoo import tools
|
||||
from odoo.addons.mail.tests.common import MailCommon, mail_new_test_user
|
||||
from odoo.tests.common import Form, users
|
||||
|
||||
from odoo.tests.common import Form, tagged, users
|
||||
from odoo.addons.base.models.res_partner import Partner
|
||||
|
||||
# samples use effective TLDs from the Mozilla public suffix
|
||||
# list at http://publicsuffix.org
|
||||
SAMPLES = [
|
||||
('"Raoul Grosbedon" <raoul@chirurgiens-dentistes.fr> ', 'Raoul Grosbedon', 'raoul@chirurgiens-dentistes.fr'),
|
||||
('ryu+giga-Sushi@aizubange.fukushima.jp', '', 'ryu+giga-Sushi@aizubange.fukushima.jp'),
|
||||
('ryu+giga-Sushi@aizubange.fukushima.jp', 'ryu+giga-sushi@aizubange.fukushima.jp', 'ryu+giga-Sushi@aizubange.fukushima.jp'),
|
||||
('Raoul chirurgiens-dentistes.fr', 'Raoul chirurgiens-dentistes.fr', ''),
|
||||
(" Raoul O'hara <!@historicalsociety.museum>", "Raoul O'hara", '!@historicalsociety.museum'),
|
||||
('Raoul Grosbedon <raoul@CHIRURGIENS-dentistes.fr> ', 'Raoul Grosbedon', 'raoul@CHIRURGIENS-dentistes.fr'),
|
||||
('Raoul megaraoul@chirurgiens-dentistes.fr', 'Raoul', 'megaraoul@chirurgiens-dentistes.fr'),
|
||||
('"Patrick Da Beast Poilvache" <PATRICK@example.com>', 'Patrick Poilvache', 'patrick@example.com'),
|
||||
('Patrick Caché <patrick@EXAMPLE.COM>', 'Patrick Poilvache', 'patrick@example.com'),
|
||||
('Patrick Caché <2patrick@EXAMPLE.COM>', 'Patrick Caché', '2patrick@example.com'),
|
||||
|
||||
('"Patrick Da Beast Poilvache" <PATRICK@example.com>', 'Patrick Da Beast Poilvache', 'patrick@example.com'),
|
||||
('Patrick Caché <patrick@EXAMPLE.COM>', 'Patrick Da Beast Poilvache', 'patrick@example.com'),
|
||||
('Patrick Caché <patrick.2@EXAMPLE.COM>', 'Patrick Caché', 'patrick.2@example.com'),
|
||||
]
|
||||
|
||||
|
||||
@tagged('res_partner')
|
||||
class TestPartner(MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls._activate_multi_company()
|
||||
|
||||
@contextmanager
|
||||
def mockPartnerCalls(self):
|
||||
_original_create = Partner.create
|
||||
_original_search = Partner.search
|
||||
self._new_partners = self.env['res.partner']
|
||||
|
||||
def _res_partner_create(model, *args, **kwargs):
|
||||
records = _original_create(model, *args, **kwargs)
|
||||
self._new_partners += records.sudo()
|
||||
return records
|
||||
|
||||
with patch.object(Partner, 'create',
|
||||
autospec=True, side_effect=_res_partner_create) as mock_partner_create, \
|
||||
patch.object(Partner, 'search',
|
||||
autospec=True, side_effect=_original_search) as mock_partner_search:
|
||||
self._mock_partner_create = mock_partner_create
|
||||
self._mock_partner_search = mock_partner_search
|
||||
yield
|
||||
|
||||
def _check_find_or_create(self, test_string, expected_name, expected_email, expected_email_normalized=False, check_partner=False, should_create=False):
|
||||
expected_email_normalized = expected_email_normalized or expected_email
|
||||
partner = self.env['res.partner'].find_or_create(test_string)
|
||||
@@ -36,16 +64,42 @@ class TestPartner(MailCommon):
|
||||
self.assertEqual(partner.email_normalized or '', expected_email_normalized)
|
||||
return partner
|
||||
|
||||
def test_res_partner_find_or_create(self):
|
||||
Partner = self.env['res.partner']
|
||||
def test_parse_partner_name(self):
|
||||
samples = [
|
||||
'Raoul raoul@grosbedon.fr',
|
||||
'Raoul chirurgiens-dentistes.fr',
|
||||
'invalid',
|
||||
'False',
|
||||
'',
|
||||
' ',
|
||||
False,
|
||||
None,
|
||||
]
|
||||
expected = [
|
||||
('Raoul', 'raoul@grosbedon.fr'),
|
||||
('Raoul chirurgiens-dentistes.fr', ''),
|
||||
('invalid', ''),
|
||||
('False', ''),
|
||||
('', ''),
|
||||
('', ''),
|
||||
('', ''),
|
||||
('', ''),
|
||||
]
|
||||
for (expected_name, expected_email), sample in zip(expected, samples):
|
||||
parsed = self.env['res.partner']._parse_partner_name(sample)
|
||||
self.assertEqual(parsed[0], expected_name)
|
||||
self.assertEqual(parsed[1], expected_email)
|
||||
|
||||
partner = Partner.browse(Partner.name_create(SAMPLES[0][0])[0])
|
||||
def test_res_partner_find_or_create(self):
|
||||
PartnerModel = self.env['res.partner']
|
||||
|
||||
partner = PartnerModel.browse(PartnerModel.name_create(SAMPLES[0][0])[0])
|
||||
self._check_find_or_create(
|
||||
SAMPLES[0][0], SAMPLES[0][1], SAMPLES[0][2],
|
||||
check_partner=partner, should_create=False
|
||||
)
|
||||
|
||||
partner_2 = Partner.browse(Partner.name_create('sarah.john@connor.com')[0])
|
||||
partner_2 = PartnerModel.browse(PartnerModel.name_create('sarah.john@connor.com')[0])
|
||||
found_2 = self._check_find_or_create(
|
||||
'john@connor.com', 'john@connor.com', 'john@connor.com',
|
||||
check_partner=partner_2, should_create=True
|
||||
@@ -76,7 +130,7 @@ class TestPartner(MailCommon):
|
||||
check_partner=new4, should_create=True
|
||||
)
|
||||
|
||||
existing = Partner.create({
|
||||
existing = PartnerModel.create({
|
||||
'name': SAMPLES[6][1],
|
||||
'email': SAMPLES[6][0],
|
||||
})
|
||||
@@ -98,6 +152,158 @@ class TestPartner(MailCommon):
|
||||
with self.assertRaises(ValueError):
|
||||
self.env['res.partner'].find_or_create("Raoul chirurgiens-dentistes.fr", assert_valid_email=True)
|
||||
|
||||
@users('employee_c2')
|
||||
def test_res_partner_find_or_create_from_emails(self):
|
||||
""" Test for _find_or_create_from_emails allowing to find or create
|
||||
partner based on emails in a batch-enabled and optimized fashion. """
|
||||
self.user_employee_c2.write({
|
||||
'groups_id': [(4, self.env.ref('base.group_partner_manager').id)],
|
||||
})
|
||||
|
||||
samples_emails = [item[0] for item in SAMPLES]
|
||||
partners = self.env['res.partner'].with_context(lang='en_US')._find_or_create_from_emails(
|
||||
samples_emails,
|
||||
additional_values=None,
|
||||
)
|
||||
self.assertEqual(len(partners), len(SAMPLES))
|
||||
for (sample, exp_name, exp_email), partner in zip(SAMPLES, partners):
|
||||
exp_email_normalized = tools.email_normalize(exp_email)
|
||||
self.assertFalse(partner.company_id)
|
||||
self.assertEqual(partner.email_normalized, exp_email_normalized)
|
||||
self.assertTrue(partner.id)
|
||||
self.assertEqual(partner.lang, 'en_US')
|
||||
self.assertEqual(partner.name, exp_name)
|
||||
|
||||
new_samples = SAMPLES + [
|
||||
('"New Customer" <new.customer@test.EXAMPLE.com>', 'New Customer', 'new.customer@test.example.com'),
|
||||
('"Duplicated Raoul" <RAOUL@chirurgiens-dentistes.fr>', 'Raoul Grosbedon', 'raoul@chirurgiens-dentistes.fr'),
|
||||
('Invalid', 'Invalid', ''),
|
||||
(False, False, False),
|
||||
(None, False, False),
|
||||
(' ', False, False),
|
||||
('', False, False),
|
||||
]
|
||||
samples_emails = [item[0] for item in new_samples]
|
||||
partners = self.env['res.partner'].with_context(lang='en_US')._find_or_create_from_emails(
|
||||
samples_emails,
|
||||
additional_values={'company_id': self.env.company.id},
|
||||
)
|
||||
self.assertEqual(len(partners), len(new_samples))
|
||||
for (sample, exp_name, exp_email), partner in zip(new_samples, partners):
|
||||
with self.subTest(sample=sample, exp_name=exp_name, exp_email=exp_email, partner=partner):
|
||||
exp_company = self.env.company if sample in [
|
||||
'"New Customer" <new.customer@test.EXAMPLE.com>', # valid email, not known -> new customer
|
||||
'Invalid' # invalid email, not known -> create a new partner
|
||||
] else self.env['res.company']
|
||||
if sample in [False, None, ' ', '']:
|
||||
self.assertFalse(partner)
|
||||
else:
|
||||
exp_email_normalized = tools.email_normalize(exp_email)
|
||||
self.assertEqual(partner.company_id, exp_company)
|
||||
self.assertEqual(partner.email_normalized, exp_email_normalized)
|
||||
self.assertEqual(partner.name, exp_name)
|
||||
|
||||
@users('employee_c2')
|
||||
def test_res_partner_find_or_create_from_emails_dupes(self):
|
||||
""" Specific test for duplicates management: based on email to avoid
|
||||
creating similar partners. """
|
||||
self.user_employee_c2.write({
|
||||
'groups_id': [(4, self.env.ref('base.group_partner_manager').id)],
|
||||
})
|
||||
|
||||
# all same partner, same email 'test.customer@test.dupe.example.com'
|
||||
email_dupes_samples = [
|
||||
'"Formatted Customer" <test.customer@TEST.DUPE.EXAMPLE.COM>',
|
||||
'test.customer@test.dupe.example.com',
|
||||
'"Another Name" <test.customer@TEST.DUPE.EXAMPLE.COM>',
|
||||
'"Mix of both" <test.customer@test.dupe.EXAMPLE.COM',
|
||||
]
|
||||
email_expected_name = "Formatted Customer" # first found email will setup partner info
|
||||
email_expected_email = 'test.customer@test.dupe.example.com' # normalized version of given email
|
||||
# all same partner, same invalid email 'test.customer.invalid.email'
|
||||
name_dupes_samples = [
|
||||
'test.customer.invalid.email',
|
||||
'test.customer.invalid.email',
|
||||
]
|
||||
name_expected_name = 'test.customer.invalid.email' # invalid email kept as both name and email
|
||||
name_expected_email = 'test.customer.invalid.email' # invalid email kept as both name and email
|
||||
|
||||
partners = self.env['res.partner']
|
||||
for samples, (expected_name, expected_email) in [
|
||||
(email_dupes_samples, (email_expected_name, email_expected_email)),
|
||||
(name_dupes_samples, (name_expected_name, name_expected_email)),
|
||||
]:
|
||||
with self.subTest(samples=samples, expected_name=expected_name, expected_email=expected_email):
|
||||
with self.mockPartnerCalls():
|
||||
partner_list = self.env['res.partner'].with_context(lang='en_US')._find_or_create_from_emails(
|
||||
samples,
|
||||
additional_values=None,
|
||||
)
|
||||
# calls
|
||||
self.assertEqual(self._mock_partner_create.call_count, 1)
|
||||
self.assertEqual(self._mock_partner_search.call_count, 1)
|
||||
self.assertEqual(len(self._new_partners), 1)
|
||||
# results
|
||||
self.assertEqual(len(partner_list), len(samples))
|
||||
self.assertTrue(len(set(partner.id for partner in partner_list)) == 1 and partner_list[0].id, 'Should have a unique new partner')
|
||||
for partner in partner_list:
|
||||
self.assertEqual(partner.email, expected_email)
|
||||
self.assertEqual(partner.name, expected_name)
|
||||
|
||||
partners += partner_list[0]
|
||||
|
||||
self.assertEqual(len(partners), 2,
|
||||
'Should have created one partner for valid email, one for invalid email')
|
||||
|
||||
new_samples = [
|
||||
'"Another Customer" <test.customer2@TEST.DUPE.EXAMPLE.COM', # actually a new valid email
|
||||
'"First Duplicate" <test.customer@TEST.DUPE.example.com', # duplicated of valid email created above
|
||||
'test.customer.invalid.email', # duplicate of an invalid email created above
|
||||
]
|
||||
with self.mockPartnerCalls():
|
||||
new_partners = self.env['res.partner'].with_context(lang='en_US')._find_or_create_from_emails(
|
||||
new_samples,
|
||||
additional_values=None,
|
||||
)
|
||||
# calls
|
||||
self.assertEqual(self._mock_partner_create.call_count, 1)
|
||||
self.assertEqual(self._mock_partner_search.call_count, 1,
|
||||
'Search once, even with both normalized and invalid emails')
|
||||
self.assertEqual(len(self._new_partners), 1)
|
||||
# results
|
||||
self.assertEqual(len(new_partners), len(new_samples))
|
||||
self.assertEqual(new_partners[0].email, "test.customer2@test.dupe.example.com")
|
||||
self.assertEqual(new_partners[0].name, "Another Customer")
|
||||
self.assertEqual(new_partners[1], partners[0])
|
||||
|
||||
other_samples = [
|
||||
'"Another Duplicate" <test.customer2@TEST.DUPE.EXAMPLE.COM',
|
||||
'"First Duplicate2" <test.customer@TEST.DUPE.example.com',
|
||||
'"Third Customer" <test.customer3@test.dupe.example.com',
|
||||
'"Falsy" <falsy>',
|
||||
'falsy',
|
||||
' ',
|
||||
]
|
||||
with self.mockPartnerCalls():
|
||||
other_partners = self.env['res.partner'].with_context(lang='en_US')._find_or_create_from_emails(
|
||||
other_samples,
|
||||
additional_values=None,
|
||||
)
|
||||
# calls
|
||||
self.assertEqual(self._mock_partner_create.call_count, 1)
|
||||
self.assertEqual(self._mock_partner_search.call_count, 1)
|
||||
self.assertEqual(len(self._new_partners), 3)
|
||||
# results
|
||||
self.assertEqual(len(other_partners), len(other_samples))
|
||||
self.assertEqual(other_partners[0], new_partners[0], 'Should take already existing partner')
|
||||
self.assertEqual(other_partners[1], partners[0], 'Should take already existing partner')
|
||||
self.assertEqual(other_partners[2].email, "test.customer3@test.dupe.example.com")
|
||||
self.assertEqual(other_partners[2].name, "Third Customer")
|
||||
self.assertEqual(other_partners[3].email, '"Falsy" <falsy>')
|
||||
self.assertEqual(other_partners[3].name, '"Falsy" <falsy>')
|
||||
self.assertEqual(other_partners[4].email, "falsy")
|
||||
self.assertEqual(other_partners[4].name, "falsy")
|
||||
|
||||
def test_res_partner_log_portal_group(self):
|
||||
Users = self.env['res.users']
|
||||
subtype_note = self.env.ref('mail.mt_note')
|
||||
|
||||
@@ -79,8 +79,9 @@
|
||||
<field name="lang" placeholder="{{ object.partner_id.lang }}"/>
|
||||
<field name="mail_server_id"/>
|
||||
<field name="auto_delete"/>
|
||||
<field name="report_template" domain="[('model','=',model)]"/>
|
||||
<field name="report_name" attrs="{'invisible':[('report_template','=',False)]}"/>
|
||||
<field name="report_template_ids" domain="[('model','=',model)]"
|
||||
widget="many2many_tags"
|
||||
options="{'no_create': True}"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
@@ -102,7 +103,6 @@
|
||||
<field name="email_from" optional="hidden"/>
|
||||
<field name="email_to" optional="hidden"/>
|
||||
<field name="partner_to" optional="hidden"/>
|
||||
<field name="report_name" optional="hidden"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -112,7 +112,7 @@
|
||||
<field name="model">mail.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Templates">
|
||||
<field name="name" filter_domain="['|', '|', '|',('name','ilike',self), ('report_name','ilike',self), ('subject','ilike',self), ('email_to','ilike',self)]" string="Templates"/>
|
||||
<field name="name" filter_domain="['|', '|', ('name','ilike',self), ('subject','ilike',self), ('email_to','ilike',self)]" string="Templates"/>
|
||||
<field name="lang"/>
|
||||
<field name="model_id"/>
|
||||
<filter name="base_templates" string="Base Templates" domain="[('template_category', '=', 'base_template')]"/>
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
<field name="context" eval="{
|
||||
'default_composition_mode': 'mass_mail',
|
||||
'default_partner_to': '{{ object.id or \'\' }}',
|
||||
'default_use_template': False,
|
||||
'default_subtype_xmlid': 'mail.mt_comment',
|
||||
'default_reply_to_force_new': True,
|
||||
}"/>
|
||||
<field name="binding_model_id" ref="base.model_res_partner"/>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,29 +11,34 @@
|
||||
<!-- truly invisible fields for control and options -->
|
||||
<field name="author_id" invisible="1"/>
|
||||
<field name="auto_delete" invisible="1"/>
|
||||
<field name="auto_delete_message" invisible="1"/>
|
||||
<field name="auto_delete_keep_log" invisible="1"/>
|
||||
<field name="composition_batch" invisible="1"/>
|
||||
<field name="composition_mode" invisible="1"/>
|
||||
<field name="email_layout_xmlid" invisible="1"/>
|
||||
<field name="is_log" invisible="1"/>
|
||||
<field name="force_send" invisible="1"/>
|
||||
<field name="lang" invisible="1"/>
|
||||
<field name="mail_server_id" invisible="1"/>
|
||||
<field name="model" invisible="1"/>
|
||||
<field name="parent_id" invisible="1"/>
|
||||
<field name="record_name" invisible="1"/>
|
||||
<field name="res_id" invisible="1"/>
|
||||
<field name="render_model" invisible="1"/>
|
||||
<field name="res_domain" invisible="1"/>
|
||||
<field name="res_domain_user_id" invisible="1"/>
|
||||
<field name="res_ids" invisible="1"/>
|
||||
<field name="scheduled_date" invisible="1"/>
|
||||
<field name="subtype_id" invisible="1"/>
|
||||
<field name="subtype_is_log" invisible="1"/>
|
||||
<field name="use_exclusion_list" invisible="1"/>
|
||||
<!-- visible wizard -->
|
||||
<field name="email_from"
|
||||
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
|
||||
<label for="partner_ids" string="Recipients" attrs="{'invisible': ['|', ('is_log', '=', True), ('composition_mode', '!=', 'comment')]}"/>
|
||||
<div groups="base.group_user" attrs="{'invisible': ['|', ('is_log', '=', True), ('composition_mode', '!=', 'comment')]}">
|
||||
<label for="partner_ids" string="Recipients" attrs="{'invisible': [('composition_mode', '!=', 'comment')]}"/>
|
||||
<div groups="base.group_user" attrs="{'invisible': [('composition_mode', '!=', 'comment')]}">
|
||||
<span name="document_followers_text" attrs="{'invisible':['|', ('model', '=', False), ('composition_mode', '=', 'mass_mail')]}">Followers of the document and</span>
|
||||
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to notify..."
|
||||
context="{'force_email':True, 'show_email':True}"/>
|
||||
</div>
|
||||
<field name="subject" placeholder="Welcome to MyCompany!" required="True"/>
|
||||
<!-- mass post -->
|
||||
<field name="notify"
|
||||
attrs="{'invisible':[('composition_mode', '!=', 'mass_post')]}"/>
|
||||
</group>
|
||||
<field name="can_edit_body" invisible="1"/>
|
||||
<div attrs="{'invisible': [('composition_mode', '=', 'mass_mail')]}">
|
||||
@@ -67,8 +72,12 @@
|
||||
</page>
|
||||
</notebook>
|
||||
<footer>
|
||||
<button string="Send" attrs="{'invisible': [('is_log', '=', True)]}" name="action_send_mail" type="object" class="btn-primary o_mail_send" data-hotkey="q"/>
|
||||
<button string="Log" attrs="{'invisible': [('is_log', '=', False)]}" name="action_send_mail" type="object" class="btn-primary" data-hotkey="q"/>
|
||||
<button string="Send" name="action_send_mail"
|
||||
type="object" class="btn-primary o_mail_send" data-hotkey="q"
|
||||
attrs="{'invisible': [('subtype_is_log', '=', True)]}"/>
|
||||
<button string="Log" name="action_send_mail"
|
||||
type="object" class="btn-primary" data-hotkey="q"
|
||||
attrs="{'invisible': [('subtype_is_log', '=', False)]}"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z" />
|
||||
<button icon="fa-lg fa-save" type="object"
|
||||
name="action_save_as_template" string="Save as new template"
|
||||
|
||||
@@ -14,6 +14,8 @@ class MailTemplatePreview(models.TransientModel):
|
||||
'email_cc',
|
||||
'email_from',
|
||||
'email_to',
|
||||
'partner_to',
|
||||
'report_template_ids',
|
||||
'reply_to',
|
||||
'scheduled_date',
|
||||
]
|
||||
@@ -43,7 +45,7 @@ class MailTemplatePreview(models.TransientModel):
|
||||
resource_ref = fields.Reference(string='Record', selection='_selection_target_model')
|
||||
lang = fields.Selection(_selection_languages, string='Template Preview Language')
|
||||
no_record = fields.Boolean('No Record', compute='_compute_no_record')
|
||||
error_msg = fields.Char('Error Message', readonly=True)
|
||||
error_msg = fields.Char('Error Message', compute='_compute_mail_template_fields')
|
||||
# Fields same than the mail.template model, computed with resource_ref and lang
|
||||
subject = fields.Char('Subject', compute='_compute_mail_template_fields')
|
||||
email_from = fields.Char('From', compute='_compute_mail_template_fields', help="Sender address")
|
||||
@@ -55,7 +57,7 @@ class MailTemplatePreview(models.TransientModel):
|
||||
help="The queue manager will send the email after the date")
|
||||
body_html = fields.Html('Body', compute='_compute_mail_template_fields', sanitize=False)
|
||||
attachment_ids = fields.Many2many('ir.attachment', 'Attachments', compute='_compute_mail_template_fields')
|
||||
# Extra fields info generated by generate_email
|
||||
# Extra fields info generated by _generate_template
|
||||
partner_ids = fields.Many2many('res.partner', string='Recipients', compute='_compute_mail_template_fields')
|
||||
|
||||
@api.depends('model_id')
|
||||
@@ -72,17 +74,17 @@ class MailTemplatePreview(models.TransientModel):
|
||||
copy_depends_values = {'lang': self.lang}
|
||||
mail_template = self.mail_template_id.with_context(lang=self.lang)
|
||||
try:
|
||||
if not self.resource_ref:
|
||||
if not self.resource_ref.id:
|
||||
self._set_mail_attributes()
|
||||
else:
|
||||
copy_depends_values['resource_ref'] = '%s,%s' % (self.resource_ref._name, self.resource_ref.id)
|
||||
mail_values = mail_template.with_context(template_preview_lang=self.lang).generate_email(
|
||||
self.resource_ref.id,
|
||||
mail_values = mail_template.with_context(template_preview_lang=self.lang)._generate_template(
|
||||
[self.resource_ref.id],
|
||||
self._MAIL_TEMPLATE_FIELDS
|
||||
)
|
||||
)[self.resource_ref.id]
|
||||
self._set_mail_attributes(values=mail_values)
|
||||
self.error_msg = False
|
||||
except UserError as user_error:
|
||||
except (ValueError, UserError) as user_error:
|
||||
self._set_mail_attributes()
|
||||
self.error_msg = user_error.args[0]
|
||||
finally:
|
||||
@@ -92,6 +94,10 @@ class MailTemplatePreview(models.TransientModel):
|
||||
|
||||
def _set_mail_attributes(self, values=None):
|
||||
for field in self._MAIL_TEMPLATE_FIELDS:
|
||||
if field in ('partner_to', 'report_template_ids'):
|
||||
# partner_to is used to generate partner_ids, handled here below
|
||||
# report_template_ids generates attachments, no usage here
|
||||
continue
|
||||
field_value = values.get(field, False) if values else self.mail_template_id[field]
|
||||
self[field] = field_value
|
||||
self.partner_ids = values.get('partner_ids', False) if values else False
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<form string="Email Preview">
|
||||
<h3>Preview of <field name="mail_template_id" readonly="1" nolabel="1" options="{'no_open' : True}"/></h3>
|
||||
<div class="alert alert-danger" role="alert" attrs="{'invisible' : [('error_msg', '=', False)]}">
|
||||
<field name="error_msg" />
|
||||
<field name="error_msg"/>
|
||||
</div>
|
||||
<field name="no_record" invisible="1"/>
|
||||
<div class="container">
|
||||
|
||||
@@ -314,7 +314,7 @@ class MailGroup(models.Model):
|
||||
values
|
||||
))
|
||||
|
||||
mail_message = Mailthread._message_create(values)
|
||||
mail_message = Mailthread._message_create([values])
|
||||
|
||||
# Find the <mail.group.message> parent
|
||||
group_message_parent_id = False
|
||||
|
||||
@@ -96,6 +96,7 @@ class MailGroupMessage(models.Model):
|
||||
field: vals.pop(field)
|
||||
for field in self.env['mail.message']._fields
|
||||
if field in vals
|
||||
and field in self.env['mail.thread']._get_message_create_valid_field_names()
|
||||
}).id
|
||||
return super(MailGroupMessage, self).create(values_list)
|
||||
|
||||
|
||||
@@ -117,10 +117,10 @@ class MailPluginController(http.Controller):
|
||||
|
||||
partner.write(partner_values)
|
||||
|
||||
partner.message_post_with_view(
|
||||
partner.message_post_with_source(
|
||||
'iap_mail.enrich_company',
|
||||
values=iap_data,
|
||||
subtype_id=request.env.ref('mail.mt_note').id,
|
||||
render_values=iap_data,
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -357,10 +357,10 @@ class MailPluginController(http.Controller):
|
||||
|
||||
new_company = request.env['res.partner'].create(new_company_info)
|
||||
|
||||
new_company.message_post_with_view(
|
||||
new_company.message_post_with_source(
|
||||
'iap_mail.enrich_company',
|
||||
values=iap_data,
|
||||
subtype_id=request.env.ref('mail.mt_note').id,
|
||||
render_values=iap_data,
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
return new_company, {'type': 'company_created'}
|
||||
|
||||
@@ -35,15 +35,15 @@ class MailMail(models.Model):
|
||||
self.ensure_one()
|
||||
return tools.hmac(self.env(su=True), 'mass_mailing-mail_mail-open', self.id)
|
||||
|
||||
def _send_prepare_body(self):
|
||||
""" Override to add the tracking URL to the body and to add
|
||||
trace ID in shortened urls """
|
||||
# TDE: temporary addition (mail was parameter) due to semi-new-API
|
||||
def _prepare_outgoing_body(self):
|
||||
""" Override to add the tracking URL to the body and to add trace ID in
|
||||
shortened urls """
|
||||
self.ensure_one()
|
||||
body = super(MailMail, self)._send_prepare_body()
|
||||
# super() already cleans pseudo-void content from editor
|
||||
body = super()._prepare_outgoing_body()
|
||||
|
||||
if self.mailing_id and body and self.mailing_trace_ids:
|
||||
for match in set(re.findall(tools.URL_REGEX, self.body_html)):
|
||||
if body and self.mailing_id and self.mailing_trace_ids:
|
||||
for match in set(re.findall(tools.URL_REGEX, body)):
|
||||
href = match[0]
|
||||
url = match[1]
|
||||
|
||||
@@ -57,31 +57,35 @@ class MailMail(models.Model):
|
||||
tracking_url = self._get_tracking_url()
|
||||
body = tools.append_content_to_html(
|
||||
body,
|
||||
'<img src="%s"/>' % tracking_url,
|
||||
f'<img src="{tracking_url}"/>',
|
||||
plaintext=False,
|
||||
)
|
||||
|
||||
body = self.env['mail.render.mixin']._replace_local_links(body)
|
||||
|
||||
return body
|
||||
|
||||
def _send_prepare_values(self, partner=None):
|
||||
# TDE: temporary addition (mail was parameter) due to semi-new-API
|
||||
res = super(MailMail, self)._send_prepare_values(partner)
|
||||
if self.mailing_id and res.get('body') and res.get('email_to'):
|
||||
base_url = self.mailing_id.get_base_url()
|
||||
emails = tools.email_split(res.get('email_to')[0])
|
||||
email_to = emails and emails[0] or False
|
||||
def _prepare_outgoing_list(self):
|
||||
""" Update mailing specific links to add tracking based on res_id """
|
||||
email_list = super()._prepare_outgoing_list()
|
||||
if not self.res_id or not self.mailing_id:
|
||||
return email_list
|
||||
|
||||
urls_to_replace = [
|
||||
(base_url + '/unsubscribe_from_list', self.mailing_id._get_unsubscribe_url(email_to, self.res_id)),
|
||||
(base_url + '/view', self.mailing_id._get_view_url(email_to, self.res_id))
|
||||
]
|
||||
base_url = self.mailing_id.get_base_url()
|
||||
for email_values in email_list:
|
||||
if tools.is_html_empty(email_values['body']) or not email_values['email_to']:
|
||||
continue
|
||||
emails = tools.email_split(email_values['email_to'][0])
|
||||
email_to = emails[0] if emails else False
|
||||
|
||||
for url_to_replace, new_url in urls_to_replace:
|
||||
if url_to_replace in res['body']:
|
||||
res['body'] = res['body'].replace(url_to_replace, new_url if new_url else '#')
|
||||
return res
|
||||
if f'{base_url}/unsubscribe_from_list' in email_values['body']:
|
||||
email_values['body'] = email_values['body'].replace(
|
||||
f'{base_url}/unsubscribe_from_list',
|
||||
self.mailing_id._get_unsubscribe_url(email_to, self.res_id),
|
||||
)
|
||||
if f'{base_url}/view' in email_values['body']:
|
||||
email_values['body'] = email_values['body'].replace(
|
||||
f'{base_url}/view',
|
||||
self.mailing_id._get_view_url(email_to, self.res_id),
|
||||
)
|
||||
return email_list
|
||||
|
||||
def _postprocess_sent_message(self, success_pids, failure_reason=False, failure_type=None):
|
||||
mail_sent = not failure_type # we consider that a recipient error is a failure with mass mailling and show them as failed
|
||||
@@ -91,4 +95,4 @@ class MailMail(models.Model):
|
||||
mail.mailing_trace_ids.set_sent()
|
||||
elif mail_sent is False and mail.mailing_trace_ids:
|
||||
mail.mailing_trace_ids.set_failed(failure_type=failure_type)
|
||||
return super(MailMail, self)._postprocess_sent_message(success_pids, failure_reason=failure_reason, failure_type=failure_type)
|
||||
return super()._postprocess_sent_message(success_pids, failure_reason=failure_reason, failure_type=failure_type)
|
||||
|
||||
@@ -28,14 +28,21 @@ class MailThread(models.AbstractModel):
|
||||
self.env['mailing.trace'].set_replied(domain=[('message_id', 'in', msg_references)])
|
||||
return super(MailThread, self)._message_route_process(message, message_dict, routes)
|
||||
|
||||
def message_post_with_template(self, template_id, **kwargs):
|
||||
def message_mail_with_source(self, source_ref, **kwargs):
|
||||
# avoid having message send through `message_post*` methods being implicitly considered as
|
||||
# mass-mailing
|
||||
no_massmail = self.with_context(
|
||||
return super(MailThread, self.with_context(
|
||||
default_mass_mailing_name=False,
|
||||
default_mass_mailing_id=False,
|
||||
)
|
||||
return super(MailThread, no_massmail).message_post_with_template(template_id, **kwargs)
|
||||
)).message_mail_with_source(source_ref, **kwargs)
|
||||
|
||||
def message_post_with_source(self, source_ref, **kwargs):
|
||||
# avoid having message send through `message_post*` methods being implicitly considered as
|
||||
# mass-mailing
|
||||
return super(MailThread, self.with_context(
|
||||
default_mass_mailing_name=False,
|
||||
default_mass_mailing_id=False,
|
||||
)).message_post_with_source(source_ref, **kwargs)
|
||||
|
||||
@api.model
|
||||
def _routing_handle_bounce(self, email_message, message_dict):
|
||||
|
||||
@@ -1046,16 +1046,19 @@ class MassMailing(models.Model):
|
||||
def action_send_mail(self, res_ids=None):
|
||||
author_id = self.env.user.partner_id.id
|
||||
|
||||
# If no recipient is passed, we don't want to use the recipients of the first
|
||||
# mailing for all the others
|
||||
initial_res_ids = res_ids
|
||||
for mailing in self:
|
||||
if not initial_res_ids:
|
||||
res_ids = mailing._get_remaining_recipients()
|
||||
if not res_ids:
|
||||
context_user = mailing.user_id or mailing.write_uid or self.env.user
|
||||
mailing = mailing.with_context(
|
||||
**self.env['res.users'].with_user(context_user).context_get()
|
||||
)
|
||||
mailing_res_ids = res_ids or mailing._get_remaining_recipients()
|
||||
if not mailing_res_ids:
|
||||
raise UserError(_('There are no recipients selected.'))
|
||||
|
||||
composer_values = {
|
||||
'auto_delete': not mailing.keep_archives,
|
||||
# email-mode: keep original message for routing
|
||||
'auto_delete_keep_log': mailing.reply_to_mode == 'update',
|
||||
'author_id': author_id,
|
||||
'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
|
||||
'body': mailing._prepend_preview(mailing.body_html, mailing.preview),
|
||||
@@ -1068,17 +1071,21 @@ class MassMailing(models.Model):
|
||||
'record_name': False,
|
||||
'reply_to_force_new': mailing.reply_to_mode == 'new',
|
||||
'subject': mailing.subject,
|
||||
'template_id': None,
|
||||
'template_id': False,
|
||||
}
|
||||
if mailing.reply_to_mode == 'new':
|
||||
composer_values['reply_to'] = mailing.reply_to
|
||||
|
||||
composer = self.env['mail.compose.message'].with_context(active_ids=res_ids).create(composer_values)
|
||||
extra_context = mailing._get_mass_mailing_context()
|
||||
composer = composer.with_context(active_ids=res_ids, **extra_context)
|
||||
composer = self.env['mail.compose.message'].with_context(
|
||||
active_ids=mailing_res_ids,
|
||||
default_composition_mode='mass_mail',
|
||||
**mailing._get_mass_mailing_context()
|
||||
).create(composer_values)
|
||||
|
||||
# auto-commit except in testing mode
|
||||
auto_commit = not getattr(threading.current_thread(), 'testing', False)
|
||||
composer._action_send_mail(auto_commit=auto_commit)
|
||||
composer._action_send_mail(
|
||||
auto_commit=not getattr(threading.current_thread(), 'testing', False)
|
||||
)
|
||||
mailing.write({
|
||||
'state': 'done',
|
||||
'sent_date': fields.Datetime.now(),
|
||||
@@ -1109,8 +1116,10 @@ class MassMailing(models.Model):
|
||||
def _process_mass_mailing_queue(self):
|
||||
mass_mailings = self.search([('state', 'in', ('in_queue', 'sending')), '|', ('schedule_date', '<', fields.Datetime.now()), ('schedule_date', '=', False)])
|
||||
for mass_mailing in mass_mailings:
|
||||
user = mass_mailing.write_uid or self.env.user
|
||||
mass_mailing = mass_mailing.with_context(**user.with_user(user).context_get())
|
||||
context_user = mass_mailing.user_id or mass_mailing.write_uid or self.env.user
|
||||
mass_mailing = mass_mailing.with_context(
|
||||
**self.env['res.users'].with_user(context_user).context_get()
|
||||
)
|
||||
if len(mass_mailing._get_remaining_recipients()) > 0:
|
||||
mass_mailing.state = 'sending'
|
||||
mass_mailing.action_send_mail()
|
||||
|
||||
@@ -28,7 +28,7 @@ class MailingTrace(models.Model):
|
||||
# mass_mailing_sms mass mode specific codes
|
||||
'sms_blacklist', 'sms_duplicate', 'sms_optout',
|
||||
* cancel:
|
||||
* mail: set in get_mail_values in composer, if email is blacklisted
|
||||
* mail: set in _prepare_mail_values in composer, if email is blacklisted
|
||||
(mail) or in opt_out / seen list (mass_mailing) or email_to is void
|
||||
or incorrectly formatted (mass_mailing) - based on mail cancel state
|
||||
* sms: set in _prepare_mass_sms_trace_values in composer if sms is
|
||||
|
||||
@@ -127,7 +127,7 @@ class MassMailCase(MailCase, MockLinkTracker):
|
||||
self.assertMailMail(partner, state_mapping[status], author=author, content=content, fields_values=fields_values)
|
||||
# specific if email is False -> could have troubles finding it if several falsy traces
|
||||
elif not email and status in ('cancel', 'bounce'):
|
||||
self.assertMailMailWId(recipient_trace.mail_mail_id_int, state_mapping[status], content=content, fields_values=fields_values)
|
||||
self.assertMailMailWId(recipient_trace.mail_mail_id_int, state_mapping[status], author=author, content=content, fields_values=fields_values)
|
||||
else:
|
||||
self.assertMailMailWEmails([email], state_mapping[status], author=author, content=content, fields_values=fields_values)
|
||||
|
||||
|
||||
@@ -45,14 +45,14 @@ class TestMassMailValues(MassMailCommon):
|
||||
composer = self.env['mail.compose.message'].with_user(self.user_marketing).with_context({
|
||||
'default_composition_mode': 'mass_mail',
|
||||
'default_model': 'res.partner',
|
||||
'default_res_id': recipient.id,
|
||||
'default_res_ids': recipient.ids,
|
||||
}).create({
|
||||
'subject': 'Mass Mail Responsive',
|
||||
'body': 'I am Responsive body',
|
||||
'mass_mailing_id': mailing.id
|
||||
})
|
||||
|
||||
mail_values = composer.get_mail_values([recipient.id])
|
||||
mail_values = composer._prepare_mail_values([recipient.id])
|
||||
body_html = mail_values[recipient.id]['body_html']
|
||||
|
||||
self.assertIn('<!DOCTYPE html>', body_html)
|
||||
|
||||
@@ -19,72 +19,78 @@ class MailComposeMessage(models.TransientModel):
|
||||
model = self.env['ir.model']._get(composer.model)
|
||||
composer.model_is_thread = model.is_mail_thread
|
||||
|
||||
def get_mail_values(self, res_ids):
|
||||
""" Override method that generated the mail content by creating the
|
||||
mailing.trace values in the o2m of mail_mail, when doing pure
|
||||
email mass mailing. """
|
||||
self.ensure_one()
|
||||
res = super(MailComposeMessage, self).get_mail_values(res_ids)
|
||||
# use only for allowed models in mass mailing
|
||||
def _action_send_mail(self, auto_commit=False):
|
||||
""" Override to generate the mass mailing in case only the name was
|
||||
given. It is used afterwards for traces generation. """
|
||||
if self.composition_mode == 'mass_mail' and \
|
||||
(self.mass_mailing_name or self.mass_mailing_id) and \
|
||||
self.mass_mailing_name and not self.mass_mailing_id and \
|
||||
self.model_is_thread:
|
||||
mass_mailing = self.mass_mailing_id
|
||||
if not mass_mailing:
|
||||
mass_mailing = self.env['mailing.mailing'].create(
|
||||
self._prepare_mailing_values()
|
||||
mass_mailing = self.env['mailing.mailing'].create(self._prepare_mailing_values())
|
||||
self.mass_mailing_id = mass_mailing.id
|
||||
return super()._action_send_mail(auto_commit=auto_commit)
|
||||
|
||||
def _prepare_mail_values(self, res_ids):
|
||||
""" When being in mass mailing mode, add 'mailing.trace' values directly
|
||||
in the o2m field of mail.mail. """
|
||||
mail_values_all = super()._prepare_mail_values(res_ids)
|
||||
|
||||
# use only for allowed models in mass mailing
|
||||
if (self.composition_mode != 'mass_mail' or
|
||||
not self.mass_mailing_id or
|
||||
not self.model_is_thread):
|
||||
return mail_values_all
|
||||
|
||||
trace_values_all = self._prepare_mail_values_mailing_traces(mail_values_all)
|
||||
for res_id, mail_values in mail_values_all.items():
|
||||
if mail_values.get('body_html'):
|
||||
body = self.env['ir.qweb']._render(
|
||||
'mass_mailing.mass_mailing_mail_layout',
|
||||
{'body': mail_values['body_html']},
|
||||
minimal_qcontext=True,
|
||||
raise_if_not_found=False
|
||||
)
|
||||
self.mass_mailing_id = mass_mailing.id
|
||||
if body:
|
||||
mail_values['body_html'] = body
|
||||
|
||||
recipients_info = self._process_recipient_values(res)
|
||||
for res_id in res_ids:
|
||||
mail_values = res[res_id]
|
||||
if mail_values.get('body_html'):
|
||||
body = self.env['ir.qweb']._render(
|
||||
'mass_mailing.mass_mailing_mail_layout',
|
||||
{'body': mail_values['body_html']},
|
||||
minimal_qcontext=True,
|
||||
raise_if_not_found=False
|
||||
)
|
||||
if body:
|
||||
mail_values['body_html'] = body
|
||||
|
||||
trace_vals = {
|
||||
# if mail_to is void, keep falsy values to allow searching / debugging traces
|
||||
'email': recipients_info[res_id]['mail_to'][0] if recipients_info[res_id]['mail_to'] else '',
|
||||
'mass_mailing_id': mass_mailing.id,
|
||||
'model': self.model,
|
||||
'res_id': res_id,
|
||||
}
|
||||
# propagate failed states to trace when still-born
|
||||
if mail_values.get('state') == 'cancel':
|
||||
trace_vals['trace_status'] = 'cancel'
|
||||
elif mail_values.get('state') == 'exception':
|
||||
trace_vals['trace_status'] = 'error'
|
||||
if mail_values.get('failure_type'):
|
||||
trace_vals['failure_type'] = mail_values['failure_type']
|
||||
|
||||
mail_values.update({
|
||||
'auto_delete': not mass_mailing.keep_archives,
|
||||
# email-mode: keep original message for routing
|
||||
'is_notification': mass_mailing.reply_to_mode == 'update',
|
||||
'mailing_id': mass_mailing.id,
|
||||
'mailing_trace_ids': [(0, 0, trace_vals)],
|
||||
})
|
||||
return res
|
||||
mail_values.update({
|
||||
'mailing_id': self.mass_mailing_id.id,
|
||||
'mailing_trace_ids': [(0, 0, trace_values_all[res_id])] if res_id in trace_values_all else False,
|
||||
})
|
||||
return mail_values_all
|
||||
|
||||
def _get_done_emails(self, mail_values_dict):
|
||||
seen_list = super(MailComposeMessage, self)._get_done_emails(mail_values_dict)
|
||||
seen_list = super()._get_done_emails(mail_values_dict)
|
||||
if self.mass_mailing_id:
|
||||
seen_list += self.mass_mailing_id._get_seen_list()
|
||||
return seen_list
|
||||
|
||||
def _get_optout_emails(self, mail_values_dict):
|
||||
opt_out_list = super(MailComposeMessage, self)._get_optout_emails(mail_values_dict)
|
||||
opt_out_list = super()._get_optout_emails(mail_values_dict)
|
||||
if self.mass_mailing_id:
|
||||
opt_out_list += self.mass_mailing_id._get_opt_out_list()
|
||||
return opt_out_list
|
||||
|
||||
def _prepare_mail_values_mailing_traces(self, mail_values_all):
|
||||
trace_values_all = dict.fromkeys(mail_values_all.keys(), False)
|
||||
recipients_info = self._get_recipients_data(mail_values_all)
|
||||
for res_id, mail_values in mail_values_all.items():
|
||||
trace_vals = {
|
||||
# if mail_to is void, keep falsy values to allow searching / debugging traces
|
||||
'email': recipients_info[res_id]['mail_to'][0] if recipients_info[res_id]['mail_to'] else '',
|
||||
'mass_mailing_id': self.mass_mailing_id.id,
|
||||
'model': self.model,
|
||||
'res_id': res_id,
|
||||
}
|
||||
# propagate failed states to trace when still-born
|
||||
if mail_values.get('state') == 'cancel':
|
||||
trace_vals['trace_status'] = 'cancel'
|
||||
elif mail_values.get('state') == 'exception':
|
||||
trace_vals['trace_status'] = 'error'
|
||||
if mail_values.get('failure_type'):
|
||||
trace_vals['failure_type'] = mail_values['failure_type']
|
||||
trace_values_all[res_id] = trace_vals
|
||||
return trace_values_all
|
||||
|
||||
def _prepare_mailing_values(self):
|
||||
now = fields.Datetime.now()
|
||||
return {
|
||||
@@ -92,7 +98,7 @@ class MailComposeMessage(models.TransientModel):
|
||||
'body_html': self.body,
|
||||
'campaign_id': self.campaign_id.id,
|
||||
'mailing_model_id': self.env['ir.model']._get(self.model).id,
|
||||
'mailing_domain': self.active_domain,
|
||||
'mailing_domain': self.res_domain if self.res_domain else f"[('id', 'in', {self.res_ids})]",
|
||||
'name': self.mass_mailing_name,
|
||||
'reply_to': self.reply_to if self.reply_to_mode == 'new' else False,
|
||||
'reply_to_mode': self.reply_to_mode,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<field name="model">mail.compose.message</field>
|
||||
<field name="inherit_id" ref="mail.email_compose_message_wizard_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='notify']" position="after">
|
||||
<xpath expr="//field[@name='auto_delete_keep_log']" position="after">
|
||||
<field name="model_is_thread" invisible="1"/>
|
||||
<field name="campaign_id" groups="mass_mailing.group_mass_mailing_campaign"
|
||||
attrs="{'invisible': ['|', ('composition_mode', '!=', 'mass_mail'), ('model_is_thread', '!=', True)]}"/>
|
||||
@@ -19,7 +19,7 @@
|
||||
<attribute name="attrs">
|
||||
{'invisible': [
|
||||
'|',
|
||||
('is_log', '=', False),
|
||||
('subtype_is_log', '=', False),
|
||||
'&',
|
||||
('mass_mailing_name', '!=', ''),
|
||||
('mass_mailing_name', '!=', False)
|
||||
@@ -31,7 +31,7 @@
|
||||
<attribute name="attrs">
|
||||
{'invisible': [
|
||||
'|',
|
||||
('is_log', '=', True),
|
||||
('subtype_is_log', '=', True),
|
||||
'&',
|
||||
('mass_mailing_name', '!=', ''),
|
||||
('mass_mailing_name', '!=', False)
|
||||
|
||||
@@ -212,7 +212,8 @@ class MrpUnbuild(models.Model):
|
||||
)
|
||||
self.mo_id.message_post(
|
||||
body=unbuild_msg,
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return self.write({'state': 'done'})
|
||||
|
||||
def _generate_consume_moves(self):
|
||||
|
||||
@@ -48,6 +48,7 @@ class StockRule(models.Model):
|
||||
|
||||
productions_values_by_company[procurement.company_id.id].append(rule._prepare_mo_vals(*procurement, bom))
|
||||
|
||||
note_subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
|
||||
for company_id, productions_values in productions_values_by_company.items():
|
||||
# create the MO as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example)
|
||||
productions = self.env['mrp.production'].with_user(SUPERUSER_ID).sudo().with_company(company_id).create(productions_values)
|
||||
@@ -61,17 +62,20 @@ class StockRule(models.Model):
|
||||
production.message_post(
|
||||
body=_('This production order has been created from Replenishment Report.'),
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_note')
|
||||
subtype_id=note_subtype_id
|
||||
)
|
||||
elif orderpoint:
|
||||
production.message_post_with_view(
|
||||
production.message_post_with_source(
|
||||
'mail.message_origin_link',
|
||||
values={'self': production, 'origin': orderpoint},
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
render_values={'self': production, 'origin': orderpoint},
|
||||
subtype_id=note_subtype_id,
|
||||
)
|
||||
elif origin_production:
|
||||
production.message_post_with_view(
|
||||
production.message_post_with_source(
|
||||
'mail.message_origin_link',
|
||||
values={'self': production, 'origin': origin_production},
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
render_values={'self': production, 'origin': origin_production},
|
||||
subtype_id=note_subtype_id,
|
||||
)
|
||||
return True
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -81,10 +81,10 @@ class ResCompany(models.Model):
|
||||
if additional_data:
|
||||
template_values = json.loads(additional_data)
|
||||
template_values['flavor_text'] = _("Company auto-completed by Odoo Partner Autocomplete Service")
|
||||
self.partner_id.message_post_with_view(
|
||||
self.partner_id.message_post_with_source(
|
||||
'iap_mail.enrich_company',
|
||||
values=template_values,
|
||||
subtype_id=self.env.ref('mail.mt_note').id,
|
||||
render_values=template_values,
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -166,10 +166,10 @@ class ResPartner(models.Model):
|
||||
if partners.additional_info:
|
||||
template_values = json.loads(partners.additional_info)
|
||||
template_values['flavor_text'] = _("Partner created by Odoo Partner Autocomplete Service")
|
||||
partners.message_post_with_view(
|
||||
partners.message_post_with_source(
|
||||
'iap_mail.enrich_company',
|
||||
values=template_values,
|
||||
subtype_id=self.env.ref('mail.mt_note').id,
|
||||
render_values=template_values,
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
partners.write({'additional_info': False})
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ class PhoneBlackList(models.Model):
|
||||
for record in new_records:
|
||||
record.with_context(mail_create_nosubscribe=True).message_post(
|
||||
body=message,
|
||||
subtype_id=self.env.ref('mail.mt_note').id,
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
records += new_records
|
||||
return records
|
||||
@@ -123,7 +123,7 @@ class PhoneBlackList(models.Model):
|
||||
for record in new_records:
|
||||
record.with_context(mail_create_nosubscribe=True).message_post(
|
||||
body=message,
|
||||
subtype_id=self.env.ref('mail.mt_note').id,
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
records += new_records
|
||||
return records
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from markupsafe import Markup
|
||||
from functools import partial
|
||||
from itertools import groupby
|
||||
from collections import defaultdict
|
||||
@@ -923,7 +924,12 @@ class PosOrder(models.Model):
|
||||
}
|
||||
|
||||
def _prepare_mail_values(self, name, client, ticket):
|
||||
message = _("<p>Dear %s,<br/>Here is your electronic ticket for the %s. </p>") % (client['name'], name)
|
||||
message = Markup(
|
||||
_("<p>Dear %(client_name)s,<br/>Here is your electronic ticket for the %(pos_name)s. </p>")
|
||||
) % {
|
||||
'client_name': client['name'],
|
||||
'pos_name': name,
|
||||
}
|
||||
|
||||
return {
|
||||
'subject': _('Receipt %s', name),
|
||||
|
||||
@@ -57,28 +57,24 @@ class PortalShare(models.TransientModel):
|
||||
record = res_model.browse(rec.res_id)
|
||||
rec.access_warning = record.access_warning
|
||||
|
||||
@api.model
|
||||
def _get_note(self):
|
||||
return self.env.ref('mail.mt_note')
|
||||
|
||||
def _send_public_link(self, note, partners=None):
|
||||
def _send_public_link(self, partners=None):
|
||||
if partners is None:
|
||||
partners = self.partner_ids
|
||||
for partner in partners:
|
||||
share_link = self.resource_ref.get_base_url() + self.resource_ref._get_share_url(redirect=True, pid=partner.id)
|
||||
saved_lang = self.env.lang
|
||||
self = self.with_context(lang=partner.lang)
|
||||
template = self.env.ref('portal.portal_share_template', False)
|
||||
self.resource_ref.message_post_with_view(template,
|
||||
values={'partner': partner, 'note': self.note, 'record': self.resource_ref,
|
||||
self.resource_ref.message_post_with_source(
|
||||
'portal.portal_share_template',
|
||||
render_values={'partner': partner, 'note': self.note, 'record': self.resource_ref,
|
||||
'share_link': share_link},
|
||||
subject=_("You are invited to access %s", self.resource_ref.display_name),
|
||||
subtype_id=note.id,
|
||||
subtype_xmlid='mail.mt_note',
|
||||
email_layout_xmlid='mail.mail_notification_light',
|
||||
partner_ids=[(6, 0, partner.ids)])
|
||||
partner_ids=partner.ids)
|
||||
self = self.with_context(lang=saved_lang)
|
||||
|
||||
def _send_signup_link(self, note, partners=None):
|
||||
def _send_signup_link(self, partners=None):
|
||||
if partners is None:
|
||||
partners = self.partner_ids.filtered(lambda partner: not partner.user_ids)
|
||||
for partner in partners:
|
||||
@@ -87,18 +83,17 @@ class PortalShare(models.TransientModel):
|
||||
share_link = partner._get_signup_url_for_action(action='/mail/view', res_id=self.res_id, model=self.res_model)[partner.id]
|
||||
saved_lang = self.env.lang
|
||||
self = self.with_context(lang=partner.lang)
|
||||
template = self.env.ref('portal.portal_share_template', False)
|
||||
self.resource_ref.message_post_with_view(template,
|
||||
values={'partner': partner, 'note': self.note, 'record': self.resource_ref,
|
||||
self.resource_ref.message_post_with_source(
|
||||
'portal.portal_share_template',
|
||||
render_values={'partner': partner, 'note': self.note, 'record': self.resource_ref,
|
||||
'share_link': share_link},
|
||||
subject=_("You are invited to access %s", self.resource_ref.display_name),
|
||||
subtype_id=note.id,
|
||||
subtype_xmlid='mail.mt_note',
|
||||
email_layout_xmlid='mail.mail_notification_light',
|
||||
partner_ids=[(6, 0, partner.ids)])
|
||||
partner_ids=partner.ids)
|
||||
self = self.with_context(lang=saved_lang)
|
||||
|
||||
def action_send_mail(self):
|
||||
note = self._get_note()
|
||||
signup_enabled = self.env['ir.config_parameter'].sudo().get_param('auth_signup.invitation_scope') == 'b2c'
|
||||
|
||||
if getattr(self.resource_ref, 'access_token', False) or not signup_enabled:
|
||||
@@ -106,9 +101,9 @@ class PortalShare(models.TransientModel):
|
||||
else:
|
||||
partner_ids = self.partner_ids.filtered(lambda x: x.user_ids)
|
||||
# if partner already user or record has access token send common link in batch to all user
|
||||
self._send_public_link(note, partner_ids)
|
||||
self._send_public_link(partner_ids)
|
||||
# when partner not user send individual mail with signup token
|
||||
self._send_signup_link(note, self.partner_ids - partner_ids)
|
||||
self._send_signup_link(self.partner_ids - partner_ids)
|
||||
|
||||
# subscribe all recipients so that they receive future communication (better than
|
||||
# using autofollow as more precise)
|
||||
|
||||
@@ -15,12 +15,13 @@ class AccountMove(models.Model):
|
||||
# subscribe the partner to the invoice
|
||||
if invoice.partner_id not in invoice.message_partner_ids:
|
||||
invoice.message_subscribe([invoice.partner_id.id])
|
||||
comment_subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
|
||||
for line in invoice.invoice_line_ids:
|
||||
if line.product_id.email_template_id:
|
||||
invoice.message_post_with_template(
|
||||
line.product_id.email_template_id.id,
|
||||
composition_mode="comment",
|
||||
email_layout_xmlid="mail.mail_notification_light"
|
||||
invoice.message_post_with_source(
|
||||
line.product_id.email_template_id,
|
||||
email_layout_xmlid="mail.mail_notification_light",
|
||||
subtype_id=comment_subtype_id,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -734,7 +734,7 @@ class Project(models.Model):
|
||||
project = self[0]
|
||||
if self.user_has_groups('project.group_project_stages') and 'stage_id' in changes and project.stage_id.mail_template_id:
|
||||
res['stage_id'] = (project.stage_id.mail_template_id, {
|
||||
'auto_delete_message': True,
|
||||
'auto_delete_keep_log': False,
|
||||
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
'email_layout_xmlid': 'mail.mail_notification_light',
|
||||
})
|
||||
@@ -2330,7 +2330,7 @@ class Task(models.Model):
|
||||
test_task = self[0]
|
||||
if 'stage_id' in changes and test_task.stage_id.mail_template_id:
|
||||
res['stage_id'] = (test_task.stage_id.mail_template_id, {
|
||||
'auto_delete_message': True,
|
||||
'auto_delete_keep_log': False,
|
||||
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
'email_layout_xmlid': 'mail.mail_notification_light'
|
||||
})
|
||||
|
||||
@@ -427,7 +427,6 @@
|
||||
<field name="target">new</field>
|
||||
<field name="context" eval="{
|
||||
'default_composition_mode': 'mass_mail',
|
||||
'default_use_template': False,
|
||||
}"/>
|
||||
<field name="binding_model_id" ref="project.model_project_task"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
@@ -440,7 +439,6 @@
|
||||
<field name="target">new</field>
|
||||
<field name="context" eval="{
|
||||
'default_composition_mode': 'mass_mail',
|
||||
'default_use_template': False,
|
||||
}"/>
|
||||
<field name="binding_model_id" ref="project.model_project_project"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
|
||||
@@ -39,10 +39,9 @@ class ProjectShareWizard(models.TransientModel):
|
||||
self.ensure_one()
|
||||
if self.access_mode == 'edit':
|
||||
portal_partners = self.partner_ids.filtered('user_ids')
|
||||
note = self._get_note()
|
||||
self.resource_ref._add_collaborators(self.partner_ids)
|
||||
self._send_public_link(note, portal_partners)
|
||||
self._send_signup_link(note, partners=self.partner_ids - portal_partners)
|
||||
self._send_public_link(portal_partners)
|
||||
self._send_signup_link(partners=self.partner_ids - portal_partners)
|
||||
self.resource_ref.message_subscribe(partner_ids=self.partner_ids.ids)
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
return super().action_send_mail()
|
||||
|
||||
@@ -30,8 +30,7 @@
|
||||
</t>
|
||||
</p>
|
||||
</div></field>
|
||||
<field name="report_template" ref="report_purchase_quotation"/>
|
||||
<field name="report_name">RFQ_{{ (object.name or '').replace('/','_') }}</field>
|
||||
<field name="report_template_ids" eval="[(4, ref('purchase.report_purchase_quotation'))]"/>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
@@ -69,8 +68,7 @@
|
||||
<br/><br/>
|
||||
</p>
|
||||
</div></field>
|
||||
<field name="report_template" ref="action_report_purchase_order"/>
|
||||
<field name="report_name">PO_{{ (object.name or '').replace('/','_') }}</field>
|
||||
<field name="report_template_ids" eval="[(4, ref('purchase.action_report_purchase_order'))]"/>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
@@ -109,8 +107,7 @@
|
||||
<br/><br/>
|
||||
</p>
|
||||
</div></field>
|
||||
<field name="report_template" ref="action_report_purchase_order"/>
|
||||
<field name="report_name">PO_{{ (object.name or '').replace('/','_') }}</field>
|
||||
<field name="report_template_ids" eval="[(4, ref('purchase.action_report_purchase_order'))]"/>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
@@ -419,10 +419,7 @@ class PurchaseOrder(models.Model):
|
||||
ctx = dict(self.env.context or {})
|
||||
ctx.update({
|
||||
'default_model': 'purchase.order',
|
||||
'active_model': 'purchase.order',
|
||||
'active_id': self.ids[0],
|
||||
'default_res_id': self.ids[0],
|
||||
'default_use_template': bool(template_id),
|
||||
'default_res_ids': self.ids,
|
||||
'default_template_id': template_id,
|
||||
'default_composition_mode': 'comment',
|
||||
'default_email_layout_xmlid': "mail.mail_notification_layout_with_responsible_signature",
|
||||
@@ -750,7 +747,11 @@ class PurchaseOrder(models.Model):
|
||||
if send_single:
|
||||
return order._send_reminder_open_composer(template.id)
|
||||
else:
|
||||
order.with_context(is_reminder=True).message_post_with_template(template.id, email_layout_xmlid="mail.mail_notification_layout_with_responsible_signature", composition_mode='comment')
|
||||
order.with_context(is_reminder=True).message_post_with_source(
|
||||
template,
|
||||
email_layout_xmlid="mail.mail_notification_layout_with_responsible_signature",
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
|
||||
def send_reminder_preview(self):
|
||||
self.ensure_one()
|
||||
@@ -777,10 +778,7 @@ class PurchaseOrder(models.Model):
|
||||
ctx = dict(self.env.context or {})
|
||||
ctx.update({
|
||||
'default_model': 'purchase.order',
|
||||
'active_model': 'purchase.order',
|
||||
'active_id': self.ids[0],
|
||||
'default_res_id': self.ids[0],
|
||||
'default_use_template': bool(template_id),
|
||||
'default_res_ids': self.ids,
|
||||
'default_template_id': template_id,
|
||||
'default_composition_mode': 'comment',
|
||||
'default_email_layout_xmlid': "mail.mail_notification_layout_with_responsible_signature",
|
||||
@@ -1100,9 +1098,11 @@ class PurchaseOrderLine(models.Model):
|
||||
if 'product_qty' in values:
|
||||
for line in self:
|
||||
if line.order_id.state == 'purchase':
|
||||
line.order_id.message_post_with_view('purchase.track_po_line_template',
|
||||
values={'line': line, 'product_qty': values['product_qty']},
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
line.order_id.message_post_with_source(
|
||||
'purchase.track_po_line_template',
|
||||
render_values={'line': line, 'product_qty': values['product_qty']},
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
if 'qty_received' in values:
|
||||
for line in self:
|
||||
line._track_qty_received(values['qty_received'])
|
||||
@@ -1419,10 +1419,10 @@ class PurchaseOrderLine(models.Model):
|
||||
def _track_qty_received(self, new_qty):
|
||||
self.ensure_one()
|
||||
if new_qty != self.qty_received and self.order_id.state == 'purchase':
|
||||
self.order_id.message_post_with_view(
|
||||
self.order_id.message_post_with_source(
|
||||
'purchase.track_po_line_qty_received_template',
|
||||
values={'line': self, 'qty_received': new_qty},
|
||||
subtype_id=self.env.ref('mail.mt_note').id
|
||||
render_values={'line': self, 'qty_received': new_qty},
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
def _validate_analytic_distribution(self):
|
||||
|
||||
@@ -142,13 +142,13 @@ class PurchaseOrder(models.Model):
|
||||
origin_po_id.purchase_group_id.order_ids |= orders
|
||||
else:
|
||||
self.env['purchase.order.group'].create({'order_ids': [Command.set(origin_po_id.ids + orders.ids)]})
|
||||
mt_note = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
|
||||
for order in orders:
|
||||
if order.requisition_id:
|
||||
order.message_post_with_view(
|
||||
order.message_post_with_source(
|
||||
'mail.message_origin_link',
|
||||
values={'self': order, 'origin': order.requisition_id},
|
||||
subtype_id=mt_note)
|
||||
render_values={'self': order, 'origin': order.requisition_id},
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return orders
|
||||
|
||||
def write(self, vals):
|
||||
@@ -158,10 +158,10 @@ class PurchaseOrder(models.Model):
|
||||
result = super(PurchaseOrder, self).write(vals)
|
||||
if vals.get('requisition_id'):
|
||||
for order in self:
|
||||
order.message_post_with_view(
|
||||
order.message_post_with_source(
|
||||
'mail.message_origin_link',
|
||||
values={'self': order, 'origin': order.requisition_id, 'edit': True},
|
||||
subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
|
||||
render_values={'self': order, 'origin': order.requisition_id, 'edit': True},
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
if vals.get('alternative_po_ids', False):
|
||||
if not self.purchase_group_id and len(self.alternative_po_ids + self) > len(self):
|
||||
|
||||
@@ -251,9 +251,11 @@ class PurchaseOrder(models.Model):
|
||||
# Get following pickings (created by push rules) to confirm them as well.
|
||||
forward_pickings = self.env['stock.picking']._get_impacted_pickings(moves)
|
||||
(pickings | forward_pickings).action_confirm()
|
||||
picking.message_post_with_view('mail.message_origin_link',
|
||||
values={'self': picking, 'origin': order},
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
picking.message_post_with_source(
|
||||
'mail.message_origin_link',
|
||||
render_values={'self': picking, 'origin': order},
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return True
|
||||
|
||||
def _add_picking_info(self, activity):
|
||||
|
||||
@@ -30,13 +30,3 @@ class MailThread(models.AbstractModel):
|
||||
self.env['rating.rating'].browse(rating_id).write({'message_id': message.id})
|
||||
|
||||
return message
|
||||
|
||||
def _message_create(self, values_list):
|
||||
""" Force usage of rating-specific methods and API allowing to delegate
|
||||
computation to records. Keep methods optimized and skip rating_ids
|
||||
support to simplify MailThrad main API. """
|
||||
if not isinstance(values_list, (list)):
|
||||
values_list = [values_list]
|
||||
if any(values.get('rating_ids') for values in values_list):
|
||||
raise ValueError(_("Posting a rating should be done using message post API."))
|
||||
return super()._message_create(values_list)
|
||||
|
||||
@@ -181,12 +181,12 @@ class RatingMixin(models.AbstractModel):
|
||||
"""
|
||||
if lang:
|
||||
template = template.with_context(lang=lang)
|
||||
for record in self.with_context(mail_notify_force_send=force_send):
|
||||
record.message_post_with_template(
|
||||
template.id,
|
||||
email_layout_xmlid='mail.mail_notification_light',
|
||||
subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
)
|
||||
self.with_context(mail_notify_force_send=force_send).message_post_with_source(
|
||||
template,
|
||||
email_layout_xmlid='mail.mail_notification_light',
|
||||
force_send=force_send,
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
def rating_apply(self, rate, token=None, rating=None, feedback=None,
|
||||
subtype_xmlid=None, notify_delay_send=False):
|
||||
@@ -223,7 +223,7 @@ class RatingMixin(models.AbstractModel):
|
||||
if subtype_xmlid is None:
|
||||
subtype_id = self._rating_apply_get_default_subtype_id()
|
||||
else:
|
||||
subtype_id = self.env['ir.model.data']._xmlid_to_res_id(subtype_xmlid)
|
||||
subtype_id = False
|
||||
feedback = tools.plaintext2html(feedback or '')
|
||||
|
||||
scheduled_datetime = (
|
||||
@@ -249,6 +249,7 @@ class RatingMixin(models.AbstractModel):
|
||||
rating_id=rating.id,
|
||||
scheduled_date=scheduled_datetime,
|
||||
subtype_id=subtype_id,
|
||||
subtype_xmlid=subtype_xmlid,
|
||||
)
|
||||
return rating
|
||||
|
||||
|
||||
@@ -28,8 +28,7 @@
|
||||
</t>
|
||||
</p>
|
||||
</div></field>
|
||||
<field name="report_template" ref="action_report_repair_order"/>
|
||||
<field name="report_name">{{ (object.name or '').replace('/','_') }}</field>
|
||||
<field name="report_template_ids" eval="[(4, ref('repair.action_report_repair_order'))]"/>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
@@ -332,8 +332,7 @@ class Repair(models.Model):
|
||||
template_id = self.env.ref('repair.mail_template_repair_quotation').id
|
||||
ctx = {
|
||||
'default_model': 'repair.order',
|
||||
'default_res_id': self.id,
|
||||
'default_use_template': bool(template_id),
|
||||
'default_res_ids': self.ids,
|
||||
'default_template_id': template_id,
|
||||
'default_composition_mode': 'comment',
|
||||
'default_email_layout_xmlid': 'mail.mail_notification_light',
|
||||
|
||||
@@ -39,8 +39,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
<field name="report_template" ref="action_report_saleorder"/>
|
||||
<field name="report_name">{{ (object.name or '').replace('/','_') }}</field>
|
||||
<field name="report_template_ids" eval="[(4, ref('sale.action_report_saleorder'))]"/>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
@@ -234,8 +233,7 @@
|
||||
</div>
|
||||
</t>
|
||||
</div></field>
|
||||
<field name="report_template" ref="action_report_saleorder"/>
|
||||
<field name="report_name">{{ (object.name or '').replace('/','_') }}</field>
|
||||
<field name="report_template_ids" eval="[(4, ref('sale.action_report_saleorder'))]"/>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
@@ -125,11 +125,15 @@ class PaymentTransaction(models.Model):
|
||||
return super()._reconcile_after_done()
|
||||
|
||||
def _send_invoice(self):
|
||||
template_id = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'sale.default_invoice_email_template'
|
||||
)
|
||||
template_id = int(self.env['ir.config_parameter'].sudo().get_param(
|
||||
'sale.default_invoice_email_template',
|
||||
default=0
|
||||
))
|
||||
if not template_id:
|
||||
return
|
||||
template = self.env['mail.template'].browse(template_id).exists()
|
||||
if not template:
|
||||
return
|
||||
|
||||
for tx in self:
|
||||
tx = tx.with_company(tx.company_id).with_context(
|
||||
@@ -139,11 +143,11 @@ class PaymentTransaction(models.Model):
|
||||
lambda i: not i.is_move_sent and i.state == 'posted' and i._is_ready_to_be_sent()
|
||||
)
|
||||
invoice_to_send.is_move_sent = True # Mark invoice as sent
|
||||
for invoice in invoice_to_send.with_user(SUPERUSER_ID):
|
||||
invoice.message_post_with_template(
|
||||
int(template_id),
|
||||
email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature',
|
||||
)
|
||||
invoice_to_send.with_user(SUPERUSER_ID).message_post_with_source(
|
||||
template,
|
||||
email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
|
||||
def _cron_send_invoice(self):
|
||||
"""
|
||||
|
||||
@@ -111,21 +111,20 @@ class ResCompany(models.Model):
|
||||
template = self.env.ref('sale.email_template_edi_sale', False)
|
||||
|
||||
message_composer = self.env['mail.compose.message'].with_context(
|
||||
default_use_template=bool(template),
|
||||
mark_so_as_sent=True,
|
||||
default_email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature',
|
||||
proforma=self.env.context.get('proforma', False),
|
||||
force_email=True,
|
||||
).create({
|
||||
'res_id': sample_sales_order.id,
|
||||
'template_id': template and template.id or False,
|
||||
'model': 'sale.order',
|
||||
'composition_mode': 'comment'
|
||||
'res_ids': sample_sales_order.ids,
|
||||
'template_id': template.id if template else False,
|
||||
'model': sample_sales_order._name,
|
||||
'composition_mode': 'comment',
|
||||
})
|
||||
|
||||
# Simulate the onchange (like trigger in form the view)
|
||||
update_values = message_composer._onchange_template_id(
|
||||
template.id, 'comment', 'sale.order', sample_sales_order.id
|
||||
template.id, 'comment', sample_sales_order._name, sample_sales_order.ids
|
||||
)['value']
|
||||
message_composer.write(update_values)
|
||||
|
||||
|
||||
@@ -713,8 +713,7 @@ class SaleOrder(models.Model):
|
||||
lang = mail_template._render_lang(self.ids)[self.id]
|
||||
ctx = {
|
||||
'default_model': 'sale.order',
|
||||
'default_res_id': self.id,
|
||||
'default_use_template': bool(mail_template),
|
||||
'default_res_ids': self.ids,
|
||||
'default_template_id': mail_template.id if mail_template else None,
|
||||
'default_composition_mode': 'comment',
|
||||
'mark_so_as_sent': True,
|
||||
@@ -842,10 +841,10 @@ class SaleOrder(models.Model):
|
||||
mail_template = sale_order._get_confirmation_template()
|
||||
if not mail_template:
|
||||
continue
|
||||
sale_order.with_context(force_send=True).message_post_with_template(
|
||||
mail_template.id,
|
||||
composition_mode='comment',
|
||||
sale_order.with_context(force_send=True).message_post_with_source(
|
||||
mail_template,
|
||||
email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
|
||||
def action_done(self):
|
||||
@@ -877,7 +876,6 @@ class SaleOrder(models.Model):
|
||||
if template.lang:
|
||||
lang = template._render_lang(self.ids)[self.id]
|
||||
ctx = {
|
||||
'default_use_template': bool(template_id),
|
||||
'default_template_id': template_id,
|
||||
'default_order_id': self.id,
|
||||
'mark_so_as_canceled': True,
|
||||
@@ -1181,10 +1179,11 @@ class SaleOrder(models.Model):
|
||||
if final:
|
||||
moves.sudo().filtered(lambda m: m.amount_total < 0).action_switch_move_type()
|
||||
for move in moves:
|
||||
move.message_post_with_view(
|
||||
move.message_post_with_source(
|
||||
'mail.message_origin_link',
|
||||
values={'self': move, 'origin': move.line_ids.sale_line_ids.order_id},
|
||||
subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'))
|
||||
render_values={'self': move, 'origin': move.line_ids.sale_line_ids.order_id},
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return moves
|
||||
|
||||
# MAIL #
|
||||
|
||||
@@ -87,7 +87,9 @@ class TestSaleSignature(HttpCaseWithUserPortal):
|
||||
# must be sent to the user so he can see it
|
||||
email_act = sales_order.action_quotation_send()
|
||||
email_ctx = email_act.get('context', {})
|
||||
sales_order.with_context(**email_ctx).message_post_with_template(
|
||||
email_ctx.get('default_template_id'))
|
||||
sales_order.with_context(**email_ctx).message_post_with_source(
|
||||
self.env['mail.template'].browse(email_ctx.get('default_template_id')),
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
|
||||
self.start_tour("/", 'sale_signature', login="portal")
|
||||
|
||||
@@ -79,7 +79,10 @@ class TestSaleOrder(SaleCommon):
|
||||
# send quotation
|
||||
email_act = self.sale_order.action_quotation_send()
|
||||
email_ctx = email_act.get('context', {})
|
||||
self.sale_order.with_context(**email_ctx).message_post_with_template(email_ctx.get('default_template_id'))
|
||||
self.sale_order.with_context(**email_ctx).message_post_with_source(
|
||||
self.env['mail.template'].browse(email_ctx.get('default_template_id')),
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
self.assertTrue(self.sale_order.state == 'sent', 'Sale: state after sending is wrong')
|
||||
self.sale_order.order_line._compute_product_updatable()
|
||||
self.assertTrue(self.sale_order.order_line[0].product_updatable)
|
||||
@@ -101,7 +104,10 @@ class TestSaleOrder(SaleCommon):
|
||||
# sent to to author or not (in case author is present in 'Recipients' of composer).
|
||||
mail_template = self.env['mail.template'].browse(email_ctx.get('default_template_id')).copy({'auto_delete': False})
|
||||
# send the mail with same user as customer
|
||||
sale_order.with_context(**email_ctx).with_user(self.sale_user).message_post_with_template(mail_template.id)
|
||||
sale_order.with_context(**email_ctx).with_user(self.sale_user).message_post_with_source(
|
||||
mail_template,
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
self.assertTrue(sale_order.state == 'sent', 'Sale : state should be changed to sent')
|
||||
mail_message = sale_order.message_ids[0]
|
||||
self.assertEqual(mail_message.author_id, sale_order.partner_id, 'Sale: author should be same as customer')
|
||||
|
||||
@@ -632,7 +632,10 @@ class TestSaleToInvoice(TestSaleCommon):
|
||||
# send quotation
|
||||
email_act = self.sale_order.action_quotation_send()
|
||||
email_ctx = email_act.get('context', {})
|
||||
self.sale_order.with_context(**email_ctx).message_post_with_template(email_ctx.get('default_template_id'))
|
||||
self.sale_order.with_context(**email_ctx).message_post_with_source(
|
||||
self.env['mail.template'].browse(email_ctx.get('default_template_id')),
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
self.assertTrue(self.sale_order.state == 'sent', 'Sale: state after sending is wrong')
|
||||
self.sale_order.order_line._compute_product_updatable()
|
||||
self.assertTrue(self.sale_order.order_line[0].product_updatable)
|
||||
|
||||
@@ -186,10 +186,11 @@ class SaleAdvancePaymentInv(models.TransientModel):
|
||||
self._prepare_invoice_values(order, down_payment_so_line)
|
||||
).with_user(self.env.uid) # Unsudo the invoice after creation
|
||||
|
||||
invoice.message_post_with_view(
|
||||
invoice.message_post_with_source(
|
||||
'mail.message_origin_link',
|
||||
values={'self': invoice, 'origin': order},
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
render_values={'self': invoice, 'origin': order},
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
return invoice
|
||||
|
||||
|
||||
@@ -11,18 +11,11 @@ class SaleOrderCancel(models.TransientModel):
|
||||
_inherit = 'mail.composer.mixin'
|
||||
_description = "Sales Order Cancel"
|
||||
|
||||
@api.model
|
||||
def _default_email_from(self):
|
||||
if self.env.user.email:
|
||||
return formataddr((self.env.user.name, self.env.user.email))
|
||||
raise UserError(_("Unable to post message, please configure the sender's email address."))
|
||||
|
||||
@api.model
|
||||
def _default_author_id(self):
|
||||
return self.env.user.partner_id
|
||||
|
||||
# origin
|
||||
email_from = fields.Char(string="From", default=_default_email_from)
|
||||
author_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string="Author",
|
||||
@@ -81,9 +74,9 @@ class SaleOrderCancel(models.TransientModel):
|
||||
def action_send_mail_and_cancel(self):
|
||||
self.ensure_one()
|
||||
self.order_id.message_post(
|
||||
author_id=self.author_id.id,
|
||||
body=self.body,
|
||||
message_type='comment',
|
||||
email_from=self.email_from,
|
||||
email_layout_xmlid='mail.mail_notification_light',
|
||||
partner_ids=self.recipient_ids.ids,
|
||||
subject=self.subject,
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group col="1">
|
||||
<field name="author_id" invisible="1"/>
|
||||
<field name="lang" invisible="1"/>
|
||||
<field name="render_model" invisible="1"/>
|
||||
<field name="order_id" invisible="1"/>
|
||||
<field name="template_id" invisible="1"/>
|
||||
|
||||
@@ -42,10 +42,11 @@ class StockMove(models.Model):
|
||||
picking_id = self.mapped('picking_id')
|
||||
sale_order_ids = self.mapped('sale_line_id.order_id')
|
||||
for sale_order_id in sale_order_ids:
|
||||
picking_id.message_post_with_view(
|
||||
picking_id.message_post_with_source(
|
||||
'mail.message_origin_link',
|
||||
values={'self': picking_id, 'origin': sale_order_id},
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
render_values={'self': picking_id, 'origin': sale_order_id},
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
|
||||
class ProcurementGroup(models.Model):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user