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 ('column', 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 # retrieve the records in the comodel
675 comodel = obj.pool[self._obj].browse(cr, user, [], context)
676 inverse = self._fields_id
677 domain = self._domain(obj) if callable(self._domain) else self._domain
678 domain = domain + [(inverse, 'in', ids)]
679 records = comodel.search(domain, limit=self._limit)
681 result = {id: [] for id in ids}
682 # read the inverse of records without prefetching other fields on them
683 for record in records.with_context(prefetch_fields=False):
684 # record[inverse] may be a record or an integer
685 result[int(record[inverse])].append(record.id)
689 def set(self, cr, obj, id, field, values, user=None, context=None):
691 context = dict(context or {})
692 context.update(self._context)
693 context['recompute'] = False # recomputation is done by outer create/write
696 obj = obj.pool[self._obj]
700 act[2][self._fields_id] = id
701 id_new = obj.create(cr, user, act[2], context=context)
702 result += obj._store_get_values(cr, user, [id_new], act[2].keys(), context)
704 obj.write(cr, user, [act[1]], act[2], context=context)
706 obj.unlink(cr, user, [act[1]], context=context)
708 reverse_rel = obj._all_columns.get(self._fields_id)
709 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
710 # if the model has on delete cascade, just delete the row
711 if reverse_rel.column.ondelete == "cascade":
712 obj.unlink(cr, user, [act[1]], context=context)
714 cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],))
716 # table of the field (parent_model in case of inherit)
717 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
718 field_table = obj.pool[field_model]._table
719 cr.execute("select 1 from {0} where id=%s and {1}=%s".format(field_table, self._fields_id), (act[1], id))
720 if not cr.fetchone():
721 # Must use write() to recompute parent_store structure if needed and check access rules
722 obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
724 reverse_rel = obj._all_columns.get(self._fields_id)
725 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
726 # if the o2m has a static domain we must respect it when unlinking
727 domain = self._domain(obj) if callable(self._domain) else self._domain
728 extra_domain = domain or []
729 ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
730 # If the model has cascade deletion, we delete the rows because it is the intended behavior,
731 # otherwise we only nullify the reverse foreign key column.
732 if reverse_rel.column.ondelete == "cascade":
733 obj.unlink(cr, user, ids_to_unlink, context=context)
735 obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context)
737 # Must use write() to recompute parent_store structure if needed
738 obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
740 cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
741 ids3 = map(lambda x:x[0], cr.fetchall())
742 obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
745 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
746 domain = self._domain(obj) if callable(self._domain) else self._domain
747 return obj.pool[self._obj].name_search(cr, uid, value, domain, operator, context=context,limit=limit)
750 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
751 raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)')
754 # Values: (0, 0, { fields }) create
755 # (1, ID, { fields }) update (write fields to ID)
756 # (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete)
757 # (3, ID) unlink (delete the relationship between the two objects but does not delete ID)
758 # (4, ID) link (add a relationship)
760 # (6, ?, ids) set a list of links
762 class many2many(_column):
763 """Encapsulates the logic of a many-to-many bidirectional relationship, handling the
764 low-level details of the intermediary relationship table transparently.
765 A many-to-many relationship is always symmetrical, and can be declared and accessed
766 from either endpoint model.
767 If ``rel`` (relationship table name), ``id1`` (source foreign key column name)
768 or id2 (destination foreign key column name) are not specified, the system will
769 provide default values. This will by default only allow one single symmetrical
770 many-to-many relationship between the source and destination model.
771 For multiple many-to-many relationship between the same models and for
772 relationships where source and destination models are the same, ``rel``, ``id1``
773 and ``id2`` should be specified explicitly.
775 :param str obj: destination model
776 :param str rel: optional name of the intermediary relationship table. If not specified,
777 a canonical name will be derived based on the alphabetically-ordered
778 model names of the source and destination (in the form: ``amodel_bmodel_rel``).
779 Automatic naming is not possible when the source and destination are
780 the same, for obvious ambiguity reasons.
781 :param str id1: optional name for the column holding the foreign key to the current
782 model in the relationship table. If not specified, a canonical name
783 will be derived based on the model name (in the form: `src_model_id`).
784 :param str id2: optional name for the column holding the foreign key to the destination
785 model in the relationship table. If not specified, a canonical name
786 will be derived based on the model name (in the form: `dest_model_id`)
787 :param str string: field label
789 _classic_read = False
790 _classic_write = False
794 def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
797 _column.__init__(self, string=string, **args)
799 if rel and '.' in rel:
800 raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\
801 'You used %s, which is not a valid SQL table name.')% (string,rel))
807 def to_field_args(self):
808 args = super(many2many, self).to_field_args()
809 args['comodel_name'] = self._obj
810 args['relation'] = self._rel
811 args['column1'] = self._id1
812 args['column2'] = self._id2
813 args['limit'] = self._limit
816 def _sql_names(self, source_model):
817 """Return the SQL names defining the structure of the m2m relationship table
819 :return: (m2m_table, local_col, dest_col) where m2m_table is the table name,
820 local_col is the name of the column holding the current model's FK, and
821 dest_col is the name of the column holding the destination model's FK, and
823 tbl, col1, col2 = self._rel, self._id1, self._id2
824 if not all((tbl, col1, col2)):
825 # the default table name is based on the stable alphabetical order of tables
826 dest_model = source_model.pool[self._obj]
827 tables = tuple(sorted([source_model._table, dest_model._table]))
829 assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\
830 'is not possible when source and destination models are '\
832 tbl = '%s_%s_rel' % tables
834 col1 = '%s_id' % source_model._table
836 col2 = '%s_id' % dest_model._table
837 return tbl, col1, col2
839 def _get_query_and_where_params(self, cr, model, ids, values, where_params):
840 """ Extracted from ``get`` to facilitate fine-tuning of the generated
842 query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
843 FROM %(rel)s, %(from_c)s \
844 WHERE %(rel)s.%(id1)s IN %%s \
845 AND %(rel)s.%(id2)s = %(tbl)s.id \
851 return query, where_params
853 def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
865 "Specifying offset at a many2many.get() is deprecated and may"
866 " produce unpredictable results.")
867 obj = model.pool[self._obj]
868 rel, id1, id2 = self._sql_names(model)
870 # static domains are lists, and are evaluated both here and on client-side, while string
871 # domains supposed by dynamic and evaluated on client-side only (thus ignored here)
872 # FIXME: make this distinction explicit in API!
873 domain = isinstance(self._domain, list) and self._domain or []
875 wquery = obj._where_calc(cr, user, domain, context=context)
876 obj._apply_ir_rules(cr, user, wquery, 'read', context=context)
877 from_c, where_c, where_params = wquery.get_sql()
879 where_c = ' AND ' + where_c
881 order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0])
884 if self._limit is not None:
885 limit_str = ' LIMIT %d' % self._limit
887 query, where_params = self._get_query_and_where_params(cr, model, ids, {'rel': rel,
894 'order_by': order_by,
898 cr.execute(query, [tuple(ids),] + where_params)
899 for r in cr.fetchall():
900 res[r[1]].append(r[0])
903 def set(self, cr, model, id, name, values, user=None, context=None):
908 rel, id1, id2 = self._sql_names(model)
909 obj = model.pool[self._obj]
911 if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
914 idnew = obj.create(cr, user, act[2], context=context)
915 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew))
917 obj.write(cr, user, [act[1]], act[2], context=context)
919 obj.unlink(cr, user, [act[1]], context=context)
921 cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1]))
923 # following queries are in the same transaction - so should be relatively safe
924 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1]))
925 if not cr.fetchone():
926 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1]))
928 cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,))
931 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
933 d1 = ' and ' + ' and '.join(d1)
936 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)
938 for act_nbr in act[2]:
939 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr))
942 # TODO: use a name_search
944 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
945 return obj.pool[self._obj].search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
948 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
949 raise NotImplementedError('Many2Many columns should not be used as record name (_rec_name)')
952 def get_nice_size(value):
954 if isinstance(value, (int,long)):
956 elif value: # this is supposed to be a string
958 return tools.human_size(size)
960 # See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
961 # and http://bugs.python.org/issue10066
962 invalid_xml_low_bytes = re.compile(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
964 def sanitize_binary_value(value):
965 # binary fields should be 7-bit ASCII base64-encoded data,
966 # but we do additional sanity checks to make sure the values
967 # are not something else that won't pass via XML-RPC
968 if isinstance(value, (xmlrpclib.Binary, tuple, list, dict)):
969 # these builtin types are meant to pass untouched
972 # Handle invalid bytes values that will cause problems
973 # for XML-RPC. See for more info:
974 # - http://bugs.python.org/issue10066
975 # - http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
977 # Coercing to unicode would normally allow it to properly pass via
978 # XML-RPC, transparently encoded as UTF-8 by xmlrpclib.
979 # (this works for _any_ byte values, thanks to the fallback
980 # to latin-1 passthrough encoding when decoding to unicode)
981 value = tools.ustr(value)
983 # Due to Python bug #10066 this could still yield invalid XML
984 # bytes, specifically in the low byte range, that will crash
985 # the decoding side: [\x00-\x08\x0b-\x0c\x0e-\x1f]
986 # So check for low bytes values, and if any, perform
987 # base64 encoding - not very smart or useful, but this is
988 # our last resort to avoid crashing the request.
989 if invalid_xml_low_bytes.search(value):
990 # b64-encode after restoring the pure bytes with latin-1
991 # passthrough encoding
992 value = base64.b64encode(value.encode('latin-1'))
997 # ---------------------------------------------------------
999 # ---------------------------------------------------------
1000 class function(_column):
1002 A field whose value is computed by a function (rather
1003 than being read from the database).
1005 :param fnct: the callable that will compute the field value.
1006 :param arg: arbitrary value to be passed to ``fnct`` when computing the value.
1007 :param fnct_inv: the callable that will allow writing values in that field
1008 (if not provided, the field is read-only).
1009 :param fnct_inv_arg: arbitrary value to be passed to ``fnct_inv`` when
1011 :param str type: type of the field simulated by the function field
1012 :param fnct_search: the callable that allows searching on the field
1013 (if not provided, search will not return any result).
1014 :param store: store computed value in database
1015 (see :ref:`The *store* parameter <field-function-store>`).
1016 :type store: True or dict specifying triggers for field computation
1017 :param multi: name of batch for batch computation of function fields.
1018 All fields with the same batch name will be computed by
1019 a single function call. This changes the signature of the
1022 .. _field-function-fnct: The ``fnct`` parameter
1024 .. rubric:: The ``fnct`` parameter
1026 The callable implementing the function field must have the following signature:
1028 .. function:: fnct(model, cr, uid, ids, field_name(s), arg, context)
1030 Implements the function field.
1032 :param orm model: model to which the field belongs (should be ``self`` for
1034 :param field_name(s): name of the field to compute, or if ``multi`` is provided,
1035 list of field names to compute.
1036 :type field_name(s): str | [str]
1037 :param arg: arbitrary value passed when declaring the function field
1039 :return: mapping of ``ids`` to computed values, or if multi is provided,
1040 to a map of field_names to computed values
1042 The values in the returned dictionary must be of the type specified by the type
1043 argument in the field declaration.
1045 Here is an example with a simple function ``char`` function field::
1048 def compute(self, cr, uid, ids, field_name, arg, context):
1052 _columns['my_char'] = fields.function(compute, type='char', size=50)
1054 # when called with ``ids=[1,2,3]``, ``compute`` could return:
1058 3: False # null values should be returned explicitly too
1061 If ``multi`` is set, then ``field_name`` is replaced by ``field_names``: a list
1062 of the field names that should be computed. Each value in the returned
1063 dictionary must then be a dictionary mapping field names to values.
1065 Here is an example where two function fields (``name`` and ``age``)
1066 are both computed by a single function field::
1069 def compute(self, cr, uid, ids, field_names, arg, context):
1073 _columns['name'] = fields.function(compute_person_data, type='char',\
1074 size=50, multi='person_data')
1075 _columns[''age'] = fields.function(compute_person_data, type='integer',\
1076 multi='person_data')
1078 # when called with ``ids=[1,2,3]``, ``compute_person_data`` could return:
1080 1: {'name': 'Bob', 'age': 23},
1081 2: {'name': 'Sally', 'age': 19},
1082 3: {'name': 'unknown', 'age': False}
1085 .. _field-function-fnct-inv:
1087 .. rubric:: The ``fnct_inv`` parameter
1089 This callable implements the write operation for the function field
1090 and must have the following signature:
1092 .. function:: fnct_inv(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context)
1094 Callable that implements the ``write`` operation for the function field.
1096 :param orm model: model to which the field belongs (should be ``self`` for
1098 :param int id: the identifier of the object to write on
1099 :param str field_name: name of the field to set
1100 :param fnct_inv_arg: arbitrary value passed when declaring the function field
1103 When writing values for a function field, the ``multi`` parameter is ignored.
1105 .. _field-function-fnct-search:
1107 .. rubric:: The ``fnct_search`` parameter
1109 This callable implements the search operation for the function field
1110 and must have the following signature:
1112 .. function:: fnct_search(model, cr, uid, model_again, field_name, criterion, context)
1114 Callable that implements the ``search`` operation for the function field by expanding
1115 a search criterion based on the function field into a new domain based only on
1116 columns that are stored in the database.
1118 :param orm model: model to which the field belongs (should be ``self`` for
1120 :param orm model_again: same value as ``model`` (seriously! this is for backwards
1122 :param str field_name: name of the field to search on
1123 :param list criterion: domain component specifying the search criterion on the field.
1125 :return: domain to use instead of ``criterion`` when performing the search.
1126 This new domain must be based only on columns stored in the database, as it
1127 will be used directly without any translation.
1129 The returned value must be a domain, that is, a list of the form [(field_name, operator, operand)].
1130 The most generic way to implement ``fnct_search`` is to directly search for the records that
1131 match the given ``criterion``, and return their ``ids`` wrapped in a domain, such as
1132 ``[('id','in',[1,3,5])]``.
1134 .. _field-function-store:
1136 .. rubric:: The ``store`` parameter
1138 The ``store`` parameter allows caching the result of the field computation in the
1139 database, and defining the triggers that will invalidate that cache and force a
1140 recomputation of the function field.
1141 When not provided, the field is computed every time its value is read.
1142 The value of ``store`` may be either ``True`` (to recompute the field value whenever
1143 any field in the same record is modified), or a dictionary specifying a more
1144 flexible set of recomputation triggers.
1146 A trigger specification is a dictionary that maps the names of the models that
1147 will trigger the computation, to a tuple describing the trigger rule, in the
1151 'trigger_model': (mapping_function,
1152 ['trigger_field1', 'trigger_field2'],
1156 A trigger rule is defined by a 3-item tuple where:
1158 * The ``mapping_function`` is defined as follows:
1160 .. function:: mapping_function(trigger_model, cr, uid, trigger_ids, context)
1162 Callable that maps record ids of a trigger model to ids of the
1163 corresponding records in the source model (whose field values
1164 need to be recomputed).
1166 :param orm model: trigger_model
1167 :param list trigger_ids: ids of the records of trigger_model that were
1170 :return: list of ids of the source model whose function field values
1171 need to be recomputed
1173 * The second item is a list of the fields who should act as triggers for
1174 the computation. If an empty list is given, all fields will act as triggers.
1175 * The last item is the priority, used to order the triggers when processing them
1176 after any write operation on a model that has function field triggers. The
1177 default priority is 10.
1179 In fact, setting store = True is the same as using the following trigger dict::
1182 'model_itself': (lambda self, cr, uid, ids, context: ids,
1188 _classic_read = False
1189 _classic_write = False
1194 # function fields are not copied by default
1198 # multi: compute several fields in one call
1200 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):
1201 _column.__init__(self, **args)
1204 self._fnct_inv = fnct_inv
1207 if 'relation' in args:
1208 self._obj = args['relation']
1210 self.digits = args.get('digits', (16,2))
1211 self.digits_compute = args.get('digits_compute', None)
1212 if callable(args.get('selection')):
1213 from openerp import api
1214 self.selection = api.expected(api.cr_uid_context, args['selection'])
1216 self._fnct_inv_arg = fnct_inv_arg
1220 self._fnct_search = fnct_search
1223 if not fnct_search and not store:
1224 self.selectable = False
1227 if self._type != 'many2one':
1228 # m2o fields need to return tuples with name_get, not just foreign keys
1229 self._classic_read = True
1230 self._classic_write = True
1232 self._symbol_get=lambda x:x and str(x)
1234 self._prefetch = True
1237 self._symbol_c = char._symbol_c
1238 self._symbol_f = lambda x: _symbol_set_char(self, x)
1239 self._symbol_set = (self._symbol_c, self._symbol_f)
1241 type_class = globals().get(type)
1242 if type_class is not None:
1243 self._symbol_c = type_class._symbol_c
1244 self._symbol_f = type_class._symbol_f
1245 self._symbol_set = type_class._symbol_set
1247 def to_field_args(self):
1248 args = super(function, self).to_field_args()
1249 if self._type in ('float',):
1250 args['digits'] = self.digits_compute or self.digits
1251 elif self._type in ('selection', 'reference'):
1252 args['selection'] = self.selection
1253 elif self._type in ('many2one', 'one2many', 'many2many'):
1254 args['comodel_name'] = self._obj
1257 def digits_change(self, cr):
1258 if self._type == 'float':
1259 if self.digits_compute:
1260 self.digits = self.digits_compute(cr)
1262 precision, scale = self.digits
1263 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
1264 precision_digits=scale),
1265 precision_digits=scale))
1267 def search(self, cr, uid, obj, name, args, context=None):
1268 if not self._fnct_search:
1269 #CHECKME: should raise an exception
1271 return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
1273 def postprocess(self, cr, uid, obj, field, value=None, context=None):
1274 return self._postprocess_batch(cr, uid, obj, field, {0: value}, context=context)[0]
1276 def _postprocess_batch(self, cr, uid, obj, field, values, context=None):
1283 field_type = obj._columns[field]._type
1284 new_values = dict(values)
1286 if field_type == 'binary':
1287 if context.get('bin_size'):
1288 # client requests only the size of binary fields
1289 for rid, value in values.iteritems():
1291 new_values[rid] = get_nice_size(value)
1292 elif not context.get('bin_raw'):
1293 for rid, value in values.iteritems():
1295 new_values[rid] = sanitize_binary_value(value)
1299 def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
1301 # if we already have a value, don't recompute it.
1302 # This happen if case of stored many2one fields
1303 if values and not multi and name in values[0]:
1304 result = dict((v['id'], v[name]) for v in values)
1305 elif values and multi and all(n in values[0] for n in name):
1306 result = dict((v['id'], dict((n, v[n]) for n in name)) for v in values)
1308 result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
1311 for rid, values in result.iteritems():
1312 for f, v in values.iteritems():
1315 swap.setdefault(f, {})[rid] = v
1317 for field, values in swap.iteritems():
1318 new_values = self._postprocess_batch(cr, uid, obj, field, values, context)
1319 for rid, value in new_values.iteritems():
1320 result[rid][field] = value
1323 result = self._postprocess_batch(cr, uid, obj, name, result, context)
1327 def set(self, cr, obj, id, name, value, user=None, context=None):
1331 self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
1334 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
1335 # Function fields are supposed to emulate a basic field type,
1336 # so they can delegate to the basic type for record name rendering
1337 return globals()[field._type]._as_display_name(field, cr, uid, obj, value, context=context)
1339 # ---------------------------------------------------------
1341 # ---------------------------------------------------------
1343 class related(function):
1344 """Field that points to some data inside another field of the current record.
1349 'foo_id': fields.many2one('my.foo', 'Foo'),
1350 'bar': fields.related('foo_id', 'frol', type='char', string='Frol of Foo'),
1354 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1355 # assume self._arg = ('foo', 'bar', 'baz')
1356 # domain = [(name, op, val)] => search [('foo.bar.baz', op, val)]
1357 field = '.'.join(self._arg)
1358 return map(lambda x: (field, x[1], x[2]), domain)
1360 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1361 if isinstance(ids, (int, long)):
1363 for instance in obj.browse(cr, uid, ids, context=context):
1364 # traverse all fields except the last one
1365 for field in self.arg[:-1]:
1366 instance = instance[field][:1]
1368 # write on the last field of the target record
1369 instance.write({self.arg[-1]: values})
1371 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1373 for record in obj.browse(cr, SUPERUSER_ID, ids, context=context):
1375 # traverse all fields except the last one
1376 for field in self.arg[:-1]:
1377 value = value[field][:1]
1378 # read the last field on the target record
1379 res[record.id] = value[self.arg[-1]]
1381 if self._type == 'many2one':
1382 # res[id] is a recordset; convert it to (id, name) or False.
1383 # Perform name_get as root, as seeing the name of a related object depends on
1384 # access right of source document, not target, so user may not have access.
1385 value_ids = list(set(value.id for value in res.itervalues() if value))
1386 value_name = dict(obj.pool[self._obj].name_get(cr, SUPERUSER_ID, value_ids, context=context))
1387 res = dict((id, bool(value) and (value.id, value_name[value.id])) for id, value in res.iteritems())
1389 elif self._type in ('one2many', 'many2many'):
1390 # res[id] is a recordset; convert it to a list of ids
1391 res = dict((id, value.ids) for id, value in res.iteritems())
1395 def __init__(self, *arg, **args):
1397 self._relations = []
1398 super(related, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1399 if self.store is True:
1400 # TODO: improve here to change self.store = {...} according to related objects
1404 class sparse(function):
1406 def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
1408 + For a many2many field, a list of tuples is expected.
1409 Here is the list of tuple that are accepted, with the corresponding semantics ::
1411 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1412 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1413 (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)
1414 (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)
1415 (4, ID) link to existing record with id = ID (adds a relationship)
1416 (5) unlink all (like using (3,ID) for all linked records)
1417 (6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
1420 [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
1422 + For a one2many field, a lits of tuples is expected.
1423 Here is the list of tuple that are accepted, with the corresponding semantics ::
1425 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1426 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1427 (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)
1430 [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
1433 if self._type == 'many2many':
1434 assert value[0][0] == 6, 'Unsupported m2m value for sparse field: %s' % value
1437 elif self._type == 'one2many':
1440 relation_obj = obj.pool[self.relation]
1442 assert vals[0] in (0,1,2), 'Unsupported o2m value for sparse field: %s' % vals
1444 read_value.append(relation_obj.create(cr, uid, vals[2], context=context))
1446 relation_obj.write(cr, uid, vals[1], vals[2], context=context)
1448 relation_obj.unlink(cr, uid, vals[1], context=context)
1449 read_value.remove(vals[1])
1454 def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
1455 if not type(ids) == list:
1457 records = obj.browse(cr, uid, ids, context=context)
1458 for record in records:
1459 # grab serialized value as object - already deserialized
1460 serialized = getattr(record, self.serialization_field)
1462 # simply delete the key to unset it.
1463 serialized.pop(field_name, None)
1465 serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context)
1466 obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context)
1469 def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None):
1471 records = obj.browse(cr, uid, ids, context=context)
1472 for record in records:
1473 # grab serialized value as object - already deserialized
1474 serialized = getattr(record, self.serialization_field)
1475 results[record.id] = {}
1476 for field_name in field_names:
1477 field_type = obj._columns[field_name]._type
1478 value = serialized.get(field_name, False)
1479 if field_type in ('one2many','many2many'):
1482 # filter out deleted records as superuser
1483 relation_obj = obj.pool[obj._columns[field_name].relation]
1484 value = relation_obj.exists(cr, openerp.SUPERUSER_ID, value)
1485 if type(value) in (int,long) and field_type == 'many2one':
1486 relation_obj = obj.pool[obj._columns[field_name].relation]
1487 # check for deleted record as superuser
1488 if not relation_obj.exists(cr, openerp.SUPERUSER_ID, [value]):
1490 results[record.id][field_name] = value
1493 def __init__(self, serialization_field, **kwargs):
1494 self.serialization_field = serialization_field
1495 super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
1499 # ---------------------------------------------------------
1501 # ---------------------------------------------------------
1503 class dummy(function):
1504 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1507 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1510 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1513 def __init__(self, *arg, **args):
1515 self._relations = []
1516 super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1518 # ---------------------------------------------------------
1520 # ---------------------------------------------------------
1522 class serialized(_column):
1523 """ A field able to store an arbitrary python data structure.
1525 Note: only plain components allowed.
1528 def _symbol_set_struct(val):
1529 return simplejson.dumps(val)
1531 def _symbol_get_struct(self, val):
1532 return simplejson.loads(val or '{}')
1535 _type = 'serialized'
1538 _symbol_f = _symbol_set_struct
1539 _symbol_set = (_symbol_c, _symbol_f)
1540 _symbol_get = _symbol_get_struct
1542 # TODO: review completly this class for speed improvement
1543 class property(function):
1545 def to_field_args(self):
1546 args = super(property, self).to_field_args()
1547 args['company_dependent'] = True
1550 def _fnct_search(self, tobj, cr, uid, obj, name, domain, context=None):
1551 ir_property = obj.pool['ir.property']
1553 for field, operator, value in domain:
1554 result += ir_property.search_multi(cr, uid, name, tobj._name, operator, value, context=context)
1557 def _fnct_write(self, obj, cr, uid, id, prop_name, value, obj_dest, context=None):
1558 ir_property = obj.pool['ir.property']
1559 ir_property.set_multi(cr, uid, prop_name, obj._name, {id: value}, context=context)
1562 def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
1563 ir_property = obj.pool['ir.property']
1565 res = {id: {} for id in ids}
1566 for prop_name in prop_names:
1567 column = obj._all_columns[prop_name].column
1568 values = ir_property.get_multi(cr, uid, prop_name, obj._name, ids, context=context)
1569 if column._type == 'many2one':
1570 # name_get the non-null values as SUPERUSER_ID
1571 vals = sum(set(filter(None, values.itervalues())),
1572 obj.pool[column._obj].browse(cr, uid, [], context=context))
1573 vals_name = dict(vals.sudo().name_get()) if vals else {}
1574 for id, value in values.iteritems():
1576 if value and value.id in vals_name:
1577 ng = value.id, vals_name[value.id]
1578 res[id][prop_name] = ng
1580 for id, value in values.iteritems():
1581 res[id][prop_name] = value
1585 def __init__(self, **args):
1586 if 'view_load' in args:
1587 _logger.warning("view_load attribute is deprecated on ir.fields. Args: %r", args)
1589 args['obj'] = args.pop('relation', '') or args.get('obj', '')
1590 super(property, self).__init__(
1591 fnct=self._fnct_read,
1592 fnct_inv=self._fnct_write,
1593 fnct_search=self._fnct_search,
1599 class column_info(object):
1600 """ Struct containing details about an osv column, either one local to
1601 its model, or one inherited via _inherits.
1607 .. attribute:: column
1609 column instance, subclass of :class:`_column`
1611 .. attribute:: parent_model
1613 if the column is inherited, name of the model that contains it,
1614 ``None`` for local columns.
1616 .. attribute:: parent_column
1618 the name of the column containing the m2o relationship to the
1619 parent model that contains this column, ``None`` for local columns.
1621 .. attribute:: original_parent
1623 if the column is inherited, name of the original parent model that
1624 contains it i.e in case of multilevel inheritance, ``None`` for
1627 def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
1629 self.column = column
1630 self.parent_model = parent_model
1631 self.parent_column = parent_column
1632 self.original_parent = original_parent
1635 return '%s(%s, %s, %s, %s, %s)' % (
1636 self.__class__.__name__, self.name, self.column,
1637 self.parent_model, self.parent_column, self.original_parent)
1640 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: