1 # -*- coding: utf-8 -*-
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
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)
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)
34 class ConversionNotFound(ValueError): pass
36 class ColumnWrapper(object):
37 def __init__(self, column, cr, uid, pool, fromtype, context=None):
38 self._converter = None
42 self._converter_args = {
45 'model': pool[column._obj],
51 if not self._converter:
52 self._converter = self._pool['ir.fields.converter'].for_model(
53 **self._converter_args)
54 return self._converter
56 def __getattr__(self, item):
57 return getattr(self._column, item)
59 class ir_fields_converter(orm.Model):
60 _name = 'ir.fields.converter'
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.
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
73 (k, ColumnWrapper(v.column, cr, uid, self.pool, fromtype, context))
74 for k, v in model._all_columns.iteritems())
76 (k, self.to_field(cr, uid, model, column, fromtype, context))
77 for k, column in columns.iteritems())
81 for field, value in record.iteritems():
82 if field in (None, 'id', '.id'): continue
84 converted[field] = False
87 converted[field], ws = converters[field](value)
89 if isinstance(w, basestring):
90 # wrap warning string in an ImportWarning for
100 def to_field(self, cr, uid, model, column, fromtype=str, context=None):
101 """ Fetches a converter for the provided column object, from the
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``.
108 By default, tries to get a method on itself with a name matching the
109 pattern ``_$fromtype_to_$column._type`` and returns it.
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.
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).
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
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.
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
139 assert isinstance(fromtype, (type, str))
141 typename = fromtype.__name__ if isinstance(fromtype, type) else fromtype
143 self, '_%s_to_%s' % (typename, column._type), None)
144 if not converter: return None
146 return functools.partial(
147 converter, cr, uid, model, column, context=context)
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),
158 if value.lower() in trues: return True, []
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),
166 if value.lower() in falses: return False, []
168 return True, [orm.ImportWarning(
169 _(u"Unknown value '%s' for boolean field '%%(field)s', assuming '%s'")
171 'moreinfo': _(u"Use '1' for yes and '0' for no")
174 def _str_to_integer(self, cr, uid, model, column, value, context=None):
176 return int(value), []
179 _(u"'%s' does not seem to be an integer for field '%%(field)s'")
182 def _str_to_float(self, cr, uid, model, column, value, context=None):
184 return float(value), []
187 _(u"'%s' does not seem to be a number for field '%%(field)s'")
190 def _str_id(self, cr, uid, model, column, value, context=None):
192 _str_to_reference = _str_to_char = _str_to_text = _str_to_binary = _str_to_html = _str_id
194 def _str_to_date(self, cr, uid, model, column, value, context=None):
196 time.strptime(value, DEFAULT_SERVER_DATE_FORMAT)
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"
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'):
208 return pytz.timezone(context['tz'])
209 except pytz.UnknownTimeZoneError:
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]
217 return pytz.timezone(user['tz'])
218 except pytz.UnknownTimeZoneError:
221 # fallback if no tz in context or on user: UTC
224 def _str_to_datetime(self, cr, uid, model, column, value, context=None):
225 if context is None: context = {}
227 parsed_value = datetime.datetime.strptime(
228 value, DEFAULT_SERVER_DATETIME_FORMAT)
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"
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), []
240 def _get_translations(self, cr, uid, types, src, context):
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]
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)
255 def _str_to_selection(self, cr, uid, model, column, value, context=None):
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)
266 if value == unicode(item) or value in labels:
269 _(u"Value '%s' not found in selection field '%%(field)s'") % (
271 'moreinfo': [label or unicode(item) for item, label in selection
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.
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
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
291 :rtype: (ID|None, unicode, list)
293 if context is None: context = {}
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")}
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)]
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
312 if RelatedModel.search(cr, uid, [('id', '=', tentative_id)],
315 except psycopg2.DataError:
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")
323 module, xid = value.split('.', 1)
325 module, xid = context.get('_import_current_module', ''), value
326 ModelData = self.pool['ir.model.data']
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)
337 warnings.append(orm.ImportWarning(
338 _(u"Found multiple matches for field '%%(field)s' (%d matches)")
342 raise Exception(_(u"Unknown sub-field '%s'") % subfield)
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
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.
358 :return: the record subfield to use for referencing and a list of warnings
361 # Can import by name_get, external id or database id
362 fieldset = set(record.iterkeys())
363 if fieldset - REFERENCING_FIELDS:
365 _(u"Can not create Many-To-One records indirectly, import the field separately"))
366 if len(fieldset) > 1:
368 _(u"Ambiguous specification for field '%(field)s', only provide one of name, external id or database id"))
370 # only one field left possible, unpack
371 [subfield] = fieldset
374 def _str_to_many2one(self, cr, uid, model, column, values, context=None):
375 # Should only be one record, unpack
378 subfield, w1 = self._referencing_subfield(record)
380 reference = record[subfield]
381 id, subfield_type, w2 = self.db_id_for(
382 cr, uid, model, column, subfield, reference, context=context)
385 def _str_to_many2many(self, cr, uid, model, column, value, context=None):
388 subfield, warnings = self._referencing_subfield(record)
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)
396 return [REPLACE_WITH(ids)], warnings
398 def _str_to_one2many(self, cr, uid, model, column, records, context=None):
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
406 subfield, ws = self._referencing_subfield(record)
408 # transform [{subfield:ref1,ref2,ref3}] into
409 # [{subfield:ref1},{subfield:ref2},{subfield:ref3}]
410 records = ({subfield:item} for item in record[subfield].split(','))
413 if not isinstance(e, Warning):
416 for record in records:
418 refs = only_ref_fields(record)
419 # there are ref fields in the record
421 subfield, w1 = self._referencing_subfield(refs)
423 reference = record[subfield]
424 id, subfield_type, w2 = self.db_id_for(
425 cr, uid, model, column, subfield, reference, context=context)
428 writable = column.converter(exclude_ref_fields(record), log)
430 commands.append(LINK_TO(id))
431 commands.append(UPDATE(id, writable))
433 commands.append(CREATE(writable))
435 return commands, warnings