diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index d93340606df..6585b3ac91b 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -7,6 +7,7 @@ from dateutil.relativedelta import relativedelta from hashlib import sha256 from json import dumps import logging +from markupsafe import Markup, escape from psycopg2 import OperationalError import re from textwrap import shorten @@ -2232,12 +2233,10 @@ class AccountMove(models.Model): default['partner_id'] = False copied_am = super().copy(default) message_origin = '' if not copied_am.auto_post_origin_id else \ - '
' + _('This recurring entry originated from %s', copied_am.auto_post_origin_id._get_html_link()) - copied_am._message_log(body=_( - 'This entry has been duplicated from %s%s', - self._get_html_link(), - message_origin, - )) + (Markup('
') + _('This recurring entry originated from %s')) % copied_am.auto_post_origin_id._get_html_link() + copied_am._message_log(body= + (escape(_('This entry has been duplicated from %s')) % self._get_html_link()) + message_origin, + ) return copied_am diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py index b108b0536d3..b95c8a935ca 100644 --- a/addons/account/models/account_move_line.py +++ b/addons/account/models/account_move_line.py @@ -3,6 +3,7 @@ from collections import defaultdict from contextlib import contextmanager from datetime import date, timedelta from functools import lru_cache +from markupsafe import escape from odoo import api, fields, models, Command, _ from odoo.exceptions import ValidationError, UserError @@ -1448,10 +1449,7 @@ class AccountMoveLine(models.Model): for line in self.filtered(lambda l: l.move_id.id == move_id): tracking_value_ids = line._mail_track(ref_fields, modified_lines)[1] if tracking_value_ids: - msg = _( - "Journal Item %s updated", - line._get_html_link(title=f"#{line.id}") - ) + msg = escape(_("Journal Item %s updated")) % line._get_html_link(title=f"#{line.id}") line.move_id._message_log( body=msg, tracking_value_ids=tracking_value_ids diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 960de318d93..c91d49dbfd7 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from markupsafe import escape + from odoo import models, fields, api, _, Command from odoo.exceptions import UserError, ValidationError from odoo.tools.misc import format_date, formatLang @@ -896,16 +898,9 @@ class AccountPayment(models.Model): }) paired_payment.move_id._post(soft=False) payment.paired_internal_transfer_payment_id = paired_payment - - body = _( - "This payment has been created from %s", - payment._get_html_link(), - ) + body = escape(_("This payment has been created from:")) + payment._get_html_link() paired_payment.message_post(body=body) - body = _( - "A second payment has been created: %s", - paired_payment._get_html_link(), - ) + body = escape(_("A second payment has been created:")) + paired_payment._get_html_link() payment.message_post(body=body) lines = (payment.move_id.line_ids + paired_payment.move_id.line_ids).filtered( diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 2b95a3d3e75..31b955e243d 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- import ast -from collections import defaultdict import csv +from collections import defaultdict from functools import wraps from inspect import getmembers + import logging import re diff --git a/addons/account/models/res_partner_bank.py b/addons/account/models/res_partner_bank.py index 94e78f15b45..5081e530f4b 100644 --- a/addons/account/models/res_partner_bank.py +++ b/addons/account/models/res_partner_bank.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import base64 from collections import defaultdict +from markupsafe import escape import werkzeug import werkzeug.exceptions @@ -241,7 +242,7 @@ class ResPartnerBank(models.Model): # EXTENDS base res.partner.bank res = super().create(vals_list) for account in res: - msg = _("Bank Account %s created", account._get_html_link(title=f"#{account.id}")) + msg = escape(_("Bank Account %s created")) % account._get_html_link(title=f"#{account.id}") account.partner_id._message_log(body=msg) return res @@ -269,7 +270,7 @@ class ResPartnerBank(models.Model): for account, initial_values in account_initial_values.items(): tracking_value_ids = account._mail_track(fields_definition, initial_values)[1] if tracking_value_ids: - msg = _("Bank Account %s updated", account._get_html_link(title=f"#{account.id}")) + msg = escape(_("Bank Account %s updated")) % account._get_html_link(title=f"#{account.id}") account.partner_id._message_log(body=msg, tracking_value_ids=tracking_value_ids) if 'partner_id' in initial_values: # notify previous partner as well initial_values['partner_id']._message_log(body=msg, tracking_value_ids=tracking_value_ids) @@ -278,7 +279,7 @@ class ResPartnerBank(models.Model): def unlink(self): # EXTENDS base res.partner.bank for account in self: - msg = _("Bank Account %s with number %s deleted", account._get_html_link(title=f"#{account.id}"), account.acc_number) + msg = escape(_("Bank Account %s with number %s deleted")) % (account._get_html_link(title=f"#{account.id}"), account.acc_number) account.partner_id._message_log(body=msg) return super().unlink() diff --git a/addons/account/wizard/account_automatic_entry_wizard.py b/addons/account/wizard/account_automatic_entry_wizard.py index 0d1a221692a..d4bc48a0fc4 100644 --- a/addons/account/wizard/account_automatic_entry_wizard.py +++ b/addons/account/wizard/account_automatic_entry_wizard.py @@ -2,9 +2,11 @@ from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError from odoo.tools.misc import format_date, formatLang +from odoo.tools.float_utils import float_repr from collections import defaultdict from itertools import groupby +from markupsafe import Markup, escape import json class AutomaticEntryWizard(models.TransientModel): @@ -249,7 +251,7 @@ class AutomaticEntryWizard(models.TransientModel): 'analytic_distribution': aml.analytic_distribution, }), (0, 0, { - 'name': self._format_strings(_('{percent:0.2f}% recognized on {new_date}'), aml.move_id), + 'name': self._format_strings(_('{percent}% recognized on {new_date}'), aml.move_id), 'debit': reported_credit, 'credit': reported_debit, 'amount_currency': -reported_amount_currency, @@ -271,7 +273,7 @@ class AutomaticEntryWizard(models.TransientModel): 'analytic_distribution': aml.analytic_distribution, }), (0, 0, { - 'name': self._format_strings(_('{percent:0.2f}% to recognize on {new_date}'), aml.move_id), + 'name': self._format_strings(_('{percent}% to recognize on {new_date}'), aml.move_id), 'debit': reported_debit, 'credit': reported_credit, 'amount_currency': reported_amount_currency, @@ -350,17 +352,30 @@ class AutomaticEntryWizard(models.TransientModel): accrual_move_lines = accrual_move.mapped('line_ids').filtered(lambda line: line.account_id == accrual_account)[accrual_move_offsets[accrual_move]:accrual_move_offsets[accrual_move]+2] accrual_move_offsets[accrual_move] += 2 (accrual_move_lines + destination_move_lines).filtered(lambda line: not line.currency_id.is_zero(line.balance)).reconcile() - move.message_post(body=self._format_strings(_('Adjusting Entries have been created for this invoice:', - link0=self._format_move_link(destination_move), - link1=self._format_move_link(accrual_move), - ), move, amount)) - destination_messages += [self._format_strings(_('Adjusting Entry {link}: {percent:.2f}% of {amount} recognized from {date}'), move, amount)] - accrual_move_messages[accrual_move] += [self._format_strings(_('Adjusting Entry for {link}: {percent:.2f}% of {amount} recognized on {new_date}'), move, amount)] + body = Markup("%(title)s") % { + 'title': _("Adjusting Entries have been created for this invoice:"), + 'link1': self._format_move_link(accrual_move), + 'second': self._format_strings(_("cancelling {percent}%% of {amount}"), move, amount), + 'link2': self._format_move_link(destination_move), + 'third': self._format_strings(_("postponing it to {new_date}"), move, amount), + } + move.message_post(body=body) + destination_messages += [ + self._format_strings( + escape(_("Adjusting Entry {link} {percent}%% of {amount} recognized from {date}")), + move, amount, + ) + ] + accrual_move_messages[accrual_move] += [ + self._format_strings( + escape(_("Adjusting Entry {link} {percent}%% of {amount} recognized on {new_date}")), + move, amount, + ) + ] - destination_move.message_post(body='
\n'.join(destination_messages)) + destination_move.message_post(body=Markup('
\n').join(destination_messages)) for accrual_move, messages in accrual_move_messages.items(): - accrual_move.message_post(body='
\n'.join(messages)) + accrual_move.message_post(body=Markup('
\n').join(messages)) # open the generated entries action = { @@ -418,31 +433,41 @@ class AutomaticEntryWizard(models.TransientModel): # Transfer utils def _format_new_transfer_move_log(self, acc_transfer_per_move): - format = _("
  • {amount} ({debit_credit}) from {link}, %(account_source_name)s
  • ") - rslt = _("This entry transfers the following amounts to %(destination)s ' + transfer_format = Markup("
  • %s, %%(account_source_name)s
  • ") % \ + _("{amount} ({debit_credit}) from {link}") + rslt = Markup(_("This entry transfers the following amounts to %(destination)s") + "") % { + 'destination': Markup("%s") % self.destination_account_id.display_name, + 'transfer_logs': Markup().join([ + self._format_strings(transfer_format, move, balance) % {'account_source_name': account.display_name} + for move, balances_per_account in acc_transfer_per_move.items() + for account, balance in balances_per_account.items() + if account != self.destination_account_id # Otherwise, logging it here is confusing for the user + ]) + } return rslt def _format_transfer_source_log(self, balances_per_account, transfer_move): - transfer_format = _("
  • {amount} ({debit_credit}) from %s were transferred to {account_target_name} by {link}
  • ") - content = '' - for account, balance in balances_per_account.items(): - if account != self.destination_account_id: - content += self._format_strings(transfer_format, transfer_move, balance) % account.display_name - return content and '' or None + if not balances_per_account: + return None + + transfer_format = Markup( + _("{amount} ({debit_credit}) from %s were transferred to {account_target_name} by {link}") + ) + + return Markup("") % Markup().join([ + Markup("
  • %s
  • ") % \ + self._format_strings(transfer_format, transfer_move, balance) % (Markup("%s") % account.display_name) + for account, balance in balances_per_account.items() + if account != self.destination_account_id + ]) def _format_move_link(self, move): return move._get_html_link() def _format_strings(self, string, move, amount=None): return string.format( - label=move.name or 'Adjusting Entry', - percent=self.percentage, + label=move.name or _('Adjusting Entry'), + percent=float_repr(self.percentage, 2), name=move.name, id=move.id, amount=formatLang(self.env, abs(amount), currency_obj=self.company_id.currency_id) if amount else '', diff --git a/addons/account/wizard/accrued_orders.py b/addons/account/wizard/accrued_orders.py index ca0f258b90e..d61c6048114 100644 --- a/addons/account/wizard/accrued_orders.py +++ b/addons/account/wizard/accrued_orders.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from dateutil.relativedelta import relativedelta +from markupsafe import escape import json from odoo import models, fields, api, _, Command from odoo.tools import format_date @@ -217,13 +218,13 @@ class AccruedExpenseRevenue(models.TransientModel): }]) reverse_move._post() for order in orders_with_entries: - body = _( + body = escape(_( 'Accrual entry created on %(date)s: %(accrual_entry)s.\ - And its reverse entry: %(reverse_entry)s.', - date=self.date, - accrual_entry=move._get_html_link(), - reverse_entry=reverse_move._get_html_link(), - ) + And its reverse entry: %(reverse_entry)s.')) % { + 'date': self.date, + 'accrual_entry': move._get_html_link(), + 'reverse_entry': reverse_move._get_html_link(), + } order.message_post(body=body) return { 'name': _('Accrual Moves'), diff --git a/addons/account_debit_note/wizard/account_debit_note.py b/addons/account_debit_note/wizard/account_debit_note.py index b03addb8d1b..3982de5d622 100644 --- a/addons/account_debit_note/wizard/account_debit_note.py +++ b/addons/account_debit_note/wizard/account_debit_note.py @@ -2,7 +2,7 @@ from odoo import models, fields, api from odoo.tools.translate import _ from odoo.exceptions import UserError - +from markupsafe import escape class AccountDebitNote(models.TransientModel): """ @@ -71,10 +71,7 @@ class AccountDebitNote(models.TransientModel): for move in self.move_ids.with_context(include_business_fields=True): #copy sale/purchase links default_values = self._prepare_default_values(move) new_move = move.copy(default=default_values) - move_msg = _( - "This debit note was created from: %s", - move._get_html_link(), - ) + move_msg = escape(_("This debit note was created from: %s")) % move._get_html_link() new_move.message_post(body=move_msg) new_moves |= new_move diff --git a/addons/account_edi_ubl_cii/models/account_edi_common.py b/addons/account_edi_ubl_cii/models/account_edi_common.py index 7db5988c2ea..2de56f97579 100644 --- a/addons/account_edi_ubl_cii/models/account_edi_common.py +++ b/addons/account_edi_ubl_cii/models/account_edi_common.py @@ -5,6 +5,7 @@ from odoo.tools import float_repr from odoo.exceptions import UserError, ValidationError from odoo.tools.float_utils import float_round +from markupsafe import Markup from zeep import Client # ------------------------------------------------------------------------- @@ -285,13 +286,14 @@ class AccountEdiCommon(models.AbstractModel): invoice.move_type = move_type logs = self._import_fill_invoice_form(invoice, tree, qty_factor) if invoice: + body = Markup("%s") % \ + _("Format used to import the invoice: %s", + self.env['ir.model']._get(self._name).name) + if logs: - body = _( - "Format used to import the invoice: %s

  • %s
  • ", - str(self._description), "
  • ".join(logs) - ) - else: - body = _("Format used to import the invoice: %s", str(self._description)) + body += Markup("") % \ + Markup().join(Markup("
  • %s
  • ") % l for l in logs) + invoice.message_post(body=body) # For UBL, we should override the computed tax amount if it is less than 0.05 different of the one in the xml. @@ -656,22 +658,24 @@ class AccountEdiCommon(models.AbstractModel): for item in response['Result']: if item['artifactPath']: report.append( - "
  • " + item['artifactPath'] + "
  • ") + Markup("
  • %s
  • ") % item['artifactPath']) for detail in item['Item']: if detail['errorLevel'] == 'WARN': errors_cnt += 1 report.append( - "
  • " + detail['errorText'] + "
  • ") + Markup("
  • %s
  • ") % detail['errorText']) elif detail['errorLevel'] == 'ERROR': errors_cnt += 1 report.append( - "
  • " + detail['errorText'] + "
  • ") + Markup("
  • %s
  • ") % detail['errorText']) if errors_cnt == 0: - invoice.message_post(body=f"ECOSIO: All clear for format {ecosio_format}!") + invoice.message_post(body=Markup("ECOSIO: All clear for format %s!") % ecosio_format) else: invoice.message_post( - body=f"ECOSIO ERRORS/WARNINGS for format {ecosio_format}: " + body=Markup("ECOSIO ERRORS/WARNINGS for format %s: