diff --git a/addons/account/data/mail_template_data.xml b/addons/account/data/mail_template_data.xml index 4ec06155817..4a6a17ec2a3 100644 --- a/addons/account/data/mail_template_data.xml +++ b/addons/account/data/mail_template_data.xml @@ -46,7 +46,7 @@

Do not hesitate to contact us if you have any questions. - +
--
Mitchell Admin
@@ -75,7 +75,7 @@ Do not hesitate to contact us if you have any questions.

Best regards, - +
--
Mitchell Admin
@@ -119,7 +119,7 @@ from YourCompany.

Do not hesitate to contact us if you have any questions. - +
--
Mitchell Admin
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index a8484dba4d2..9330d3c3e17 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -3,7 +3,7 @@ from odoo import api, fields, models, Command, _ from odoo.exceptions import RedirectWarning, UserError, ValidationError, AccessError from odoo.tools import float_compare, date_utils, email_split, email_re, html_escape, is_html_empty, sql -from odoo.tools.misc import formatLang, format_date, get_lang +from odoo.tools.misc import format_amount, formatLang, format_date, get_lang from datetime import date, timedelta from collections import defaultdict @@ -11,6 +11,7 @@ from contextlib import contextmanager from itertools import zip_longest from hashlib import sha256 from json import dumps +from markupsafe import Markup import ast import json @@ -2346,41 +2347,6 @@ class AccountMove(models.Model): result.append((move.id, name)) return result - def _creation_subtype(self): - # OVERRIDE - if self.move_type in ('out_invoice', 'out_refund', 'out_receipt'): - return self.env.ref('account.mt_invoice_created') - else: - return super(AccountMove, self)._creation_subtype() - - def _track_subtype(self, init_values): - # OVERRIDE to add custom subtype depending of the state. - self.ensure_one() - - if not self.is_invoice(include_receipts=True): - if self.payment_id and 'state' in init_values: - self.payment_id._message_track(['state'], {self.payment_id.id: init_values}) - return super(AccountMove, self)._track_subtype(init_values) - - if 'payment_state' in init_values and self.payment_state == 'paid': - return self.env.ref('account.mt_invoice_paid') - elif 'state' in init_values and self.state == 'posted' and self.is_sale_document(include_receipts=True): - return self.env.ref('account.mt_invoice_validated') - return super(AccountMove, self)._track_subtype(init_values) - - def _creation_message(self): - # OVERRIDE - if not self.is_invoice(include_receipts=True): - return super()._creation_message() - return { - 'out_invoice': _('Invoice Created'), - 'out_refund': _('Credit Note Created'), - 'in_invoice': _('Vendor Bill Created'), - 'in_refund': _('Refund Created'), - 'out_receipt': _('Sales Receipt Created'), - 'in_receipt': _('Purchase Receipt Created'), - }[self.move_type] - # ------------------------------------------------------------------------- # RECONCILIATION METHODS # ------------------------------------------------------------------------- @@ -2857,59 +2823,6 @@ class AccountMove(models.Model): 'views': [(self.env.ref('account.view_move_tree').id, 'tree'), (False, 'form')], } - @api.model - def message_new(self, msg_dict, custom_values=None): - # OVERRIDE - # Add custom behavior when receiving a new invoice through the mail's gateway. - if (custom_values or {}).get('move_type', 'entry') not in ('out_invoice', 'in_invoice'): - return super().message_new(msg_dict, custom_values=custom_values) - - def is_internal_partner(partner): - # Helper to know if the partner is an internal one. - return partner.user_ids and all(user.has_group('base.group_user') for user in partner.user_ids) - - extra_domain = False - if custom_values.get('company_id'): - extra_domain = ['|', ('company_id', '=', custom_values['company_id']), ('company_id', '=', False)] - - # Search for partners in copy. - cc_mail_addresses = email_split(msg_dict.get('cc', '')) - followers = [partner for partner in self._mail_find_partner_from_emails(cc_mail_addresses, extra_domain) if partner] - - # Search for partner that sent the mail. - from_mail_addresses = email_split(msg_dict.get('from', '')) - senders = partners = [partner for partner in self._mail_find_partner_from_emails(from_mail_addresses, extra_domain) if partner] - - # Search for partners using the user. - if not senders: - senders = partners = list(self._mail_search_on_user(from_mail_addresses)) - - if partners: - # Check we are not in the case when an internal user forwarded the mail manually. - if is_internal_partner(partners[0]): - # Search for partners in the mail's body. - body_mail_addresses = set(email_re.findall(msg_dict.get('body'))) - partners = [partner for partner in self._mail_find_partner_from_emails(body_mail_addresses, extra_domain) if not is_internal_partner(partner)] - - # Little hack: Inject the mail's subject in the body. - if msg_dict.get('subject') and msg_dict.get('body'): - msg_dict['body'] = '

%s

%s
' % (msg_dict['subject'], msg_dict['body']) - - # Create the invoice. - values = { - 'name': '/', # we have to give the name otherwise it will be set to the mail's subject - 'invoice_source_email': from_mail_addresses[0], - 'partner_id': partners and partners[0].id or False, - } - move_ctx = self.with_context(default_move_type=custom_values['move_type'], default_journal_id=custom_values['journal_id']) - move = super(AccountMove, move_ctx).message_new(msg_dict, custom_values=values) - move._compute_name() # because the name is given, we need to recompute in case it is the first invoice of the journal - - # Assign followers. - all_followers_ids = set(partner.id for partner in followers + senders + partners if is_internal_partner(partner)) - move.message_subscribe(list(all_followers_ids)) - return move - def post(self): warnings.warn( "RedirectWarning method 'post()' is a deprecated alias to 'action_post()' or _post()", @@ -3403,6 +3316,92 @@ class AccountMove(models.Model): return rslt + def _get_create_invoice_from_attachment_decoders(self): + """ Returns a list of method that are able to create an invoice from an attachment and a priority. + + :returns: A list of tuples (priority, method) where method takes an attachment as parameter. + """ + return [] + + def _get_update_invoice_from_attachment_decoders(self, invoice): + """ Returns a list of method that are able to create an invoice from an attachment and a priority. + + :param invoice: The invoice on which to update the data. + :returns: A list of tuples (priority, method) where method takes an attachment as parameter. + """ + return [] + + @api.depends('move_type', 'partner_id', 'company_id') + def _compute_narration(self): + use_invoice_terms = self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms') + for move in self.filtered(lambda am: not am.narration): + if not use_invoice_terms or not move.is_sale_document(include_receipts=True): + move.narration = False + else: + if not move.company_id.terms_type == 'html': + narration = move.company_id.invoice_terms if not is_html_empty(move.company_id.invoice_terms) else '' + else: + baseurl = self.env.company.get_base_url() + '/terms' + narration = _('Terms & Conditions: %s', baseurl) + move.narration = narration or False + + # ------------------------------------------------------------ + # MAIL.THREAD + # ------------------------------------------------------------ + + @api.model + def message_new(self, msg_dict, custom_values=None): + # OVERRIDE + # Add custom behavior when receiving a new invoice through the mail's gateway. + if (custom_values or {}).get('move_type', 'entry') not in ('out_invoice', 'in_invoice'): + return super().message_new(msg_dict, custom_values=custom_values) + + def is_internal_partner(partner): + # Helper to know if the partner is an internal one. + return partner.user_ids and all(user.has_group('base.group_user') for user in partner.user_ids) + + extra_domain = False + if custom_values.get('company_id'): + extra_domain = ['|', ('company_id', '=', custom_values['company_id']), ('company_id', '=', False)] + + # Search for partners in copy. + cc_mail_addresses = email_split(msg_dict.get('cc', '')) + followers = [partner for partner in self._mail_find_partner_from_emails(cc_mail_addresses, extra_domain) if partner] + + # Search for partner that sent the mail. + from_mail_addresses = email_split(msg_dict.get('from', '')) + senders = partners = [partner for partner in self._mail_find_partner_from_emails(from_mail_addresses, extra_domain) if partner] + + # Search for partners using the user. + if not senders: + senders = partners = list(self._mail_search_on_user(from_mail_addresses)) + + if partners: + # Check we are not in the case when an internal user forwarded the mail manually. + if is_internal_partner(partners[0]): + # Search for partners in the mail's body. + body_mail_addresses = set(email_re.findall(msg_dict.get('body'))) + partners = [partner for partner in self._mail_find_partner_from_emails(body_mail_addresses, extra_domain) if not is_internal_partner(partner)] + + # Little hack: Inject the mail's subject in the body. + if msg_dict.get('subject') and msg_dict.get('body'): + msg_dict['body'] = '

%s

%s
' % (msg_dict['subject'], msg_dict['body']) + + # Create the invoice. + values = { + 'name': '/', # we have to give the name otherwise it will be set to the mail's subject + 'invoice_source_email': from_mail_addresses[0], + 'partner_id': partners and partners[0].id or False, + } + move_ctx = self.with_context(default_move_type=custom_values['move_type'], default_journal_id=custom_values['journal_id']) + move = super(AccountMove, move_ctx).message_new(msg_dict, custom_values=values) + move._compute_name() # because the name is given, we need to recompute in case it is the first invoice of the journal + + # Assign followers. + all_followers_ids = set(partner.id for partner in followers + senders + partners if is_internal_partner(partner)) + move.message_subscribe(list(all_followers_ids)) + return move + def _message_post_after_hook(self, new_message, message_values): # OVERRIDE # When posting a message, check the attachment to see if it's an invoice and update with the imported data. @@ -3437,34 +3436,56 @@ class AccountMove(models.Model): return res - def _get_create_invoice_from_attachment_decoders(self): - """ Returns a list of method that are able to create an invoice from an attachment and a priority. + def _creation_subtype(self): + # OVERRIDE + if self.move_type in ('out_invoice', 'out_refund', 'out_receipt'): + return self.env.ref('account.mt_invoice_created') + else: + return super(AccountMove, self)._creation_subtype() - :returns: A list of tuples (priority, method) where method takes an attachment as parameter. - """ - return [] + def _track_subtype(self, init_values): + # OVERRIDE to add custom subtype depending of the state. + self.ensure_one() - def _get_update_invoice_from_attachment_decoders(self, invoice): - """ Returns a list of method that are able to create an invoice from an attachment and a priority. + if not self.is_invoice(include_receipts=True): + if self.payment_id and 'state' in init_values: + self.payment_id._message_track(['state'], {self.payment_id.id: init_values}) + return super(AccountMove, self)._track_subtype(init_values) - :param invoice: The invoice on which to update the data. - :returns: A list of tuples (priority, method) where method takes an attachment as parameter. - """ - return [] + if 'payment_state' in init_values and self.payment_state == 'paid': + return self.env.ref('account.mt_invoice_paid') + elif 'state' in init_values and self.state == 'posted' and self.is_sale_document(include_receipts=True): + return self.env.ref('account.mt_invoice_validated') + return super(AccountMove, self)._track_subtype(init_values) - @api.depends('move_type', 'partner_id', 'company_id') - def _compute_narration(self): - use_invoice_terms = self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms') - for move in self.filtered(lambda am: not am.narration): - if not use_invoice_terms or not move.is_sale_document(include_receipts=True): - move.narration = False - else: - if not move.company_id.terms_type == 'html': - narration = move.company_id.invoice_terms if not is_html_empty(move.company_id.invoice_terms) else '' - else: - baseurl = self.env.company.get_base_url() + '/terms' - narration = _('Terms & Conditions: %s', baseurl) - move.narration = narration or False + def _creation_message(self): + # OVERRIDE + if not self.is_invoice(include_receipts=True): + return super()._creation_message() + return { + 'out_invoice': _('Invoice Created'), + 'out_refund': _('Credit Note Created'), + 'in_invoice': _('Vendor Bill Created'), + 'in_refund': _('Refund Created'), + 'out_receipt': _('Sales Receipt Created'), + 'in_receipt': _('Purchase Receipt Created'), + }[self.move_type] + + def _notify_by_email_prepare_rendering_context(self, message, msg_vals, model_description=False, + force_email_company=False, force_email_lang=False): + render_context = super()._notify_by_email_prepare_rendering_context( + message, msg_vals, model_description=model_description, + force_email_company=force_email_company, force_email_lang=force_email_lang + ) + if self.invoice_date_due: + amount_txt = _('%(amount)s due %(date)s', + amount=format_amount(self.env, self.amount_total, self.currency_id, lang_code=render_context.get('lang')), + date=format_date(self.env, self.invoice_date_due, date_format='short', lang_code=render_context.get('lang')) + ) + else: + amount_txt = format_amount(self.env, self.amount_total, self.currency_id, lang_code=render_context.get('lang')) + render_context['subtitle'] = Markup("%s
%s
") % (self.name, amount_txt) + return render_context class AccountMoveLine(models.Model): diff --git a/addons/auth_signup/data/mail_template_data.xml b/addons/auth_signup/data/mail_template_data.xml index 79ff8ee3467..ba5c3768499 100644 --- a/addons/auth_signup/data/mail_template_data.xml +++ b/addons/auth_signup/data/mail_template_data.xml @@ -9,8 +9,8 @@ "{{ object.company_id.name }}" <{{ (object.company_id.email or user.email) }}> {{ object.email_formatted }} -
- +
+ @@ -104,8 +104,8 @@ "{{ object.company_id.name }}" <{{ (object.company_id.email or user.email) }}>{{ object.email_formatted }} -
- +
+ @@ -199,8 +199,8 @@ {{ object.company_id.partner_id.email_formatted }}{{ object.email_formatted }} -
- +
+ @@ -250,8 +250,8 @@ "{{ object.company_id.name }}" <{{ (object.company_id.email or user.email) }}>{{ object.email_formatted }} -
- +
+ diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 9bcd56bd1cd..d7b909347fb 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -1751,10 +1751,10 @@ class Lead(models.Model): return self.env.ref('crm.mt_lead_lost') return super(Lead, self)._track_subtype(init_values) - def _notify_get_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, msg_vals=None): """ Handle salesman recipients that can convert leads into opportunities and set opportunities as won / lost. """ - groups = super(Lead, self)._notify_get_groups(msg_vals=msg_vals) + groups = super(Lead, self)._notify_get_recipients_groups(msg_vals=msg_vals) local_msg_vals = dict(msg_vals or {}) self.ensure_one() @@ -1777,19 +1777,20 @@ class Lead(models.Model): salesman_group_id = self.env.ref('sales_team.group_sale_salesman').id new_group = ( - 'group_sale_salesman', lambda pdata: pdata['type'] == 'user' and salesman_group_id in pdata['groups'], { - 'actions': salesman_actions, - }) + 'group_sale_salesman', + lambda pdata: pdata['type'] == 'user' and salesman_group_id in pdata['groups'], + {'actions': salesman_actions} + ) return [new_group] + groups - def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None): + def _notify_get_reply_to(self, default=None): """ Override to set alias of lead and opportunities to their sales team if any. """ - aliases = self.mapped('team_id').sudo()._notify_get_reply_to(default=default, records=None, company=company, doc_names=None) + aliases = self.mapped('team_id').sudo()._notify_get_reply_to(default=default) res = {lead.id: aliases.get(lead.team_id.id) for lead in self} leftover = self.filtered(lambda rec: not rec.team_id) if leftover: - res.update(super(Lead, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names)) + res.update(super(Lead, leftover)._notify_get_reply_to(default=default)) return res def _message_get_default_recipients(self): diff --git a/addons/google_calendar/tests/test_sync_odoo2google.py b/addons/google_calendar/tests/test_sync_odoo2google.py index c69dfdeac86..75374bf6412 100644 --- a/addons/google_calendar/tests/test_sync_odoo2google.py +++ b/addons/google_calendar/tests/test_sync_odoo2google.py @@ -70,7 +70,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__=1211): + with self.assertQueryCount(__system__=1112): events = self.env['calendar.event'].create([{ 'name': "Event %s" % (i), 'start': datetime(2020, 1, 15, 8, 0), @@ -102,7 +102,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__=3635): + with self.assertQueryCount(__system__=2916): event = self.env['calendar.event'].create({ 'name': "Event", 'start': datetime(2020, 1, 15, 8, 0), diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 1d38a5057bf..7d8a4b8dcfc 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -1430,10 +1430,10 @@ class HolidaysRequest(models.Model): return leave_notif_subtype or self.env.ref('hr_holidays.mt_leave') return super(HolidaysRequest, self)._track_subtype(init_values) - def _notify_get_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, msg_vals=None): """ Handle HR users and officers recipients that can validate or refuse holidays directly from email. """ - groups = super(HolidaysRequest, self)._notify_get_groups(msg_vals=msg_vals) + groups = super(HolidaysRequest, self)._notify_get_recipients_groups(msg_vals=msg_vals) local_msg_vals = dict(msg_vals or {}) self.ensure_one() @@ -1447,9 +1447,10 @@ class HolidaysRequest(models.Model): holiday_user_group_id = self.env.ref('hr_holidays.group_hr_holidays_user').id new_group = ( - 'group_hr_holidays_user', lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'], { - 'actions': hr_actions, - }) + 'group_hr_holidays_user', + lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'], + {'actions': hr_actions} + ) return [new_group] + groups diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index c204e89d82e..fa80f799a33 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -743,10 +743,10 @@ class HolidaysAllocation(models.Model): return allocation_notif_subtype_id or self.env.ref('hr_holidays.mt_leave_allocation') return super(HolidaysAllocation, self)._track_subtype(init_values) - def _notify_get_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, msg_vals=None): """ Handle HR users and officers recipients that can validate or refuse holidays directly from email. """ - groups = super(HolidaysAllocation, self)._notify_get_groups(msg_vals=msg_vals) + groups = super(HolidaysAllocation, self)._notify_get_recipients_groups(msg_vals=msg_vals) local_msg_vals = dict(msg_vals or {}) self.ensure_one() @@ -760,9 +760,10 @@ class HolidaysAllocation(models.Model): holiday_user_group_id = self.env.ref('hr_holidays.group_hr_holidays_user').id new_group = ( - 'group_hr_holidays_user', lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'], { - 'actions': hr_actions, - }) + 'group_hr_holidays_user', + lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'], + {'actions': hr_actions} + ) return [new_group] + groups diff --git a/addons/hr_holidays/tests/test_company_leave.py b/addons/hr_holidays/tests/test_company_leave.py index 2a7b78b4b67..73acca240e1 100644 --- a/addons/hr_holidays/tests/test_company_leave.py +++ b/addons/hr_holidays/tests/test_company_leave.py @@ -323,7 +323,7 @@ class TestCompanyLeave(TransactionCase): }) company_leave._compute_date_from_to() - with self.assertQueryCount(__system__=773, admin=865): + with self.assertQueryCount(__system__=659, admin=865): # Original query count: 1987 # Without tracking/activity context keys: 5154 company_leave.action_validate() diff --git a/addons/hr_recruitment/models/hr_recruitment.py b/addons/hr_recruitment/models/hr_recruitment.py index ae3a479aeff..8dd4e0ed81a 100644 --- a/addons/hr_recruitment/models/hr_recruitment.py +++ b/addons/hr_recruitment/models/hr_recruitment.py @@ -500,13 +500,13 @@ class Applicant(models.Model): return self.env.ref('hr_recruitment.mt_applicant_stage_changed') return super(Applicant, self)._track_subtype(init_values) - def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None): + def _notify_get_reply_to(self, default=None): """ Override to set alias of applicants to their job definition if any. """ - aliases = self.mapped('job_id')._notify_get_reply_to(default=default, records=None, company=company, doc_names=None) + aliases = self.mapped('job_id')._notify_get_reply_to(default=default) res = {app.id: aliases.get(app.job_id.id) for app in self} leftover = self.filtered(lambda rec: not rec.job_id) if leftover: - res.update(super(Applicant, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names)) + res.update(super(Applicant, leftover)._notify_get_reply_to(default=default)) return res def _message_get_suggested_recipients(self): diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index bf0987a60bc..c4b7a755e7b 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -2,7 +2,7 @@ { 'name': 'Discuss', - 'version': '1.6', + 'version': '1.7', 'category': 'Productivity/Discuss', 'sequence': 145, 'summary': 'Chat, mail gateway and private channels', diff --git a/addons/mail/controllers/mail.py b/addons/mail/controllers/mail.py index 209afa1d9a6..57d842c0150 100644 --- a/addons/mail/controllers/mail.py +++ b/addons/mail/controllers/mail.py @@ -93,9 +93,9 @@ class MailController(http.Controller): except AccessError: return cls._redirect_to_messaging() else: - record_action = record_sudo.get_access_action(access_uid=uid) + record_action = record_sudo._get_access_action(access_uid=uid) else: - record_action = record_sudo.get_access_action() + record_action = record_sudo._get_access_action() if record_action['type'] == 'ir.actions.act_url' and record_action.get('target_type') != 'public': return cls._redirect_to_messaging() diff --git a/addons/mail/data/mail_templates_email_layouts.xml b/addons/mail/data/mail_templates_email_layouts.xml index 890ea6c5f25..4dba8f28efe 100644 --- a/addons/mail/data/mail_templates_email_layouts.xml +++ b/addons/mail/data/mail_templates_email_layouts.xml @@ -49,7 +49,7 @@
  • : ->
  • -
    +

    Sent @@ -131,33 +131,39 @@

    - +