[MERGE] Forward-port saas-4 up to 5ceded9
[odoo/odoo.git] / openerp / addons / base / ir / ir_fields.py
1 # -*- coding: utf-8 -*-
2 import cStringIO
3 import datetime
4 import functools
5 import operator
6 import itertools
7 import time
8
9 import psycopg2
10 import pytz
11
12 from openerp.osv import orm
13 from openerp.tools.translate import _
14 from openerp.tools.misc import DEFAULT_SERVER_DATE_FORMAT,\
15                                DEFAULT_SERVER_DATETIME_FORMAT
16 from openerp.tools import html_sanitize
17
18 REFERENCING_FIELDS = set([None, 'id', '.id'])
19 def only_ref_fields(record):
20     return dict((k, v) for k, v in record.iteritems()
21                 if k in REFERENCING_FIELDS)
22 def exclude_ref_fields(record):
23     return dict((k, v) for k, v in record.iteritems()
24                 if k not in REFERENCING_FIELDS)
25
26 CREATE = lambda values: (0, False, values)
27 UPDATE = lambda id, values: (1, id, values)
28 DELETE = lambda id: (2, id, False)
29 FORGET = lambda id: (3, id, False)
30 LINK_TO = lambda id: (4, id, False)
31 DELETE_ALL = lambda: (5, False, False)
32 REPLACE_WITH = lambda ids: (6, False, ids)
33
34 class ConversionNotFound(ValueError): pass
35
36 class ColumnWrapper(object):
37     def __init__(self, column, cr, uid, pool, fromtype, context=None):
38         self._converter = None
39         self._column = column
40         if column._obj:
41             self._pool = pool
42             self._converter_args = {
43                 'cr': cr,
44                 'uid': uid,
45                 'model': pool[column._obj],
46                 'fromtype': fromtype,
47                 'context': context
48             }
49     @property
50     def converter(self):
51         if not self._converter:
52             self._converter = self._pool['ir.fields.converter'].for_model(
53                 **self._converter_args)
54         return self._converter
55
56     def __getattr__(self, item):
57         return getattr(self._column, item)
58
59 class ir_fields_converter(orm.Model):
60     _name = 'ir.fields.converter'
61
62     def for_model(self, cr, uid, model, fromtype=str, context=None):
63         """ Returns a converter object for the model. A converter is a
64         callable taking a record-ish (a dictionary representing an openerp
65         record with values of typetag ``fromtype``) and returning a converted
66         records matching what :meth:`openerp.osv.orm.Model.write` expects.
67
68         :param model: :class:`openerp.osv.orm.Model` for the conversion base
69         :returns: a converter callable
70         :rtype: (record: dict, logger: (field, error) -> None) -> dict
71         """
72         columns = dict(
73             (k, ColumnWrapper(v.column, cr, uid, self.pool, fromtype, context))
74             for k, v in model._all_columns.iteritems())
75         converters = dict(
76             (k, self.to_field(cr, uid, model, column, fromtype, context))
77             for k, column in columns.iteritems())
78
79         def fn(record, log):
80             converted = {}
81             for field, value in record.iteritems():
82                 if field in (None, 'id', '.id'): continue
83                 if not value:
84                     converted[field] = False
85                     continue
86                 try:
87                     converted[field], ws = converters[field](value)
88                     for w in ws:
89                         if isinstance(w, basestring):
90                             # wrap warning string in an ImportWarning for
91                             # uniform handling
92                             w = ImportWarning(w)
93                         log(field, w)
94                 except ValueError, e:
95                     log(field, e)
96
97             return converted
98         return fn
99
100     def to_field(self, cr, uid, model, column, fromtype=str, context=None):
101         """ Fetches a converter for the provided column object, from the
102         specified type.
103
104         A converter is simply a callable taking a value of type ``fromtype``
105         (or a composite of ``fromtype``, e.g. list or dict) and returning a
106         value acceptable for a write() on the column ``column``.
107
108         By default, tries to get a method on itself with a name matching the
109         pattern ``_$fromtype_to_$column._type`` and returns it.
110
111         Converter callables can either return a value and a list of warnings
112         to their caller or raise ``ValueError``, which will be interpreted as a
113         validation & conversion failure.
114
115         ValueError can have either one or two parameters. The first parameter
116         is mandatory, **must** be a unicode string and will be used as the
117         user-visible message for the error (it should be translatable and
118         translated). It can contain a ``field`` named format placeholder so the
119         caller can inject the field's translated, user-facing name (@string).
120
121         The second parameter is optional and, if provided, must be a mapping.
122         This mapping will be merged into the error dictionary returned to the
123         client.
124
125         If a converter can perform its function but has to make assumptions
126         about the data, it can send a warning to the user through adding an
127         instance of :class:`~openerp.osv.orm.ImportWarning` to the second value
128         it returns. The handling of a warning at the upper levels is the same
129         as ``ValueError`` above.
130
131         :param column: column object to generate a value for
132         :type column: :class:`fields._column`
133         :param fromtype: type to convert to something fitting for ``column``
134         :type fromtype: type | str
135         :param context: openerp request context
136         :return: a function (fromtype -> column.write_type), if a converter is found
137         :rtype: Callable | None
138         """
139         assert isinstance(fromtype, (type, str))
140         # FIXME: return None
141         typename = fromtype.__name__ if isinstance(fromtype, type) else fromtype
142         converter = getattr(
143             self, '_%s_to_%s' % (typename, column._type), None)
144         if not converter: return None
145
146         return functools.partial(
147             converter, cr, uid, model, column, context=context)
148
149     def _str_to_boolean(self, cr, uid, model, column, value, context=None):
150         # all translatables used for booleans
151         true, yes, false, no = _(u"true"), _(u"yes"), _(u"false"), _(u"no")
152         # potentially broken casefolding? What about locales?
153         trues = set(word.lower() for word in itertools.chain(
154             [u'1', u"true", u"yes"], # don't use potentially translated values
155             self._get_translations(cr, uid, ['code'], u"true", context=context),
156             self._get_translations(cr, uid, ['code'], u"yes", context=context),
157         ))
158         if value.lower() in trues: return True, []
159
160         # potentially broken casefolding? What about locales?
161         falses = set(word.lower() for word in itertools.chain(
162             [u'', u"0", u"false", u"no"],
163             self._get_translations(cr, uid, ['code'], u"false", context=context),
164             self._get_translations(cr, uid, ['code'], u"no", context=context),
165         ))
166         if value.lower() in falses: return False, []
167
168         return True, [orm.ImportWarning(
169             _(u"Unknown value '%s' for boolean field '%%(field)s', assuming '%s'")
170                 % (value, yes), {
171                 'moreinfo': _(u"Use '1' for yes and '0' for no")
172             })]
173
174     def _str_to_integer(self, cr, uid, model, column, value, context=None):
175         try:
176             return int(value), []
177         except ValueError:
178             raise ValueError(
179                 _(u"'%s' does not seem to be an integer for field '%%(field)s'")
180                 % value)
181
182     def _str_to_float(self, cr, uid, model, column, value, context=None):
183         try:
184             return float(value), []
185         except ValueError:
186             raise ValueError(
187                 _(u"'%s' does not seem to be a number for field '%%(field)s'")
188                 % value)
189
190     def _str_id(self, cr, uid, model, column, value, context=None):
191         return value, []
192     _str_to_reference = _str_to_char = _str_to_text = _str_to_binary = _str_to_html = _str_id
193
194     def _str_to_date(self, cr, uid, model, column, value, context=None):
195         try:
196             time.strptime(value, DEFAULT_SERVER_DATE_FORMAT)
197             return value, []
198         except ValueError:
199             raise ValueError(
200                 _(u"'%s' does not seem to be a valid date for field '%%(field)s'") % value, {
201                     'moreinfo': _(u"Use the format '%s'") % u"2012-12-31"
202                 })
203
204     def _input_tz(self, cr, uid, context):
205         # if there's a tz in context, try to use that
206         if context.get('tz'):
207             try:
208                 return pytz.timezone(context['tz'])
209             except pytz.UnknownTimeZoneError:
210                 pass
211
212         # if the current user has a tz set, try to use that
213         user = self.pool['res.users'].read(
214             cr, uid, [uid], ['tz'], context=context)[0]
215         if user['tz']:
216             try:
217                 return pytz.timezone(user['tz'])
218             except pytz.UnknownTimeZoneError:
219                 pass
220
221         # fallback if no tz in context or on user: UTC
222         return pytz.UTC
223
224     def _str_to_datetime(self, cr, uid, model, column, value, context=None):
225         if context is None: context = {}
226         try:
227             parsed_value = datetime.datetime.strptime(
228                 value, DEFAULT_SERVER_DATETIME_FORMAT)
229         except ValueError:
230             raise ValueError(
231                 _(u"'%s' does not seem to be a valid datetime for field '%%(field)s'") % value, {
232                     'moreinfo': _(u"Use the format '%s'") % u"2012-12-31 23:59:59"
233                 })
234
235         input_tz = self._input_tz(cr, uid, context)# Apply input tz to the parsed naive datetime
236         dt = input_tz.localize(parsed_value, is_dst=False)
237         # And convert to UTC before reformatting for writing
238         return dt.astimezone(pytz.UTC).strftime(DEFAULT_SERVER_DATETIME_FORMAT), []
239
240     def _get_translations(self, cr, uid, types, src, context):
241         types = tuple(types)
242         # Cache translations so they don't have to be reloaded from scratch on
243         # every row of the file
244         tnx_cache = cr.cache.setdefault(self._name, {})
245         if tnx_cache.setdefault(types, {}) and src in tnx_cache[types]:
246             return tnx_cache[types][src]
247
248         Translations = self.pool['ir.translation']
249         tnx_ids = Translations.search(
250             cr, uid, [('type', 'in', types), ('src', '=', src)], context=context)
251         tnx = Translations.read(cr, uid, tnx_ids, ['value'], context=context)
252         result = tnx_cache[types][src] = map(operator.itemgetter('value'), tnx)
253         return result
254
255     def _str_to_selection(self, cr, uid, model, column, value, context=None):
256
257         selection = column.selection
258         if not isinstance(selection, (tuple, list)):
259             # FIXME: Don't pass context to avoid translations?
260             #        Or just copy context & remove lang?
261             selection = selection(model, cr, uid, context=None)
262         for item, label in selection:
263             labels = self._get_translations(
264                 cr, uid, ('selection', 'model', 'code'), label, context=context)
265             labels.append(label)
266             if value == unicode(item) or value in labels:
267                 return item, []
268         raise ValueError(
269             _(u"Value '%s' not found in selection field '%%(field)s'") % (
270                 value), {
271                 'moreinfo': [label or unicode(item) for item, label in selection
272                              if label or item]
273             })
274
275
276     def db_id_for(self, cr, uid, model, column, subfield, value, context=None):
277         """ Finds a database id for the reference ``value`` in the referencing
278         subfield ``subfield`` of the provided column of the provided model.
279
280         :param model: model to which the column belongs
281         :param column: relational column for which references are provided
282         :param subfield: a relational subfield allowing building of refs to
283                          existing records: ``None`` for a name_get/name_search,
284                          ``id`` for an external id and ``.id`` for a database
285                          id
286         :param value: value of the reference to match to an actual record
287         :param context: OpenERP request context
288         :return: a pair of the matched database identifier (if any), the
289                  translated user-readable name for the field and the list of
290                  warnings
291         :rtype: (ID|None, unicode, list)
292         """
293         if context is None: context = {}
294         id = None
295         warnings = []
296         action = {'type': 'ir.actions.act_window', 'target': 'new',
297                   'view_mode': 'tree,form', 'view_type': 'form',
298                   'views': [(False, 'tree'), (False, 'form')],
299                   'help': _(u"See all possible values")}
300         if subfield is None:
301             action['res_model'] = column._obj
302         elif subfield in ('id', '.id'):
303             action['res_model'] = 'ir.model.data'
304             action['domain'] = [('model', '=', column._obj)]
305
306         RelatedModel = self.pool[column._obj]
307         if subfield == '.id':
308             field_type = _(u"database id")
309             try: tentative_id = int(value)
310             except ValueError: tentative_id = value
311             try:
312                 if RelatedModel.search(cr, uid, [('id', '=', tentative_id)],
313                                        context=context):
314                     id = tentative_id
315             except psycopg2.DataError:
316                 # type error
317                 raise ValueError(
318                     _(u"Invalid database id '%s' for the field '%%(field)s'") % value,
319                     {'moreinfo': action})
320         elif subfield == 'id':
321             field_type = _(u"external id")
322             if '.' in value:
323                 module, xid = value.split('.', 1)
324             else:
325                 module, xid = context.get('_import_current_module', ''), value
326             ModelData = self.pool['ir.model.data']
327             try:
328                 _model, id = ModelData.get_object_reference(
329                     cr, uid, module, xid)
330             except ValueError: pass # leave id is None
331         elif subfield is None:
332             field_type = _(u"name")
333             ids = RelatedModel.name_search(
334                 cr, uid, name=value, operator='=', context=context)
335             if ids:
336                 if len(ids) > 1:
337                     warnings.append(orm.ImportWarning(
338                         _(u"Found multiple matches for field '%%(field)s' (%d matches)")
339                         % (len(ids))))
340                 id, _name = ids[0]
341         else:
342             raise Exception(_(u"Unknown sub-field '%s'") % subfield)
343
344         if id is None:
345             raise ValueError(
346                 _(u"No matching record found for %(field_type)s '%(value)s' in field '%%(field)s'")
347                 % {'field_type': field_type, 'value': value},
348                 {'moreinfo': action})
349         return id, field_type, warnings
350
351     def _referencing_subfield(self, record):
352         """ Checks the record for the subfields allowing referencing (an
353         existing record in an other table), errors out if it finds potential
354         conflicts (multiple referencing subfields) or non-referencing subfields
355         returns the name of the correct subfield.
356
357         :param record:
358         :return: the record subfield to use for referencing and a list of warnings
359         :rtype: str, list
360         """
361         # Can import by name_get, external id or database id
362         fieldset = set(record.iterkeys())
363         if fieldset - REFERENCING_FIELDS:
364             raise ValueError(
365                 _(u"Can not create Many-To-One records indirectly, import the field separately"))
366         if len(fieldset) > 1:
367             raise ValueError(
368                 _(u"Ambiguous specification for field '%(field)s', only provide one of name, external id or database id"))
369
370         # only one field left possible, unpack
371         [subfield] = fieldset
372         return subfield, []
373
374     def _str_to_many2one(self, cr, uid, model, column, values, context=None):
375         # Should only be one record, unpack
376         [record] = values
377
378         subfield, w1 = self._referencing_subfield(record)
379
380         reference = record[subfield]
381         id, subfield_type, w2 = self.db_id_for(
382             cr, uid, model, column, subfield, reference, context=context)
383         return id, w1 + w2
384
385     def _str_to_many2many(self, cr, uid, model, column, value, context=None):
386         [record] = value
387
388         subfield, warnings = self._referencing_subfield(record)
389
390         ids = []
391         for reference in record[subfield].split(','):
392             id, subfield_type, ws = self.db_id_for(
393                 cr, uid, model, column, subfield, reference, context=context)
394             ids.append(id)
395             warnings.extend(ws)
396         return [REPLACE_WITH(ids)], warnings
397
398     def _str_to_one2many(self, cr, uid, model, column, records, context=None):
399         commands = []
400         warnings = []
401
402         if len(records) == 1 and exclude_ref_fields(records[0]) == {}:
403             # only one row with only ref field, field=ref1,ref2,ref3 as in
404             # m2o/m2m
405             record = records[0]
406             subfield, ws = self._referencing_subfield(record)
407             warnings.extend(ws)
408             # transform [{subfield:ref1,ref2,ref3}] into
409             # [{subfield:ref1},{subfield:ref2},{subfield:ref3}]
410             records = ({subfield:item} for item in record[subfield].split(','))
411
412         def log(_, e):
413             if not isinstance(e, Warning):
414                 raise e
415             warnings.append(e)
416         for record in records:
417             id = None
418             refs = only_ref_fields(record)
419             # there are ref fields in the record
420             if refs:
421                 subfield, w1 = self._referencing_subfield(refs)
422                 warnings.extend(w1)
423                 reference = record[subfield]
424                 id, subfield_type, w2 = self.db_id_for(
425                     cr, uid, model, column, subfield, reference, context=context)
426                 warnings.extend(w2)
427
428             writable = column.converter(exclude_ref_fields(record), log)
429             if id:
430                 commands.append(LINK_TO(id))
431                 commands.append(UPDATE(id, writable))
432             else:
433                 commands.append(CREATE(writable))
434
435         return commands, warnings