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.groups 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 res = dict((id, []) for id in ids)
676 comodel = obj.pool[self._obj].browse(cr, user, [], context)
677 inverse = self._fields_id
678 domain = self._domain(obj) if callable(self._domain) else self._domain
679 domain = domain + [(inverse, 'in', ids)]
681 for record in comodel.search(domain, limit=self._limit):
682 # Note: record[inverse] can be a record or an integer!
683 assert int(record[inverse]) in res
684 res[int(record[inverse])].append(record.id)
688 def set(self, cr, obj, id, field, values, user=None, context=None):
690 context = dict(context or {})
691 context.update(self._context)
692 context['recompute'] = False # recomputation is done by outer create/write
695 obj = obj.pool[self._obj]
699 act[2][self._fields_id] = id
700 id_new = obj.create(cr, user, act[2], context=context)
701 result += obj._store_get_values(cr, user, [id_new], act[2].keys(), context)
703 obj.write(cr, user, [act[1]], act[2], context=context)
705 obj.unlink(cr, user, [act[1]], context=context)
707 reverse_rel = obj._all_columns.get(self._fields_id)
708 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
709 # if the model has on delete cascade, just delete the row
710 if reverse_rel.column.ondelete == "cascade":
711 obj.unlink(cr, user, [act[1]], context=context)
713 cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],))
715 # table of the field (parent_model in case of inherit)
716 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
717 field_table = obj.pool[field_model]._table
718 cr.execute("select 1 from {0} where id=%s and {1}=%s".format(field_table, self._fields_id), (act[1], id))
719 if not cr.fetchone():
720 # Must use write() to recompute parent_store structure if needed and check access rules
721 obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
723 reverse_rel = obj._all_columns.get(self._fields_id)
724 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
725 # if the o2m has a static domain we must respect it when unlinking
726 domain = self._domain(obj) if callable(self._domain) else self._domain
727 extra_domain = domain or []
728 ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
729 # If the model has cascade deletion, we delete the rows because it is the intended behavior,
730 # otherwise we only nullify the reverse foreign key column.
731 if reverse_rel.column.ondelete == "cascade":
732 obj.unlink(cr, user, ids_to_unlink, context=context)
734 obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context)
736 # Must use write() to recompute parent_store structure if needed
737 obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
739 cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
740 ids3 = map(lambda x:x[0], cr.fetchall())
741 obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
744 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
745 domain = self._domain(obj) if callable(self._domain) else self._domain
746 return obj.pool[self._obj].name_search(cr, uid, value, domain, operator, context=context,limit=limit)
749 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
750 raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)')
753 # Values: (0, 0, { fields }) create
754 # (1, ID, { fields }) update (write fields to ID)
755 # (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete)
756 # (3, ID) unlink (delete the relationship between the two objects but does not delete ID)
757 # (4, ID) link (add a relationship)
759 # (6, ?, ids) set a list of links
761 class many2many(_column):
762 """Encapsulates the logic of a many-to-many bidirectional relationship, handling the
763 low-level details of the intermediary relationship table transparently.
764 A many-to-many relationship is always symmetrical, and can be declared and accessed
765 from either endpoint model.
766 If ``rel`` (relationship table name), ``id1`` (source foreign key column name)
767 or id2 (destination foreign key column name) are not specified, the system will
768 provide default values. This will by default only allow one single symmetrical
769 many-to-many relationship between the source and destination model.
770 For multiple many-to-many relationship between the same models and for
771 relationships where source and destination models are the same, ``rel``, ``id1``
772 and ``id2`` should be specified explicitly.
774 :param str obj: destination model
775 :param str rel: optional name of the intermediary relationship table. If not specified,
776 a canonical name will be derived based on the alphabetically-ordered
777 model names of the source and destination (in the form: ``amodel_bmodel_rel``).
778 Automatic naming is not possible when the source and destination are
779 the same, for obvious ambiguity reasons.
780 :param str id1: optional name for the column holding the foreign key to the current
781 model in the relationship table. If not specified, a canonical name
782 will be derived based on the model name (in the form: `src_model_id`).
783 :param str id2: optional name for the column holding the foreign key to the destination
784 model in the relationship table. If not specified, a canonical name
785 will be derived based on the model name (in the form: `dest_model_id`)
786 :param str string: field label
788 _classic_read = False
789 _classic_write = False
793 def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
796 _column.__init__(self, string=string, **args)
798 if rel and '.' in rel:
799 raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\
800 'You used %s, which is not a valid SQL table name.')% (string,rel))
806 def to_field_args(self):
807 args = super(many2many, self).to_field_args()
808 args['comodel_name'] = self._obj
809 args['relation'] = self._rel
810 args['column1'] = self._id1
811 args['column2'] = self._id2
812 args['limit'] = self._limit
815 def _sql_names(self, source_model):
816 """Return the SQL names defining the structure of the m2m relationship table
818 :return: (m2m_table, local_col, dest_col) where m2m_table is the table name,
819 local_col is the name of the column holding the current model's FK, and
820 dest_col is the name of the column holding the destination model's FK, and
822 tbl, col1, col2 = self._rel, self._id1, self._id2
823 if not all((tbl, col1, col2)):
824 # the default table name is based on the stable alphabetical order of tables
825 dest_model = source_model.pool[self._obj]
826 tables = tuple(sorted([source_model._table, dest_model._table]))
828 assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\
829 'is not possible when source and destination models are '\
831 tbl = '%s_%s_rel' % tables
833 col1 = '%s_id' % source_model._table
835 col2 = '%s_id' % dest_model._table
836 return tbl, col1, col2
838 def _get_query_and_where_params(self, cr, model, ids, values, where_params):
839 """ Extracted from ``get`` to facilitate fine-tuning of the generated
841 query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
842 FROM %(rel)s, %(from_c)s \
843 WHERE %(rel)s.%(id1)s IN %%s \
844 AND %(rel)s.%(id2)s = %(tbl)s.id \
850 return query, where_params
852 def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
864 "Specifying offset at a many2many.get() is deprecated and may"
865 " produce unpredictable results.")
866 obj = model.pool[self._obj]
867 rel, id1, id2 = self._sql_names(model)
869 # static domains are lists, and are evaluated both here and on client-side, while string
870 # domains supposed by dynamic and evaluated on client-side only (thus ignored here)
871 # FIXME: make this distinction explicit in API!
872 domain = isinstance(self._domain, list) and self._domain or []
874 wquery = obj._where_calc(cr, user, domain, context=context)
875 obj._apply_ir_rules(cr, user, wquery, 'read', context=context)
876 from_c, where_c, where_params = wquery.get_sql()
878 where_c = ' AND ' + where_c
880 order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0])
883 if self._limit is not None:
884 limit_str = ' LIMIT %d' % self._limit
886 query, where_params = self._get_query_and_where_params(cr, model, ids, {'rel': rel,
893 'order_by': order_by,
897 cr.execute(query, [tuple(ids),] + where_params)
898 for r in cr.fetchall():
899 res[r[1]].append(r[0])
902 def set(self, cr, model, id, name, values, user=None, context=None):
907 rel, id1, id2 = self._sql_names(model)
908 obj = model.pool[self._obj]
910 if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
913 idnew = obj.create(cr, user, act[2], context=context)
914 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew))
916 obj.write(cr, user, [act[1]], act[2], context=context)
918 obj.unlink(cr, user, [act[1]], context=context)
920 cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1]))
922 # following queries are in the same transaction - so should be relatively safe
923 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1]))
924 if not cr.fetchone():
925 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1]))
927 cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,))
930 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
932 d1 = ' and ' + ' and '.join(d1)
935 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)
937 for act_nbr in act[2]:
938 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr))
941 # TODO: use a name_search
943 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
944 return obj.pool[self._obj].search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
947 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
948 raise NotImplementedError('Many2Many columns should not be used as record name (_rec_name)')
951 def get_nice_size(value):
953 if isinstance(value, (int,long)):
955 elif value: # this is supposed to be a string
957 return tools.human_size(size)
959 # See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
960 # and http://bugs.python.org/issue10066
961 invalid_xml_low_bytes = re.compile(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
963 def sanitize_binary_value(value):
964 # binary fields should be 7-bit ASCII base64-encoded data,
965 # but we do additional sanity checks to make sure the values
966 # are not something else that won't pass via XML-RPC
967 if isinstance(value, (xmlrpclib.Binary, tuple, list, dict)):
968 # these builtin types are meant to pass untouched
971 # Handle invalid bytes values that will cause problems
972 # for XML-RPC. See for more info:
973 # - http://bugs.python.org/issue10066
974 # - http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
976 # Coercing to unicode would normally allow it to properly pass via
977 # XML-RPC, transparently encoded as UTF-8 by xmlrpclib.
978 # (this works for _any_ byte values, thanks to the fallback
979 # to latin-1 passthrough encoding when decoding to unicode)
980 value = tools.ustr(value)
982 # Due to Python bug #10066 this could still yield invalid XML
983 # bytes, specifically in the low byte range, that will crash
984 # the decoding side: [\x00-\x08\x0b-\x0c\x0e-\x1f]
985 # So check for low bytes values, and if any, perform
986 # base64 encoding - not very smart or useful, but this is
987 # our last resort to avoid crashing the request.
988 if invalid_xml_low_bytes.search(value):
989 # b64-encode after restoring the pure bytes with latin-1
990 # passthrough encoding
991 value = base64.b64encode(value.encode('latin-1'))
996 # ---------------------------------------------------------
998 # ---------------------------------------------------------
999 class function(_column):
1001 A field whose value is computed by a function (rather
1002 than being read from the database).
1004 :param fnct: the callable that will compute the field value.
1005 :param arg: arbitrary value to be passed to ``fnct`` when computing the value.
1006 :param fnct_inv: the callable that will allow writing values in that field
1007 (if not provided, the field is read-only).
1008 :param fnct_inv_arg: arbitrary value to be passed to ``fnct_inv`` when
1010 :param str type: type of the field simulated by the function field
1011 :param fnct_search: the callable that allows searching on the field
1012 (if not provided, search will not return any result).
1013 :param store: store computed value in database
1014 (see :ref:`The *store* parameter <field-function-store>`).
1015 :type store: True or dict specifying triggers for field computation
1016 :param multi: name of batch for batch computation of function fields.
1017 All fields with the same batch name will be computed by
1018 a single function call. This changes the signature of the
1021 .. _field-function-fnct: The ``fnct`` parameter
1023 .. rubric:: The ``fnct`` parameter
1025 The callable implementing the function field must have the following signature:
1027 .. function:: fnct(model, cr, uid, ids, field_name(s), arg, context)
1029 Implements the function field.
1031 :param orm model: model to which the field belongs (should be ``self`` for
1033 :param field_name(s): name of the field to compute, or if ``multi`` is provided,
1034 list of field names to compute.
1035 :type field_name(s): str | [str]
1036 :param arg: arbitrary value passed when declaring the function field
1038 :return: mapping of ``ids`` to computed values, or if multi is provided,
1039 to a map of field_names to computed values
1041 The values in the returned dictionary must be of the type specified by the type
1042 argument in the field declaration.
1044 Here is an example with a simple function ``char`` function field::
1047 def compute(self, cr, uid, ids, field_name, arg, context):
1051 _columns['my_char'] = fields.function(compute, type='char', size=50)
1053 # when called with ``ids=[1,2,3]``, ``compute`` could return:
1057 3: False # null values should be returned explicitly too
1060 If ``multi`` is set, then ``field_name`` is replaced by ``field_names``: a list
1061 of the field names that should be computed. Each value in the returned
1062 dictionary must then be a dictionary mapping field names to values.
1064 Here is an example where two function fields (``name`` and ``age``)
1065 are both computed by a single function field::
1068 def compute(self, cr, uid, ids, field_names, arg, context):
1072 _columns['name'] = fields.function(compute_person_data, type='char',\
1073 size=50, multi='person_data')
1074 _columns[''age'] = fields.function(compute_person_data, type='integer',\
1075 multi='person_data')
1077 # when called with ``ids=[1,2,3]``, ``compute_person_data`` could return:
1079 1: {'name': 'Bob', 'age': 23},
1080 2: {'name': 'Sally', 'age': 19},
1081 3: {'name': 'unknown', 'age': False}
1084 .. _field-function-fnct-inv:
1086 .. rubric:: The ``fnct_inv`` parameter
1088 This callable implements the write operation for the function field
1089 and must have the following signature:
1091 .. function:: fnct_inv(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context)
1093 Callable that implements the ``write`` operation for the function field.
1095 :param orm model: model to which the field belongs (should be ``self`` for
1097 :param int id: the identifier of the object to write on
1098 :param str field_name: name of the field to set
1099 :param fnct_inv_arg: arbitrary value passed when declaring the function field
1102 When writing values for a function field, the ``multi`` parameter is ignored.
1104 .. _field-function-fnct-search:
1106 .. rubric:: The ``fnct_search`` parameter
1108 This callable implements the search operation for the function field
1109 and must have the following signature:
1111 .. function:: fnct_search(model, cr, uid, model_again, field_name, criterion, context)
1113 Callable that implements the ``search`` operation for the function field by expanding
1114 a search criterion based on the function field into a new domain based only on
1115 columns that are stored in the database.
1117 :param orm model: model to which the field belongs (should be ``self`` for
1119 :param orm model_again: same value as ``model`` (seriously! this is for backwards
1121 :param str field_name: name of the field to search on
1122 :param list criterion: domain component specifying the search criterion on the field.
1124 :return: domain to use instead of ``criterion`` when performing the search.
1125 This new domain must be based only on columns stored in the database, as it
1126 will be used directly without any translation.
1128 The returned value must be a domain, that is, a list of the form [(field_name, operator, operand)].
1129 The most generic way to implement ``fnct_search`` is to directly search for the records that
1130 match the given ``criterion``, and return their ``ids`` wrapped in a domain, such as
1131 ``[('id','in',[1,3,5])]``.
1133 .. _field-function-store:
1135 .. rubric:: The ``store`` parameter
1137 The ``store`` parameter allows caching the result of the field computation in the
1138 database, and defining the triggers that will invalidate that cache and force a
1139 recomputation of the function field.
1140 When not provided, the field is computed every time its value is read.
1141 The value of ``store`` may be either ``True`` (to recompute the field value whenever
1142 any field in the same record is modified), or a dictionary specifying a more
1143 flexible set of recomputation triggers.
1145 A trigger specification is a dictionary that maps the names of the models that
1146 will trigger the computation, to a tuple describing the trigger rule, in the
1150 'trigger_model': (mapping_function,
1151 ['trigger_field1', 'trigger_field2'],
1155 A trigger rule is defined by a 3-item tuple where:
1157 * The ``mapping_function`` is defined as follows:
1159 .. function:: mapping_function(trigger_model, cr, uid, trigger_ids, context)
1161 Callable that maps record ids of a trigger model to ids of the
1162 corresponding records in the source model (whose field values
1163 need to be recomputed).
1165 :param orm model: trigger_model
1166 :param list trigger_ids: ids of the records of trigger_model that were
1169 :return: list of ids of the source model whose function field values
1170 need to be recomputed
1172 * The second item is a list of the fields who should act as triggers for
1173 the computation. If an empty list is given, all fields will act as triggers.
1174 * The last item is the priority, used to order the triggers when processing them
1175 after any write operation on a model that has function field triggers. The
1176 default priority is 10.
1178 In fact, setting store = True is the same as using the following trigger dict::
1181 'model_itself': (lambda self, cr, uid, ids, context: ids,
1187 _classic_read = False
1188 _classic_write = False
1193 # function fields are not copied by default
1197 # multi: compute several fields in one call
1199 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):
1200 _column.__init__(self, **args)
1203 self._fnct_inv = fnct_inv
1206 if 'relation' in args:
1207 self._obj = args['relation']
1209 self.digits = args.get('digits', (16,2))
1210 self.digits_compute = args.get('digits_compute', None)
1211 if callable(args.get('selection')):
1212 from openerp import api
1213 self.selection = api.expected(api.cr_uid_context, args['selection'])
1215 self._fnct_inv_arg = fnct_inv_arg
1219 self._fnct_search = fnct_search
1222 if not fnct_search and not store:
1223 self.selectable = False
1226 if self._type != 'many2one':
1227 # m2o fields need to return tuples with name_get, not just foreign keys
1228 self._classic_read = True
1229 self._classic_write = True
1231 self._symbol_get=lambda x:x and str(x)
1233 self._prefetch = True
1236 self._symbol_c = char._symbol_c
1237 self._symbol_f = lambda x: _symbol_set_char(self, x)
1238 self._symbol_set = (self._symbol_c, self._symbol_f)
1240 type_class = globals().get(type)
1241 if type_class is not None:
1242 self._symbol_c = type_class._symbol_c
1243 self._symbol_f = type_class._symbol_f
1244 self._symbol_set = type_class._symbol_set
1246 def to_field_args(self):
1247 args = super(function, self).to_field_args()
1248 if self._type in ('float',):
1249 args['digits'] = self.digits_compute or self.digits
1250 elif self._type in ('selection', 'reference'):
1251 args['selection'] = self.selection
1252 elif self._type in ('many2one', 'one2many', 'many2many'):
1253 args['comodel_name'] = self._obj
1256 def digits_change(self, cr):
1257 if self._type == 'float':
1258 if self.digits_compute:
1259 self.digits = self.digits_compute(cr)
1261 precision, scale = self.digits
1262 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
1263 precision_digits=scale),
1264 precision_digits=scale))
1266 def search(self, cr, uid, obj, name, args, context=None):
1267 if not self._fnct_search:
1268 #CHECKME: should raise an exception
1270 return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
1272 def postprocess(self, cr, uid, obj, field, value=None, context=None):
1273 return self._postprocess_batch(cr, uid, obj, field, {0: value}, context=context)[0]
1275 def _postprocess_batch(self, cr, uid, obj, field, values, context=None):
1282 field_type = obj._columns[field]._type
1283 new_values = dict(values)
1285 if field_type == 'binary':
1286 if context.get('bin_size'):
1287 # client requests only the size of binary fields
1288 for rid, value in values.iteritems():
1290 new_values[rid] = get_nice_size(value)
1291 elif not context.get('bin_raw'):
1292 for rid, value in values.iteritems():
1294 new_values[rid] = sanitize_binary_value(value)
1298 def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
1300 # if we already have a value, don't recompute it.
1301 # This happen if case of stored many2one fields
1302 if values and not multi and name in values[0]:
1303 result = dict((v['id'], v[name]) for v in values)
1304 elif values and multi and all(n in values[0] for n in name):
1305 result = dict((v['id'], dict((n, v[n]) for n in name)) for v in values)
1307 result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
1310 for rid, values in result.iteritems():
1311 for f, v in values.iteritems():
1314 swap.setdefault(f, {})[rid] = v
1316 for field, values in swap.iteritems():
1317 new_values = self._postprocess_batch(cr, uid, obj, field, values, context)
1318 for rid, value in new_values.iteritems():
1319 result[rid][field] = value
1322 result = self._postprocess_batch(cr, uid, obj, name, result, context)
1326 def set(self, cr, obj, id, name, value, user=None, context=None):
1330 self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
1333 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
1334 # Function fields are supposed to emulate a basic field type,
1335 # so they can delegate to the basic type for record name rendering
1336 return globals()[field._type]._as_display_name(field, cr, uid, obj, value, context=context)
1338 # ---------------------------------------------------------
1340 # ---------------------------------------------------------
1342 class related(function):
1343 """Field that points to some data inside another field of the current record.
1348 'foo_id': fields.many2one('my.foo', 'Foo'),
1349 'bar': fields.related('foo_id', 'frol', type='char', string='Frol of Foo'),
1353 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1354 # assume self._arg = ('foo', 'bar', 'baz')
1355 # domain = [(name, op, val)] => search [('foo.bar.baz', op, val)]
1356 field = '.'.join(self._arg)
1357 return map(lambda x: (field, x[1], x[2]), domain)
1359 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1360 if isinstance(ids, (int, long)):
1362 for instance in obj.browse(cr, uid, ids, context=context):
1363 # traverse all fields except the last one
1364 for field in self.arg[:-1]:
1365 instance = instance[field][:1]
1367 # write on the last field of the target record
1368 instance.write({self.arg[-1]: values})
1370 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1372 for record in obj.browse(cr, SUPERUSER_ID, ids, context=context):
1374 # traverse all fields except the last one
1375 for field in self.arg[:-1]:
1376 value = value[field][:1]
1377 # read the last field on the target record
1378 res[record.id] = value[self.arg[-1]]
1380 if self._type == 'many2one':
1381 # res[id] is a recordset; convert it to (id, name) or False.
1382 # Perform name_get as root, as seeing the name of a related object depends on
1383 # access right of source document, not target, so user may not have access.
1384 value_ids = list(set(value.id for value in res.itervalues() if value))
1385 value_name = dict(obj.pool[self._obj].name_get(cr, SUPERUSER_ID, value_ids, context=context))
1386 res = dict((id, bool(value) and (value.id, value_name[value.id])) for id, value in res.iteritems())
1388 elif self._type in ('one2many', 'many2many'):
1389 # res[id] is a recordset; convert it to a list of ids
1390 res = dict((id, value.ids) for id, value in res.iteritems())
1394 def __init__(self, *arg, **args):
1396 self._relations = []
1397 super(related, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1398 if self.store is True:
1399 # TODO: improve here to change self.store = {...} according to related objects
1403 class sparse(function):
1405 def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
1407 + For a many2many field, a list of tuples is expected.
1408 Here is the list of tuple that are accepted, with the corresponding semantics ::
1410 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1411 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1412 (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)
1413 (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)
1414 (4, ID) link to existing record with id = ID (adds a relationship)
1415 (5) unlink all (like using (3,ID) for all linked records)
1416 (6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
1419 [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
1421 + For a one2many field, a lits of tuples is expected.
1422 Here is the list of tuple that are accepted, with the corresponding semantics ::
1424 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1425 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1426 (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)
1429 [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
1432 if self._type == 'many2many':
1433 assert value[0][0] == 6, 'Unsupported m2m value for sparse field: %s' % value
1436 elif self._type == 'one2many':
1439 relation_obj = obj.pool[self.relation]
1441 assert vals[0] in (0,1,2), 'Unsupported o2m value for sparse field: %s' % vals
1443 read_value.append(relation_obj.create(cr, uid, vals[2], context=context))
1445 relation_obj.write(cr, uid, vals[1], vals[2], context=context)
1447 relation_obj.unlink(cr, uid, vals[1], context=context)
1448 read_value.remove(vals[1])
1453 def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
1454 if not type(ids) == list:
1456 records = obj.browse(cr, uid, ids, context=context)
1457 for record in records:
1458 # grab serialized value as object - already deserialized
1459 serialized = getattr(record, self.serialization_field)
1461 # simply delete the key to unset it.
1462 serialized.pop(field_name, None)
1464 serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context)
1465 obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context)
1468 def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None):
1470 records = obj.browse(cr, uid, ids, context=context)
1471 for record in records:
1472 # grab serialized value as object - already deserialized
1473 serialized = getattr(record, self.serialization_field)
1474 results[record.id] = {}
1475 for field_name in field_names:
1476 field_type = obj._columns[field_name]._type
1477 value = serialized.get(field_name, False)
1478 if field_type in ('one2many','many2many'):
1481 # filter out deleted records as superuser
1482 relation_obj = obj.pool[obj._columns[field_name].relation]
1483 value = relation_obj.exists(cr, openerp.SUPERUSER_ID, value)
1484 if type(value) in (int,long) and field_type == 'many2one':
1485 relation_obj = obj.pool[obj._columns[field_name].relation]
1486 # check for deleted record as superuser
1487 if not relation_obj.exists(cr, openerp.SUPERUSER_ID, [value]):
1489 results[record.id][field_name] = value
1492 def __init__(self, serialization_field, **kwargs):
1493 self.serialization_field = serialization_field
1494 super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
1498 # ---------------------------------------------------------
1500 # ---------------------------------------------------------
1502 class dummy(function):
1503 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1506 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1509 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1512 def __init__(self, *arg, **args):
1514 self._relations = []
1515 super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1517 # ---------------------------------------------------------
1519 # ---------------------------------------------------------
1521 class serialized(_column):
1522 """ A field able to store an arbitrary python data structure.
1524 Note: only plain components allowed.
1527 def _symbol_set_struct(val):
1528 return simplejson.dumps(val)
1530 def _symbol_get_struct(self, val):
1531 return simplejson.loads(val or '{}')
1534 _type = 'serialized'
1537 _symbol_f = _symbol_set_struct
1538 _symbol_set = (_symbol_c, _symbol_f)
1539 _symbol_get = _symbol_get_struct
1541 # TODO: review completly this class for speed improvement
1542 class property(function):
1544 def to_field_args(self):
1545 args = super(property, self).to_field_args()
1546 args['company_dependent'] = True
1549 def _fnct_search(self, tobj, cr, uid, obj, name, domain, context=None):
1550 ir_property = obj.pool['ir.property']
1552 for field, operator, value in domain:
1553 result += ir_property.search_multi(cr, uid, name, tobj._name, operator, value, context=context)
1556 def _fnct_write(self, obj, cr, uid, id, prop_name, value, obj_dest, context=None):
1557 ir_property = obj.pool['ir.property']
1558 ir_property.set_multi(cr, uid, prop_name, obj._name, {id: value}, context=context)
1561 def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
1562 ir_property = obj.pool['ir.property']
1564 res = {id: {} for id in ids}
1565 for prop_name in prop_names:
1566 column = obj._all_columns[prop_name].column
1567 values = ir_property.get_multi(cr, uid, prop_name, obj._name, ids, context=context)
1568 if column._type == 'many2one':
1569 # name_get the non-null values as SUPERUSER_ID
1570 vals = sum(set(filter(None, values.itervalues())),
1571 obj.pool[column._obj].browse(cr, uid, [], context=context))
1572 vals_name = dict(vals.sudo().name_get()) if vals else {}
1573 for id, value in values.iteritems():
1575 if value and value.id in vals_name:
1576 ng = value.id, vals_name[value.id]
1577 res[id][prop_name] = ng
1579 for id, value in values.iteritems():
1580 res[id][prop_name] = value
1584 def __init__(self, **args):
1585 if 'view_load' in args:
1586 _logger.warning("view_load attribute is deprecated on ir.fields. Args: %r", args)
1587 obj = 'relation' in args and args['relation'] or ''
1588 super(property, self).__init__(
1589 fnct=self._fnct_read,
1590 fnct_inv=self._fnct_write,
1591 fnct_search=self._fnct_search,
1598 class column_info(object):
1599 """ Struct containing details about an osv column, either one local to
1600 its model, or one inherited via _inherits.
1606 .. attribute:: column
1608 column instance, subclass of :class:`_column`
1610 .. attribute:: parent_model
1612 if the column is inherited, name of the model that contains it,
1613 ``None`` for local columns.
1615 .. attribute:: parent_column
1617 the name of the column containing the m2o relationship to the
1618 parent model that contains this column, ``None`` for local columns.
1620 .. attribute:: original_parent
1622 if the column is inherited, name of the original parent model that
1623 contains it i.e in case of multilevel inheritance, ``None`` for
1626 def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
1628 self.column = column
1629 self.parent_model = parent_model
1630 self.parent_column = parent_column
1631 self.original_parent = original_parent
1634 return '%s(%s, %s, %s, %s, %s)' % (
1635 self.__class__.__name__, self.name, self.column,
1636 self.parent_model, self.parent_column, self.original_parent)
1639 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: