# -*- coding: utf-8 -*-
import base64
import re
from collections import OrderedDict
from io import BytesIO
from odoo import api, fields, models, _
from PIL import Image
import babel
import babel.dates
from lxml import etree
import math
from odoo.tools import html_escape as escape, posix_to_ldml, float_utils, format_date, format_duration, pycompat
from odoo.tools.misc import get_lang
import logging
_logger = logging.getLogger(__name__)
def nl2br(string):
""" Converts newlines to HTML linebreaks in ``string``. returns
the unicode result
:param str string:
:rtype: unicode
"""
return pycompat.to_text(string).replace(u'\n', u'
\n')
def html_escape(string, options):
""" Automatically escapes content unless options['html-escape']
is set to False
:param str string:
:param dict options:
"""
return escape(string) if not options or options.get('html-escape', True) else string
#--------------------------------------------------------------------
# QWeb Fields converters
#--------------------------------------------------------------------
class FieldConverter(models.AbstractModel):
""" Used to convert a t-field specification into an output HTML field.
:meth:`~.to_html` is the entry point of this conversion from QWeb, it:
* converts the record value to html using :meth:`~.record_to_html`
* generates the metadata attributes (``data-oe-``) to set on the root
result node
* generates the root result node itself through :meth:`~.render_element`
"""
_name = 'ir.qweb.field'
_description = 'Qweb Field'
@api.model
def get_available_options(self):
"""
Get the available option informations.
Returns a dict of dict with:
* key equal to the option key.
* dict: type, params, name, description, default_value
* type:
'string'
'integer'
'float'
'model' (e.g. 'res.partner')
'array'
'selection' (e.g. [key1, key2...])
"""
return {}
@api.model
def attributes(self, record, field_name, options, values=None):
""" attributes(record, field_name, field, options, values)
Generates the metadata attributes (prefixed by ``data-oe-``) for the
root node of the field conversion.
The default attributes are:
* ``model``, the name of the record's model
* ``id`` the id of the record to which the field belongs
* ``type`` the logical field type (widget, may not match the field's
``type``, may not be any Field subclass name)
* ``translate``, a boolean flag (``0`` or ``1``) denoting whether the
field is translatable
* ``readonly``, has this attribute if the field is readonly
* ``expression``, the original expression
:returns: OrderedDict (attribute name, attribute value).
"""
data = OrderedDict()
field = record._fields[field_name]
if not options['inherit_branding'] and not options['translate']:
return data
data['data-oe-model'] = record._name
data['data-oe-id'] = record.id
data['data-oe-field'] = field.name
data['data-oe-type'] = options.get('type')
data['data-oe-expression'] = options.get('expression')
if field.readonly:
data['data-oe-readonly'] = 1
return data
@api.model
def value_to_html(self, value, options):
""" value_to_html(value, field, options=None)
Converts a single value to its HTML version/output
:rtype: unicode
"""
return html_escape(pycompat.to_text(value), options)
@api.model
def record_to_html(self, record, field_name, options):
""" record_to_html(record, field_name, options)
Converts the specified field of the ``record`` to HTML
:rtype: unicode
"""
if not record:
return False
value = record[field_name]
return False if value is False else record.env[self._name].value_to_html(value, options=options)
@api.model
def user_lang(self):
""" user_lang()
Fetches the res.lang record corresponding to the language code stored
in the user's context.
:returns: Model[res.lang]
"""
return get_lang(self.env)
class IntegerConverter(models.AbstractModel):
_name = 'ir.qweb.field.integer'
_description = 'Qweb Field Integer'
_inherit = 'ir.qweb.field'
@api.model
def value_to_html(self, value, options):
return pycompat.to_text(self.user_lang().format('%d', value, grouping=True).replace(r'-', u'-\N{ZERO WIDTH NO-BREAK SPACE}'))
class FloatConverter(models.AbstractModel):
_name = 'ir.qweb.field.float'
_description = 'Qweb Field Float'
_inherit = 'ir.qweb.field'
@api.model
def get_available_options(self):
options = super(FloatConverter, self).get_available_options()
options.update(
precision=dict(type='integer', string=_('Rounding precision')),
)
return options
@api.model
def value_to_html(self, value, options):
if 'decimal_precision' in options:
precision = self.env['decimal.precision'].precision_get(options['decimal_precision'])
else:
precision = options['precision']
if precision is None:
fmt = '%f'
else:
value = float_utils.float_round(value, precision_digits=precision)
fmt = '%.{precision}f'.format(precision=precision)
formatted = self.user_lang().format(fmt, value, grouping=True).replace(r'-', u'-\N{ZERO WIDTH NO-BREAK SPACE}')
# %f does not strip trailing zeroes. %g does but its precision causes
# it to switch to scientific notation starting at a million *and* to
# strip decimals. So use %f and if no precision was specified manually
# strip trailing 0.
if precision is None:
formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted)
return pycompat.to_text(formatted)
@api.model
def record_to_html(self, record, field_name, options):
if 'precision' not in options and 'decimal_precision' not in options:
_, precision = record._fields[field_name].get_digits(record.env) or (None, None)
options = dict(options, precision=precision)
return super(FloatConverter, self).record_to_html(record, field_name, options)
class DateConverter(models.AbstractModel):
_name = 'ir.qweb.field.date'
_description = 'Qweb Field Date'
_inherit = 'ir.qweb.field'
@api.model
def get_available_options(self):
options = super(DateConverter, self).get_available_options()
options.update(
format=dict(type='string', string=_('Date format'))
)
return options
@api.model
def value_to_html(self, value, options):
return format_date(self.env, value, date_format=options.get('format'))
class DateTimeConverter(models.AbstractModel):
_name = 'ir.qweb.field.datetime'
_description = 'Qweb Field Datetime'
_inherit = 'ir.qweb.field'
@api.model
def get_available_options(self):
options = super(DateTimeConverter, self).get_available_options()
options.update(
format=dict(type='string', string=_('Pattern to format')),
time_only=dict(type='boolean', string=_('Display only the time')),
hide_seconds=dict(type='boolean', string=_('Hide seconds')),
date_only=dict(type='boolean', string=_('Display only the date')),
)
return options
@api.model
def value_to_html(self, value, options):
if not value:
return ''
options = options or {}
lang = self.user_lang()
locale = babel.Locale.parse(lang.code)
format_func = babel.dates.format_datetime
if isinstance(value, str):
value = fields.Datetime.from_string(value)
value = fields.Datetime.context_timestamp(self, value)
if 'format' in options:
pattern = options['format']
else:
if options.get('time_only'):
strftime_pattern = (u"%s" % (lang.time_format))
elif options.get('date_only'):
strftime_pattern = (u"%s" % (lang.date_format))
else:
strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format))
pattern = posix_to_ldml(strftime_pattern, locale=locale)
if options.get('hide_seconds'):
pattern = pattern.replace(":ss", "").replace(":s", "")
if options.get('time_only'):
format_func = babel.dates.format_time
if options.get('date_only'):
format_func = babel.dates.format_date
return pycompat.to_text(format_func(value, format=pattern, locale=locale))
class TextConverter(models.AbstractModel):
_name = 'ir.qweb.field.text'
_description = 'Qweb Field Text'
_inherit = 'ir.qweb.field'
@api.model
def value_to_html(self, value, options):
"""
Escapes the value and converts newlines to br. This is bullshit.
"""
return nl2br(html_escape(value, options)) if value else ''
class SelectionConverter(models.AbstractModel):
_name = 'ir.qweb.field.selection'
_description = 'Qweb Field Selection'
_inherit = 'ir.qweb.field'
@api.model
def get_available_options(self):
options = super(SelectionConverter, self).get_available_options()
options.update(
selection=dict(type='selection', string=_('Selection'), description=_('By default the widget uses the field informations'), required=True)
)
return options
@api.model
def value_to_html(self, value, options):
if not value:
return ''
return html_escape(pycompat.to_text(options['selection'][value]) or u'', options)
@api.model
def record_to_html(self, record, field_name, options):
if 'selection' not in options:
options = dict(options, selection=dict(record._fields[field_name].get_description(self.env)['selection']))
return super(SelectionConverter, self).record_to_html(record, field_name, options)
class ManyToOneConverter(models.AbstractModel):
_name = 'ir.qweb.field.many2one'
_description = 'Qweb Field Many to One'
_inherit = 'ir.qweb.field'
@api.model
def value_to_html(self, value, options):
if not value:
return False
value = value.sudo().display_name
if not value:
return False
return nl2br(html_escape(value, options)) if value else ''
class ManyToManyConverter(models.AbstractModel):
_name = 'ir.qweb.field.many2many'
_description = 'Qweb field many2many'
_inherit = 'ir.qweb.field'
@api.model
def value_to_html(self, value, options):
if not value:
return False
text = ', '.join(value.sudo().mapped('display_name'))
return nl2br(html_escape(text, options))
class HTMLConverter(models.AbstractModel):
_name = 'ir.qweb.field.html'
_description = 'Qweb Field HTML'
_inherit = 'ir.qweb.field'
@api.model
def value_to_html(self, value, options):
irQweb = self.env['ir.qweb']
# wrap value inside a body and parse it as HTML
body = etree.fromstring("