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 operator import itemgetter
46 from psycopg2 import Binary
49 import openerp.tools as tools
50 from openerp.tools.translate import _
51 from openerp.tools import float_round, float_repr
52 from openerp.tools import html_sanitize
54 from openerp import SUPERUSER_ID
56 _logger = logging.getLogger(__name__)
58 def _symbol_set(symb):
59 if symb is None or symb == False:
61 elif isinstance(symb, unicode):
62 return symb.encode('utf-8')
66 class _column(object):
67 """ Base of all fields, a database column
69 An instance of this object is a *description* of a database column. It will
70 not hold any data, but only provide the methods to manipulate data of an
71 ORM record or even prepare/update the database to hold such a field of data.
82 _symbol_f = _symbol_set
83 _symbol_set = (_symbol_c, _symbol_f)
87 copy = True # whether value is copied by BaseModel.copy()
96 change_default = False
105 group_operator = False
106 groups = False # CSV list of ext IDs of groups
107 deprecated = False # Optional deprecation warning
109 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):
112 The 'manual' keyword argument specifies if the field is a custom one.
113 It corresponds to the 'state' column in ir_model_fields.
118 'required': required,
119 'readonly': readonly,
123 'priority': priority,
124 'change_default': change_default,
126 'ondelete': ondelete.lower() if ondelete else None,
127 'translate': translate,
131 for key, val in args0.iteritems():
133 setattr(self, key, val)
136 for key, val in args.iteritems():
137 setattr(self, key, val)
139 # prefetch only if self._classic_write, not self.groups, and not
141 if not self._classic_write or self.deprecated:
142 self._prefetch = False
144 def new(self, **args):
145 """ return a column like `self` with the given parameters """
146 # memory optimization: reuse self whenever possible; you can reduce the
147 # average memory usage per registry by 10 megabytes!
148 return self if self.same_parameters(args) else type(self)(**args)
150 def same_parameters(self, args):
153 # either both are falsy, or they are equal
154 (not val1 and not val) or (val1 == val)
155 for key, val in args.iteritems()
156 for val1 in [getattr(self, key, getattr(self, '_' + key, dummy))]
160 """ convert column `self` to a new-style field """
161 from openerp.fields import Field
162 return Field.by_type[self._type](**self.to_field_args())
164 def to_field_args(self):
165 """ return a dictionary with all the arguments to pass to the field """
167 ('column', self), # field interfaces self
170 truthy_items = filter(itemgetter(1), [
171 ('index', self.select),
172 ('manual', self.manual),
173 ('string', self.string),
175 ('readonly', self.readonly),
176 ('required', self.required),
177 ('states', self.states),
178 ('groups', self.groups),
179 ('change_default', self.change_default),
180 ('deprecated', self.deprecated),
181 ('group_operator', self.group_operator),
183 ('ondelete', self.ondelete),
184 ('translate', self.translate),
185 ('domain', self._domain),
186 ('context', self._context),
188 return dict(base_items + truthy_items + self._args.items())
193 def set(self, cr, obj, id, name, value, user=None, context=None):
194 cr.execute('update '+obj._table+' set '+name+'='+self._symbol_set[0]+' where id=%s', (self._symbol_set[1](value), id))
196 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
197 raise Exception(_('undefined get method !'))
199 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
200 ids = obj.search(cr, uid, args+self._domain+[(name, 'ilike', value)], offset, limit, context=context)
201 res = obj.read(cr, uid, ids, [name], context=context)
202 return [x[name] for x in res]
204 def as_display_name(self, cr, uid, obj, value, context=None):
205 """Converts a field value to a suitable string representation for a record,
206 e.g. when this field is used as ``rec_name``.
208 :param obj: the ``BaseModel`` instance this column belongs to
209 :param value: a proper value as returned by :py:meth:`~openerp.orm.osv.BaseModel.read`
212 # delegated to class method, so a column type A can delegate
213 # to a column type B.
214 return self._as_display_name(self, cr, uid, obj, value, context=None)
217 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
218 # This needs to be a class method, in case a column type A as to delegate
219 # to a column type B.
220 return tools.ustr(value)
222 # ---------------------------------------------------------
224 # ---------------------------------------------------------
225 class boolean(_column):
229 _symbol_set = (_symbol_c, _symbol_f)
231 def __init__(self, string='unknown', required=False, **args):
232 super(boolean, self).__init__(string=string, required=required, **args)
235 "required=True is deprecated: making a boolean field"
236 " `required` has no effect, as NULL values are "
237 "automatically turned into False. args: %r",args)
239 class integer(_column):
242 _symbol_f = lambda x: int(x or 0)
243 _symbol_set = (_symbol_c, _symbol_f)
244 _symbol_get = lambda self,x: x or 0
246 def __init__(self, string='unknown', required=False, **args):
247 super(integer, self).__init__(string=string, required=required, **args)
249 class reference(_column):
251 _classic_read = False # post-process to handle missing target
253 def __init__(self, string, selection, size=None, **args):
254 if callable(selection):
255 from openerp import api
256 selection = api.expected(api.cr_uid_context, selection)
257 _column.__init__(self, string=string, size=size, selection=selection, **args)
259 def to_field_args(self):
260 args = super(reference, self).to_field_args()
261 args['selection'] = self.selection
264 def get(self, cr, obj, ids, name, uid=None, context=None, values=None):
266 # copy initial values fetched previously.
268 result[value['id']] = value[name]
270 model, res_id = value[name].split(',')
271 if not obj.pool[model].exists(cr, uid, [int(res_id)], context=context):
272 result[value['id']] = False
276 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
278 # reference fields have a 'model,id'-like value, that we need to convert
280 model_name, res_id = value.split(',')
281 if model_name in obj.pool and res_id:
282 model = obj.pool[model_name]
283 names = model.name_get(cr, uid, [int(res_id)], context=context)
284 return names[0][1] if names else False
285 return tools.ustr(value)
287 # takes a string (encoded in utf8) and returns a string (encoded in utf8)
288 def _symbol_set_char(self, symb):
291 # * we need to remove the "symb==False" from the next line BUT
292 # for now too many things rely on this broken behavior
293 # * the symb==None test should be common to all data types
294 if symb is None or symb == False:
297 # we need to convert the string to a unicode object to be able
298 # to evaluate its length (and possibly truncate it) reliably
299 u_symb = tools.ustr(symb)
300 return u_symb[:self.size].encode('utf8')
305 def __init__(self, string="unknown", size=None, **args):
306 _column.__init__(self, string=string, size=size or None, **args)
307 # self._symbol_set_char defined to keep the backward compatibility
308 self._symbol_f = self._symbol_set_char = lambda x: _symbol_set_char(self, x)
309 self._symbol_set = (self._symbol_c, self._symbol_f)
319 def _symbol_set_html(self, value):
320 if value is None or value is False:
322 if not self._sanitize:
324 return html_sanitize(value)
326 def __init__(self, string='unknown', sanitize=True, **args):
327 super(html, self).__init__(string=string, **args)
328 self._sanitize = sanitize
329 # symbol_set redefinition because of sanitize specific behavior
330 self._symbol_f = self._symbol_set_html
331 self._symbol_set = (self._symbol_c, self._symbol_f)
333 def to_field_args(self):
334 args = super(html, self).to_field_args()
335 args['sanitize'] = self._sanitize
340 class float(_column):
343 _symbol_f = lambda x: __builtin__.float(x or 0.0)
344 _symbol_set = (_symbol_c, _symbol_f)
345 _symbol_get = lambda self,x: x or 0.0
347 def __init__(self, string='unknown', digits=None, digits_compute=None, required=False, **args):
348 _column.__init__(self, string=string, required=required, **args)
350 # synopsis: digits_compute(cr) -> (precision, scale)
351 self.digits_compute = digits_compute
353 def new(self, **args):
354 # float columns are database-dependent, so always recreate them
355 return type(self)(**args)
357 def to_field_args(self):
358 args = super(float, self).to_field_args()
359 args['digits'] = self.digits_compute or self.digits
362 def digits_change(self, cr):
363 if self.digits_compute:
364 self.digits = self.digits_compute(cr)
366 precision, scale = self.digits
367 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
368 precision_digits=scale),
369 precision_digits=scale))
391 """ Returns the current date in a format fit for being a
392 default value to a ``date`` field.
394 This method should be provided as is to the _defaults dict, it
395 should not be called.
397 return DT.date.today().strftime(
398 tools.DEFAULT_SERVER_DATE_FORMAT)
401 def context_today(model, cr, uid, context=None, timestamp=None):
402 """Returns the current date as seen in the client's timezone
403 in a format fit for date fields.
404 This method may be passed as value to initialize _defaults.
406 :param Model model: model (osv) for which the date value is being
407 computed - automatically passed when used in
409 :param datetime timestamp: optional datetime value to use instead of
410 the current date and time (must be a
411 datetime, regular dates can't be converted
413 :param dict context: the 'tz' key in the context should give the
414 name of the User/Client timezone (otherwise
418 today = timestamp or DT.datetime.now()
420 if context and context.get('tz'):
421 tz_name = context['tz']
423 user = model.pool['res.users'].browse(cr, SUPERUSER_ID, uid)
427 utc = pytz.timezone('UTC')
428 context_tz = pytz.timezone(tz_name)
429 utc_today = utc.localize(today, is_dst=False) # UTC = no DST
430 context_today = utc_today.astimezone(context_tz)
432 _logger.debug("failed to compute context/client-specific today date, "
433 "using the UTC value for `today`",
435 return (context_today or today).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
438 def date_to_datetime(model, cr, uid, userdate, context=None):
439 """ Convert date values expressed in user's timezone to
440 server-side UTC timestamp, assuming a default arbitrary
441 time of 12:00 AM - because a time is needed.
443 :param str userdate: date string in in user time zone
444 :return: UTC datetime string for server-side use
446 user_date = DT.datetime.strptime(userdate, tools.DEFAULT_SERVER_DATE_FORMAT)
447 if context and context.get('tz'):
448 tz_name = context['tz']
450 tz_name = model.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
452 utc = pytz.timezone('UTC')
453 context_tz = pytz.timezone(tz_name)
454 user_datetime = user_date + DT.timedelta(hours=12.0)
455 local_timestamp = context_tz.localize(user_datetime, is_dst=False)
456 user_datetime = local_timestamp.astimezone(utc)
457 return user_datetime.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
458 return user_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
461 class datetime(_column):
481 """ Returns the current datetime in a format fit for being a
482 default value to a ``datetime`` field.
484 This method should be provided as is to the _defaults dict, it
485 should not be called.
487 return DT.datetime.now().strftime(
488 tools.DEFAULT_SERVER_DATETIME_FORMAT)
491 def context_timestamp(cr, uid, timestamp, context=None):
492 """Returns the given timestamp converted to the client's timezone.
493 This method is *not* meant for use as a _defaults initializer,
494 because datetime fields are automatically converted upon
495 display on client side. For _defaults you :meth:`fields.datetime.now`
496 should be used instead.
498 :param datetime timestamp: naive datetime value (expressed in UTC)
499 to be converted to the client timezone
500 :param dict context: the 'tz' key in the context should give the
501 name of the User/Client timezone (otherwise
504 :return: timestamp converted to timezone-aware datetime in context
507 assert isinstance(timestamp, DT.datetime), 'Datetime instance expected'
508 if context and context.get('tz'):
509 tz_name = context['tz']
511 registry = openerp.modules.registry.RegistryManager.get(cr.dbname)
512 user = registry['res.users'].browse(cr, SUPERUSER_ID, uid)
514 utc_timestamp = pytz.utc.localize(timestamp, is_dst=False) # UTC = no DST
517 context_tz = pytz.timezone(tz_name)
518 return utc_timestamp.astimezone(context_tz)
520 _logger.debug("failed to compute context/client-specific timestamp, "
521 "using the UTC value",
525 class binary(_column):
529 # Binary values may be byte strings (python 2.6 byte array), but
530 # the legacy OpenERP convention is to transfer and store binaries
531 # as base64-encoded strings. The base64 string may be provided as a
532 # unicode in some circumstances, hence the str() cast in symbol_f.
533 # This str coercion will only work for pure ASCII unicode strings,
534 # on purpose - non base64 data must be passed as a 8bit byte strings.
535 _symbol_f = lambda symb: symb and Binary(str(symb)) or None
537 _symbol_set = (_symbol_c, _symbol_f)
538 _symbol_get = lambda self, x: x and str(x)
540 _classic_read = False
543 def __init__(self, string='unknown', filters=None, **args):
544 _column.__init__(self, string=string, **args)
545 self.filters = filters
547 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
560 # If client is requesting only the size of the field, we return it instead
561 # of the content. Presumably a separate request will be done to read the actual
562 # content if it's needed at some point.
563 # TODO: after 6.0 we should consider returning a dict with size and content instead of
564 # having an implicit convention for the value
565 if val and context.get('bin_size_%s' % name, context.get('bin_size')):
566 res[i] = tools.human_size(long(val))
571 class selection(_column):
574 def __init__(self, selection, string='unknown', **args):
575 if callable(selection):
576 from openerp import api
577 selection = api.expected(api.cr_uid_context, selection)
578 _column.__init__(self, string=string, **args)
579 self.selection = selection
581 def to_field_args(self):
582 args = super(selection, self).to_field_args()
583 args['selection'] = self.selection
587 def reify(cls, cr, uid, model, field, context=None):
588 """ Munges the field's ``selection`` attribute as necessary to get
589 something useable out of it: calls it if it's a function, applies
590 translations to labels if it's not.
592 A callable ``selection`` is considered translated on its own.
594 :param orm.Model model:
595 :param _column field:
597 if callable(field.selection):
598 return field.selection(model, cr, uid, context)
600 if not (context and 'lang' in context):
601 return field.selection
603 # field_to_dict isn't given a field name, only a field object, we
604 # need to get the name back in order to perform the translation lookup
606 name for name, column in model._columns.iteritems()
609 translation_filter = "%s,%s" % (model._name, field_name)
610 translate = functools.partial(
611 model.pool['ir.translation']._get_source,
612 cr, uid, translation_filter, 'selection', context['lang'])
615 (value, translate(label))
616 for value, label in field.selection
619 # ---------------------------------------------------------
621 # ---------------------------------------------------------
624 # Values: (0, 0, { fields }) create
625 # (1, ID, { fields }) update
626 # (2, ID) remove (delete)
627 # (3, ID) unlink one (target id or target of relation)
629 # (5) unlink all (only valid for one2many)
632 class many2one(_column):
633 _classic_read = False
634 _classic_write = True
637 _symbol_f = lambda x: x or None
638 _symbol_set = (_symbol_c, _symbol_f)
640 ondelete = 'set null'
642 def __init__(self, obj, string='unknown', auto_join=False, **args):
643 _column.__init__(self, string=string, **args)
645 self._auto_join = auto_join
647 def to_field_args(self):
648 args = super(many2one, self).to_field_args()
649 args['comodel_name'] = self._obj
650 args['auto_join'] = self._auto_join
653 def set(self, cr, obj_src, id, field, values, user=None, context=None):
656 obj = obj_src.pool[self._obj]
657 self._table = obj._table
658 if type(values) == type([]):
661 id_new = obj.create(cr, act[2])
662 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (id_new, id))
664 obj.write(cr, [act[1]], act[2], context=context)
666 cr.execute('delete from '+self._table+' where id=%s', (act[1],))
667 elif act[0] == 3 or act[0] == 5:
668 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
670 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (act[1], id))
673 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (values, id))
675 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
677 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
678 return obj.pool[self._obj].search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context)
681 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
682 return value[1] if isinstance(value, tuple) else tools.ustr(value)
685 class one2many(_column):
686 _classic_read = False
687 _classic_write = False
691 # one2many columns are not copied by default
694 def __init__(self, obj, fields_id, string='unknown', limit=None, auto_join=False, **args):
695 _column.__init__(self, string=string, **args)
697 self._fields_id = fields_id
699 self._auto_join = auto_join
700 #one2many can't be used as condition for defaults
701 assert(self.change_default != True)
703 def to_field_args(self):
704 args = super(one2many, self).to_field_args()
705 args['comodel_name'] = self._obj
706 args['inverse_name'] = self._fields_id
707 args['auto_join'] = self._auto_join
708 args['limit'] = self._limit
711 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
713 context = dict(context or {})
714 context.update(self._context)
716 # retrieve the records in the comodel
717 comodel = obj.pool[self._obj].browse(cr, user, [], context)
718 inverse = self._fields_id
719 domain = self._domain(obj) if callable(self._domain) else self._domain
720 domain = domain + [(inverse, 'in', ids)]
721 records = comodel.search(domain, limit=self._limit)
723 result = {id: [] for id in ids}
724 # read the inverse of records without prefetching other fields on them
725 for record in records.with_context(prefetch_fields=False):
726 # record[inverse] may be a record or an integer
727 result[int(record[inverse])].append(record.id)
731 def set(self, cr, obj, id, field, values, user=None, context=None):
733 context = dict(context or {})
734 context.update(self._context)
735 context['recompute'] = False # recomputation is done by outer create/write
738 obj = obj.pool[self._obj]
742 act[2][self._fields_id] = id
743 id_new = obj.create(cr, user, act[2], context=context)
744 result += obj._store_get_values(cr, user, [id_new], act[2].keys(), context)
746 obj.write(cr, user, [act[1]], act[2], context=context)
748 obj.unlink(cr, user, [act[1]], context=context)
750 inverse_field = obj._fields.get(self._fields_id)
751 assert inverse_field, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
752 # if the model has on delete cascade, just delete the row
753 if inverse_field.ondelete == "cascade":
754 obj.unlink(cr, user, [act[1]], context=context)
756 cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],))
758 # table of the field (parent_model in case of inherit)
759 field = obj.pool[self._obj]._fields[self._fields_id]
760 field_model = field.base_field.model_name
761 field_table = obj.pool[field_model]._table
762 cr.execute("select 1 from {0} where id=%s and {1}=%s".format(field_table, self._fields_id), (act[1], id))
763 if not cr.fetchone():
764 # Must use write() to recompute parent_store structure if needed and check access rules
765 obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
767 inverse_field = obj._fields.get(self._fields_id)
768 assert inverse_field, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
769 # if the o2m has a static domain we must respect it when unlinking
770 domain = self._domain(obj) if callable(self._domain) else self._domain
771 extra_domain = domain or []
772 ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
773 # If the model has cascade deletion, we delete the rows because it is the intended behavior,
774 # otherwise we only nullify the reverse foreign key column.
775 if inverse_field.ondelete == "cascade":
776 obj.unlink(cr, user, ids_to_unlink, context=context)
778 obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context)
780 # Must use write() to recompute parent_store structure if needed
781 obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
783 cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
784 ids3 = map(lambda x:x[0], cr.fetchall())
785 obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
788 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
789 domain = self._domain(obj) if callable(self._domain) else self._domain
790 return obj.pool[self._obj].name_search(cr, uid, value, domain, operator, context=context,limit=limit)
793 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
794 raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)')
797 # Values: (0, 0, { fields }) create
798 # (1, ID, { fields }) update (write fields to ID)
799 # (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete)
800 # (3, ID) unlink (delete the relationship between the two objects but does not delete ID)
801 # (4, ID) link (add a relationship)
803 # (6, ?, ids) set a list of links
805 class many2many(_column):
806 """Encapsulates the logic of a many-to-many bidirectional relationship, handling the
807 low-level details of the intermediary relationship table transparently.
808 A many-to-many relationship is always symmetrical, and can be declared and accessed
809 from either endpoint model.
810 If ``rel`` (relationship table name), ``id1`` (source foreign key column name)
811 or id2 (destination foreign key column name) are not specified, the system will
812 provide default values. This will by default only allow one single symmetrical
813 many-to-many relationship between the source and destination model.
814 For multiple many-to-many relationship between the same models and for
815 relationships where source and destination models are the same, ``rel``, ``id1``
816 and ``id2`` should be specified explicitly.
818 :param str obj: destination model
819 :param str rel: optional name of the intermediary relationship table. If not specified,
820 a canonical name will be derived based on the alphabetically-ordered
821 model names of the source and destination (in the form: ``amodel_bmodel_rel``).
822 Automatic naming is not possible when the source and destination are
823 the same, for obvious ambiguity reasons.
824 :param str id1: optional name for the column holding the foreign key to the current
825 model in the relationship table. If not specified, a canonical name
826 will be derived based on the model name (in the form: `src_model_id`).
827 :param str id2: optional name for the column holding the foreign key to the destination
828 model in the relationship table. If not specified, a canonical name
829 will be derived based on the model name (in the form: `dest_model_id`)
830 :param str string: field label
832 _classic_read = False
833 _classic_write = False
837 def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
840 _column.__init__(self, string=string, **args)
842 if rel and '.' in rel:
843 raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\
844 'You used %s, which is not a valid SQL table name.')% (string,rel))
850 def to_field_args(self):
851 args = super(many2many, self).to_field_args()
852 args['comodel_name'] = self._obj
853 args['relation'] = self._rel
854 args['column1'] = self._id1
855 args['column2'] = self._id2
856 args['limit'] = self._limit
859 def _sql_names(self, source_model):
860 """Return the SQL names defining the structure of the m2m relationship table
862 :return: (m2m_table, local_col, dest_col) where m2m_table is the table name,
863 local_col is the name of the column holding the current model's FK, and
864 dest_col is the name of the column holding the destination model's FK, and
866 tbl, col1, col2 = self._rel, self._id1, self._id2
867 if not all((tbl, col1, col2)):
868 # the default table name is based on the stable alphabetical order of tables
869 dest_model = source_model.pool[self._obj]
870 tables = tuple(sorted([source_model._table, dest_model._table]))
872 assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\
873 'is not possible when source and destination models are '\
875 tbl = '%s_%s_rel' % tables
877 col1 = '%s_id' % source_model._table
879 col2 = '%s_id' % dest_model._table
880 return tbl, col1, col2
882 def _get_query_and_where_params(self, cr, model, ids, values, where_params):
883 """ Extracted from ``get`` to facilitate fine-tuning of the generated
885 query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
886 FROM %(rel)s, %(from_c)s \
887 WHERE %(rel)s.%(id1)s IN %%s \
888 AND %(rel)s.%(id2)s = %(tbl)s.id \
894 return query, where_params
896 def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
908 "Specifying offset at a many2many.get() is deprecated and may"
909 " produce unpredictable results.")
910 obj = model.pool[self._obj]
911 rel, id1, id2 = self._sql_names(model)
913 # static domains are lists, and are evaluated both here and on client-side, while string
914 # domains supposed by dynamic and evaluated on client-side only (thus ignored here)
915 # FIXME: make this distinction explicit in API!
916 domain = isinstance(self._domain, list) and self._domain or []
918 wquery = obj._where_calc(cr, user, domain, context=context)
919 obj._apply_ir_rules(cr, user, wquery, 'read', context=context)
920 from_c, where_c, where_params = wquery.get_sql()
922 where_c = ' AND ' + where_c
924 order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0])
927 if self._limit is not None:
928 limit_str = ' LIMIT %d' % self._limit
930 query, where_params = self._get_query_and_where_params(cr, model, ids, {'rel': rel,
937 'order_by': order_by,
941 cr.execute(query, [tuple(ids),] + where_params)
942 for r in cr.fetchall():
943 res[r[1]].append(r[0])
946 def set(self, cr, model, id, name, values, user=None, context=None):
951 rel, id1, id2 = self._sql_names(model)
952 obj = model.pool[self._obj]
954 if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
957 idnew = obj.create(cr, user, act[2], context=context)
958 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew))
960 obj.write(cr, user, [act[1]], act[2], context=context)
962 obj.unlink(cr, user, [act[1]], context=context)
964 cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1]))
966 # following queries are in the same transaction - so should be relatively safe
967 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1]))
968 if not cr.fetchone():
969 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1]))
971 cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,))
974 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
976 d1 = ' and ' + ' and '.join(d1)
979 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)
981 for act_nbr in act[2]:
982 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr))
985 # TODO: use a name_search
987 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
988 return obj.pool[self._obj].search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
991 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
992 raise NotImplementedError('Many2Many columns should not be used as record name (_rec_name)')
995 def get_nice_size(value):
997 if isinstance(value, (int,long)):
999 elif value: # this is supposed to be a string
1001 return tools.human_size(size)
1003 # See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
1004 # and http://bugs.python.org/issue10066
1005 invalid_xml_low_bytes = re.compile(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
1007 def sanitize_binary_value(value):
1008 # binary fields should be 7-bit ASCII base64-encoded data,
1009 # but we do additional sanity checks to make sure the values
1010 # are not something else that won't pass via XML-RPC
1011 if isinstance(value, (xmlrpclib.Binary, tuple, list, dict)):
1012 # these builtin types are meant to pass untouched
1015 # Handle invalid bytes values that will cause problems
1016 # for XML-RPC. See for more info:
1017 # - http://bugs.python.org/issue10066
1018 # - http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
1020 # Coercing to unicode would normally allow it to properly pass via
1021 # XML-RPC, transparently encoded as UTF-8 by xmlrpclib.
1022 # (this works for _any_ byte values, thanks to the fallback
1023 # to latin-1 passthrough encoding when decoding to unicode)
1024 value = tools.ustr(value)
1026 # Due to Python bug #10066 this could still yield invalid XML
1027 # bytes, specifically in the low byte range, that will crash
1028 # the decoding side: [\x00-\x08\x0b-\x0c\x0e-\x1f]
1029 # So check for low bytes values, and if any, perform
1030 # base64 encoding - not very smart or useful, but this is
1031 # our last resort to avoid crashing the request.
1032 if invalid_xml_low_bytes.search(value):
1033 # b64-encode after restoring the pure bytes with latin-1
1034 # passthrough encoding
1035 value = base64.b64encode(value.encode('latin-1'))
1040 # ---------------------------------------------------------
1042 # ---------------------------------------------------------
1043 class function(_column):
1045 A field whose value is computed by a function (rather
1046 than being read from the database).
1048 :param fnct: the callable that will compute the field value.
1049 :param arg: arbitrary value to be passed to ``fnct`` when computing the value.
1050 :param fnct_inv: the callable that will allow writing values in that field
1051 (if not provided, the field is read-only).
1052 :param fnct_inv_arg: arbitrary value to be passed to ``fnct_inv`` when
1054 :param str type: type of the field simulated by the function field
1055 :param fnct_search: the callable that allows searching on the field
1056 (if not provided, search will not return any result).
1057 :param store: store computed value in database
1058 (see :ref:`The *store* parameter <field-function-store>`).
1059 :type store: True or dict specifying triggers for field computation
1060 :param multi: name of batch for batch computation of function fields.
1061 All fields with the same batch name will be computed by
1062 a single function call. This changes the signature of the
1065 .. _field-function-fnct: The ``fnct`` parameter
1067 .. rubric:: The ``fnct`` parameter
1069 The callable implementing the function field must have the following signature:
1071 .. function:: fnct(model, cr, uid, ids, field_name(s), arg, context)
1073 Implements the function field.
1075 :param orm model: model to which the field belongs (should be ``self`` for
1077 :param field_name(s): name of the field to compute, or if ``multi`` is provided,
1078 list of field names to compute.
1079 :type field_name(s): str | [str]
1080 :param arg: arbitrary value passed when declaring the function field
1082 :return: mapping of ``ids`` to computed values, or if multi is provided,
1083 to a map of field_names to computed values
1085 The values in the returned dictionary must be of the type specified by the type
1086 argument in the field declaration.
1088 Here is an example with a simple function ``char`` function field::
1091 def compute(self, cr, uid, ids, field_name, arg, context):
1095 _columns['my_char'] = fields.function(compute, type='char', size=50)
1097 # when called with ``ids=[1,2,3]``, ``compute`` could return:
1101 3: False # null values should be returned explicitly too
1104 If ``multi`` is set, then ``field_name`` is replaced by ``field_names``: a list
1105 of the field names that should be computed. Each value in the returned
1106 dictionary must then be a dictionary mapping field names to values.
1108 Here is an example where two function fields (``name`` and ``age``)
1109 are both computed by a single function field::
1112 def compute(self, cr, uid, ids, field_names, arg, context):
1116 _columns['name'] = fields.function(compute_person_data, type='char',\
1117 size=50, multi='person_data')
1118 _columns[''age'] = fields.function(compute_person_data, type='integer',\
1119 multi='person_data')
1121 # when called with ``ids=[1,2,3]``, ``compute_person_data`` could return:
1123 1: {'name': 'Bob', 'age': 23},
1124 2: {'name': 'Sally', 'age': 19},
1125 3: {'name': 'unknown', 'age': False}
1128 .. _field-function-fnct-inv:
1130 .. rubric:: The ``fnct_inv`` parameter
1132 This callable implements the write operation for the function field
1133 and must have the following signature:
1135 .. function:: fnct_inv(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context)
1137 Callable that implements the ``write`` operation for the function field.
1139 :param orm model: model to which the field belongs (should be ``self`` for
1141 :param int id: the identifier of the object to write on
1142 :param str field_name: name of the field to set
1143 :param fnct_inv_arg: arbitrary value passed when declaring the function field
1146 When writing values for a function field, the ``multi`` parameter is ignored.
1148 .. _field-function-fnct-search:
1150 .. rubric:: The ``fnct_search`` parameter
1152 This callable implements the search operation for the function field
1153 and must have the following signature:
1155 .. function:: fnct_search(model, cr, uid, model_again, field_name, criterion, context)
1157 Callable that implements the ``search`` operation for the function field by expanding
1158 a search criterion based on the function field into a new domain based only on
1159 columns that are stored in the database.
1161 :param orm model: model to which the field belongs (should be ``self`` for
1163 :param orm model_again: same value as ``model`` (seriously! this is for backwards
1165 :param str field_name: name of the field to search on
1166 :param list criterion: domain component specifying the search criterion on the field.
1168 :return: domain to use instead of ``criterion`` when performing the search.
1169 This new domain must be based only on columns stored in the database, as it
1170 will be used directly without any translation.
1172 The returned value must be a domain, that is, a list of the form [(field_name, operator, operand)].
1173 The most generic way to implement ``fnct_search`` is to directly search for the records that
1174 match the given ``criterion``, and return their ``ids`` wrapped in a domain, such as
1175 ``[('id','in',[1,3,5])]``.
1177 .. _field-function-store:
1179 .. rubric:: The ``store`` parameter
1181 The ``store`` parameter allows caching the result of the field computation in the
1182 database, and defining the triggers that will invalidate that cache and force a
1183 recomputation of the function field.
1184 When not provided, the field is computed every time its value is read.
1185 The value of ``store`` may be either ``True`` (to recompute the field value whenever
1186 any field in the same record is modified), or a dictionary specifying a more
1187 flexible set of recomputation triggers.
1189 A trigger specification is a dictionary that maps the names of the models that
1190 will trigger the computation, to a tuple describing the trigger rule, in the
1194 'trigger_model': (mapping_function,
1195 ['trigger_field1', 'trigger_field2'],
1199 A trigger rule is defined by a 3-item tuple where:
1201 * The ``mapping_function`` is defined as follows:
1203 .. function:: mapping_function(trigger_model, cr, uid, trigger_ids, context)
1205 Callable that maps record ids of a trigger model to ids of the
1206 corresponding records in the source model (whose field values
1207 need to be recomputed).
1209 :param orm model: trigger_model
1210 :param list trigger_ids: ids of the records of trigger_model that were
1213 :return: list of ids of the source model whose function field values
1214 need to be recomputed
1216 * The second item is a list of the fields who should act as triggers for
1217 the computation. If an empty list is given, all fields will act as triggers.
1218 * The last item is the priority, used to order the triggers when processing them
1219 after any write operation on a model that has function field triggers. The
1220 default priority is 10.
1222 In fact, setting store = True is the same as using the following trigger dict::
1225 'model_itself': (lambda self, cr, uid, ids, context: ids,
1231 _classic_read = False
1232 _classic_write = False
1237 # function fields are not copied by default
1241 # multi: compute several fields in one call
1243 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):
1244 _column.__init__(self, **args)
1247 self._fnct_inv = fnct_inv
1250 if 'relation' in args:
1251 self._obj = args['relation']
1253 self.digits = args.get('digits', (16,2))
1254 self.digits_compute = args.get('digits_compute', None)
1255 if callable(args.get('selection')):
1256 from openerp import api
1257 self.selection = api.expected(api.cr_uid_context, args['selection'])
1259 self._fnct_inv_arg = fnct_inv_arg
1263 self._fnct_search = fnct_search
1266 if not fnct_search and not store:
1267 self.selectable = False
1270 if self._type != 'many2one':
1271 # m2o fields need to return tuples with name_get, not just foreign keys
1272 self._classic_read = True
1273 self._classic_write = True
1275 self._symbol_get=lambda x:x and str(x)
1277 self._prefetch = True
1280 self._symbol_c = char._symbol_c
1281 self._symbol_f = lambda x: _symbol_set_char(self, x)
1282 self._symbol_set = (self._symbol_c, self._symbol_f)
1284 type_class = globals().get(type)
1285 if type_class is not None:
1286 self._symbol_c = type_class._symbol_c
1287 self._symbol_f = type_class._symbol_f
1288 self._symbol_set = type_class._symbol_set
1290 def new(self, **args):
1291 # HACK: function fields are tricky to recreate, simply return a copy
1293 return copy.copy(self)
1295 def to_field_args(self):
1296 args = super(function, self).to_field_args()
1297 args['store'] = bool(self.store)
1298 if self._type in ('float',):
1299 args['digits'] = self.digits_compute or self.digits
1300 elif self._type in ('selection', 'reference'):
1301 args['selection'] = self.selection
1302 elif self._type in ('many2one', 'one2many', 'many2many'):
1303 args['comodel_name'] = self._obj
1306 def digits_change(self, cr):
1307 if self._type == 'float':
1308 if self.digits_compute:
1309 self.digits = self.digits_compute(cr)
1311 precision, scale = self.digits
1312 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
1313 precision_digits=scale),
1314 precision_digits=scale))
1316 def search(self, cr, uid, obj, name, args, context=None):
1317 if not self._fnct_search:
1318 #CHECKME: should raise an exception
1320 return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
1322 def postprocess(self, cr, uid, obj, field, value=None, context=None):
1323 return self._postprocess_batch(cr, uid, obj, field, {0: value}, context=context)[0]
1325 def _postprocess_batch(self, cr, uid, obj, field, values, context=None):
1332 field_type = obj._columns[field]._type
1333 new_values = dict(values)
1335 if field_type == 'binary':
1336 if context.get('bin_size'):
1337 # client requests only the size of binary fields
1338 for rid, value in values.iteritems():
1340 new_values[rid] = get_nice_size(value)
1341 elif not context.get('bin_raw'):
1342 for rid, value in values.iteritems():
1344 new_values[rid] = sanitize_binary_value(value)
1348 def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
1350 # if we already have a value, don't recompute it.
1351 # This happen if case of stored many2one fields
1352 if values and not multi and name in values[0]:
1353 result = dict((v['id'], v[name]) for v in values)
1354 elif values and multi and all(n in values[0] for n in name):
1355 result = dict((v['id'], dict((n, v[n]) for n in name)) for v in values)
1357 result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
1360 for rid, values in result.iteritems():
1361 for f, v in values.iteritems():
1364 swap.setdefault(f, {})[rid] = v
1366 for field, values in swap.iteritems():
1367 new_values = self._postprocess_batch(cr, uid, obj, field, values, context)
1368 for rid, value in new_values.iteritems():
1369 result[rid][field] = value
1372 result = self._postprocess_batch(cr, uid, obj, name, result, context)
1376 def set(self, cr, obj, id, name, value, user=None, context=None):
1380 self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
1383 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
1384 # Function fields are supposed to emulate a basic field type,
1385 # so they can delegate to the basic type for record name rendering
1386 return globals()[field._type]._as_display_name(field, cr, uid, obj, value, context=context)
1388 # ---------------------------------------------------------
1390 # ---------------------------------------------------------
1392 class related(function):
1393 """Field that points to some data inside another field of the current record.
1398 'foo_id': fields.many2one('my.foo', 'Foo'),
1399 'bar': fields.related('foo_id', 'frol', type='char', string='Frol of Foo'),
1403 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1404 # assume self._arg = ('foo', 'bar', 'baz')
1405 # domain = [(name, op, val)] => search [('foo.bar.baz', op, val)]
1406 field = '.'.join(self._arg)
1407 return map(lambda x: (field, x[1], x[2]), domain)
1409 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1410 if isinstance(ids, (int, long)):
1412 for instance in obj.browse(cr, uid, ids, context=context):
1413 # traverse all fields except the last one
1414 for field in self.arg[:-1]:
1415 instance = instance[field][:1]
1417 # write on the last field of the target record
1418 instance.write({self.arg[-1]: values})
1420 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1422 for record in obj.browse(cr, SUPERUSER_ID, ids, context=context):
1424 # traverse all fields except the last one
1425 for field in self.arg[:-1]:
1426 value = value[field][:1]
1427 # read the last field on the target record
1428 res[record.id] = value[self.arg[-1]]
1430 if self._type == 'many2one':
1431 # res[id] is a recordset; convert it to (id, name) or False.
1432 # Perform name_get as root, as seeing the name of a related object depends on
1433 # access right of source document, not target, so user may not have access.
1434 value_ids = list(set(value.id for value in res.itervalues() if value))
1435 value_name = dict(obj.pool[self._obj].name_get(cr, SUPERUSER_ID, value_ids, context=context))
1436 res = dict((id, bool(value) and (value.id, value_name[value.id])) for id, value in res.iteritems())
1438 elif self._type in ('one2many', 'many2many'):
1439 # res[id] is a recordset; convert it to a list of ids
1440 res = dict((id, value.ids) for id, value in res.iteritems())
1444 def __init__(self, *arg, **args):
1446 self._relations = []
1447 super(related, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1448 if self.store is True:
1449 # TODO: improve here to change self.store = {...} according to related objects
1453 class sparse(function):
1455 def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
1457 + For a many2many field, a list of tuples is expected.
1458 Here is the list of tuple that are accepted, with the corresponding semantics ::
1460 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1461 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1462 (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)
1463 (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)
1464 (4, ID) link to existing record with id = ID (adds a relationship)
1465 (5) unlink all (like using (3,ID) for all linked records)
1466 (6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
1469 [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
1471 + For a one2many field, a lits of tuples is expected.
1472 Here is the list of tuple that are accepted, with the corresponding semantics ::
1474 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1475 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1476 (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)
1479 [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
1482 if self._type == 'many2many':
1483 assert value[0][0] == 6, 'Unsupported m2m value for sparse field: %s' % value
1486 elif self._type == 'one2many':
1489 relation_obj = obj.pool[self.relation]
1491 assert vals[0] in (0,1,2), 'Unsupported o2m value for sparse field: %s' % vals
1493 read_value.append(relation_obj.create(cr, uid, vals[2], context=context))
1495 relation_obj.write(cr, uid, vals[1], vals[2], context=context)
1497 relation_obj.unlink(cr, uid, vals[1], context=context)
1498 read_value.remove(vals[1])
1503 def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
1504 if not type(ids) == list:
1506 records = obj.browse(cr, uid, ids, context=context)
1507 for record in records:
1508 # grab serialized value as object - already deserialized
1509 serialized = getattr(record, self.serialization_field)
1511 # simply delete the key to unset it.
1512 serialized.pop(field_name, None)
1514 serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context)
1515 obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context)
1518 def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None):
1520 records = obj.browse(cr, uid, ids, context=context)
1521 for record in records:
1522 # grab serialized value as object - already deserialized
1523 serialized = getattr(record, self.serialization_field)
1524 results[record.id] = {}
1525 for field_name in field_names:
1526 field_type = obj._columns[field_name]._type
1527 value = serialized.get(field_name, False)
1528 if field_type in ('one2many','many2many'):
1531 # filter out deleted records as superuser
1532 relation_obj = obj.pool[obj._columns[field_name].relation]
1533 value = relation_obj.exists(cr, openerp.SUPERUSER_ID, value)
1534 if type(value) in (int,long) and field_type == 'many2one':
1535 relation_obj = obj.pool[obj._columns[field_name].relation]
1536 # check for deleted record as superuser
1537 if not relation_obj.exists(cr, openerp.SUPERUSER_ID, [value]):
1539 results[record.id][field_name] = value
1542 def __init__(self, serialization_field, **kwargs):
1543 self.serialization_field = serialization_field
1544 super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
1548 # ---------------------------------------------------------
1550 # ---------------------------------------------------------
1552 class dummy(function):
1553 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1556 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1559 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1562 def __init__(self, *arg, **args):
1564 self._relations = []
1565 super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1567 # ---------------------------------------------------------
1569 # ---------------------------------------------------------
1571 class serialized(_column):
1572 """ A field able to store an arbitrary python data structure.
1574 Note: only plain components allowed.
1577 def _symbol_set_struct(val):
1578 return simplejson.dumps(val)
1580 def _symbol_get_struct(self, val):
1581 return simplejson.loads(val or '{}')
1584 _type = 'serialized'
1587 _symbol_f = _symbol_set_struct
1588 _symbol_set = (_symbol_c, _symbol_f)
1589 _symbol_get = _symbol_get_struct
1591 # TODO: review completly this class for speed improvement
1592 class property(function):
1594 def to_field_args(self):
1595 args = super(property, self).to_field_args()
1596 args['company_dependent'] = True
1599 def _fnct_search(self, tobj, cr, uid, obj, name, domain, context=None):
1600 ir_property = obj.pool['ir.property']
1602 for field, operator, value in domain:
1603 result += ir_property.search_multi(cr, uid, name, tobj._name, operator, value, context=context)
1606 def _fnct_write(self, obj, cr, uid, id, prop_name, value, obj_dest, context=None):
1607 ir_property = obj.pool['ir.property']
1608 ir_property.set_multi(cr, uid, prop_name, obj._name, {id: value}, context=context)
1611 def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
1612 ir_property = obj.pool['ir.property']
1614 res = {id: {} for id in ids}
1615 for prop_name in prop_names:
1616 field = obj._fields[prop_name]
1617 values = ir_property.get_multi(cr, uid, prop_name, obj._name, ids, context=context)
1618 if field.type == 'many2one':
1619 # name_get the non-null values as SUPERUSER_ID
1620 vals = sum(set(filter(None, values.itervalues())),
1621 obj.pool[field.comodel_name].browse(cr, uid, [], context=context))
1622 vals_name = dict(vals.sudo().name_get()) if vals else {}
1623 for id, value in values.iteritems():
1625 if value and value.id in vals_name:
1626 ng = value.id, vals_name[value.id]
1627 res[id][prop_name] = ng
1629 for id, value in values.iteritems():
1630 res[id][prop_name] = value
1634 def __init__(self, **args):
1635 if 'view_load' in args:
1636 _logger.warning("view_load attribute is deprecated on ir.fields. Args: %r", args)
1638 args['obj'] = args.pop('relation', '') or args.get('obj', '')
1639 super(property, self).__init__(
1640 fnct=self._fnct_read,
1641 fnct_inv=self._fnct_write,
1642 fnct_search=self._fnct_search,
1648 class column_info(object):
1649 """ Struct containing details about an osv column, either one local to
1650 its model, or one inherited via _inherits.
1656 .. attribute:: column
1658 column instance, subclass of :class:`_column`
1660 .. attribute:: parent_model
1662 if the column is inherited, name of the model that contains it,
1663 ``None`` for local columns.
1665 .. attribute:: parent_column
1667 the name of the column containing the m2o relationship to the
1668 parent model that contains this column, ``None`` for local columns.
1670 .. attribute:: original_parent
1672 if the column is inherited, name of the original parent model that
1673 contains it i.e in case of multilevel inheritance, ``None`` for
1676 def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
1678 self.column = column
1679 self.parent_model = parent_model
1680 self.parent_column = parent_column
1681 self.original_parent = original_parent
1684 return '%s(%s, %s, %s, %s, %s)' % (
1685 self.__class__.__name__, self.name, self.column,
1686 self.parent_model, self.parent_column, self.original_parent)
1689 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: