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