flectra/odoo/addons/base/ir/ir_qweb/fields.py
flectra-admin 769eafb483 [INIT] Inception of Flectra from Odoo
Flectra is Forked from Odoo v11 commit : (6135e82d73)
2018-01-16 11:45:59 +05:30

500 lines
17 KiB
Python

# -*- 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
from odoo.tools import html_escape as escape, posix_to_ldml, safe_eval, float_utils, format_date, pycompat
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'<br>\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'
@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. Fallbacks to en_US if no lang is present in the
context *or the language code is not valid*.
:returns: Model[res.lang]
"""
lang_code = self._context.get('lang') or 'en_US'
return self.env['res.lang']._lang_get(lang_code)
class IntegerConverter(models.AbstractModel):
_name = 'ir.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'\u2011'))
class FloatConverter(models.AbstractModel):
_name = 'ir.qweb.field.float'
_inherit = 'ir.qweb.field'
@api.model
def value_to_html(self, value, options):
if 'decimal_precision' in options:
precision = self.env['decimal.precision'].search([('name', '=', options['decimal_precision'])]).digits
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'\u2011')
# %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].digits 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'
_inherit = 'ir.qweb.field'
@api.model
def value_to_html(self, value, options):
return format_date(self.env, value, date_format=(options or {}).get('format'))
class DateTimeConverter(models.AbstractModel):
_name = 'ir.qweb.field.datetime'
_inherit = 'ir.qweb.field'
@api.model
def value_to_html(self, value, options):
if not value:
return ''
lang = self.user_lang()
locale = babel.Locale.parse(lang.code)
if isinstance(value, pycompat.string_types):
value = fields.Datetime.from_string(value)
value = fields.Datetime.context_timestamp(self, value)
if options and 'format' in options:
pattern = options['format']
else:
if options and options.get('time_only'):
strftime_pattern = (u"%s" % (lang.time_format))
else:
strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format))
pattern = posix_to_ldml(strftime_pattern, locale=locale)
if options and options.get('hide_seconds'):
pattern = pattern.replace(":ss", "").replace(":s", "")
return pycompat.to_text(babel.dates.format_datetime(value, format=pattern, locale=locale))
class TextConverter(models.AbstractModel):
_name = 'ir.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'
_inherit = 'ir.qweb.field'
@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'
_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 HTMLConverter(models.AbstractModel):
_name = 'ir.qweb.field.html'
_inherit = 'ir.qweb.field'
@api.model
def value_to_html(self, value, options):
return pycompat.to_text(value)
class ImageConverter(models.AbstractModel):
""" ``image`` widget rendering, inserts a data:uri-using image tag in the
document. May be overridden by e.g. the website module to generate links
instead.
.. todo:: what happens if different output need different converters? e.g.
reports may need embedded images or FS links whereas website
needs website-aware
"""
_name = 'ir.qweb.field.image'
_inherit = 'ir.qweb.field'
@api.model
def value_to_html(self, value, options):
try: # FIXME: maaaaaybe it could also take raw bytes?
image = Image.open(BytesIO(base64.b64decode(value)))
image.verify()
except IOError:
raise ValueError("Non-image binary fields can not be converted to HTML")
except: # image.verify() throws "suitable exceptions", I have no idea what they are
raise ValueError("Invalid image content")
return u'<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value.decode('ascii'))
class MonetaryConverter(models.AbstractModel):
""" ``monetary`` converter, has a mandatory option
``display_currency`` only if field is not of type Monetary.
Otherwise, if we are in presence of a monetary field, the field definition must
have a currency_field attribute set.
The currency is used for formatting *and rounding* of the float value. It
is assumed that the linked res_currency has a non-empty rounding value and
res.currency's ``round`` method is used to perform rounding.
.. note:: the monetary converter internally adds the qweb context to its
options mapping, so that the context is available to callees.
It's set under the ``_values`` key.
"""
_name = 'ir.qweb.field.monetary'
_inherit = 'ir.qweb.field'
@api.model
def value_to_html(self, value, options):
display_currency = options['display_currency']
# lang.format mandates a sprintf-style format. These formats are non-
# minimal (they have a default fixed precision instead), and
# lang.format will not set one by default. currency.round will not
# provide one either. So we need to generate a precision value
# (integer > 0) from the currency's rounding (a float generally < 1.0).
fmt = "%.{0}f".format(display_currency.decimal_places)
if options.get('from_currency'):
value = options['from_currency'].compute(value, display_currency)
lang = self.user_lang()
formatted_amount = lang.format(fmt, display_currency.round(value),
grouping=True, monetary=True).replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'\u2011')
pre = post = u''
if display_currency.position == 'before':
pre = u'{symbol}\N{NO-BREAK SPACE}'.format(symbol=display_currency.symbol or '')
else:
post = u'\N{NO-BREAK SPACE}{symbol}'.format(symbol=display_currency.symbol or '')
return u'{pre}<span class="oe_currency_value">{0}</span>{post}'.format(formatted_amount, pre=pre, post=post)
@api.model
def record_to_html(self, record, field_name, options):
options = dict(options)
#currency should be specified by monetary field
field = record._fields[field_name]
if not options.get('display_currency') and field.type == 'monetary' and field.currency_field:
options['display_currency'] = record[field.currency_field]
return super(MonetaryConverter, self).record_to_html(record, field_name, options)
TIMEDELTA_UNITS = (
('year', 3600 * 24 * 365),
('month', 3600 * 24 * 30),
('week', 3600 * 24 * 7),
('day', 3600 * 24),
('hour', 3600),
('minute', 60),
('second', 1)
)
class FloatTimeConverter(models.AbstractModel):
""" ``float_time`` converter, to display integral or fractional values as
human-readable time spans (e.g. 1.5 as "01:30").
Can be used on any numerical field.
"""
_name = 'ir.qweb.field.float_time'
_inherit = 'ir.qweb.field'
@api.model
def value_to_html(self, value, options):
hours, minutes = divmod(value * 60, 60)
return '%02d:%02d' % (hours, minutes)
class DurationConverter(models.AbstractModel):
""" ``duration`` converter, to display integral or fractional values as
human-readable time spans (e.g. 1.5 as "1 hour 30 minutes").
Can be used on any numerical field.
Has a mandatory option ``unit`` which can be one of ``second``, ``minute``,
``hour``, ``day``, ``week`` or ``year``, used to interpret the numerical
field value before converting it.
Sub-second values will be ignored.
"""
_name = 'ir.qweb.field.duration'
_inherit = 'ir.qweb.field'
@api.model
def value_to_html(self, value, options):
units = dict(TIMEDELTA_UNITS)
if value < 0:
raise ValueError(_("Durations can't be negative"))
if not options or options.get('unit') not in units:
raise ValueError(_("A unit must be provided to duration widgets"))
locale = babel.Locale.parse(self.user_lang().code)
factor = units[options['unit']]
sections = []
r = value * factor
if options.get('round') in units:
round_to = units[options['round']]
r = round(r / round_to) * round_to
for unit, secs_per_unit in TIMEDELTA_UNITS:
v, r = divmod(r, secs_per_unit)
if not v:
continue
section = babel.dates.format_timedelta(
v*secs_per_unit, threshold=1, locale=locale)
if section:
sections.append(section)
return u' '.join(sections)
class RelativeDatetimeConverter(models.AbstractModel):
_name = 'ir.qweb.field.relative'
_inherit = 'ir.qweb.field'
@api.model
def value_to_html(self, value, options):
locale = babel.Locale.parse(self.user_lang().code)
if isinstance(value, pycompat.string_types):
value = fields.Datetime.from_string(value)
# value should be a naive datetime in UTC. So is fields.Datetime.now()
reference = fields.Datetime.from_string(options['now'])
return pycompat.to_text(babel.dates.format_timedelta(value - reference, add_direction=True, locale=locale))
@api.model
def record_to_html(self, record, field_name, options):
if 'now' not in options:
options = dict(options, now=record._fields[field_name].now())
return super(RelativeDatetimeConverter, self).record_to_html(record, field_name, options)
class BarcodeConverter(models.AbstractModel):
""" ``barcode`` widget rendering, inserts a data:uri-using image tag in the
document. May be overridden by e.g. the website module to generate links
instead.
"""
_name = 'ir.qweb.field.barcode'
_inherit = 'ir.qweb.field'
@api.model
def value_to_html(self, value, options=None):
barcode_type = options.get('type', 'Code128')
barcode = self.env['ir.actions.report'].barcode(
barcode_type,
value,
**{key: value for key, value in options.items() if key in ['width', 'height', 'humanreadable']})
return u'<img src="data:png;base64,%s">' % base64.b64encode(barcode).decode('ascii')
@api.model
def from_html(self, model, field, element):
return None
class Contact(models.AbstractModel):
_name = 'ir.qweb.field.contact'
_inherit = 'ir.qweb.field.many2one'
@api.model
def value_to_html(self, value, options):
if not value.exists():
return False
opf = options and options.get('fields') or ["name", "address", "phone", "mobile", "email"]
value = value.sudo().with_context(show_address=True)
name_get = value.name_get()[0][1]
val = {
'name': name_get.split("\n")[0],
'address': escape("\n".join(name_get.split("\n")[1:])).strip(),
'phone': value.phone,
'mobile': value.mobile,
'city': value.city,
'country_id': value.country_id.display_name,
'website': value.website,
'email': value.email,
'fields': opf,
'object': value,
'options': options
}
return self.env['ir.qweb'].render('base.contact', val)
class QwebView(models.AbstractModel):
_name = 'ir.qweb.field.qweb'
_inherit = 'ir.qweb.field.many2one'
@api.model
def record_to_html(self, record, field_name, options):
if not getattr(record, field_name):
return None
view = getattr(record, field_name)
if view._name != "ir.ui.view":
_logger.warning("%s.%s must be a 'ir.ui.view' model." % (record, field_name))
return None
view = view.with_context(object=record)
return pycompat.to_text(view.render(view._context, engine='ir.qweb'))