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