1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
24 - relations (one2many, many2one, many2many)
28 * _classic_read: is a classic sql fields
30 * _auto_join: for one2many and many2one fields, tells whether select
31 queries will join the relational table instead of replacing the
32 field condition by an equivalent-one based on a search.
45 from psycopg2 import Binary
48 import openerp.tools as tools
49 from openerp.tools.translate import _
50 from openerp.tools import float_round, float_repr
51 from openerp.tools import html_sanitize
53 from openerp import SUPERUSER_ID
55 _logger = logging.getLogger(__name__)
57 def _symbol_set(symb):
58 if symb is None or symb == False:
60 elif isinstance(symb, unicode):
61 return symb.encode('utf-8')
65 class _column(object):
66 """ Base of all fields, a database column
68 An instance of this object is a *description* of a database column. It will
69 not hold any data, but only provide the methods to manipulate data of an
70 ORM record or even prepare/update the database to hold such a field of data.
81 _symbol_f = _symbol_set
82 _symbol_set = (_symbol_c, _symbol_f)
85 copy = True # whether the field is copied by BaseModel.copy()
87 def __init__(self, string='unknown', required=False, readonly=False, domain=None, context=None, states=None, priority=0, change_default=False, size=None, ondelete=None, translate=False, select=False, manual=False, **args):
90 The 'manual' keyword argument specifies if the field is a custom one.
91 It corresponds to the 'state' column in ir_model_fields.
98 self.states = states or {}
100 self.readonly = readonly
101 self.required = required
103 self.help = args.get('help', '')
104 self.priority = priority
105 self.change_default = change_default
106 self.ondelete = ondelete.lower() if ondelete else None # defaults to 'set null' in ORM
107 self.translate = translate
108 self._domain = domain
109 self._context = context
114 self.selectable = True
115 self.group_operator = args.get('group_operator', False)
116 self.groups = False # CSV list of ext IDs of groups that can access this field
117 self.deprecated = False # Optional deprecation warning
119 setattr(self, a, args[a])
121 # prefetch only if self._classic_write, not self.groups, and not
123 if not self._classic_write or self.deprecated:
124 self._prefetch = False
127 """ convert column `self` to a new-style field """
128 from openerp.fields import Field
129 return Field.by_type[self._type](**self.to_field_args())
131 def to_field_args(self):
132 """ return a dictionary with all the arguments to pass to the field """
134 ('_origin', self), # field interfaces self
136 ('index', self.select),
137 ('manual', self.manual),
138 ('string', self.string),
140 ('readonly', self.readonly),
141 ('required', self.required),
142 ('states', self.states),
143 ('groups', self.groups),
145 ('ondelete', self.ondelete),
146 ('translate', self.translate),
147 ('domain', self._domain),
148 ('context', self._context),
149 ('change_default', self.change_default),
150 ('deprecated', self.deprecated),
152 return dict(item for item in items if item[1])
157 def set(self, cr, obj, id, name, value, user=None, context=None):
158 cr.execute('update '+obj._table+' set '+name+'='+self._symbol_set[0]+' where id=%s', (self._symbol_set[1](value), id))
160 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
161 raise Exception(_('undefined get method !'))
163 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
164 ids = obj.search(cr, uid, args+self._domain+[(name, 'ilike', value)], offset, limit, context=context)
165 res = obj.read(cr, uid, ids, [name], context=context)
166 return [x[name] for x in res]
168 def as_display_name(self, cr, uid, obj, value, context=None):
169 """Converts a field value to a suitable string representation for a record,
170 e.g. when this field is used as ``rec_name``.
172 :param obj: the ``BaseModel`` instance this column belongs to
173 :param value: a proper value as returned by :py:meth:`~openerp.orm.osv.BaseModel.read`
176 # delegated to class method, so a column type A can delegate
177 # to a column type B.
178 return self._as_display_name(self, cr, uid, obj, value, context=None)
181 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
182 # This needs to be a class method, in case a column type A as to delegate
183 # to a column type B.
184 return tools.ustr(value)
186 # ---------------------------------------------------------
188 # ---------------------------------------------------------
189 class boolean(_column):
193 _symbol_set = (_symbol_c, _symbol_f)
195 def __init__(self, string='unknown', required=False, **args):
196 super(boolean, self).__init__(string=string, required=required, **args)
199 "required=True is deprecated: making a boolean field"
200 " `required` has no effect, as NULL values are "
201 "automatically turned into False. args: %r",args)
203 class integer(_column):
206 _symbol_f = lambda x: int(x or 0)
207 _symbol_set = (_symbol_c, _symbol_f)
208 _symbol_get = lambda self,x: x or 0
210 def __init__(self, string='unknown', required=False, **args):
211 super(integer, self).__init__(string=string, required=required, **args)
213 class reference(_column):
215 _classic_read = False # post-process to handle missing target
217 def __init__(self, string, selection, size=None, **args):
218 if callable(selection):
219 from openerp import api
220 selection = api.expected(api.cr_uid_context, selection)
221 _column.__init__(self, string=string, size=size, selection=selection, **args)
223 def to_field_args(self):
224 args = super(reference, self).to_field_args()
225 args['selection'] = self.selection
228 def get(self, cr, obj, ids, name, uid=None, context=None, values=None):
230 # copy initial values fetched previously.
232 result[value['id']] = value[name]
234 model, res_id = value[name].split(',')
235 if not obj.pool[model].exists(cr, uid, [int(res_id)], context=context):
236 result[value['id']] = False
240 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
242 # reference fields have a 'model,id'-like value, that we need to convert
244 model_name, res_id = value.split(',')
245 if model_name in obj.pool and res_id:
246 model = obj.pool[model_name]
247 names = model.name_get(cr, uid, [int(res_id)], context=context)
248 return names[0][1] if names else False
249 return tools.ustr(value)
251 # takes a string (encoded in utf8) and returns a string (encoded in utf8)
252 def _symbol_set_char(self, symb):
255 # * we need to remove the "symb==False" from the next line BUT
256 # for now too many things rely on this broken behavior
257 # * the symb==None test should be common to all data types
258 if symb is None or symb == False:
261 # we need to convert the string to a unicode object to be able
262 # to evaluate its length (and possibly truncate it) reliably
263 u_symb = tools.ustr(symb)
264 return u_symb[:self.size].encode('utf8')
269 def __init__(self, string="unknown", size=None, **args):
270 _column.__init__(self, string=string, size=size or None, **args)
271 # self._symbol_set_char defined to keep the backward compatibility
272 self._symbol_f = self._symbol_set_char = lambda x: _symbol_set_char(self, x)
273 self._symbol_set = (self._symbol_c, self._symbol_f)
283 def _symbol_set_html(self, value):
284 if value is None or value is False:
286 if not self._sanitize:
288 return html_sanitize(value)
290 def __init__(self, string='unknown', sanitize=True, **args):
291 super(html, self).__init__(string=string, **args)
292 self._sanitize = sanitize
293 # symbol_set redefinition because of sanitize specific behavior
294 self._symbol_f = self._symbol_set_html
295 self._symbol_set = (self._symbol_c, self._symbol_f)
297 def to_field_args(self):
298 args = super(html, self).to_field_args()
299 args['sanitize'] = self._sanitize
304 class float(_column):
307 _symbol_f = lambda x: __builtin__.float(x or 0.0)
308 _symbol_set = (_symbol_c, _symbol_f)
309 _symbol_get = lambda self,x: x or 0.0
311 def __init__(self, string='unknown', digits=None, digits_compute=None, required=False, **args):
312 _column.__init__(self, string=string, required=required, **args)
314 # synopsis: digits_compute(cr) -> (precision, scale)
315 self.digits_compute = digits_compute
317 def to_field_args(self):
318 args = super(float, self).to_field_args()
319 args['digits'] = self.digits_compute or self.digits
322 def digits_change(self, cr):
323 if self.digits_compute:
324 self.digits = self.digits_compute(cr)
326 precision, scale = self.digits
327 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
328 precision_digits=scale),
329 precision_digits=scale))
351 """ Returns the current date in a format fit for being a
352 default value to a ``date`` field.
354 This method should be provided as is to the _defaults dict, it
355 should not be called.
357 return DT.date.today().strftime(
358 tools.DEFAULT_SERVER_DATE_FORMAT)
361 def context_today(model, cr, uid, context=None, timestamp=None):
362 """Returns the current date as seen in the client's timezone
363 in a format fit for date fields.
364 This method may be passed as value to initialize _defaults.
366 :param Model model: model (osv) for which the date value is being
367 computed - automatically passed when used in
369 :param datetime timestamp: optional datetime value to use instead of
370 the current date and time (must be a
371 datetime, regular dates can't be converted
373 :param dict context: the 'tz' key in the context should give the
374 name of the User/Client timezone (otherwise
378 today = timestamp or DT.datetime.now()
380 if context and context.get('tz'):
381 tz_name = context['tz']
383 user = model.pool['res.users'].browse(cr, SUPERUSER_ID, uid)
387 utc = pytz.timezone('UTC')
388 context_tz = pytz.timezone(tz_name)
389 utc_today = utc.localize(today, is_dst=False) # UTC = no DST
390 context_today = utc_today.astimezone(context_tz)
392 _logger.debug("failed to compute context/client-specific today date, "
393 "using the UTC value for `today`",
395 return (context_today or today).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
398 def date_to_datetime(model, cr, uid, userdate, context=None):
399 """ Convert date values expressed in user's timezone to
400 server-side UTC timestamp, assuming a default arbitrary
401 time of 12:00 AM - because a time is needed.
403 :param str userdate: date string in in user time zone
404 :return: UTC datetime string for server-side use
406 user_date = DT.datetime.strptime(userdate, tools.DEFAULT_SERVER_DATE_FORMAT)
407 if context and context.get('tz'):
408 tz_name = context['tz']
410 tz_name = model.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
412 utc = pytz.timezone('UTC')
413 context_tz = pytz.timezone(tz_name)
414 user_datetime = user_date + DT.timedelta(hours=12.0)
415 local_timestamp = context_tz.localize(user_datetime, is_dst=False)
416 user_datetime = local_timestamp.astimezone(utc)
417 return user_datetime.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
418 return user_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
421 class datetime(_column):
441 """ Returns the current datetime in a format fit for being a
442 default value to a ``datetime`` field.
444 This method should be provided as is to the _defaults dict, it
445 should not be called.
447 return DT.datetime.now().strftime(
448 tools.DEFAULT_SERVER_DATETIME_FORMAT)
451 def context_timestamp(cr, uid, timestamp, context=None):
452 """Returns the given timestamp converted to the client's timezone.
453 This method is *not* meant for use as a _defaults initializer,
454 because datetime fields are automatically converted upon
455 display on client side. For _defaults you :meth:`fields.datetime.now`
456 should be used instead.
458 :param datetime timestamp: naive datetime value (expressed in UTC)
459 to be converted to the client timezone
460 :param dict context: the 'tz' key in the context should give the
461 name of the User/Client timezone (otherwise
464 :return: timestamp converted to timezone-aware datetime in context
467 assert isinstance(timestamp, DT.datetime), 'Datetime instance expected'
468 if context and context.get('tz'):
469 tz_name = context['tz']
471 registry = openerp.modules.registry.RegistryManager.get(cr.dbname)
472 user = registry['res.users'].browse(cr, SUPERUSER_ID, uid)
474 utc_timestamp = pytz.utc.localize(timestamp, is_dst=False) # UTC = no DST
477 context_tz = pytz.timezone(tz_name)
478 return utc_timestamp.astimezone(context_tz)
480 _logger.debug("failed to compute context/client-specific timestamp, "
481 "using the UTC value",
485 class binary(_column):
489 # Binary values may be byte strings (python 2.6 byte array), but
490 # the legacy OpenERP convention is to transfer and store binaries
491 # as base64-encoded strings. The base64 string may be provided as a
492 # unicode in some circumstances, hence the str() cast in symbol_f.
493 # This str coercion will only work for pure ASCII unicode strings,
494 # on purpose - non base64 data must be passed as a 8bit byte strings.
495 _symbol_f = lambda symb: symb and Binary(str(symb)) or None
497 _symbol_set = (_symbol_c, _symbol_f)
498 _symbol_get = lambda self, x: x and str(x)
500 _classic_read = False
503 def __init__(self, string='unknown', filters=None, **args):
504 _column.__init__(self, string=string, **args)
505 self.filters = filters
507 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
520 # If client is requesting only the size of the field, we return it instead
521 # of the content. Presumably a separate request will be done to read the actual
522 # content if it's needed at some point.
523 # TODO: after 6.0 we should consider returning a dict with size and content instead of
524 # having an implicit convention for the value
525 if val and context.get('bin_size_%s' % name, context.get('bin_size')):
526 res[i] = tools.human_size(long(val))
531 class selection(_column):
534 def __init__(self, selection, string='unknown', **args):
535 if callable(selection):
536 from openerp import api
537 selection = api.expected(api.cr_uid_context, selection)
538 _column.__init__(self, string=string, **args)
539 self.selection = selection
541 def to_field_args(self):
542 args = super(selection, self).to_field_args()
543 args['selection'] = self.selection
547 def reify(cls, cr, uid, model, field, context=None):
548 """ Munges the field's ``selection`` attribute as necessary to get
549 something useable out of it: calls it if it's a function, applies
550 translations to labels if it's not.
552 A callable ``selection`` is considered translated on its own.
554 :param orm.Model model:
555 :param _column field:
557 if callable(field.selection):
558 return field.selection(model, cr, uid, context)
560 if not (context and 'lang' in context):
561 return field.selection
563 # field_to_dict isn't given a field name, only a field object, we
564 # need to get the name back in order to perform the translation lookup
566 name for name, column in model._columns.iteritems()
569 translation_filter = "%s,%s" % (model._name, field_name)
570 translate = functools.partial(
571 model.pool['ir.translation']._get_source,
572 cr, uid, translation_filter, 'selection', context['lang'])
575 (value, translate(label))
576 for value, label in field.selection
579 # ---------------------------------------------------------
581 # ---------------------------------------------------------
584 # Values: (0, 0, { fields }) create
585 # (1, ID, { fields }) update
586 # (2, ID) remove (delete)
587 # (3, ID) unlink one (target id or target of relation)
589 # (5) unlink all (only valid for one2many)
592 class many2one(_column):
593 _classic_read = False
594 _classic_write = True
597 _symbol_f = lambda x: x or None
598 _symbol_set = (_symbol_c, _symbol_f)
600 def __init__(self, obj, string='unknown', auto_join=False, **args):
601 _column.__init__(self, string=string, **args)
603 self._auto_join = auto_join
605 def to_field_args(self):
606 args = super(many2one, self).to_field_args()
607 args['comodel_name'] = self._obj
608 args['auto_join'] = self._auto_join
611 def set(self, cr, obj_src, id, field, values, user=None, context=None):
614 obj = obj_src.pool[self._obj]
615 self._table = obj._table
616 if type(values) == type([]):
619 id_new = obj.create(cr, act[2])
620 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (id_new, id))
622 obj.write(cr, [act[1]], act[2], context=context)
624 cr.execute('delete from '+self._table+' where id=%s', (act[1],))
625 elif act[0] == 3 or act[0] == 5:
626 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
628 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (act[1], id))
631 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (values, id))
633 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
635 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
636 return obj.pool[self._obj].search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context)
639 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
640 return value[1] if isinstance(value, tuple) else tools.ustr(value)
643 class one2many(_column):
644 _classic_read = False
645 _classic_write = False
649 # one2many columns are not copied by default
652 def __init__(self, obj, fields_id, string='unknown', limit=None, auto_join=False, **args):
653 _column.__init__(self, string=string, **args)
655 self._fields_id = fields_id
657 self._auto_join = auto_join
658 #one2many can't be used as condition for defaults
659 assert(self.change_default != True)
661 def to_field_args(self):
662 args = super(one2many, self).to_field_args()
663 args['comodel_name'] = self._obj
664 args['inverse_name'] = self._fields_id
665 args['auto_join'] = self._auto_join
666 args['limit'] = self._limit
669 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
671 context = dict(context or {})
672 context.update(self._context)
674 comodel = obj.pool[self._obj].browse(cr, user, [], context)
675 inverse = self._fields_id
676 domain = self._domain(obj) if callable(self._domain) else self._domain
677 domain = domain + [(inverse, 'in', ids)]
679 records = comodel.search(domain, limit=self._limit)
680 record_ids = map(int, records)
682 res = dict((id, []) for id in ids)
684 cr.execute('SELECT id, %(inverse)s \
686 WHERE id in %%s ' % {
688 'rel': comodel._table,
689 }, (tuple(record_ids),))
690 record_value_id = dict(cr.fetchall())
691 # match the result per id, preserving the order
692 for record in records:
693 key = record_value_id[record.id]
694 res[key].append(record.id)
698 def set(self, cr, obj, id, field, values, user=None, context=None):
700 context = dict(context or {})
701 context.update(self._context)
702 context['recompute'] = False # recomputation is done by outer create/write
705 obj = obj.pool[self._obj]
709 act[2][self._fields_id] = id
710 id_new = obj.create(cr, user, act[2], context=context)
711 result += obj._store_get_values(cr, user, [id_new], act[2].keys(), context)
713 obj.write(cr, user, [act[1]], act[2], context=context)
715 obj.unlink(cr, user, [act[1]], context=context)
717 reverse_rel = obj._all_columns.get(self._fields_id)
718 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
719 # if the model has on delete cascade, just delete the row
720 if reverse_rel.column.ondelete == "cascade":
721 obj.unlink(cr, user, [act[1]], context=context)
723 cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],))
725 # table of the field (parent_model in case of inherit)
726 field_model = self._fields_id in obj.pool[self._obj]._columns and self._obj or obj.pool[self._obj]._all_columns[self._fields_id].parent_model
727 field_table = obj.pool[field_model]._table
728 cr.execute("select 1 from {0} where id=%s and {1}=%s".format(field_table, self._fields_id), (act[1], id))
729 if not cr.fetchone():
730 # Must use write() to recompute parent_store structure if needed and check access rules
731 obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
733 reverse_rel = obj._all_columns.get(self._fields_id)
734 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
735 # if the o2m has a static domain we must respect it when unlinking
736 domain = self._domain(obj) if callable(self._domain) else self._domain
737 extra_domain = domain or []
738 ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
739 # If the model has cascade deletion, we delete the rows because it is the intended behavior,
740 # otherwise we only nullify the reverse foreign key column.
741 if reverse_rel.column.ondelete == "cascade":
742 obj.unlink(cr, user, ids_to_unlink, context=context)
744 obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context)
746 # Must use write() to recompute parent_store structure if needed
747 obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
749 cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
750 ids3 = map(lambda x:x[0], cr.fetchall())
751 obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
754 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
755 domain = self._domain(obj) if callable(self._domain) else self._domain
756 return obj.pool[self._obj].name_search(cr, uid, value, domain, operator, context=context,limit=limit)
759 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
760 raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)')
763 # Values: (0, 0, { fields }) create
764 # (1, ID, { fields }) update (write fields to ID)
765 # (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete)
766 # (3, ID) unlink (delete the relationship between the two objects but does not delete ID)
767 # (4, ID) link (add a relationship)
769 # (6, ?, ids) set a list of links
771 class many2many(_column):
772 """Encapsulates the logic of a many-to-many bidirectional relationship, handling the
773 low-level details of the intermediary relationship table transparently.
774 A many-to-many relationship is always symmetrical, and can be declared and accessed
775 from either endpoint model.
776 If ``rel`` (relationship table name), ``id1`` (source foreign key column name)
777 or id2 (destination foreign key column name) are not specified, the system will
778 provide default values. This will by default only allow one single symmetrical
779 many-to-many relationship between the source and destination model.
780 For multiple many-to-many relationship between the same models and for
781 relationships where source and destination models are the same, ``rel``, ``id1``
782 and ``id2`` should be specified explicitly.
784 :param str obj: destination model
785 :param str rel: optional name of the intermediary relationship table. If not specified,
786 a canonical name will be derived based on the alphabetically-ordered
787 model names of the source and destination (in the form: ``amodel_bmodel_rel``).
788 Automatic naming is not possible when the source and destination are
789 the same, for obvious ambiguity reasons.
790 :param str id1: optional name for the column holding the foreign key to the current
791 model in the relationship table. If not specified, a canonical name
792 will be derived based on the model name (in the form: `src_model_id`).
793 :param str id2: optional name for the column holding the foreign key to the destination
794 model in the relationship table. If not specified, a canonical name
795 will be derived based on the model name (in the form: `dest_model_id`)
796 :param str string: field label
798 _classic_read = False
799 _classic_write = False
803 def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
806 _column.__init__(self, string=string, **args)
808 if rel and '.' in rel:
809 raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\
810 'You used %s, which is not a valid SQL table name.')% (string,rel))
816 def to_field_args(self):
817 args = super(many2many, self).to_field_args()
818 args['comodel_name'] = self._obj
819 args['relation'] = self._rel
820 args['column1'] = self._id1
821 args['column2'] = self._id2
822 args['limit'] = self._limit
825 def _sql_names(self, source_model):
826 """Return the SQL names defining the structure of the m2m relationship table
828 :return: (m2m_table, local_col, dest_col) where m2m_table is the table name,
829 local_col is the name of the column holding the current model's FK, and
830 dest_col is the name of the column holding the destination model's FK, and
832 tbl, col1, col2 = self._rel, self._id1, self._id2
833 if not all((tbl, col1, col2)):
834 # the default table name is based on the stable alphabetical order of tables
835 dest_model = source_model.pool[self._obj]
836 tables = tuple(sorted([source_model._table, dest_model._table]))
838 assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\
839 'is not possible when source and destination models are '\
841 tbl = '%s_%s_rel' % tables
843 col1 = '%s_id' % source_model._table
845 col2 = '%s_id' % dest_model._table
846 return tbl, col1, col2
848 def _get_query_and_where_params(self, cr, model, ids, values, where_params):
849 """ Extracted from ``get`` to facilitate fine-tuning of the generated
851 query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
852 FROM %(rel)s, %(from_c)s \
853 WHERE %(rel)s.%(id1)s IN %%s \
854 AND %(rel)s.%(id2)s = %(tbl)s.id \
860 return query, where_params
862 def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
874 "Specifying offset at a many2many.get() is deprecated and may"
875 " produce unpredictable results.")
876 obj = model.pool[self._obj]
877 rel, id1, id2 = self._sql_names(model)
879 # static domains are lists, and are evaluated both here and on client-side, while string
880 # domains supposed by dynamic and evaluated on client-side only (thus ignored here)
881 # FIXME: make this distinction explicit in API!
882 domain = isinstance(self._domain, list) and self._domain or []
884 wquery = obj._where_calc(cr, user, domain, context=context)
885 obj._apply_ir_rules(cr, user, wquery, 'read', context=context)
886 from_c, where_c, where_params = wquery.get_sql()
888 where_c = ' AND ' + where_c
890 order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0])
893 if self._limit is not None:
894 limit_str = ' LIMIT %d' % self._limit
896 query, where_params = self._get_query_and_where_params(cr, model, ids, {'rel': rel,
903 'order_by': order_by,
907 cr.execute(query, [tuple(ids),] + where_params)
908 for r in cr.fetchall():
909 res[r[1]].append(r[0])
912 def set(self, cr, model, id, name, values, user=None, context=None):
917 rel, id1, id2 = self._sql_names(model)
918 obj = model.pool[self._obj]
920 if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
923 idnew = obj.create(cr, user, act[2], context=context)
924 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew))
926 obj.write(cr, user, [act[1]], act[2], context=context)
928 obj.unlink(cr, user, [act[1]], context=context)
930 cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1]))
932 # following queries are in the same transaction - so should be relatively safe
933 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1]))
934 if not cr.fetchone():
935 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1]))
937 cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,))
940 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
942 d1 = ' and ' + ' and '.join(d1)
945 cr.execute('delete from '+rel+' where '+id1+'=%s AND '+id2+' IN (SELECT '+rel+'.'+id2+' FROM '+rel+', '+','.join(tables)+' WHERE '+rel+'.'+id1+'=%s AND '+rel+'.'+id2+' = '+obj._table+'.id '+ d1 +')', [id, id]+d2)
947 for act_nbr in act[2]:
948 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr))
951 # TODO: use a name_search
953 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
954 return obj.pool[self._obj].search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
957 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
958 raise NotImplementedError('Many2Many columns should not be used as record name (_rec_name)')
961 def get_nice_size(value):
963 if isinstance(value, (int,long)):
965 elif value: # this is supposed to be a string
967 return tools.human_size(size)
969 # See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
970 # and http://bugs.python.org/issue10066
971 invalid_xml_low_bytes = re.compile(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
973 def sanitize_binary_value(value):
974 # binary fields should be 7-bit ASCII base64-encoded data,
975 # but we do additional sanity checks to make sure the values
976 # are not something else that won't pass via XML-RPC
977 if isinstance(value, (xmlrpclib.Binary, tuple, list, dict)):
978 # these builtin types are meant to pass untouched
981 # Handle invalid bytes values that will cause problems
982 # for XML-RPC. See for more info:
983 # - http://bugs.python.org/issue10066
984 # - http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
986 # Coercing to unicode would normally allow it to properly pass via
987 # XML-RPC, transparently encoded as UTF-8 by xmlrpclib.
988 # (this works for _any_ byte values, thanks to the fallback
989 # to latin-1 passthrough encoding when decoding to unicode)
990 value = tools.ustr(value)
992 # Due to Python bug #10066 this could still yield invalid XML
993 # bytes, specifically in the low byte range, that will crash
994 # the decoding side: [\x00-\x08\x0b-\x0c\x0e-\x1f]
995 # So check for low bytes values, and if any, perform
996 # base64 encoding - not very smart or useful, but this is
997 # our last resort to avoid crashing the request.
998 if invalid_xml_low_bytes.search(value):
999 # b64-encode after restoring the pure bytes with latin-1
1000 # passthrough encoding
1001 value = base64.b64encode(value.encode('latin-1'))
1006 # ---------------------------------------------------------
1008 # ---------------------------------------------------------
1009 class function(_column):
1011 A field whose value is computed by a function (rather
1012 than being read from the database).
1014 :param fnct: the callable that will compute the field value.
1015 :param arg: arbitrary value to be passed to ``fnct`` when computing the value.
1016 :param fnct_inv: the callable that will allow writing values in that field
1017 (if not provided, the field is read-only).
1018 :param fnct_inv_arg: arbitrary value to be passed to ``fnct_inv`` when
1020 :param str type: type of the field simulated by the function field
1021 :param fnct_search: the callable that allows searching on the field
1022 (if not provided, search will not return any result).
1023 :param store: store computed value in database
1024 (see :ref:`The *store* parameter <field-function-store>`).
1025 :type store: True or dict specifying triggers for field computation
1026 :param multi: name of batch for batch computation of function fields.
1027 All fields with the same batch name will be computed by
1028 a single function call. This changes the signature of the
1031 .. _field-function-fnct: The ``fnct`` parameter
1033 .. rubric:: The ``fnct`` parameter
1035 The callable implementing the function field must have the following signature:
1037 .. function:: fnct(model, cr, uid, ids, field_name(s), arg, context)
1039 Implements the function field.
1041 :param orm model: model to which the field belongs (should be ``self`` for
1043 :param field_name(s): name of the field to compute, or if ``multi`` is provided,
1044 list of field names to compute.
1045 :type field_name(s): str | [str]
1046 :param arg: arbitrary value passed when declaring the function field
1048 :return: mapping of ``ids`` to computed values, or if multi is provided,
1049 to a map of field_names to computed values
1051 The values in the returned dictionary must be of the type specified by the type
1052 argument in the field declaration.
1054 Here is an example with a simple function ``char`` function field::
1057 def compute(self, cr, uid, ids, field_name, arg, context):
1061 _columns['my_char'] = fields.function(compute, type='char', size=50)
1063 # when called with ``ids=[1,2,3]``, ``compute`` could return:
1067 3: False # null values should be returned explicitly too
1070 If ``multi`` is set, then ``field_name`` is replaced by ``field_names``: a list
1071 of the field names that should be computed. Each value in the returned
1072 dictionary must then be a dictionary mapping field names to values.
1074 Here is an example where two function fields (``name`` and ``age``)
1075 are both computed by a single function field::
1078 def compute(self, cr, uid, ids, field_names, arg, context):
1082 _columns['name'] = fields.function(compute_person_data, type='char',\
1083 size=50, multi='person_data')
1084 _columns[''age'] = fields.function(compute_person_data, type='integer',\
1085 multi='person_data')
1087 # when called with ``ids=[1,2,3]``, ``compute_person_data`` could return:
1089 1: {'name': 'Bob', 'age': 23},
1090 2: {'name': 'Sally', 'age': 19},
1091 3: {'name': 'unknown', 'age': False}
1094 .. _field-function-fnct-inv:
1096 .. rubric:: The ``fnct_inv`` parameter
1098 This callable implements the write operation for the function field
1099 and must have the following signature:
1101 .. function:: fnct_inv(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context)
1103 Callable that implements the ``write`` operation for the function field.
1105 :param orm model: model to which the field belongs (should be ``self`` for
1107 :param int id: the identifier of the object to write on
1108 :param str field_name: name of the field to set
1109 :param fnct_inv_arg: arbitrary value passed when declaring the function field
1112 When writing values for a function field, the ``multi`` parameter is ignored.
1114 .. _field-function-fnct-search:
1116 .. rubric:: The ``fnct_search`` parameter
1118 This callable implements the search operation for the function field
1119 and must have the following signature:
1121 .. function:: fnct_search(model, cr, uid, model_again, field_name, criterion, context)
1123 Callable that implements the ``search`` operation for the function field by expanding
1124 a search criterion based on the function field into a new domain based only on
1125 columns that are stored in the database.
1127 :param orm model: model to which the field belongs (should be ``self`` for
1129 :param orm model_again: same value as ``model`` (seriously! this is for backwards
1131 :param str field_name: name of the field to search on
1132 :param list criterion: domain component specifying the search criterion on the field.
1134 :return: domain to use instead of ``criterion`` when performing the search.
1135 This new domain must be based only on columns stored in the database, as it
1136 will be used directly without any translation.
1138 The returned value must be a domain, that is, a list of the form [(field_name, operator, operand)].
1139 The most generic way to implement ``fnct_search`` is to directly search for the records that
1140 match the given ``criterion``, and return their ``ids`` wrapped in a domain, such as
1141 ``[('id','in',[1,3,5])]``.
1143 .. _field-function-store:
1145 .. rubric:: The ``store`` parameter
1147 The ``store`` parameter allows caching the result of the field computation in the
1148 database, and defining the triggers that will invalidate that cache and force a
1149 recomputation of the function field.
1150 When not provided, the field is computed every time its value is read.
1151 The value of ``store`` may be either ``True`` (to recompute the field value whenever
1152 any field in the same record is modified), or a dictionary specifying a more
1153 flexible set of recomputation triggers.
1155 A trigger specification is a dictionary that maps the names of the models that
1156 will trigger the computation, to a tuple describing the trigger rule, in the
1160 'trigger_model': (mapping_function,
1161 ['trigger_field1', 'trigger_field2'],
1165 A trigger rule is defined by a 3-item tuple where:
1167 * The ``mapping_function`` is defined as follows:
1169 .. function:: mapping_function(trigger_model, cr, uid, trigger_ids, context)
1171 Callable that maps record ids of a trigger model to ids of the
1172 corresponding records in the source model (whose field values
1173 need to be recomputed).
1175 :param orm model: trigger_model
1176 :param list trigger_ids: ids of the records of trigger_model that were
1179 :return: list of ids of the source model whose function field values
1180 need to be recomputed
1182 * The second item is a list of the fields who should act as triggers for
1183 the computation. If an empty list is given, all fields will act as triggers.
1184 * The last item is the priority, used to order the triggers when processing them
1185 after any write operation on a model that has function field triggers. The
1186 default priority is 10.
1188 In fact, setting store = True is the same as using the following trigger dict::
1191 'model_itself': (lambda self, cr, uid, ids, context: ids,
1197 _classic_read = False
1198 _classic_write = False
1203 # function fields are not copied by default
1207 # multi: compute several fields in one call
1209 def __init__(self, fnct, arg=None, fnct_inv=None, fnct_inv_arg=None, type='float', fnct_search=None, obj=None, store=False, multi=False, **args):
1210 _column.__init__(self, **args)
1213 self._fnct_inv = fnct_inv
1216 if 'relation' in args:
1217 self._obj = args['relation']
1219 self.digits = args.get('digits', (16,2))
1220 self.digits_compute = args.get('digits_compute', None)
1221 if callable(args.get('selection')):
1222 from openerp import api
1223 self.selection = api.expected(api.cr_uid_context, args['selection'])
1225 self._fnct_inv_arg = fnct_inv_arg
1229 self._fnct_search = fnct_search
1232 if not fnct_search and not store:
1233 self.selectable = False
1236 if self._type != 'many2one':
1237 # m2o fields need to return tuples with name_get, not just foreign keys
1238 self._classic_read = True
1239 self._classic_write = True
1241 self._symbol_get=lambda x:x and str(x)
1243 self._prefetch = True
1246 self._symbol_c = char._symbol_c
1247 self._symbol_f = lambda x: _symbol_set_char(self, x)
1248 self._symbol_set = (self._symbol_c, self._symbol_f)
1250 type_class = globals().get(type)
1251 if type_class is not None:
1252 self._symbol_c = type_class._symbol_c
1253 self._symbol_f = type_class._symbol_f
1254 self._symbol_set = type_class._symbol_set
1256 def to_field_args(self):
1257 args = super(function, self).to_field_args()
1258 if self._type in ('float',):
1259 args['digits'] = self.digits_compute or self.digits
1260 elif self._type in ('selection', 'reference'):
1261 args['selection'] = self.selection
1262 elif self._type in ('many2one', 'one2many', 'many2many'):
1263 args['comodel_name'] = self._obj
1266 def digits_change(self, cr):
1267 if self._type == 'float':
1268 if self.digits_compute:
1269 self.digits = self.digits_compute(cr)
1271 precision, scale = self.digits
1272 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
1273 precision_digits=scale),
1274 precision_digits=scale))
1276 def search(self, cr, uid, obj, name, args, context=None):
1277 if not self._fnct_search:
1278 #CHECKME: should raise an exception
1280 return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
1282 def postprocess(self, cr, uid, obj, field, value=None, context=None):
1283 return self._postprocess_batch(cr, uid, obj, field, {0: value}, context=context)[0]
1285 def _postprocess_batch(self, cr, uid, obj, field, values, context=None):
1292 field_type = obj._columns[field]._type
1293 new_values = dict(values)
1295 if field_type == 'binary':
1296 if context.get('bin_size'):
1297 # client requests only the size of binary fields
1298 for rid, value in values.iteritems():
1300 new_values[rid] = get_nice_size(value)
1301 elif not context.get('bin_raw'):
1302 for rid, value in values.iteritems():
1304 new_values[rid] = sanitize_binary_value(value)
1308 def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
1310 # if we already have a value, don't recompute it.
1311 # This happen if case of stored many2one fields
1312 if values and not multi and name in values[0]:
1313 result = dict((v['id'], v[name]) for v in values)
1314 elif values and multi and all(n in values[0] for n in name):
1315 result = dict((v['id'], dict((n, v[n]) for n in name)) for v in values)
1317 result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
1320 for rid, values in result.iteritems():
1321 for f, v in values.iteritems():
1324 swap.setdefault(f, {})[rid] = v
1326 for field, values in swap.iteritems():
1327 new_values = self._postprocess_batch(cr, uid, obj, field, values, context)
1328 for rid, value in new_values.iteritems():
1329 result[rid][field] = value
1332 result = self._postprocess_batch(cr, uid, obj, name, result, context)
1336 def set(self, cr, obj, id, name, value, user=None, context=None):
1340 self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
1343 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
1344 # Function fields are supposed to emulate a basic field type,
1345 # so they can delegate to the basic type for record name rendering
1346 return globals()[field._type]._as_display_name(field, cr, uid, obj, value, context=context)
1348 # ---------------------------------------------------------
1350 # ---------------------------------------------------------
1352 class related(function):
1353 """Field that points to some data inside another field of the current record.
1358 'foo_id': fields.many2one('my.foo', 'Foo'),
1359 'bar': fields.related('foo_id', 'frol', type='char', string='Frol of Foo'),
1363 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1364 # assume self._arg = ('foo', 'bar', 'baz')
1365 # domain = [(name, op, val)] => search [('foo.bar.baz', op, val)]
1366 field = '.'.join(self._arg)
1367 return map(lambda x: (field, x[1], x[2]), domain)
1369 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1370 if isinstance(ids, (int, long)):
1372 for instance in obj.browse(cr, uid, ids, context=context):
1373 # traverse all fields except the last one
1374 for field in self.arg[:-1]:
1375 instance = instance[field][:1]
1377 # write on the last field of the target record
1378 instance.write({self.arg[-1]: values})
1380 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1382 for record in obj.browse(cr, SUPERUSER_ID, ids, context=context):
1384 # traverse all fields except the last one
1385 for field in self.arg[:-1]:
1386 value = value[field][:1]
1387 # read the last field on the target record
1388 res[record.id] = value[self.arg[-1]]
1390 if self._type == 'many2one':
1391 # res[id] is a recordset; convert it to (id, name) or False.
1392 # Perform name_get as root, as seeing the name of a related object depends on
1393 # access right of source document, not target, so user may not have access.
1394 value_ids = list(set(value.id for value in res.itervalues() if value))
1395 value_name = dict(obj.pool[self._obj].name_get(cr, SUPERUSER_ID, value_ids, context=context))
1396 res = dict((id, bool(value) and (value.id, value_name[value.id])) for id, value in res.iteritems())
1398 elif self._type in ('one2many', 'many2many'):
1399 # res[id] is a recordset; convert it to a list of ids
1400 res = dict((id, value.ids) for id, value in res.iteritems())
1404 def __init__(self, *arg, **args):
1406 self._relations = []
1407 super(related, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1408 if self.store is True:
1409 # TODO: improve here to change self.store = {...} according to related objects
1413 class sparse(function):
1415 def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
1417 + For a many2many field, a list of tuples is expected.
1418 Here is the list of tuple that are accepted, with the corresponding semantics ::
1420 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1421 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1422 (2, ID) remove and delete the linked record with id = ID (calls unlink on ID, that will delete the object completely, and the link to it as well)
1423 (3, ID) cut the link to the linked record with id = ID (delete the relationship between the two objects but does not delete the target object itself)
1424 (4, ID) link to existing record with id = ID (adds a relationship)
1425 (5) unlink all (like using (3,ID) for all linked records)
1426 (6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
1429 [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
1431 + For a one2many field, a lits of tuples is expected.
1432 Here is the list of tuple that are accepted, with the corresponding semantics ::
1434 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1435 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1436 (2, ID) remove and delete the linked record with id = ID (calls unlink on ID, that will delete the object completely, and the link to it as well)
1439 [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
1442 if self._type == 'many2many':
1443 assert value[0][0] == 6, 'Unsupported m2m value for sparse field: %s' % value
1446 elif self._type == 'one2many':
1449 relation_obj = obj.pool[self.relation]
1451 assert vals[0] in (0,1,2), 'Unsupported o2m value for sparse field: %s' % vals
1453 read_value.append(relation_obj.create(cr, uid, vals[2], context=context))
1455 relation_obj.write(cr, uid, vals[1], vals[2], context=context)
1457 relation_obj.unlink(cr, uid, vals[1], context=context)
1458 read_value.remove(vals[1])
1463 def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
1464 if not type(ids) == list:
1466 records = obj.browse(cr, uid, ids, context=context)
1467 for record in records:
1468 # grab serialized value as object - already deserialized
1469 serialized = getattr(record, self.serialization_field)
1471 # simply delete the key to unset it.
1472 serialized.pop(field_name, None)
1474 serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context)
1475 obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context)
1478 def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None):
1480 records = obj.browse(cr, uid, ids, context=context)
1481 for record in records:
1482 # grab serialized value as object - already deserialized
1483 serialized = getattr(record, self.serialization_field)
1484 results[record.id] = {}
1485 for field_name in field_names:
1486 field_type = obj._columns[field_name]._type
1487 value = serialized.get(field_name, False)
1488 if field_type in ('one2many','many2many'):
1491 # filter out deleted records as superuser
1492 relation_obj = obj.pool[obj._columns[field_name].relation]
1493 value = relation_obj.exists(cr, openerp.SUPERUSER_ID, value)
1494 if type(value) in (int,long) and field_type == 'many2one':
1495 relation_obj = obj.pool[obj._columns[field_name].relation]
1496 # check for deleted record as superuser
1497 if not relation_obj.exists(cr, openerp.SUPERUSER_ID, [value]):
1499 results[record.id][field_name] = value
1502 def __init__(self, serialization_field, **kwargs):
1503 self.serialization_field = serialization_field
1504 super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
1508 # ---------------------------------------------------------
1510 # ---------------------------------------------------------
1512 class dummy(function):
1513 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1516 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1519 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1522 def __init__(self, *arg, **args):
1524 self._relations = []
1525 super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1527 # ---------------------------------------------------------
1529 # ---------------------------------------------------------
1531 class serialized(_column):
1532 """ A field able to store an arbitrary python data structure.
1534 Note: only plain components allowed.
1537 def _symbol_set_struct(val):
1538 return simplejson.dumps(val)
1540 def _symbol_get_struct(self, val):
1541 return simplejson.loads(val or '{}')
1544 _type = 'serialized'
1547 _symbol_f = _symbol_set_struct
1548 _symbol_set = (_symbol_c, _symbol_f)
1549 _symbol_get = _symbol_get_struct
1551 # TODO: review completly this class for speed improvement
1552 class property(function):
1554 def to_field_args(self):
1555 args = super(property, self).to_field_args()
1556 args['company_dependent'] = True
1559 def _fnct_search(self, tobj, cr, uid, obj, name, domain, context=None):
1560 ir_property = obj.pool['ir.property']
1562 for field, operator, value in domain:
1563 result += ir_property.search_multi(cr, uid, name, tobj._name, operator, value, context=context)
1566 def _fnct_write(self, obj, cr, uid, id, prop_name, value, obj_dest, context=None):
1567 ir_property = obj.pool['ir.property']
1568 ir_property.set_multi(cr, uid, prop_name, obj._name, {id: value}, context=context)
1571 def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
1572 ir_property = obj.pool['ir.property']
1574 res = {id: {} for id in ids}
1575 for prop_name in prop_names:
1576 column = obj._all_columns[prop_name].column
1577 values = ir_property.get_multi(cr, uid, prop_name, obj._name, ids, context=context)
1578 if column._type == 'many2one':
1579 # name_get the non-null values as SUPERUSER_ID
1580 vals = sum(set(filter(None, values.itervalues())),
1581 obj.pool[column._obj].browse(cr, uid, [], context=context))
1582 vals_name = dict(vals.sudo().name_get()) if vals else {}
1583 for id, value in values.iteritems():
1585 if value and value.id in vals_name:
1586 ng = value.id, vals_name[value.id]
1587 res[id][prop_name] = ng
1589 for id, value in values.iteritems():
1590 res[id][prop_name] = value
1594 def __init__(self, **args):
1595 if 'view_load' in args:
1596 _logger.warning("view_load attribute is deprecated on ir.fields. Args: %r", args)
1598 args['obj'] = args.pop('relation', '') or args.get('obj', '')
1599 super(property, self).__init__(
1600 fnct=self._fnct_read,
1601 fnct_inv=self._fnct_write,
1602 fnct_search=self._fnct_search,
1608 class column_info(object):
1609 """ Struct containing details about an osv column, either one local to
1610 its model, or one inherited via _inherits.
1616 .. attribute:: column
1618 column instance, subclass of :class:`_column`
1620 .. attribute:: parent_model
1622 if the column is inherited, name of the model that contains it,
1623 ``None`` for local columns.
1625 .. attribute:: parent_column
1627 the name of the column containing the m2o relationship to the
1628 parent model that contains this column, ``None`` for local columns.
1630 .. attribute:: original_parent
1632 if the column is inherited, name of the original parent model that
1633 contains it i.e in case of multilevel inheritance, ``None`` for
1636 def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
1638 self.column = column
1639 self.parent_model = parent_model
1640 self.parent_column = parent_column
1641 self.original_parent = original_parent
1644 return '%s(%s, %s, %s, %s, %s)' % (
1645 self.__class__.__name__, self.name, self.column,
1646 self.parent_model, self.parent_column, self.original_parent)
1649 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: