[FW][MERGE] various: Html fields everywhere !
PURPOSE Replace text fields by html ones now that our own html editor has been merged. Indeed it gives more options to users in the way they format their content without weighting too much on the UI as tools appear on demand and not by default. SPECIFICATIONS Convert a lot of text fields to Html fields as we have our own OdooEditor. In some modules code adaptation will be necessary to handle html / plaintext conversion when required. See individual commits per main application scope for more details. LINKS Task ID-2499504 closes odoo/odoo#71776 Forward-port-of: odoo/odoo#70663 Related: odoo/enterprise#18778 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import RedirectWarning, UserError, ValidationError, AccessError
|
||||
from odoo.tools import float_compare, date_utils, email_split, email_re, html_escape
|
||||
from odoo.tools import float_compare, date_utils, email_split, email_re, html_escape, is_html_empty
|
||||
from odoo.tools.misc import formatLang, format_date, get_lang
|
||||
|
||||
from datetime import date, timedelta
|
||||
@@ -144,7 +144,7 @@ class AccountMove(models.Model):
|
||||
default=fields.Date.context_today
|
||||
)
|
||||
ref = fields.Char(string='Reference', copy=False, tracking=True)
|
||||
narration = fields.Text(string='Terms and Conditions')
|
||||
narration = fields.Html(string='Terms and Conditions')
|
||||
state = fields.Selection(selection=[
|
||||
('draft', 'Draft'),
|
||||
('posted', 'Posted'),
|
||||
@@ -493,7 +493,7 @@ class AccountMove(models.Model):
|
||||
''' Onchange made to filter the partners depending of the type. '''
|
||||
if self.is_sale_document(include_receipts=True):
|
||||
if self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms'):
|
||||
self.narration = self.company_id.invoice_terms or self.env.company.invoice_terms
|
||||
self.narration = self.company_id.invoice_terms if not is_html_empty(self.company_id.invoice_terms) else self.env.company.invoice_terms
|
||||
|
||||
@api.onchange('invoice_line_ids')
|
||||
def _onchange_invoice_line_ids(self):
|
||||
|
||||
@@ -16,7 +16,7 @@ class AccountPaymentTerm(models.Model):
|
||||
|
||||
name = fields.Char(string='Payment Terms', translate=True, required=True)
|
||||
active = fields.Boolean(default=True, help="If the active field is set to False, it will allow you to hide the payment terms without removing it.")
|
||||
note = fields.Text(string='Description on the Invoice', translate=True)
|
||||
note = fields.Html(string='Description on the Invoice', translate=True)
|
||||
line_ids = fields.One2many('account.payment.term.line', 'payment_id', string='Terms', copy=True, default=_default_line_ids)
|
||||
company_id = fields.Many2one('res.company', string='Company')
|
||||
sequence = fields.Integer(required=True, default=10)
|
||||
|
||||
@@ -102,7 +102,7 @@ class ResCompany(models.Model):
|
||||
# account dashboard onboarding
|
||||
account_invoice_onboarding_state = fields.Selection(DASHBOARD_ONBOARDING_STATES, string="State of the account invoice onboarding panel", default='not_done')
|
||||
account_dashboard_onboarding_state = fields.Selection(DASHBOARD_ONBOARDING_STATES, string="State of the account dashboard onboarding panel", default='not_done')
|
||||
invoice_terms = fields.Text(string='Default Terms and Conditions', translate=True)
|
||||
invoice_terms = fields.Html(string='Default Terms and Conditions', translate=True)
|
||||
terms_type = fields.Selection([('plain', 'Terms as Notes'), ('html', 'Terms as Web Page')],
|
||||
string='Terms & Conditions format', default='plain')
|
||||
invoice_terms_html = fields.Html(string='Default Terms and Conditions as a Web page', translate=True,
|
||||
|
||||
@@ -28,7 +28,7 @@ class AccountFiscalPosition(models.Model):
|
||||
default=lambda self: self.env.company)
|
||||
account_ids = fields.One2many('account.fiscal.position.account', 'position_id', string='Account Mapping', copy=True)
|
||||
tax_ids = fields.One2many('account.fiscal.position.tax', 'position_id', string='Tax Mapping', copy=True)
|
||||
note = fields.Text('Notes', translate=True, help="Legal mentions that have to be printed on the invoices.")
|
||||
note = fields.Html('Notes', translate=True, help="Legal mentions that have to be printed on the invoices.")
|
||||
auto_apply = fields.Boolean(string='Detect Automatically', help="Apply automatically this fiscal position.")
|
||||
vat_required = fields.Boolean(string='VAT required', help="Apply only if partner has a VAT number.")
|
||||
company_country_id = fields.Many2one(string="Company Country", related='company_id.country_id')
|
||||
|
||||
@@ -124,7 +124,7 @@ class ResConfigSettings(models.TransientModel):
|
||||
invoice_is_print = fields.Boolean(string='Print', related='company_id.invoice_is_print', readonly=False)
|
||||
invoice_is_email = fields.Boolean(string='Send Email', related='company_id.invoice_is_email', readonly=False)
|
||||
incoterm_id = fields.Many2one('account.incoterms', string='Default incoterm', related='company_id.incoterm_id', help='International Commercial Terms are a series of predefined commercial terms used in international transactions.', readonly=False)
|
||||
invoice_terms = fields.Text(related='company_id.invoice_terms', string="Terms & Conditions", readonly=False)
|
||||
invoice_terms = fields.Html(related='company_id.invoice_terms', string="Terms & Conditions", readonly=False)
|
||||
invoice_terms_html = fields.Html(related='company_id.invoice_terms_html', string="Terms & Conditions as a Web page",
|
||||
readonly=False)
|
||||
terms_type = fields.Selection(
|
||||
|
||||
@@ -1022,12 +1022,13 @@
|
||||
<field name="invoice_payments_widget" colspan="2" nolabel="1" widget="payment"/>
|
||||
<field name="amount_residual" class="oe_subtotal_footer_separator" attrs="{'invisible': [('state', '=', 'draft')]}"/>
|
||||
</group>
|
||||
<field name="narration" placeholder="Terms and Conditions" class="oe_inline" nolabel="1"/>
|
||||
|
||||
<field name="invoice_outstanding_credits_debits_widget"
|
||||
class="oe_invoice_outstanding_credits_debits"
|
||||
colspan="2" nolabel="1" widget="payment"
|
||||
attrs="{'invisible': ['|', ('state', '!=', 'posted'), ('move_type', 'in', ('out_receipt', 'in_receipt'))]}"/>
|
||||
<group>
|
||||
<field name="narration" placeholder="Terms and Conditions" class="oe_inline" nolabel="1"/>
|
||||
</group>
|
||||
</page>
|
||||
<page id="aml_tab" string="Journal Items" groups="account.group_account_readonly">
|
||||
<div class="alert alert-info text-center mb-0" role="alert" attrs="{'invisible': ['|', ('payment_state', '!=', 'invoicing_legacy'), ('move_type', '=', 'entry')]}">
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_global_click">
|
||||
<div><strong class="o_kanban_record_title"><t t-esc="record.name.value"/></strong></div>
|
||||
<div t-if="record.note.value"><t t-esc="record.note.value"/></div>
|
||||
<div t-if="!widget.isHtmlEmpty(record.note.raw_value)"><t t-raw="record.note.raw_value"/></div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
@@ -191,10 +191,10 @@
|
||||
<p t-if="o.invoice_payment_term_id" name="payment_term">
|
||||
<span t-field="o.invoice_payment_term_id.note"/>
|
||||
</p>
|
||||
<p t-if="o.narration" name="comment">
|
||||
<p t-if="not is_html_empty(o.narration)" name="comment">
|
||||
<span t-field="o.narration"/>
|
||||
</p>
|
||||
<p t-if="o.fiscal_position_id.note" name="note">
|
||||
<p t-if="not is_html_empty(o.fiscal_position_id.note)" name="note">
|
||||
<span t-field="o.fiscal_position_id.note"/>
|
||||
</p>
|
||||
<p t-if="o.invoice_incoterm_id" name="incoterm">
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
<span class="d-block font-weight-bold" t-if="line.partner_id" t-field="line.partner_id"/>
|
||||
<span class="d-block" t-if="line.partner_bank_id" t-field="line.partner_bank_id"/>
|
||||
<span class="d-block" t-if="line.payment_ref" t-field="line.payment_ref"/>
|
||||
<span class="d-block" t-if="line.narration" t-field="line.narration"/>
|
||||
<span class="d-block" t-if=" not is_html_empty(line.narration)" t-field="line.narration"/>
|
||||
</td>
|
||||
<td class="text-right p-0">
|
||||
<span class="d-block font-weight-bold" t-field="line.amount"/>
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
<ram:IssueDateTime>
|
||||
<udt:DateTimeString format="102" t-esc="format_date(record.invoice_date)"/>
|
||||
</ram:IssueDateTime>
|
||||
<ram:IncludedNote t-if="record.narration">
|
||||
<ram:IncludedNote t-if="not is_html_empty(record.narration)">
|
||||
<ram:Content t-esc="record.narration"/>
|
||||
</ram:IncludedNote>
|
||||
</rsm:ExchangedDocument>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, models, fields, tools, _
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, float_repr
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, float_repr, is_html_empty
|
||||
from odoo.tests.common import Form
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
@@ -61,6 +61,7 @@ class AccountEdiFormat(models.Model):
|
||||
**invoice._prepare_edi_vals_to_export(),
|
||||
'format_date': format_date,
|
||||
'format_monetary': format_monetary,
|
||||
'is_html_empty': is_html_empty,
|
||||
}
|
||||
|
||||
xml_content = b"<?xml version='1.0' encoding='UTF-8'?>"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -92,18 +92,6 @@
|
||||
% if not object.event_id.allday and object.event_id.duration
|
||||
<li>Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}</li>
|
||||
% endif
|
||||
% if not is_online and object.event_id.description :
|
||||
<li>Description: ${object.event_id.description}</li>
|
||||
% elif is_online and object.event_id.description:
|
||||
% set splitted_description = object.event_id.description_to_html_lines()
|
||||
<li>Description:
|
||||
<ul>
|
||||
% for description_line in splitted_description:
|
||||
<li>${description_line | safe}</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</li>
|
||||
% endif
|
||||
<li>Attendees
|
||||
<ul>
|
||||
% for attendee in object.event_id.attendee_ids:
|
||||
@@ -121,6 +109,10 @@
|
||||
<li>Join Video Call: <a href="${object.event_id.videocall_location}" target="_blank">${object.event_id.videocall_location}</a></li>
|
||||
% endif
|
||||
</ul>
|
||||
% if not is_html_empty(object.event_id.description):
|
||||
<p><strong>Description of the event</strong></p>
|
||||
${object.event_id.description | safe}
|
||||
% endif
|
||||
</td>
|
||||
</tr></table>
|
||||
<br/>
|
||||
@@ -222,18 +214,6 @@
|
||||
% if not object.event_id.allday and object.event_id.duration
|
||||
<li>Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}</li>
|
||||
% endif
|
||||
% if not is_online and object.event_id.description :
|
||||
<li>Description: ${object.event_id.description}</li>
|
||||
% elif is_online and object.event_id.description:
|
||||
% set splitted_description = object.event_id.description_to_html_lines()
|
||||
<li>Description:
|
||||
<ul>
|
||||
% for description_line in splitted_description:
|
||||
<li>${description_line | safe}</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</li>
|
||||
% endif
|
||||
<li>Attendees
|
||||
<ul>
|
||||
% for attendee in object.event_id.attendee_ids:
|
||||
@@ -251,6 +231,10 @@
|
||||
<li>Join Video Call: <a href="${object.event_id.videocall_location}" target="_blank">${object.event_id.videocall_location}</a></li>
|
||||
% endif
|
||||
</ul>
|
||||
% if not is_html_empty(object.event_id.description):
|
||||
<p><strong>Description of the event</strong></p>
|
||||
${object.event_id.description | safe}
|
||||
% endif
|
||||
</td>
|
||||
</tr></table>
|
||||
<br/>
|
||||
@@ -332,18 +316,6 @@
|
||||
% if not object.event_id.allday and object.event_id.duration
|
||||
<li>Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}</li>
|
||||
% endif
|
||||
% if not is_online and object.event_id.description :
|
||||
<li>Description: ${object.event_id.description}</li>
|
||||
% elif is_online and object.event_id.description:
|
||||
% set splitted_description = object.event_id.description_to_html_lines()
|
||||
<li>Description:
|
||||
<ul>
|
||||
% for description_line in splitted_description:
|
||||
<li>${description_line | safe}</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</li>
|
||||
% endif
|
||||
<li>Attendees
|
||||
<ul>
|
||||
% for attendee in object.event_id.attendee_ids:
|
||||
@@ -361,6 +333,10 @@
|
||||
<li>Join Video Call: <a href="${object.event_id.videocall_location}" target="_blank">${object.event_id.videocall_location}</a></li>
|
||||
% endif
|
||||
</ul>
|
||||
% if not is_html_empty(object.event_id.description):
|
||||
<p><strong>Description of the event</strong></p>
|
||||
${object.event_id.description | safe}
|
||||
% endif
|
||||
</td>
|
||||
</tr></table>
|
||||
<br/>
|
||||
|
||||
@@ -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 = ["<ul>", "</ul>", "<li>"]
|
||||
for tag in tags_to_replace:
|
||||
desc_str = desc_str.replace(tag, "")
|
||||
desc_str = desc_str.replace("</li>", "<br/>")
|
||||
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<br />as multi line',
|
||||
'Mobile: +320475000000',
|
||||
'Email: my.email@test.example.com',
|
||||
'SingleLine: answer',
|
||||
'MultiLine:<br />Answer1<br />Answer2<br />Answer3',
|
||||
'Dropdown: answer',
|
||||
'Radio: answer',
|
||||
'Checkboxes: answer1, answer2',
|
||||
'Some free text salespeople added<br .>as multi line']
|
||||
|
||||
Each item of returned list is escaped so that only our intended <br />
|
||||
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('<br />'.join(html_escape(line) for line in parsed_lines))
|
||||
parsed_lines = [line.lstrip(' *')]
|
||||
else:
|
||||
parsed_lines.append(line)
|
||||
if parsed_lines:
|
||||
final_lines.append('<br />'.join(html_escape(line) for line in parsed_lines))
|
||||
return final_lines
|
||||
|
||||
@@ -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<br />%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
|
||||
|
||||
|
||||
@@ -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 & <br> Description'
|
||||
test_note3 = '<p>Test & <br> Description</p>'
|
||||
test_event.write({
|
||||
'description': test_description3,
|
||||
|
||||
@@ -121,7 +121,7 @@ class Lead(models.Model):
|
||||
'res.company', string='Company', index=True,
|
||||
compute='_compute_company_id', readonly=False, store=True)
|
||||
referred = fields.Char('Referred By')
|
||||
description = fields.Text('Notes')
|
||||
description = fields.Html('Notes')
|
||||
active = fields.Boolean('Active', default=True, tracking=True)
|
||||
type = fields.Selection([
|
||||
('lead', 'Lead'), ('opportunity', 'Opportunity')],
|
||||
@@ -1376,7 +1376,7 @@ class Lead(models.Model):
|
||||
|
||||
def _merge_get_fields_specific(self):
|
||||
return {
|
||||
'description': lambda fname, leads: '\n\n'.join(desc for desc in leads.mapped('description') if desc),
|
||||
'description': lambda fname, leads: '<br/><br/>'.join(desc for desc in leads.mapped('description') if not is_html_empty(desc)),
|
||||
'type': lambda fname, leads: 'opportunity' if any(lead.type == 'opportunity' for lead in leads) else 'lead',
|
||||
'priority': lambda fname, leads: max(leads.mapped('priority')) if leads else False,
|
||||
}
|
||||
|
||||
@@ -399,7 +399,7 @@ class TestCrmCommon(TestSalesCommon, MailCase):
|
||||
|
||||
def _get_description():
|
||||
values = [_find_value(lead, 'description') for lead in leads]
|
||||
return '\n\n'.join(value for value in values if value)
|
||||
return '<br><br>'.join(value for value in values if value)
|
||||
|
||||
def _get_priority():
|
||||
values = [_find_value(lead, 'priority') for lead in leads]
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestLeadMerge(TestLeadMergeCommon):
|
||||
# and exclude inactive leads, but that's not written anywhere ... intended ??
|
||||
self.assertEqual(merge.opportunity_ids, self.leads - self.lead_w_partner_company - self.lead_w_email_lost)
|
||||
ordered_merge = self.lead_w_contact + self.lead_w_email + self.lead_1 + self.lead_w_partner
|
||||
ordered_merge_description = '\n\n'.join(l.description for l in ordered_merge)
|
||||
ordered_merge_description = '<br><br>'.join(l.description for l in ordered_merge)
|
||||
|
||||
# merged opportunity: in this test, all input are leads. Confidence is based on stage
|
||||
# sequence -> lead_w_contact has a stage sequence of 30
|
||||
|
||||
@@ -28,9 +28,9 @@ class MailChannel(models.Model):
|
||||
:param key: operator input in chat ('/lead Lead about Product')
|
||||
"""
|
||||
description = ''.join(
|
||||
'%s: %s\n' % (message.author_id.name or self.anonymous_name, message.body)
|
||||
'%s: %s<br/>' % (message.author_id.name or self.anonymous_name, html2plaintext(message.body))
|
||||
for message in self.message_ids.sorted('id')
|
||||
)
|
||||
) # converting message body back to plaintext for correct data formating in description
|
||||
# if public user is part of the chat: consider lead to be linked to an
|
||||
# anonymous user whatever the participants. Otherwise keep only share
|
||||
# partners (no user or portal user) to link to the lead.
|
||||
@@ -48,7 +48,7 @@ class MailChannel(models.Model):
|
||||
'partner_id': customers[0].id if customers else False,
|
||||
'user_id': False,
|
||||
'team_id': False,
|
||||
'description': html2plaintext(description),
|
||||
'description': description,
|
||||
'referred': partner.name,
|
||||
'source_id': utm_source and utm_source.id,
|
||||
})
|
||||
|
||||
@@ -106,7 +106,7 @@ class EventEvent(models.Model):
|
||||
return self.env['ir.ui.view']._render_template('event.event_default_descripton')
|
||||
|
||||
name = fields.Char(string='Event', translate=True, required=True)
|
||||
note = fields.Text(string='Note')
|
||||
note = fields.Html(string='Note')
|
||||
description = fields.Html(string='Description', translate=html_translate, sanitize_attributes=False, sanitize_form=False, default=_default_description)
|
||||
active = fields.Boolean(default=True)
|
||||
user_id = fields.Many2one(
|
||||
|
||||
@@ -177,7 +177,7 @@ class EventLeadRule(models.Model):
|
||||
additionnal_description = group_registrations._get_lead_description(_("New registrations"), line_counter=True)
|
||||
for lead in toupdate_leads:
|
||||
lead.write({
|
||||
'description': "%s\n%s" % (lead.description, additionnal_description),
|
||||
'description': "%s<br/>%s" % (lead.description, additionnal_description),
|
||||
'registration_ids': [(4, reg.id) for reg in group_registrations],
|
||||
})
|
||||
elif group_registrations:
|
||||
|
||||
@@ -132,7 +132,7 @@ class EventRegistration(models.Model):
|
||||
upd_description_fields = [field for field in self._get_lead_description_fields() if field in new_vals.keys()]
|
||||
if any(new_vals[field] != old_vals[field] for field in upd_description_fields):
|
||||
for lead in leads_attendee:
|
||||
lead_values['description'] = "%s\n%s" % (
|
||||
lead_values['description'] = "%s<br/>%s" % (
|
||||
lead.description,
|
||||
registration._get_lead_description(_("Updated registrations"), line_counter=True)
|
||||
)
|
||||
@@ -148,7 +148,7 @@ class EventRegistration(models.Model):
|
||||
if not lead.partner_id:
|
||||
lead_values['description'] = lead.registration_ids._get_lead_description(_("Participants"), line_counter=True)
|
||||
elif new_vals['partner_id'] != lead.partner_id.id:
|
||||
lead_values['description'] = lead.description + "\n" + lead.registration_ids._get_lead_description(_("Updated registrations"), line_counter=True, line_suffix=_("(updated)"))
|
||||
lead_values['description'] = lead.description + "<br/>" + lead.registration_ids._get_lead_description(_("Updated registrations"), line_counter=True, line_suffix=_("(updated)"))
|
||||
if lead_values:
|
||||
lead.write(lead_values)
|
||||
|
||||
@@ -232,17 +232,16 @@ class EventRegistration(models.Model):
|
||||
"""
|
||||
reg_lines = [
|
||||
registration._get_lead_description_registration(
|
||||
prefix="%s. " % (index + 1) if line_counter else "",
|
||||
line_suffix=line_suffix
|
||||
) for index, registration in enumerate(self)
|
||||
) for registration in self
|
||||
]
|
||||
return ("%s\n" % prefix if prefix else "") + ("\n".join(reg_lines))
|
||||
return ("%s<br/>" % prefix if prefix else "") + (
|
||||
"<ol>" if line_counter else "<ul>") + ("".join(reg_lines)) + ("</ol>" if line_counter else "</ul>")
|
||||
|
||||
def _get_lead_description_registration(self, prefix='', line_suffix=''):
|
||||
def _get_lead_description_registration(self, line_suffix=''):
|
||||
""" Build the description line specific to a given registration. """
|
||||
self.ensure_one()
|
||||
return "%s%s (%s)%s" % (
|
||||
prefix or "",
|
||||
return "<li>%s (%s)%s</li>" % (
|
||||
self.name or self.partner_id.name or self.email,
|
||||
" - ".join(self[field] for field in ('email', 'phone') if self[field]),
|
||||
" %s" % line_suffix if line_suffix else "",
|
||||
|
||||
@@ -18,7 +18,7 @@ class FleetVehicle(models.Model):
|
||||
return state if state and state.id else False
|
||||
|
||||
name = fields.Char(compute="_compute_vehicle_name", store=True)
|
||||
description = fields.Text("Vehicle Description")
|
||||
description = fields.Html("Vehicle Description")
|
||||
active = fields.Boolean('Active', default=True, tracking=True)
|
||||
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
|
||||
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
|
||||
|
||||
@@ -46,7 +46,7 @@ class FleetVehicleLogContract(models.Model):
|
||||
help='Choose whether the contract is still valid or not',
|
||||
tracking=True,
|
||||
copy=False)
|
||||
notes = fields.Text('Terms and Conditions', help='Write here all supplementary information relative to this contract', copy=False)
|
||||
notes = fields.Html('Terms and Conditions', help='Write here all supplementary information relative to this contract', copy=False)
|
||||
cost_generated = fields.Monetary('Recurring Cost')
|
||||
cost_frequency = fields.Selection([
|
||||
('no', 'No'),
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<div>
|
||||
Congratulations ${object.user_id.name} !<br/>
|
||||
You just received badge <strong>${object.badge_id.name}</strong> !<br/>
|
||||
% if object.badge_id.description
|
||||
% if not is_html_empty(object.badge_id.description)
|
||||
<table cellspacing="0" cellpadding="0" border="0" style="width: 560px; margin-top: 5px;">
|
||||
<tbody><tr>
|
||||
<td valign="center">
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -144,8 +144,8 @@
|
||||
<strong><t t-esc="record.granted_count.raw_value"/></strong> granted,
|
||||
<strong><t t-esc="record.stat_this_month.raw_value"/></strong> this month
|
||||
</div>
|
||||
<div t-if="record.description.value" class="o_kanban_badge_description">
|
||||
<em><field name="description"/></em>
|
||||
<div t-if="!widget.isHtmlEmpty(record.description.value)" class="o_kanban_badge_description">
|
||||
<em t-raw="record.description.raw_value"/>
|
||||
<div>
|
||||
<t t-foreach="record.unique_owner_ids.raw_value.slice(0,11)" t-as="owner">
|
||||
<img class="oe_kanban_avatar o_image_24_cover" t-att-src="kanban_image('res.users', 'avatar_128', owner)" t-att-data-member_id="owner" alt="Owner"/>
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -20,7 +20,7 @@ class Job(models.Model):
|
||||
no_of_hired_employee = fields.Integer(string='Hired Employees', copy=False,
|
||||
help='Number of hired employees for this job position during recruitment phase.')
|
||||
employee_ids = fields.One2many('hr.employee', 'job_id', string='Employees', groups='base.group_user')
|
||||
description = fields.Text(string='Job Description')
|
||||
description = fields.Html(string='Job Description')
|
||||
requirements = fields.Text('Requirements')
|
||||
department_id = fields.Many2one('hr.department', string='Department', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
|
||||
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company)
|
||||
|
||||
@@ -33,7 +33,7 @@ class Contract(models.Model):
|
||||
default=lambda self: self.env.company.resource_calendar_id.id, copy=False, index=True,
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
|
||||
wage = fields.Monetary('Wage', required=True, tracking=True, help="Employee's monthly gross wage.")
|
||||
notes = fields.Text('Notes')
|
||||
notes = fields.Html('Notes')
|
||||
state = fields.Selection([
|
||||
('draft', 'New'),
|
||||
('open', 'Running'),
|
||||
|
||||
@@ -66,7 +66,7 @@ class HolidaysAllocation(models.Model):
|
||||
'hr.employee', compute='_compute_from_holiday_type', store=True, string='Employee', index=True, readonly=False, ondelete="restrict", tracking=True,
|
||||
states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]})
|
||||
manager_id = fields.Many2one('hr.employee', compute='_compute_from_employee_id', store=True, string='Manager')
|
||||
notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
|
||||
notes = fields.Html('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
|
||||
# duration
|
||||
number_of_days = fields.Float(
|
||||
'Number of Days', compute='_compute_from_holiday_status_id', store=True, readonly=False, tracking=True, default=1,
|
||||
|
||||
@@ -99,7 +99,7 @@ class Applicant(models.Model):
|
||||
|
||||
name = fields.Char("Subject / Application Name", required=True, help="Email subject for applications sent via email")
|
||||
active = fields.Boolean("Active", default=True, help="If the active field is set to false, it will allow you to hide the case without removing it.")
|
||||
description = fields.Text("Description")
|
||||
description = fields.Html("Description")
|
||||
email_from = fields.Char("Email", size=128, help="Applicant email", compute='_compute_partner_phone_email',
|
||||
inverse='_inverse_partner_email', store=True)
|
||||
probability = fields.Float("Probability")
|
||||
|
||||
@@ -225,7 +225,7 @@ class AccountEdiFormat(models.Model):
|
||||
# Comment. <2.1.1.11>
|
||||
elements = body_tree.xpath('.//DatiGeneraliDocumento//Causale')
|
||||
for element in elements:
|
||||
invoice_form.narration = '%s%s\n' % (invoice_form.narration or '', element.text)
|
||||
invoice_form.narration = '%s%s<br/>' % (invoice_form.narration or '', element.text)
|
||||
|
||||
# Informations relative to the purchase order, the contract, the agreement,
|
||||
# the reception phase or invoices previously transmitted
|
||||
|
||||
@@ -58,7 +58,7 @@ class AccountFiscalPosition(models.Model):
|
||||
_inherit = 'account.fiscal.position'
|
||||
|
||||
name = fields.Char(translate=True)
|
||||
note = fields.Text(translate=True)
|
||||
note = fields.Html(translate=True)
|
||||
|
||||
|
||||
class AccountFiscalPositionTemplate(models.Model):
|
||||
|
||||
@@ -41,7 +41,7 @@ class LunchOrder(models.Model):
|
||||
|
||||
display_toppings = fields.Text('Extras', compute='_compute_display_toppings', store=True)
|
||||
|
||||
product_description = fields.Text('Description', related='product_id.description')
|
||||
product_description = fields.Html('Description', related='product_id.description')
|
||||
topping_label_1 = fields.Char(related='product_id.category_id.topping_label_1')
|
||||
topping_label_2 = fields.Char(related='product_id.category_id.topping_label_2')
|
||||
topping_label_3 = fields.Char(related='product_id.category_id.topping_label_3')
|
||||
|
||||
@@ -111,7 +111,7 @@ class LunchProduct(models.Model):
|
||||
|
||||
name = fields.Char('Product Name', required=True, translate=True)
|
||||
category_id = fields.Many2one('lunch.product.category', 'Product Category', check_company=True, required=True)
|
||||
description = fields.Text('Description', translate=True)
|
||||
description = fields.Html('Description', translate=True)
|
||||
price = fields.Float('Price', digits='Account', required=True)
|
||||
supplier_id = fields.Many2one('lunch.supplier', 'Vendor', check_company=True, required=True)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
@@ -15,7 +15,7 @@ class LunchProductReport(models.Model):
|
||||
product_id = fields.Many2one('lunch.product', 'Product')
|
||||
name = fields.Char('Product Name', related='product_id.name')
|
||||
category_id = fields.Many2one('lunch.product.category', 'Product Category')
|
||||
description = fields.Text('Description', related='product_id.description')
|
||||
description = fields.Html('Description', related='product_id.description')
|
||||
price = fields.Float('Price')
|
||||
supplier_id = fields.Many2one('lunch.supplier', 'Vendor')
|
||||
company_id = fields.Many2one('res.company')
|
||||
|
||||
@@ -84,9 +84,10 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="new_until"/>
|
||||
<field name='description'/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
<label for="description"/>
|
||||
<field name='description'/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
@@ -148,7 +149,7 @@
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<ul>
|
||||
<li t-esc="record.description.value" class="text-muted"/>
|
||||
<li t-raw="record.description.raw_value" class="text-muted"/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,7 +193,7 @@
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<ul>
|
||||
<li t-esc="record.description.value" class="text-muted"/>
|
||||
<li t-raw="record.description.raw_value" class="text-muted"/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -40,7 +40,7 @@ class MaintenanceEquipmentCategory(models.Model):
|
||||
default=lambda self: self.env.company)
|
||||
technician_user_id = fields.Many2one('res.users', 'Responsible', tracking=True, default=lambda self: self.env.uid)
|
||||
color = fields.Integer('Color Index')
|
||||
note = fields.Text('Comments', translate=True)
|
||||
note = fields.Html('Comments', translate=True)
|
||||
equipment_ids = fields.One2many('maintenance.equipment', 'category_id', string='Equipments', copy=False)
|
||||
equipment_count = fields.Integer(string="Equipment", compute='_compute_equipment_count')
|
||||
maintenance_ids = fields.One2many('maintenance.request', 'category_id', copy=False)
|
||||
@@ -125,7 +125,7 @@ class MaintenanceEquipment(models.Model):
|
||||
assign_date = fields.Date('Assigned Date', tracking=True)
|
||||
effective_date = fields.Date('Effective Date', default=fields.Date.context_today, required=True, help="Date at which the equipment became effective. This date will be used to compute the Mean Time Between Failure.")
|
||||
cost = fields.Float('Cost')
|
||||
note = fields.Text('Note')
|
||||
note = fields.Html('Note')
|
||||
warranty_date = fields.Date('Warranty Expiration Date')
|
||||
color = fields.Integer('Color Index')
|
||||
scrap_date = fields.Date('Scrap Date')
|
||||
@@ -280,7 +280,7 @@ class MaintenanceRequest(models.Model):
|
||||
name = fields.Char('Subjects', required=True)
|
||||
company_id = fields.Many2one('res.company', string='Company',
|
||||
default=lambda self: self.env.company)
|
||||
description = fields.Text('Description')
|
||||
description = fields.Html('Description')
|
||||
request_date = fields.Date('Request Date', tracking=True, default=fields.Date.context_today,
|
||||
help="Date requested for the maintenance to happen")
|
||||
owner_user_id = fields.Many2one('res.users', string='Created by User', default=lambda s: s.env.uid)
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class MrpRoutingWorkcenter(models.Model):
|
||||
string="Work Sheet", default="text",
|
||||
help="Defines if you want to use a PDF or a Google Slide as work sheet."
|
||||
)
|
||||
note = fields.Text('Description', help="Text worksheet description")
|
||||
note = fields.Html('Description', help="Text worksheet description")
|
||||
worksheet = fields.Binary('PDF')
|
||||
worksheet_google_slide = fields.Char('Google Slide', help="Paste the url of your Google Slide. Make sure the access to the document is public.")
|
||||
time_mode = fields.Selection([
|
||||
|
||||
@@ -26,7 +26,7 @@ class MrpWorkcenter(models.Model):
|
||||
active = fields.Boolean('Active', related='resource_id.active', default=True, store=True, readonly=False)
|
||||
|
||||
code = fields.Char('Code', copy=False)
|
||||
note = fields.Text(
|
||||
note = fields.Html(
|
||||
'Description',
|
||||
help="Description of the Work Center.")
|
||||
capacity = fields.Float(
|
||||
|
||||
@@ -112,7 +112,7 @@ class MrpWorkorder(models.Model):
|
||||
string='Worksheet Type', related='operation_id.worksheet_type', readonly=True)
|
||||
worksheet_google_slide = fields.Char(
|
||||
'Worksheet URL', related='operation_id.worksheet_google_slide', readonly=True)
|
||||
operation_note = fields.Text("Description", related='operation_id.note', readonly=True)
|
||||
operation_note = fields.Html("Description", related='operation_id.note', readonly=True)
|
||||
move_raw_ids = fields.One2many(
|
||||
'stock.move', 'workorder_id', 'Raw Moves',
|
||||
domain=[('raw_material_production_id', '!=', False), ('production_id', '=', False)])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -103,7 +103,7 @@ class PurchaseOrder(models.Model):
|
||||
('cancel', 'Cancelled')
|
||||
], string='Status', readonly=True, index=True, copy=False, default='draft', tracking=True)
|
||||
order_line = fields.One2many('purchase.order.line', 'order_id', string='Order Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True)
|
||||
notes = fields.Text('Terms and Conditions')
|
||||
notes = fields.Html('Terms and Conditions')
|
||||
|
||||
invoice_count = fields.Integer(compute="_compute_invoice", string='Bill Count', copy=False, default=0, store=True)
|
||||
invoice_ids = fields.Many2many('account.move', compute="_compute_invoice", string='Bills', copy=False, store=True)
|
||||
|
||||
@@ -57,7 +57,7 @@ class PurchaseRequisition(models.Model):
|
||||
user_id = fields.Many2one(
|
||||
'res.users', string='Purchase Representative',
|
||||
default=lambda self: self.env.user, check_company=True)
|
||||
description = fields.Text()
|
||||
description = fields.Html()
|
||||
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
|
||||
purchase_ids = fields.One2many('purchase.order', 'requisition_id', string='Purchase Orders', states={'done': [('readonly', True)]})
|
||||
line_ids = fields.One2many('purchase.requisition.line', 'requisition_id', string='Products to Purchase', states={'done': [('readonly', True)]}, copy=True)
|
||||
|
||||
@@ -100,8 +100,8 @@ class Repair(models.Model):
|
||||
fees_lines = fields.One2many(
|
||||
'repair.fee', 'repair_id', 'Operations',
|
||||
copy=True, readonly=False)
|
||||
internal_notes = fields.Text('Internal Notes')
|
||||
quotation_notes = fields.Text('Quotation Notes')
|
||||
internal_notes = fields.Html('Internal Notes')
|
||||
quotation_notes = fields.Html('Quotation Notes')
|
||||
user_id = fields.Many2one('res.users', string="Responsible", default=lambda self: self.env.user, check_company=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company',
|
||||
@@ -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'] += '<br/>' + narration
|
||||
|
||||
# Create invoice lines from operations.
|
||||
for operation in repair.operations.filtered(lambda op: op.type == 'add'):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -198,10 +198,10 @@
|
||||
<div class="oe_structure"/>
|
||||
|
||||
<p t-field="doc.note" />
|
||||
<p t-if="doc.payment_term_id.note">
|
||||
<p t-if="not is_html_empty(doc.payment_term_id.note)">
|
||||
<span t-field="doc.payment_term_id.note"/>
|
||||
</p>
|
||||
<p id="fiscal_position_remark" t-if="doc.fiscal_position_id and doc.fiscal_position_id.sudo().note">
|
||||
<p id="fiscal_position_remark" t-if="doc.fiscal_position_id and not is_html_empty(doc.fiscal_position_id.sudo().note)">
|
||||
<strong>Fiscal Position Remark:</strong>
|
||||
<span t-field="doc.fiscal_position_id.sudo().note"/>
|
||||
</p>
|
||||
|
||||
@@ -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, "<p>Coin coin</p>")
|
||||
|
||||
# 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("<p>Terms & Conditions: "))
|
||||
|
||||
def test_validity_days(self):
|
||||
self.env['ir.config_parameter'].sudo().set_param('sale.use_quotation_validity_days', True)
|
||||
|
||||
@@ -526,7 +526,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="terms" class="mt-5" t-if="sale_order.note">
|
||||
<section id="terms" class="mt-5" t-if="not is_html_empty(sale_order.note)">
|
||||
<h3 class="">Terms & Conditions</h3>
|
||||
<hr class="mt-0 mb-1"/>
|
||||
<t t-if="sale_order.terms_type == 'html'">
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<div t-field="option_line.website_description" class="oe_no_empty"/>
|
||||
</t>
|
||||
</t>
|
||||
<section id="terms" class="container" t-if="template.note">
|
||||
<section id="terms" class="container" t-if="not is_html_empty(template.note)">
|
||||
<h1 t-ignore="True">Terms & Conditions</h1>
|
||||
<p t-field="template.note"/>
|
||||
</section>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
* '<p style="..." class=".."><br></p>' added by some web editor(s).
|
||||
* Note that because the use of this method is limited, we ignore the cases
|
||||
* like there's one <img> 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
|
||||
*
|
||||
|
||||
@@ -92,6 +92,17 @@ table {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: investigate further for proper fix
|
||||
// By default there is 40px padding on the lists (both <ol> and <ul>) and it is
|
||||
// also the case with HTML reports, but when PDFs are rendered with Wkhtmltopdf,
|
||||
// the padding is not applied on <ol>, it is strangely applied on <ul> 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
|
||||
|
||||
@@ -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: "<span class='text-info'>hello</hello>"},
|
||||
{id: 12, display_name: "product 12", description: "<p class='a'><span style='color:red;'/><br/></p>"},
|
||||
);
|
||||
|
||||
var kanban = await createView({
|
||||
View: KanbanView,
|
||||
model: 'product',
|
||||
data: this.data,
|
||||
arch: `<kanban>
|
||||
<field name="description"/>
|
||||
<templates><t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<field name="display_name"/>
|
||||
<div class="test" t-if="!widget.isHtmlEmpty(record.description.raw_value)">
|
||||
<t t-raw="record.description.raw_value"/>
|
||||
</div>
|
||||
</div>
|
||||
</t></templates>
|
||||
</kanban>`,
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<field name="font" widget="font"/>
|
||||
<field name="report_header" widget="char" placeholder="e.g. Global Business Solutions" />
|
||||
<field name="report_header" placeholder="e.g. Global Business Solutions" />
|
||||
<field name="report_footer" string="Footer" placeholder="e.g. Opening hours, bank accounts (one per line)" />
|
||||
<field name="paperformat_id" required="1" />
|
||||
</group>
|
||||
|
||||
@@ -7,18 +7,18 @@ from odoo import models, _
|
||||
class EventRegistration(models.Model):
|
||||
_inherit = 'event.registration'
|
||||
|
||||
def _get_lead_description_registration(self, prefix='', line_suffix=''):
|
||||
def _get_lead_description_registration(self, line_suffix=''):
|
||||
"""Add the questions and answers linked to the registrations into the description of the lead."""
|
||||
reg_description = super(EventRegistration, self)._get_lead_description_registration(prefix=prefix, line_suffix=line_suffix)
|
||||
reg_description = super(EventRegistration, self)._get_lead_description_registration(line_suffix=line_suffix)
|
||||
if not self.registration_answer_ids:
|
||||
return reg_description
|
||||
|
||||
answer_descriptions = []
|
||||
for answer in self.registration_answer_ids:
|
||||
answer_value = answer.value_answer_id.name if answer.question_type == "simple_choice" else answer.value_text_box
|
||||
answer_value = "\n".join([" %s" % line for line in answer_value.split('\n')])
|
||||
answer_descriptions.append(" - %s\n%s" % (answer.question_id.title, answer_value))
|
||||
return "%s\n%s\n%s" % (reg_description, _("Questions"), '\n'.join(answer_descriptions))
|
||||
answer_value = "<br/>".join([" %s" % line for line in answer_value.split('\n')])
|
||||
answer_descriptions.append(" - %s<br/>%s" % (answer.question_id.title, answer_value))
|
||||
return "%s%s<br/>%s" % (reg_description, _("Questions"), '<br/>'.join(answer_descriptions))
|
||||
|
||||
def _get_lead_description_fields(self):
|
||||
res = super(EventRegistration, self)._get_lead_description_fields()
|
||||
|
||||
@@ -3,16 +3,13 @@
|
||||
|
||||
import base64
|
||||
import json
|
||||
import pytz
|
||||
|
||||
from datetime import datetime
|
||||
from psycopg2 import IntegrityError
|
||||
from werkzeug.exceptions import BadRequest
|
||||
|
||||
from odoo import http, SUPERUSER_ID, _
|
||||
from odoo.http import request
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from odoo.tools.translate import _
|
||||
from odoo.tools import plaintext2html
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
from odoo.addons.base.models.ir_qweb_fields import nl2br
|
||||
|
||||
@@ -100,6 +97,9 @@ class WebsiteForm(http.Controller):
|
||||
def floating(self, field_label, field_input):
|
||||
return float(field_input)
|
||||
|
||||
def html(self, field_label, field_input):
|
||||
return plaintext2html(field_input)
|
||||
|
||||
def boolean(self, field_label, field_input):
|
||||
return bool(field_input)
|
||||
|
||||
@@ -115,7 +115,7 @@ class WebsiteForm(http.Controller):
|
||||
_input_filters = {
|
||||
'char': identity,
|
||||
'text': identity,
|
||||
'html': identity,
|
||||
'html': html,
|
||||
'date': identity,
|
||||
'datetime': identity,
|
||||
'many2one': integer,
|
||||
|
||||
@@ -52,10 +52,10 @@
|
||||
<h5 t-if="job.no_of_recruitment > 1">
|
||||
<t t-esc="job.no_of_recruitment"/> open positions
|
||||
</h5>
|
||||
<p t-if="editable"
|
||||
<div t-if="editable"
|
||||
t-field="job.description"
|
||||
class="mt16 mb0 css_non_editable_mode_hidden"/>
|
||||
<p t-esc="len((job.description or '').split(' ')) > 35 and '%s ...' % ' '.join(job.description.split(' ')[:35]) or job.description"
|
||||
<div t-esc="len((job.description or '').split(' ')) > 35 and '%s ...' % ' '.join(job.description.split(' ')[:35]) or job.description"
|
||||
class="mt16 mb0 css_editable_mode_hidden"
|
||||
/>
|
||||
<div class="o_job_infos mt16">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="media-body px-3">
|
||||
<t t-set="description" t-value="widget.displayDescription and product['description_sale']"/>
|
||||
<h6 t-attf-class="font-weight-bold #{description ? '' : 'mb-0'}" t-esc="product['name']"/>
|
||||
<p t-if="description" class="mb-0" t-esc="description"/>
|
||||
<p t-if="description" class="mb-0" t-raw="description"/>
|
||||
</div>
|
||||
<div t-if="widget.displayPrice" class="flex-shrink-0">
|
||||
<t t-if="product['has_discounted_price']">
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
# ---------------------------------------------------------
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
<div class="col-12 col-md-8 col-lg-9 d-flex flex-column">
|
||||
<div class="d-flex flex-column">
|
||||
<h1 t-field="channel.name"/>
|
||||
<p class="mb-0 mb-xl-3" t-field="channel.description"/>
|
||||
<div class="mb-0 mb-xl-3" t-field="channel.description"/>
|
||||
|
||||
<div t-if="channel.channel_type == 'documentation'" class="d-flex mb-md-5">
|
||||
<button role="button" class="btn text-white pl-0" title="Share Channel"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -35,6 +35,7 @@ class TestServerActionsBase(common.TransactionCase):
|
||||
# Model data
|
||||
Model = self.env['ir.model']
|
||||
Fields = self.env['ir.model.fields']
|
||||
self.comment_html = '<p>MyComment</p>'
|
||||
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
|
||||
|
||||
@@ -329,7 +329,7 @@ class TestHtmlTools(BaseCase):
|
||||
self.assertTrue(is_html_empty(content))
|
||||
|
||||
void_html_samples = ['<p><br></p>', '<p><br> </p>', '<p><br /></p >', '<p style="margin: 4px"></p>',
|
||||
'<div style="margin: 4px"></div>']
|
||||
'<div style="margin: 4px"></div>', '<p class="oe_testing"><br></p>']
|
||||
for content in void_html_samples:
|
||||
self.assertTrue(is_html_empty(content), 'Failed with %s' % content)
|
||||
|
||||
|
||||
+1
-1
@@ -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())
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user