From 57261b100f8bd06eefb557e2d09f05576e4389be Mon Sep 17 00:00:00 2001 From: dht-odoo Date: Fri, 21 May 2021 06:09:55 +0000 Subject: [PATCH 01/15] [IMP] base, mail, portal: improves is_html_empty method Before this commit there were few cases in which is_html_empty method was not working as our expectation. In cases such as "


", "


" and many other cases. In this commit we improves regex of is_html_empty method. Mow from this commit all kind of attributes will be consider in this method. In this commit we also have added is_html_empty in mail template, portal values and in report values also for rendering templates. Task id: 2499504 X-original-commit: fd0a05f2955b9f7e9ae7233afebfd6240c9244dd --- addons/mail/models/mail_render_mixin.py | 3 ++- addons/portal/models/ir_ui_view.py | 2 ++ odoo/addons/base/models/ir_actions_report.py | 3 ++- odoo/addons/base/tests/test_mail.py | 2 +- odoo/tools/mail.py | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/addons/mail/models/mail_render_mixin.py b/addons/mail/models/mail_render_mixin.py index 7476becde46..27b7613669f 100644 --- a/addons/mail/models/mail_render_mixin.py +++ b/addons/mail/models/mail_render_mixin.py @@ -14,7 +14,7 @@ from werkzeug import urls from odoo import _, api, fields, models, tools from odoo.exceptions import UserError -from odoo.tools import safe_eval +from odoo.tools import is_html_empty, safe_eval _logger = logging.getLogger(__name__) @@ -279,6 +279,7 @@ class MailRenderMixin(models.AbstractModel): 'format_duration': lambda value: tools.format_duration(value), 'user': self.env.user, 'ctx': self._context, + 'is_html_empty': is_html_empty, } @api.model diff --git a/addons/portal/models/ir_ui_view.py b/addons/portal/models/ir_ui_view.py index ca4051755f7..64a63bfee0d 100644 --- a/addons/portal/models/ir_ui_view.py +++ b/addons/portal/models/ir_ui_view.py @@ -4,6 +4,7 @@ from odoo import api, models, fields from odoo.http import request from odoo.addons.http_routing.models.ir_http import url_for +from odoo.tools import is_html_empty class View(models.Model): @@ -24,5 +25,6 @@ class View(models.Model): self._context.copy(), languages=[lang for lang in Lang.get_available() if lang[0] in portal_lang_code], url_for=url_for, + is_html_empty=is_html_empty, )) return qcontext diff --git a/odoo/addons/base/models/ir_actions_report.py b/odoo/addons/base/models/ir_actions_report.py index 9eaeb72bfb9..c00b41e6b72 100644 --- a/odoo/addons/base/models/ir_actions_report.py +++ b/odoo/addons/base/models/ir_actions_report.py @@ -6,7 +6,7 @@ from odoo import api, fields, models, tools, SUPERUSER_ID, _ from odoo.exceptions import UserError, AccessError from odoo.tools.safe_eval import safe_eval, time from odoo.tools.misc import find_in_path -from odoo.tools import config +from odoo.tools import config, is_html_empty from odoo.sql_db import TestCursor from odoo.http import request from odoo.osv.expression import NEGATIVE_TERM_OPERATORS, FALSE_DOMAIN @@ -886,6 +886,7 @@ class IrActionsReport(models.Model): 'doc_model': self_sudo.model, 'docs': docs, }) + data['is_html_empty'] = is_html_empty return data def _render(self, res_ids, data=None): diff --git a/odoo/addons/base/tests/test_mail.py b/odoo/addons/base/tests/test_mail.py index ff92a840a2b..755b959f573 100644 --- a/odoo/addons/base/tests/test_mail.py +++ b/odoo/addons/base/tests/test_mail.py @@ -329,7 +329,7 @@ class TestHtmlTools(BaseCase): self.assertTrue(is_html_empty(content)) void_html_samples = ['


', '


', '


', '

', - '
'] + '
', '


'] for content in void_html_samples: self.assertTrue(is_html_empty(content), 'Failed with %s' % content) diff --git a/odoo/tools/mail.py b/odoo/tools/mail.py index bfbfddea275..0dc0bf658e5 100644 --- a/odoo/tools/mail.py +++ b/odoo/tools/mail.py @@ -279,7 +279,7 @@ def is_html_empty(html_content): """ if not html_content: return True - tag_re = re.compile(r'\<\s*\/?(?:p|div|span|br|b|i)\s*(?:style=\".+\")?/?\s*\>') + tag_re = re.compile(r'\<\s*\/?(?:p|div|span|br|b|i)(?:(?=\s+\w*)[^/>]*|\s*)/?\s*\>') return not bool(re.sub(tag_re, '', html_content).strip()) From 18ad07c001ee5fd5fea95cf88d415fbd32657cc4 Mon Sep 17 00:00:00 2001 From: dht-odoo Date: Thu, 27 May 2021 03:24:08 +0000 Subject: [PATCH 02/15] [IMP] web: add isHtmlEmpty method in kanban view In this commit we have added new method name isHtmlEmpty in kanban view. As we are convertting text field to html field, we will need to check html field in if conditions. As Html field will contain tags we cannot check it every time perfectly. So, we need this method to make sure html field is empty or not. Task id: 2499504 X-original-commit: 2968dcea7fb58795eeb34949f4bbfc1029938cd1 --- .../src/js/views/kanban/kanban_record.js | 16 ++++++++ addons/web/static/tests/views/kanban_tests.js | 37 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/addons/web/static/src/js/views/kanban/kanban_record.js b/addons/web/static/src/js/views/kanban/kanban_record.js index 6929ca6f2a2..a217fc51b30 100644 --- a/addons/web/static/src/js/views/kanban/kanban_record.js +++ b/addons/web/static/src/js/views/kanban/kanban_record.js @@ -87,6 +87,22 @@ var KanbanRecord = Widget.extend(WidgetAdapterMixin, { // Public //-------------------------------------------------------------------------- + /** + * Check if a html content is empty. If there are only formatting tags with + * style attributes or a void content. Famous use case is + * '


' added by some web editor(s). + * Note that because the use of this method is limited, we ignore the cases + * like there's one tag in the content. In such case, even if it's the + * actual content, we consider it empty. + * + * @param {string} htmlContent + * @returns {boolean} true if no content found or if containing only formatting tags + */ + isHtmlEmpty: function (htmlContent) { + let div = document.createElement('div'); + div.innerHTML = htmlContent || ""; + return div.innerText.trim() === ""; + }, /** * Re-renders the record with a new state * diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index b548b5838e3..6385a430f16 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -7528,6 +7528,43 @@ QUnit.module('Views', { kanban.destroy(); }); + + QUnit.test('kanban with isHtmlEmpty method', async function (assert) { + assert.expect(3); + + this.data.product.fields.description = {string: 'Description', type: 'html'}; + this.data.product.records.push( + {id: 11, display_name: "product 11", description: "hello"}, + {id: 12, display_name: "product 12", description: "


"}, + ); + + var kanban = await createView({ + View: KanbanView, + model: 'product', + data: this.data, + arch: ` + + +
+ +
+ +
+
+
+
`, + domain: [['id', 'in', [11, 12]]], + + }); + assert.containsOnce(kanban, '.o_kanban_record:first div.test', + "the container is displayed if description have actual content"); + assert.strictEqual(kanban.$('.o_kanban_record:first div.test span.text-info').html().trim(), 'hello', + "the inner html content is rendered properly"); + assert.containsNone(kanban, '.o_kanban_record:last div.test', + "the container is not displayed if description just have formatting tags and no actual content"); + + kanban.destroy(); + }); }); }); From 1ef6fd9098aa2e3a9aba4495b1a111afe06c3c0d Mon Sep 17 00:00:00 2001 From: dht-odoo Date: Wed, 14 Apr 2021 10:42:13 +0000 Subject: [PATCH 03/15] [IMP] web, {base}_setup: improves field type from text to html Replace text fields to html fields as we have our own 'OdooEditor'. Indeed, it gives more options to users in the way they format their content without weighting too much on the UI (tools appear on demand and not by default). In this commit we replace report_footer and report_header field of "base.document.layout, res.config.settings" model as they are related fields of res.company models field report_footer, report_header. Task Id: 2499504 X-original-commit: 4c474d0d2055efe81fdc21502aa5957fe80af7ef --- addons/base_setup/models/res_config_settings.py | 2 +- addons/gamification/data/mail_template_data.xml | 2 +- addons/gamification/models/badge.py | 2 +- addons/gamification/views/badge.xml | 4 ++-- addons/web/models/base_document_layout.py | 4 ++-- addons/web/views/base_document_layout_views.xml | 2 +- odoo/addons/base/models/res_company.py | 4 ++-- odoo/addons/base/models/res_partner.py | 2 +- odoo/addons/base/tests/test_ir_actions.py | 5 +++-- 9 files changed, 14 insertions(+), 13 deletions(-) diff --git a/addons/base_setup/models/res_config_settings.py b/addons/base_setup/models/res_config_settings.py index 0e73157e100..5f6c98483ac 100644 --- a/addons/base_setup/models/res_config_settings.py +++ b/addons/base_setup/models/res_config_settings.py @@ -38,7 +38,7 @@ class ResConfigSettings(models.TransientModel): module_partner_autocomplete = fields.Boolean("Partner Autocomplete") module_base_geolocalize = fields.Boolean("GeoLocalize") module_google_recaptcha = fields.Boolean("reCAPTCHA") - report_footer = fields.Text(related="company_id.report_footer", string='Custom Report Footer', help="Footer text displayed at the bottom of all reports.", readonly=False) + report_footer = fields.Html(related="company_id.report_footer", string='Custom Report Footer', help="Footer text displayed at the bottom of all reports.", readonly=False) group_multi_currency = fields.Boolean(string='Multi-Currencies', implied_group='base.group_multi_currency', help="Allows to work in a multi currency environment") diff --git a/addons/gamification/data/mail_template_data.xml b/addons/gamification/data/mail_template_data.xml index 380e18fd470..6a0fe284e85 100644 --- a/addons/gamification/data/mail_template_data.xml +++ b/addons/gamification/data/mail_template_data.xml @@ -36,7 +36,7 @@
Congratulations ${object.user_id.name} !
You just received badge ${object.badge_id.name} !
- % if object.badge_id.description + % if not is_html_empty(object.badge_id.description)
diff --git a/addons/gamification/models/badge.py b/addons/gamification/models/badge.py index aa065140e8f..8517582f66d 100644 --- a/addons/gamification/models/badge.py +++ b/addons/gamification/models/badge.py @@ -74,7 +74,7 @@ class GamificationBadge(models.Model): name = fields.Char('Badge', required=True, translate=True) active = fields.Boolean('Active', default=True) - description = fields.Text('Description', translate=True) + description = fields.Html('Description', translate=True) level = fields.Selection([ ('bronze', 'Bronze'), ('silver', 'Silver'), ('gold', 'Gold')], string='Forum Badge Level', default='bronze') diff --git a/addons/gamification/views/badge.xml b/addons/gamification/views/badge.xml index 6193548c091..f59c5985d2a 100644 --- a/addons/gamification/views/badge.xml +++ b/addons/gamification/views/badge.xml @@ -144,8 +144,8 @@ granted, this month -
- +
+
Owner diff --git a/addons/web/models/base_document_layout.py b/addons/web/models/base_document_layout.py index 8502031278f..7f729990c19 100644 --- a/addons/web/models/base_document_layout.py +++ b/addons/web/models/base_document_layout.py @@ -29,8 +29,8 @@ class BaseDocumentLayout(models.TransientModel): logo = fields.Binary(related='company_id.logo', readonly=False) preview_logo = fields.Binary(related='logo', string="Preview logo") - report_header = fields.Text(related='company_id.report_header', readonly=False) - report_footer = fields.Text(related='company_id.report_footer', readonly=False) + report_header = fields.Html(related='company_id.report_header', readonly=False) + report_footer = fields.Html(related='company_id.report_footer', readonly=False) # The paper format changes won't be reflected in the preview. paperformat_id = fields.Many2one(related='company_id.paperformat_id', readonly=False) diff --git a/addons/web/views/base_document_layout_views.xml b/addons/web/views/base_document_layout_views.xml index 7f9e5c033ff..eda3c0dd7e6 100644 --- a/addons/web/views/base_document_layout_views.xml +++ b/addons/web/views/base_document_layout_views.xml @@ -24,7 +24,7 @@
- + diff --git a/odoo/addons/base/models/res_company.py b/odoo/addons/base/models/res_company.py index 64a28f4dd9d..2f2a8e93128 100644 --- a/odoo/addons/base/models/res_company.py +++ b/odoo/addons/base/models/res_company.py @@ -61,8 +61,8 @@ class Company(models.Model): parent_id = fields.Many2one('res.company', string='Parent Company', index=True) child_ids = fields.One2many('res.company', 'parent_id', string='Child Companies') partner_id = fields.Many2one('res.partner', string='Partner', required=True) - report_header = fields.Text(string='Company Tagline', help="Appears by default on the top right corner of your printed documents (report header).") - report_footer = fields.Text(string='Report Footer', translate=True, help="Footer text displayed at the bottom of all reports.") + report_header = fields.Html(string='Company Tagline', help="Appears by default on the top right corner of your printed documents (report header).") + report_footer = fields.Html(string='Report Footer', translate=True, help="Footer text displayed at the bottom of all reports.") logo = fields.Binary(related='partner_id.image_1920', default=_get_logo, string="Company Logo", readonly=False) # logo_web: do not store in attachments, since the image is retrieved in SQL for # performance reasons (see addons/web/controllers/main.py, Binary.company_logo) diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py index 9ab65d6b573..8d5b2088a6f 100644 --- a/odoo/addons/base/models/res_partner.py +++ b/odoo/addons/base/models/res_partner.py @@ -173,7 +173,7 @@ class Partner(models.Model): same_vat_partner_id = fields.Many2one('res.partner', string='Partner with same Tax ID', compute='_compute_same_vat_partner_id', store=False) bank_ids = fields.One2many('res.partner.bank', 'partner_id', string='Banks') website = fields.Char('Website Link') - comment = fields.Text(string='Notes') + comment = fields.Html(string='Notes') category_id = fields.Many2many('res.partner.category', column1='partner_id', column2='category_id', string='Tags', default=_default_category) diff --git a/odoo/addons/base/tests/test_ir_actions.py b/odoo/addons/base/tests/test_ir_actions.py index 29d2f890038..1aa08108d96 100644 --- a/odoo/addons/base/tests/test_ir_actions.py +++ b/odoo/addons/base/tests/test_ir_actions.py @@ -35,6 +35,7 @@ class TestServerActionsBase(common.TransactionCase): # Model data Model = self.env['ir.model'] Fields = self.env['ir.model.fields'] + self.comment_html = '

MyComment

' self.res_partner_model = Model.search([('model', '=', 'res.partner')]) self.res_partner_name_field = Fields.search([('model', '=', 'res.partner'), ('name', '=', 'name')]) self.res_partner_city_field = Fields.search([('model', '=', 'res.partner'), ('name', '=', 'city')]) @@ -54,7 +55,7 @@ class TestServerActionsBase(common.TransactionCase): 'model_id': self.res_partner_model.id, 'model_name': 'res.partner', 'state': 'code', - 'code': 'record.write({"comment": "MyComment"})', + 'code': 'record.write({"comment": "%s"})' % self.comment_html, }) @@ -62,7 +63,7 @@ class TestServerActions(TestServerActionsBase): def test_00_action(self): self.action.with_context(self.context).run() - self.assertEqual(self.test_partner.comment, 'MyComment', 'ir_actions_server: invalid condition check') + self.assertEqual(self.test_partner.comment, self.comment_html, 'ir_actions_server: invalid condition check') self.test_partner.write({'comment': False}) # Do: create contextual action From bea5790713020209e0527840b7dbc1ea4806b762 Mon Sep 17 00:00:00 2001 From: dht-odoo Date: Thu, 15 Apr 2021 05:44:23 +0000 Subject: [PATCH 04/15] [IMP] sale, product: improves field type from text to html It replace text fields to html fields as we have our own 'OdooEditor'. Indeed, it gives more options to users in the way they format their content without weighting too much on the UI (tools appear on demand and not by default). Models -> Fields 1) sale.order -> note 2) sale.order.template -> note Task Id: 2499504 X-original-commit: 6a345c2695129a5cd907e5e23e057871ebf9ebfa --- addons/product/models/product_template.py | 2 +- addons/repair/models/repair.py | 2 +- addons/sale/models/sale_order.py | 12 +++++++----- addons/sale/report/sale_report_templates.xml | 4 ++-- addons/sale/tests/test_sale_order.py | 4 ++-- addons/sale/views/sale_portal_templates.xml | 2 +- addons/sale_management/models/sale_order.py | 5 +++-- addons/sale_management/models/sale_order_template.py | 2 +- .../views/sale_portal_templates.xml | 2 +- .../static/src/xml/website_sale_utils.xml | 2 +- 10 files changed, 20 insertions(+), 17 deletions(-) diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index e73d09df8e8..d96442d78a8 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -36,7 +36,7 @@ class ProductTemplate(models.Model): name = fields.Char('Name', index=True, required=True, translate=True) sequence = fields.Integer('Sequence', default=1, help='Gives the sequence order when displaying a product list') - description = fields.Text( + description = fields.Html( 'Description', translate=True) description_purchase = fields.Text( 'Purchase Description', translate=True) diff --git a/addons/repair/models/repair.py b/addons/repair/models/repair.py index 692d637322a..adfbf94e2d6 100644 --- a/addons/repair/models/repair.py +++ b/addons/repair/models/repair.py @@ -360,7 +360,7 @@ class Repair(models.Model): if not invoice_vals['narration']: invoice_vals['narration'] = narration else: - invoice_vals['narration'] += '\n' + narration + invoice_vals['narration'] += '
' + narration # Create invoice lines from operations. for operation in repair.operations.filtered(lambda op: op.type == 'add'): diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 02e27385d70..57be726b3cf 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -9,7 +9,7 @@ from odoo import api, fields, models, SUPERUSER_ID, _ from odoo.exceptions import AccessError, UserError, ValidationError from odoo.tools.misc import formatLang from odoo.osv import expression -from odoo.tools import float_is_zero +from odoo.tools import float_is_zero, html_keep_url, is_html_empty class SaleOrder(models.Model): @@ -111,7 +111,8 @@ class SaleOrder(models.Model): def _default_note(self): use_invoice_terms = self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms') if use_invoice_terms and self.env.company.terms_type == "html": - return _('Terms & Conditions: %s/terms', self._default_note_url()) + baseurl = html_keep_url(self._default_note_url() + '/terms') + return _('Terms & Conditions: %s', baseurl) return use_invoice_terms and self.env.company.invoice_terms or '' @api.model @@ -225,7 +226,7 @@ class SaleOrder(models.Model): ('no', 'Nothing to Invoice') ], string='Invoice Status', compute='_get_invoice_status', store=True, readonly=True) - note = fields.Text('Terms and conditions', default=_default_note) + note = fields.Html('Terms and conditions', default=_default_note) terms_type = fields.Selection(related='company_id.terms_type') amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', tracking=5) @@ -409,8 +410,9 @@ class SaleOrder(models.Model): if self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms'): if self.terms_type == 'html' and self.env.company.invoice_terms_html: - values['note'] = _('Terms & Conditions: %s/terms', self.get_base_url()) - elif self.env.company.invoice_terms: + baseurl = html_keep_url(self.get_base_url() + '/terms') + values['note'] = _('Terms & Conditions: %s', baseurl) + elif not is_html_empty(self.env.company.invoice_terms): values['note'] = self.with_context(lang=self.partner_id.lang).env.company.invoice_terms if not self.env.context.get('not_self_saleperson') or not self.team_id: values['team_id'] = self.env['crm.team'].with_context( diff --git a/addons/sale/report/sale_report_templates.xml b/addons/sale/report/sale_report_templates.xml index f9bba65dac8..0c958b339a4 100644 --- a/addons/sale/report/sale_report_templates.xml +++ b/addons/sale/report/sale_report_templates.xml @@ -198,10 +198,10 @@

-

+

-

+

Fiscal Position Remark:

diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index 593bc0b3ba5..05009fd9f94 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -611,12 +611,12 @@ class TestSaleOrder(TestSaleCommon): self.env.company.terms_type = 'plain' self.env.company.invoice_terms = "Coin coin" sale_order = self._create_sale_order() - self.assertEqual(sale_order.note, "Coin coin") + self.assertEqual(sale_order.note, "

Coin coin

") # Html invoice terms (/terms page) self.env.company.terms_type = 'html' sale_order = self._create_sale_order() - self.assertTrue(sale_order.note.startswith("Terms & Conditions: ")) + self.assertTrue(sale_order.note.startswith("

Terms & Conditions: ")) def test_validity_days(self): self.env['ir.config_parameter'].sudo().set_param('sale.use_quotation_validity_days', True) diff --git a/addons/sale/views/sale_portal_templates.xml b/addons/sale/views/sale_portal_templates.xml index c0aa8604eb0..1514b4adf52 100644 --- a/addons/sale/views/sale_portal_templates.xml +++ b/addons/sale/views/sale_portal_templates.xml @@ -526,7 +526,7 @@

-
+

Terms & Conditions


diff --git a/addons/sale_management/models/sale_order.py b/addons/sale_management/models/sale_order.py index 165d5acf59b..4bbc71c34de 100644 --- a/addons/sale_management/models/sale_order.py +++ b/addons/sale_management/models/sale_order.py @@ -5,6 +5,7 @@ from datetime import timedelta from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError +from odoo.tools import is_html_empty class SaleOrder(models.Model): @@ -53,7 +54,7 @@ class SaleOrder(models.Model): def onchange_partner_id(self): super(SaleOrder, self).onchange_partner_id() template = self.sale_order_template_id.with_context(lang=self.partner_id.lang) - self.note = template.note or self.note + self.note = template.note if not is_html_empty(template.note) else self.note def _compute_line_data_for_template_change(self, line): return { @@ -145,7 +146,7 @@ class SaleOrder(models.Model): self.require_signature = template.require_signature self.require_payment = template.require_payment - if template.note: + if not is_html_empty(template.note): self.note = template.note def action_confirm(self): diff --git a/addons/sale_management/models/sale_order_template.py b/addons/sale_management/models/sale_order_template.py index f1cdeb1bb7a..1f303f298d4 100644 --- a/addons/sale_management/models/sale_order_template.py +++ b/addons/sale_management/models/sale_order_template.py @@ -17,7 +17,7 @@ class SaleOrderTemplate(models.Model): name = fields.Char('Quotation Template', required=True) sale_order_template_line_ids = fields.One2many('sale.order.template.line', 'sale_order_template_id', 'Lines', copy=True) - note = fields.Text('Terms and conditions', translate=True) + note = fields.Html('Terms and conditions', translate=True) sale_order_template_option_ids = fields.One2many('sale.order.template.option', 'sale_order_template_id', 'Optional Products', copy=True) number_of_days = fields.Integer('Quotation Duration', help='Number of days for the validity date computation of the quotation') diff --git a/addons/sale_quotation_builder/views/sale_portal_templates.xml b/addons/sale_quotation_builder/views/sale_portal_templates.xml index 7b4d9c36f8b..527ad78318e 100644 --- a/addons/sale_quotation_builder/views/sale_portal_templates.xml +++ b/addons/sale_quotation_builder/views/sale_portal_templates.xml @@ -71,7 +71,7 @@
-
+

Terms & Conditions

diff --git a/addons/website_sale/static/src/xml/website_sale_utils.xml b/addons/website_sale/static/src/xml/website_sale_utils.xml index 8a470613325..9aaed80e1ea 100644 --- a/addons/website_sale/static/src/xml/website_sale_utils.xml +++ b/addons/website_sale/static/src/xml/website_sale_utils.xml @@ -17,7 +17,7 @@
-

+

From 366aa2907e792b3cc30cb6f4300ddad6146118fe Mon Sep 17 00:00:00 2001 From: dht-odoo Date: Thu, 15 Apr 2021 07:30:11 +0000 Subject: [PATCH 05/15] [IMP] stock: improves field type from text to html Replace text fields to html fields as we have our own 'OdooEditor'. Indeed, it gives more options to users in the way they format their content without weighting too much on the UI (tools appear on demand and not by default). Models -> Fieldss 1) stock.location -> comment 2) stock.picking -> note Task Id: 2499504 X-original-commit: 96f5316a219568add44bee528245c2c8e49c3bbc --- addons/stock/models/product.py | 4 ++-- addons/stock/models/stock_location.py | 2 +- addons/stock/models/stock_picking.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 3fc466b6a38..a7052f0fd18 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -626,8 +626,8 @@ class ProductTemplate(models.Model): ('lot', 'By Lots'), ('none', 'No Tracking')], string="Tracking", help="Ensure the traceability of a storable product in your warehouse.", default='none', required=True) description_picking = fields.Text('Description on Picking', translate=True) - description_pickingout = fields.Text('Description on Delivery Orders', translate=True) - description_pickingin = fields.Text('Description on Receptions', translate=True) + description_pickingout = fields.Html('Description on Delivery Orders', translate=True) + description_pickingin = fields.Html('Description on Receptions', translate=True) qty_available = fields.Float( 'Quantity On Hand', compute='_compute_quantities', search='_search_qty_available', compute_sudo=False, digits='Product Unit of Measure') diff --git a/addons/stock/models/stock_location.py b/addons/stock/models/stock_location.py index dadb07e7ffa..5d330d40f32 100644 --- a/addons/stock/models/stock_location.py +++ b/addons/stock/models/stock_location.py @@ -51,7 +51,7 @@ class Location(models.Model): 'stock.location', 'Parent Location', index=True, ondelete='cascade', check_company=True, help="The parent location that includes this location. Example : The 'Dispatch Zone' is the 'Gate 1' parent location.") child_ids = fields.One2many('stock.location', 'location_id', 'Contains') - comment = fields.Text('Additional Information') + comment = fields.Html('Additional Information') posx = fields.Integer('Corridor (X)', default=0, help="Optional localization details, for information purpose only") posy = fields.Integer('Shelves (Y)', default=0, help="Optional localization details, for information purpose only") posz = fields.Integer('Height (Z)', default=0, help="Optional localization details, for information purpose only") diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index d00d3a06e51..b6b31b39255 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -255,7 +255,7 @@ class Picking(models.Model): 'Source Document', index=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Reference of the document") - note = fields.Text('Notes') + note = fields.Html('Notes') backorder_id = fields.Many2one( 'stock.picking', 'Back Order of', copy=False, index=True, readonly=True, From dbf1ab157dbc985370a03ded818945cb2ed726dd Mon Sep 17 00:00:00 2001 From: dht-odoo Date: Mon, 3 May 2021 10:00:51 +0000 Subject: [PATCH 06/15] [IMP] calendar: improves field type from text to html Replace text fields to html fields as we have our own 'OdooEditor'. Indeed, it gives more options to users in the way they format their content without weighting too much on the UI (tools appear on demand and not by default). Adding a new method convert_online_event_desc_to_text Because online events have fixed format for the description, this method removes some specific html tags, and converts it into readable plaintext (to be used in external calendars). Models -> Fields 1) calendar.event -> description Task Id: 2499504 X-original-commit: 0d5ca6a6fe8821b4ec19dd603bde116518721792 --- addons/calendar/data/mail_template_data.xml | 48 +++-------- addons/calendar/models/calendar_event.py | 85 ++++++------------- addons/calendar/models/mail_activity.py | 5 +- addons/calendar/tests/test_calendar.py | 2 +- addons/google_calendar/models/calendar.py | 4 +- .../tests/test_sync_google2odoo.py | 3 +- addons/microsoft_calendar/models/calendar.py | 5 +- 7 files changed, 50 insertions(+), 102 deletions(-) diff --git a/addons/calendar/data/mail_template_data.xml b/addons/calendar/data/mail_template_data.xml index 4da9984478d..8a1532e49ba 100644 --- a/addons/calendar/data/mail_template_data.xml +++ b/addons/calendar/data/mail_template_data.xml @@ -92,18 +92,6 @@ % if not object.event_id.allday and object.event_id.duration
  • Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}
  • % endif - % if not is_online and object.event_id.description : -
  • Description: ${object.event_id.description}
  • - % elif is_online and object.event_id.description: - % set splitted_description = object.event_id.description_to_html_lines() -
  • Description: -
      - % for description_line in splitted_description: -
    • ${description_line | safe}
    • - % endfor -
    -
  • - % endif
  • Attendees + % if not is_html_empty(object.event_id.description): +

    Description of the event

    + ${object.event_id.description | safe} + % endif

  • @@ -222,18 +214,6 @@ % if not object.event_id.allday and object.event_id.duration
  • Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}
  • % endif - % if not is_online and object.event_id.description : -
  • Description: ${object.event_id.description}
  • - % elif is_online and object.event_id.description: - % set splitted_description = object.event_id.description_to_html_lines() -
  • Description: -
      - % for description_line in splitted_description: -
    • ${description_line | safe}
    • - % endfor -
    -
  • - % endif
  • Attendees + % if not is_html_empty(object.event_id.description): +

    Description of the event

    + ${object.event_id.description | safe} + % endif
    @@ -332,18 +316,6 @@ % if not object.event_id.allday and object.event_id.duration
  • Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}
  • % endif - % if not is_online and object.event_id.description : -
  • Description: ${object.event_id.description}
  • - % elif is_online and object.event_id.description: - % set splitted_description = object.event_id.description_to_html_lines() -
  • Description: -
      - % for description_line in splitted_description: -
    • ${description_line | safe}
    • - % endfor -
    -
  • - % endif
  • Attendees + % if not is_html_empty(object.event_id.description): +

    Description of the event

    + ${object.event_id.description | safe} + % endif
    diff --git a/addons/calendar/models/calendar_event.py b/addons/calendar/models/calendar_event.py index c8ee1a71b04..6ae4f68e7f8 100644 --- a/addons/calendar/models/calendar_event.py +++ b/addons/calendar/models/calendar_event.py @@ -9,13 +9,12 @@ import pytz from werkzeug.urls import url_join from odoo import api, fields, models -from odoo import tools from odoo.addons.base.models.res_partner import _tz_get from odoo.addons.calendar.models.calendar_attendee import Attendee from odoo.addons.calendar.models.calendar_recurrence import weekday_to_field, RRULE_TYPE_SELECTION, END_TYPE_SELECTION, MONTH_BY_SELECTION, WEEKDAY_SELECTION, BYDAY_SELECTION from odoo.tools.translate import _ from odoo.tools.misc import get_lang -from odoo.tools import pycompat, html_escape +from odoo.tools import pycompat, html2plaintext, is_html_empty from odoo.exceptions import UserError, ValidationError, AccessError _logger = logging.getLogger(__name__) @@ -96,7 +95,7 @@ class Meeting(models.Model): # description name = fields.Char('Meeting Subject', required=True) - description = fields.Text('Description') + description = fields.Html('Description') user_id = fields.Many2one('res.users', 'Organizer', default=lambda self: self.env.user) partner_id = fields.Many2one( 'res.partner', string='Scheduled by', related='user_id.partner_id', readonly=True) @@ -627,7 +626,7 @@ class Meeting(models.Model): if 'name' in fields: activity_values['summary'] = event.name if 'description' in fields: - activity_values['note'] = tools.plaintext2html(event.description) + activity_values['note'] = event.description if 'start' in fields: # self.start is a datetime UTC *only when the event is not allday* # activty.date_deadline is a date (No TZ, but should represent the day in which the user's TZ is) @@ -837,8 +836,12 @@ class Meeting(models.Model): event.add('dtstart').value = ics_datetime(meeting.start, meeting.allday) event.add('dtend').value = ics_datetime(meeting.stop, meeting.allday) event.add('summary').value = meeting.name - if meeting.description: - event.add('description').value = meeting.description + if not is_html_empty(meeting.description): + if 'appointment_type_id' in meeting._fields and self.appointment_type_id: + # convert_online_event_desc_to_text method for correct data formatting in external calendars + event.add('description').value = self.convert_online_event_desc_to_text(meeting.description) + else: + event.add('description').value = html2plaintext(meeting.description) if meeting.location: event.add('location').value = meeting.location if meeting.rrule: @@ -866,6 +869,23 @@ class Meeting(models.Model): return result + def convert_online_event_desc_to_text(self, description): + """ + We can sync the calendar events with google calendar, iCal and Outlook, and we + also pass the event description along with other data. This description needs + to be in plaintext to be displayed properly in above platforms. Because online + events have fixed format for the description, this method removes some specific + html tags, and converts it into readable plaintext (to be used in external + calendars). Note that for regular (offline) events, we simply use the standard + `html2plaintext` method instead. + """ + desc_str = str(description) + tags_to_replace = ["
      ", "
    ", "
  • "] + for tag in tags_to_replace: + desc_str = desc_str.replace(tag, "") + desc_str = desc_str.replace("
  • ", "
    ") + return html2plaintext(desc_str) + @api.model def _get_display_time(self, start, stop, zduration, zallday): """ Return date and time (from to from) based on duration with timezone in string. Eg : @@ -948,56 +968,3 @@ class Meeting(models.Model): 'id', 'active', 'allday', 'duration', 'user_id', 'interval', 'count', 'rrule', 'recurrence_id', 'show_as', 'privacy'} - - def description_to_html_lines(self): - """ Description could contain some structure content, depending on - its use. Notably appointment could add some structured data in it - in addition to free text. Purpose of this method is to generate a - simple html. - - Input (self.description) example: -Some free text salespeople added -as multi line - * Mobile: +320475000000 - * Email: my.email@test.example.com - * SingleLine: answer - * MultiLine: -Answer1 -Answer2 -Answer3 - * Dropdown: answer - * Radio: answer - * Checkboxes: answer1, answer2 -Some free text salespeople added -as multi line - - Output: a list of items[ -'Some free text salespeople added
    as multi line', -'Mobile: +320475000000', -'Email: my.email@test.example.com', -'SingleLine: answer', -'MultiLine:
    Answer1
    Answer2
    Answer3', -'Dropdown: answer', -'Radio: answer', -'Checkboxes: answer1, answer2', -'Some free text salespeople added
    as multi line'] - - Each item of returned list is escaped so that only our intended
    - are html tags. It should therefore be safe. - """ - - final_lines = [] - parsed_lines = [] - for line in self.description.split('\n'): - if not line.strip(): - continue - # new line - if line.startswith(' *'): - if parsed_lines: - final_lines.append('
    '.join(html_escape(line) for line in parsed_lines)) - parsed_lines = [line.lstrip(' *')] - else: - parsed_lines.append(line) - if parsed_lines: - final_lines.append('
    '.join(html_escape(line) for line in parsed_lines)) - return final_lines diff --git a/addons/calendar/models/mail_activity.py b/addons/calendar/models/mail_activity.py index 58ca5ff75d4..7f483ca27af 100644 --- a/addons/calendar/models/mail_activity.py +++ b/addons/calendar/models/mail_activity.py @@ -34,7 +34,10 @@ class MailActivity(models.Model): if feedback: for event in events: description = event.description - description = '%s\n%s%s' % (description or '', _("Feedback: "), feedback) + description = '%s
    %s' % ( + description if not tools.is_html_empty(description) else '', + _('Feedback: %(feedback)s', tools.plaintext2html(feedback)) if feedback else '', + ) event.write({'description': description}) return messages, activities diff --git a/addons/calendar/tests/test_calendar.py b/addons/calendar/tests/test_calendar.py index ebece3f11de..4083877516c 100644 --- a/addons/calendar/tests/test_calendar.py +++ b/addons/calendar/tests/test_calendar.py @@ -123,7 +123,7 @@ class TestCalendar(SavepointCaseWithUserDemo): self.assertEqual(test_record.activity_ids.date_deadline, (now + timedelta(days=-2)).date()) # update event with a description that have a special character and a new line - test_description3 = 'Test & \n Description' + test_description3 = 'Test &
    Description' test_note3 = '

    Test &
    Description

    ' test_event.write({ 'description': test_description3, diff --git a/addons/google_calendar/models/calendar.py b/addons/google_calendar/models/calendar.py index 24878c93be7..54a76ec40a0 100644 --- a/addons/google_calendar/models/calendar.py +++ b/addons/google_calendar/models/calendar.py @@ -82,7 +82,7 @@ class Meeting(models.Model): name = google_event.summary or related_event and related_event.name or _("(No title)") values = { 'name': name, - 'description': google_event.description, + 'description': tools.plaintext2html(google_event.description), 'location': google_event.location, 'user_id': google_event.owner(self.env).id, 'privacy': google_event.visibility or self.default_get(['privacy'])['privacy'], @@ -214,7 +214,7 @@ class Meeting(models.Model): 'start': start, 'end': end, 'summary': self.name, - 'description': self.description or '', + 'description': tools.html2plaintext(self.description) if not tools.is_html_empty(self.description) else '', 'location': self.location or '', 'guestsCanModify': True, 'organizer': {'email': self.user_id.email, 'self': self.user_id == self.env.user}, diff --git a/addons/google_calendar/tests/test_sync_google2odoo.py b/addons/google_calendar/tests/test_sync_google2odoo.py index 6cdca96f2ca..97cc230bbb9 100644 --- a/addons/google_calendar/tests/test_sync_google2odoo.py +++ b/addons/google_calendar/tests/test_sync_google2odoo.py @@ -8,6 +8,7 @@ from dateutil.relativedelta import relativedelta from odoo.tests.common import new_test_user from odoo.addons.google_calendar.tests.test_sync_common import TestSyncGoogle, patch_api from odoo.addons.google_calendar.utils.google_calendar import GoogleEvent +from odoo.tools import html2plaintext class TestSyncGoogle2Odoo(TestSyncGoogle): @@ -50,7 +51,7 @@ class TestSyncGoogle2Odoo(TestSyncGoogle): self.assertTrue(event, "It should have created an event") self.assertEqual(event.name, values.get('summary')) self.assertFalse(event.allday) - self.assertEqual(event.description, values.get('description')) + self.assertEqual(html2plaintext(event.description), values.get('description')) self.assertEqual(event.start, datetime(2020, 1, 13, 15, 55)) self.assertEqual(event.stop, datetime(2020, 1, 13, 18, 55)) admin_attendee = event.attendee_ids.filtered(lambda e: e.email == 'admin@yourcompany.example.com') diff --git a/addons/microsoft_calendar/models/calendar.py b/addons/microsoft_calendar/models/calendar.py index 1c98788a596..566bd629873 100644 --- a/addons/microsoft_calendar/models/calendar.py +++ b/addons/microsoft_calendar/models/calendar.py @@ -7,6 +7,7 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError +from odoo.tools import html2plaintext, is_html_empty, plaintext2html ATTENDEE_CONVERTER_O2M = { 'needsAction': 'notresponded', @@ -87,7 +88,7 @@ class Meeting(models.Model): values = { **default_values, 'name': microsoft_event.subject or _("(No title)"), - 'description': microsoft_event.bodyPreview, + 'description': plaintext2html(microsoft_event.bodyPreview), 'location': microsoft_event.location and microsoft_event.location.get('displayName') or False, 'user_id': microsoft_event.owner(self.env).id, 'privacy': sensitivity_o2m.get(microsoft_event.sensitivity, self.default_get(['privacy'])['privacy']), @@ -247,7 +248,7 @@ class Meeting(models.Model): if 'description' in fields_to_sync: values['body'] = { - 'content': self.description or '', + 'content': html2plaintext(self.description) if not is_html_empty(self.description) else '', 'contentType': "text", } From b8fb33817b5a6af868a89452a7a150fa9ec6a515 Mon Sep 17 00:00:00 2001 From: dht-odoo Date: Tue, 4 May 2021 11:37:18 +0000 Subject: [PATCH 07/15] [IMP] website_slides: improves field type from text to html Replace text fields to html fields as we have our own 'OdooEditor'. Indeed, it gives more options to users in the way they format their content without weighting too much on the UI (tools appear on demand and not by default). Models -> Fields 1) slide.channel -> description 2) slide.channel -> description_short 3) slide.slide -> description Task Id: 2499504 X-original-commit: 6d914c005183aa5ae69650e3002a5a85ca83b4a6 --- addons/website_slides/models/slide_channel.py | 9 +++++---- addons/website_slides/models/slide_slide.py | 8 ++++---- .../views/website_slides_templates_course.xml | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index 60e74c019b6..338d71f16da 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -11,6 +11,7 @@ from odoo import api, fields, models, tools, _ from odoo.addons.http_routing.models.ir_http import slug from odoo.exceptions import AccessError from odoo.osv import expression +from odoo.tools import is_html_empty _logger = logging.getLogger(__name__) @@ -148,8 +149,8 @@ class Channel(models.Model): # description name = fields.Char('Name', translate=True, required=True) active = fields.Boolean(default=True, tracking=100) - description = fields.Text('Description', translate=True, help="The description that is displayed on top of the course page, just below the title") - description_short = fields.Text('Short Description', translate=True, help="The description that is displayed on the course card") + description = fields.Html('Description', translate=True, help="The description that is displayed on top of the course page, just below the title") + description_short = fields.Html('Short Description', translate=True, help="The description that is displayed on the course card") description_html = fields.Html('Detailed Description', translate=tools.html_translate, sanitize_attributes=False, sanitize_form=False) channel_type = fields.Selection([ ('training', 'Training'), ('documentation', 'Documentation')], @@ -455,7 +456,7 @@ class Channel(models.Model): vals['channel_partner_ids'] = [(0, 0, { 'partner_id': self.env.user.partner_id.id })] - if vals.get('description') and not vals.get('description_short'): + if not is_html_empty(vals.get('description')) and is_html_empty(vals.get('description_short')): vals['description_short'] = vals['description'] channel = super(Channel, self.with_context(mail_create_nosubscribe=True)).create(vals) @@ -468,7 +469,7 @@ class Channel(models.Model): def write(self, vals): # If description_short wasn't manually modified, there is an implicit link between this field and description. - if vals.get('description') and not vals.get('description_short') and self.description == self.description_short: + if not is_html_empty(vals.get('description')) and is_html_empty(vals.get('description_short')) and self.description == self.description_short: vals['description_short'] = vals.get('description') res = super(Channel, self).write(vals) diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index 8c8c3474bce..546ab68309b 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -18,7 +18,7 @@ from odoo.addons.http_routing.models.ir_http import slug from odoo.exceptions import Warning, UserError, AccessError from odoo.http import request from odoo.addons.http_routing.models.ir_http import url_for -from odoo.tools import sql +from odoo.tools import html2plaintext, sql class SlidePartnerRelation(models.Model): @@ -132,7 +132,7 @@ class Slide(models.Model): active = fields.Boolean(default=True, tracking=100) sequence = fields.Integer('Sequence', default=0) user_id = fields.Many2one('res.users', string='Uploaded by', default=lambda self: self.env.uid) - description = fields.Text('Description', translate=True) + description = fields.Html('Description', translate=True) channel_id = fields.Many2one('slide.channel', string="Course", required=True) tag_ids = fields.Many2many('slide.tag', 'rel_slide_tag', 'slide_id', 'tag_id', string='Tags') is_preview = fields.Boolean('Allow Preview', default=False, help="The course is accessible by anyone : the users don't need to join the channel to access the content of the course.") @@ -876,9 +876,9 @@ class Slide(models.Model): def _default_website_meta(self): res = super(Slide, self)._default_website_meta() res['default_opengraph']['og:title'] = res['default_twitter']['twitter:title'] = self.name - res['default_opengraph']['og:description'] = res['default_twitter']['twitter:description'] = self.description + res['default_opengraph']['og:description'] = res['default_twitter']['twitter:description'] = html2plaintext(self.description) res['default_opengraph']['og:image'] = res['default_twitter']['twitter:image'] = self.env['website'].image_url(self, 'image_1024') - res['default_meta_description'] = self.description + res['default_meta_description'] = html2plaintext(self.description) return res # --------------------------------------------------------- diff --git a/addons/website_slides/views/website_slides_templates_course.xml b/addons/website_slides/views/website_slides_templates_course.xml index 837455628c5..a2b40070242 100644 --- a/addons/website_slides/views/website_slides_templates_course.xml +++ b/addons/website_slides/views/website_slides_templates_course.xml @@ -130,7 +130,7 @@

    -

    +

    @@ -192,7 +193,7 @@
      -
    • +

    diff --git a/addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml b/addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml index e5681b16d6c..30dc3abebdc 100644 --- a/addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml +++ b/addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml @@ -52,10 +52,10 @@
    open positions
    -

    -

    From 025004309c796c5edaf5afc749f9e93662cd295e Mon Sep 17 00:00:00 2001 From: dht-odoo Date: Mon, 31 May 2021 09:25:47 +0000 Subject: [PATCH 14/15] [FIX] web: fix js traceback of imageType variable X-original-commit: 895f262e03d0aaa6d00f2ba937561be0d8b8fd28 --- addons/web/static/src/js/libs/jSignatureCustom.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/addons/web/static/src/js/libs/jSignatureCustom.js b/addons/web/static/src/js/libs/jSignatureCustom.js index ca8306af286..da4ab05be15 100644 --- a/addons/web/static/src/js/libs/jSignatureCustom.js +++ b/addons/web/static/src/js/libs/jSignatureCustom.js @@ -34,15 +34,11 @@ function _renderImageOnCanvas( data, formattype, rerendercallable ) { img.src = 'data:' + formattype + ',' + data; } +// Override // Supported image types -const imageTypes = [ +[ 'image', 'image/png;base64', 'image/jpeg;base64', 'image/jpg;base64', -]; - -// Override -for (const imageType of imageTypes) { - $.fn.jSignature("addPlugin", "import", imageType, _renderImageOnCanvas); -} +].forEach(imageType => $.fn.jSignature("addPlugin", "import", imageType, _renderImageOnCanvas)); From 3d35ea645f9a55df1fdb80010258e6a24a1b587c Mon Sep 17 00:00:00 2001 From: dht-odoo Date: Tue, 1 Jun 2021 10:10:26 +0000 Subject: [PATCH 15/15] [FIX] web: fix scss of ol tag in report This is a temporary fix, it needs to be fix properly in future. It was working properly with HTML reports but when it is render with wkhtmltopdf it does not apply padding on left. Due to which issue is occuring. Task id: 2499504 X-original-commit: 2958c8d08f4bdab94ba55fc944db6d314fb3fa43 --- addons/web/static/src/scss/report.scss | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/addons/web/static/src/scss/report.scss b/addons/web/static/src/scss/report.scss index 8e29f0ceacb..27cbfe85e18 100644 --- a/addons/web/static/src/scss/report.scss +++ b/addons/web/static/src/scss/report.scss @@ -92,6 +92,17 @@ table { } } +// TODO: investigate further for proper fix +// By default there is 40px padding on the lists (both
      and
        ) and it is +// also the case with HTML reports, but when PDFs are rendered with Wkhtmltopdf, +// the padding is not applied on
          , it is strangely applied on
            only. +// So we simply remove the left padding, and apply left margin instead which +// seems to do the job while not breaking layout in both html/pdf. +ol { + margin-left: 40px; + padding-left: 0; +} + // Wkhtmltopdf doesn't handle flexbox properly, both the content // of columns and columns themselves does not wrap over new lines // when needed: the font of the pdf will reduce to make the content