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