1 # -*- coding: utf-8 -*-
11 from openerp import models, api, _
12 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, ustr
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)
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)
30 class ImportWarning(Warning):
31 """ Used to send warnings upwards the stack during the import process """
34 class ConversionNotFound(ValueError): pass
37 class ir_fields_converter(models.Model):
38 _name = 'ir.fields.converter'
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.
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
51 # make sure model is new api
52 model = self.env[model._name]
55 name: self.to_field(model, field, fromtype)
56 for name, field in model._fields.iteritems()
61 for field, value in record.iteritems():
62 if field in (None, 'id', '.id'):
65 converted[field] = False
68 converted[field], ws = converters[field](value)
70 if isinstance(w, basestring):
71 # wrap warning string in an ImportWarning for
82 def to_field(self, model, field, fromtype=str):
83 """ Fetches a converter for the provided field object, from the
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``.
90 By default, tries to get a method on itself with a name matching the
91 pattern ``_$fromtype_to_$field.type`` and returns it.
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.
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).
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
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.
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
121 assert isinstance(fromtype, (type, str))
123 typename = fromtype.__name__ if isinstance(fromtype, type) else fromtype
124 converter = getattr(self, '_%s_to_%s' % (typename, field.type), None)
127 return functools.partial(converter, model, field)
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"),
139 if value.lower() in trues:
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"),
148 if value.lower() in falses:
151 return True, [ImportWarning(
152 _(u"Unknown value '%s' for boolean field '%%(field)s', assuming '%s'")
154 'moreinfo': _(u"Use '1' for yes and '0' for no")
158 def _str_to_integer(self, model, field, value):
160 return int(value), []
163 _(u"'%s' does not seem to be an integer for field '%%(field)s'")
167 def _str_to_float(self, model, field, value):
169 return float(value), []
172 _(u"'%s' does not seem to be a number for field '%%(field)s'")
176 def _str_id(self, model, field, value):
179 _str_to_reference = _str_to_char = _str_to_text = _str_to_binary = _str_to_html = _str_id
182 def _str_to_date(self, model, field, value):
184 time.strptime(value, DEFAULT_SERVER_DATE_FORMAT)
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"
194 # if there's a tz in context, try to use that
195 if self._context.get('tz'):
197 return pytz.timezone(self._context['tz'])
198 except pytz.UnknownTimeZoneError:
201 # if the current user has a tz set, try to use that
205 return pytz.timezone(user.tz)
206 except pytz.UnknownTimeZoneError:
209 # fallback if no tz in context or on user: UTC
213 def _str_to_datetime(self, model, field, value):
215 parsed_value = datetime.datetime.strptime(
216 value, DEFAULT_SERVER_DATETIME_FORMAT)
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"
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), []
229 def _get_translations(self, types, src):
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]
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]
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']
248 for item, label in selection:
250 labels = [label] + self._get_translations(('selection', 'model', 'code'), label)
251 if value == unicode(item) or value in labels:
255 _(u"Value '%s' not found in selection field '%%(field)s'") % (
257 'moreinfo': [_label or unicode(item) for item, _label in selection
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.
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
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
277 :rtype: (ID|None, unicode, list)
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")}
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)]
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
297 if RelatedModel.search([('id', '=', tentative_id)]):
299 except psycopg2.DataError:
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")
309 xmlid = "%s.%s" % (self._context.get('_import_current_module', ''), value)
311 id = self.env.ref(xmlid).id
313 pass # leave id is None
314 elif subfield is None:
315 field_type = _(u"name")
316 ids = RelatedModel.name_search(name=value, operator='=')
319 warnings.append(ImportWarning(
320 _(u"Found multiple matches for field '%%(field)s' (%d matches)")
324 raise Exception(_(u"Unknown sub-field '%s'") % subfield)
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
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.
340 :return: the record subfield to use for referencing and a list of warnings
343 # Can import by name_get, external id or database id
344 fieldset = set(record.iterkeys())
345 if fieldset - REFERENCING_FIELDS:
347 _(u"Can not create Many-To-One records indirectly, import the field separately"))
348 if len(fieldset) > 1:
350 _(u"Ambiguous specification for field '%(field)s', only provide one of name, external id or database id"))
352 # only one field left possible, unpack
353 [subfield] = fieldset
357 def _str_to_many2one(self, model, field, values):
358 # Should only be one record, unpack
361 subfield, w1 = self._referencing_subfield(record)
363 reference = record[subfield]
364 id, _, w2 = self.db_id_for(model, field, subfield, reference)
368 def _str_to_many2many(self, model, field, value):
371 subfield, warnings = self._referencing_subfield(record)
374 for reference in record[subfield].split(','):
375 id, _, ws = self.db_id_for(model, field, subfield, reference)
378 return [REPLACE_WITH(ids)], warnings
381 def _str_to_one2many(self, model, field, records):
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
389 subfield, ws = self._referencing_subfield(record)
391 # transform [{subfield:ref1,ref2,ref3}] into
392 # [{subfield:ref1},{subfield:ref2},{subfield:ref3}]
393 records = ({subfield:item} for item in record[subfield].split(','))
396 if not isinstance(e, Warning):
400 convert = self.for_model(self.env[field.comodel_name])
402 for record in records:
404 refs = only_ref_fields(record)
405 # there are ref fields in the record
407 subfield, w1 = self._referencing_subfield(refs)
409 reference = record[subfield]
410 id, _, w2 = self.db_id_for(model, field, subfield, reference)
413 writable = convert(exclude_ref_fields(record), log)
415 commands.append(LINK_TO(id))
416 commands.append(UPDATE(id, writable))
418 commands.append(CREATE(writable))
420 return commands, warnings