1 # -*- coding: utf-8 -*-
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
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)
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)
33 class ImportWarning(Warning):
34 """ Used to send warnings upwards the stack during the import process """
37 class ConversionNotFound(ValueError): pass
39 class ColumnWrapper(object):
40 def __init__(self, column, cr, uid, pool, fromtype, context=None):
41 self._converter = None
45 self._converter_args = {
48 'model': pool[column._obj],
54 if not self._converter:
55 self._converter = self._pool['ir.fields.converter'].for_model(
56 **self._converter_args)
57 return self._converter
59 def __getattr__(self, item):
60 return getattr(self._column, item)
62 class ir_fields_converter(orm.Model):
63 _name = 'ir.fields.converter'
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.
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
76 (k, ColumnWrapper(v.column, cr, uid, self.pool, fromtype, context))
77 for k, v in model._all_columns.iteritems())
79 (k, self.to_field(cr, uid, model, column, fromtype, context))
80 for k, column in columns.iteritems())
84 for field, value in record.iteritems():
85 if field in (None, 'id', '.id'): continue
87 converted[field] = False
90 converted[field], ws = converters[field](value)
92 if isinstance(w, basestring):
93 # wrap warning string in an ImportWarning for
103 def to_field(self, cr, uid, model, column, fromtype=str, context=None):
104 """ Fetches a converter for the provided column object, from the
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``.
111 By default, tries to get a method on itself with a name matching the
112 pattern ``_$fromtype_to_$column._type`` and returns it.
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.
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).
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
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.
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
142 assert isinstance(fromtype, (type, str))
144 typename = fromtype.__name__ if isinstance(fromtype, type) else fromtype
146 self, '_%s_to_%s' % (typename, column._type), None)
147 if not converter: return None
149 return functools.partial(
150 converter, cr, uid, model, column, context=context)
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),
161 if value.lower() in trues: return True, []
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),
169 if value.lower() in falses: return False, []
171 return True, [ImportWarning(
172 _(u"Unknown value '%s' for boolean field '%%(field)s', assuming '%s'")
174 'moreinfo': _(u"Use '1' for yes and '0' for no")
177 def _str_to_integer(self, cr, uid, model, column, value, context=None):
179 return int(value), []
182 _(u"'%s' does not seem to be an integer for field '%%(field)s'")
185 def _str_to_float(self, cr, uid, model, column, value, context=None):
187 return float(value), []
190 _(u"'%s' does not seem to be a number for field '%%(field)s'")
193 def _str_id(self, cr, uid, model, column, value, context=None):
195 _str_to_reference = _str_to_char = _str_to_text = _str_to_binary = _str_to_html = _str_id
197 def _str_to_date(self, cr, uid, model, column, value, context=None):
199 time.strptime(value, DEFAULT_SERVER_DATE_FORMAT)
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"
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'):
211 return pytz.timezone(context['tz'])
212 except pytz.UnknownTimeZoneError:
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]
220 return pytz.timezone(user['tz'])
221 except pytz.UnknownTimeZoneError:
224 # fallback if no tz in context or on user: UTC
227 def _str_to_datetime(self, cr, uid, model, column, value, context=None):
228 if context is None: context = {}
230 parsed_value = datetime.datetime.strptime(
231 value, DEFAULT_SERVER_DATETIME_FORMAT)
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"
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), []
243 def _get_translations(self, cr, uid, types, src, context):
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]
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]
258 def _str_to_selection(self, cr, uid, model, column, value, context=None):
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)
269 if value == unicode(item) or value in labels:
272 _(u"Value '%s' not found in selection field '%%(field)s'") % (
274 'moreinfo': [label or unicode(item) for item, label in selection
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.
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
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
294 :rtype: (ID|None, unicode, list)
296 if context is None: context = {}
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")}
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)]
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
315 if RelatedModel.search(cr, uid, [('id', '=', tentative_id)],
318 except psycopg2.DataError:
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")
326 module, xid = value.split('.', 1)
328 module, xid = context.get('_import_current_module', ''), value
329 ModelData = self.pool['ir.model.data']
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)
340 warnings.append(ImportWarning(
341 _(u"Found multiple matches for field '%%(field)s' (%d matches)")
345 raise Exception(_(u"Unknown sub-field '%s'") % subfield)
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
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.
361 :return: the record subfield to use for referencing and a list of warnings
364 # Can import by name_get, external id or database id
365 fieldset = set(record.iterkeys())
366 if fieldset - REFERENCING_FIELDS:
368 _(u"Can not create Many-To-One records indirectly, import the field separately"))
369 if len(fieldset) > 1:
371 _(u"Ambiguous specification for field '%(field)s', only provide one of name, external id or database id"))
373 # only one field left possible, unpack
374 [subfield] = fieldset
377 def _str_to_many2one(self, cr, uid, model, column, values, context=None):
378 # Should only be one record, unpack
381 subfield, w1 = self._referencing_subfield(record)
383 reference = record[subfield]
384 id, subfield_type, w2 = self.db_id_for(
385 cr, uid, model, column, subfield, reference, context=context)
388 def _str_to_many2many(self, cr, uid, model, column, value, context=None):
391 subfield, warnings = self._referencing_subfield(record)
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)
399 return [REPLACE_WITH(ids)], warnings
401 def _str_to_one2many(self, cr, uid, model, column, records, context=None):
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
409 subfield, ws = self._referencing_subfield(record)
411 # transform [{subfield:ref1,ref2,ref3}] into
412 # [{subfield:ref1},{subfield:ref2},{subfield:ref3}]
413 records = ({subfield:item} for item in record[subfield].split(','))
416 if not isinstance(e, Warning):
419 for record in records:
421 refs = only_ref_fields(record)
422 # there are ref fields in the record
424 subfield, w1 = self._referencing_subfield(refs)
426 reference = record[subfield]
427 id, subfield_type, w2 = self.db_id_for(
428 cr, uid, model, column, subfield, reference, context=context)
431 writable = column.converter(exclude_ref_fields(record), log)
433 commands.append(LINK_TO(id))
434 commands.append(UPDATE(id, writable))
436 commands.append(CREATE(writable))
438 return commands, warnings