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
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)
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)
32 class ConversionNotFound(ValueError): pass
34 class ColumnWrapper(object):
35 def __init__(self, column, cr, uid, pool, fromtype, context=None):
36 self._converter = None
40 self._converter_args = {
43 'model': pool[column._obj],
49 if not self._converter:
50 self._converter = self._pool['ir.fields.converter'].for_model(
51 **self._converter_args)
52 return self._converter
54 def __getattr__(self, item):
55 return getattr(self._column, item)
57 class ir_fields_converter(orm.Model):
58 _name = 'ir.fields.converter'
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.
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
71 (k, ColumnWrapper(v.column, cr, uid, self.pool, fromtype, context))
72 for k, v in model._all_columns.iteritems())
74 (k, self.to_field(cr, uid, model, column, fromtype, context))
75 for k, column in columns.iteritems())
79 for field, value in record.iteritems():
80 if field in (None, 'id', '.id'): continue
82 converted[field] = False
85 converted[field], ws = converters[field](value)
87 if isinstance(w, basestring):
88 # wrap warning string in an ImportWarning for
98 def to_field(self, cr, uid, model, column, fromtype=str, context=None):
99 """ Fetches a converter for the provided column object, from the
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``.
106 By default, tries to get a method on itself with a name matching the
107 pattern ``_$fromtype_to_$column._type`` and returns it.
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.
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).
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
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.
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
138 self, '_%s_to_%s' % (fromtype.__name__, column._type), None)
139 if not converter: return None
141 return functools.partial(
142 converter, cr, uid, model, column, context=context)
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),
153 if value.lower() in trues: return True, []
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),
161 if value.lower() in falses: return False, []
163 return True, [orm.ImportWarning(
164 _(u"Unknown value '%s' for boolean field '%%(field)s', assuming '%s'")
166 'moreinfo': _(u"Use '1' for yes and '0' for no")
169 def _str_to_integer(self, cr, uid, model, column, value, context=None):
171 return int(value), []
174 _(u"'%s' does not seem to be an integer for field '%%(field)s'")
177 def _str_to_float(self, cr, uid, model, column, value, context=None):
179 return float(value), []
182 _(u"'%s' does not seem to be a number for field '%%(field)s'")
185 def _str_id(self, cr, uid, model, column, value, context=None):
187 _str_to_reference = _str_to_char = _str_to_text = _str_to_binary = _str_id
189 def _str_to_date(self, cr, uid, model, column, value, context=None):
191 time.strptime(value, DEFAULT_SERVER_DATE_FORMAT)
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"
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'):
203 return pytz.timezone(context['tz'])
204 except pytz.UnknownTimeZoneError:
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]
212 return pytz.timezone(user['tz'])
213 except pytz.UnknownTimeZoneError:
216 # fallback if no tz in context or on user: UTC
219 def _str_to_datetime(self, cr, uid, model, column, value, context=None):
220 if context is None: context = {}
222 parsed_value = datetime.datetime.strptime(
223 value, DEFAULT_SERVER_DATETIME_FORMAT)
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"
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), []
235 def _get_translations(self, cr, uid, types, src, context):
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]
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)
250 def _str_to_selection(self, cr, uid, model, column, value, context=None):
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)
261 if value == unicode(item) or value in labels:
264 _(u"Value '%s' not found in selection field '%%(field)s'") % (
266 'moreinfo': [label or unicode(item) for item, label in selection
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.
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
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
286 :rtype: (ID|None, unicode, list)
288 if context is None: context = {}
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")}
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)]
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
307 if RelatedModel.search(cr, uid, [('id', '=', tentative_id)],
310 except psycopg2.DataError:
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")
318 module, xid = value.split('.', 1)
320 module, xid = context.get('_import_current_module', ''), value
321 ModelData = self.pool['ir.model.data']
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)
332 warnings.append(orm.ImportWarning(
333 _(u"Found multiple matches for field '%%(field)s' (%d matches)")
337 raise Exception(_(u"Unknown sub-field '%s'") % subfield)
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
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.
353 :return: the record subfield to use for referencing and a list of warnings
356 # Can import by name_get, external id or database id
357 fieldset = set(record.iterkeys())
358 if fieldset - REFERENCING_FIELDS:
360 _(u"Can not create Many-To-One records indirectly, import the field separately"))
361 if len(fieldset) > 1:
363 _(u"Ambiguous specification for field '%(field)s', only provide one of name, external id or database id"))
365 # only one field left possible, unpack
366 [subfield] = fieldset
369 def _str_to_many2one(self, cr, uid, model, column, values, context=None):
370 # Should only be one record, unpack
373 subfield, w1 = self._referencing_subfield(record)
375 reference = record[subfield]
376 id, subfield_type, w2 = self.db_id_for(
377 cr, uid, model, column, subfield, reference, context=context)
380 def _str_to_many2many(self, cr, uid, model, column, value, context=None):
383 subfield, warnings = self._referencing_subfield(record)
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)
391 return [REPLACE_WITH(ids)], warnings
393 def _str_to_one2many(self, cr, uid, model, column, records, context=None):
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
401 subfield, ws = self._referencing_subfield(record)
403 # transform [{subfield:ref1,ref2,ref3}] into
404 # [{subfield:ref1},{subfield:ref2},{subfield:ref3}]
405 records = ({subfield:item} for item in record[subfield].split(','))
408 if not isinstance(e, Warning):
411 for record in records:
413 refs = only_ref_fields(record)
414 # there are ref fields in the record
416 subfield, w1 = self._referencing_subfield(refs)
418 reference = record[subfield]
419 id, subfield_type, w2 = self.db_id_for(
420 cr, uid, model, column, subfield, reference, context=context)
423 writable = column.converter(exclude_ref_fields(record), log)
425 commands.append(LINK_TO(id))
426 commands.append(UPDATE(id, writable))
428 commands.append(CREATE(writable))
430 return commands, warnings