summaryrefslogtreecommitdiff
path: root/odoo/addons/base/ir/ir_qweb/fields.py
diff options
context:
space:
mode:
Diffstat (limited to 'odoo/addons/base/ir/ir_qweb/fields.py')
-rw-r--r--odoo/addons/base/ir/ir_qweb/fields.py470
1 files changed, 470 insertions, 0 deletions
diff --git a/odoo/addons/base/ir/ir_qweb/fields.py b/odoo/addons/base/ir/ir_qweb/fields.py
new file mode 100644
index 00000000000..bd819e4c92d
--- /dev/null
+++ b/odoo/addons/base/ir/ir_qweb/fields.py
@@ -0,0 +1,470 @@
+# -*- coding: utf-8 -*-
+import re
+from collections import OrderedDict
+from odoo import api, fields, models, _
+from PIL import Image
+from cStringIO import StringIO
+import babel
+from openerp.tools import html_escape as escape, posix_to_ldml, safe_eval, float_utils
+from .qweb import unicodifier
+
+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 unicodifier(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(unicodifier(value) or u'', 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 browse_record ``record`` to HTML
+ :rtype: unicode
+ """
+ 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: res.lang browse_record
+ """
+ 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 unicodifier(self.user_lang().format('%d', value, grouping=True))
+
+
+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)
+
+ # %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 unicodifier(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):
+ if not value or len(value) < 10:
+ return ''
+ lang = self.user_lang()
+ locale = babel.Locale.parse(lang.code)
+
+ if isinstance(value, basestring):
+ value = fields.Datetime.from_string(value[:10])
+
+ if options and 'format' in options:
+ pattern = options['format']
+ else:
+ strftime_pattern = lang.date_format
+ pattern = posix_to_ldml(strftime_pattern, locale=locale)
+
+ return babel.dates.format_date(value, format=pattern, locale=locale)
+
+
+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, basestring):
+ value = fields.Datetime.from_string(value)
+
+ value = fields.Datetime.context_timestamp(self, value)
+
+ if options and 'format' in options:
+ pattern = options['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 unicodifier(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(unicodifier(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 unicodifier(value) or u''
+
+
+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:
+ image = Image.open(StringIO(value.decode('base64')))
+ 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 unicodifier('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value))
+
+
+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}')
+
+ 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 self.value_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 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, basestring):
+ 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 unicodifier(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 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", "fax", "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,
+ 'fax': value.fax,
+ '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 unicodifier(view.render(view._context, engine='ir.qweb'))