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 # used to hide a certain field type in the list of field types
88 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):
91 The 'manual' keyword argument specifies if the field is a custom one.
92 It corresponds to the 'state' column in ir_model_fields.
99 self.states = states or {}
101 self.readonly = readonly
102 self.required = required
104 self.help = args.get('help', '')
105 self.priority = priority
106 self.change_default = change_default
107 self.ondelete = ondelete.lower() if ondelete else None # defaults to 'set null' in ORM
108 self.translate = translate
109 self._domain = domain
110 self._context = context
115 self.selectable = True
116 self.group_operator = args.get('group_operator', False)
117 self.groups = False # CSV list of ext IDs of groups that can access this field
118 self.deprecated = False # Optional deprecation warning
120 setattr(self, a, args[a])
125 def set(self, cr, obj, id, name, value, user=None, context=None):
126 cr.execute('update '+obj._table+' set '+name+'='+self._symbol_set[0]+' where id=%s', (self._symbol_set[1](value), id))
128 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
129 raise Exception(_('undefined get method !'))
131 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
132 ids = obj.search(cr, uid, args+self._domain+[(name, 'ilike', value)], offset, limit, context=context)
133 res = obj.read(cr, uid, ids, [name], context=context)
134 return [x[name] for x in res]
136 def as_display_name(self, cr, uid, obj, value, context=None):
137 """Converts a field value to a suitable string representation for a record,
138 e.g. when this field is used as ``rec_name``.
140 :param obj: the ``BaseModel`` instance this column belongs to
141 :param value: a proper value as returned by :py:meth:`~openerp.orm.osv.BaseModel.read`
144 # delegated to class method, so a column type A can delegate
145 # to a column type B.
146 return self._as_display_name(self, cr, uid, obj, value, context=None)
149 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
150 # This needs to be a class method, in case a column type A as to delegate
151 # to a column type B.
152 return tools.ustr(value)
154 # ---------------------------------------------------------
156 # ---------------------------------------------------------
157 class boolean(_column):
160 _symbol_f = lambda x: x and 'True' or 'False'
161 _symbol_set = (_symbol_c, _symbol_f)
163 def __init__(self, string='unknown', required=False, **args):
164 super(boolean, self).__init__(string=string, required=required, **args)
167 "required=True is deprecated: making a boolean field"
168 " `required` has no effect, as NULL values are "
169 "automatically turned into False. args: %r",args)
171 class integer(_column):
174 _symbol_f = lambda x: int(x or 0)
175 _symbol_set = (_symbol_c, _symbol_f)
176 _symbol_get = lambda self,x: x or 0
178 def __init__(self, string='unknown', required=False, **args):
179 super(integer, self).__init__(string=string, required=required, **args)
181 class reference(_column):
183 _classic_read = False # post-process to handle missing target
185 def __init__(self, string, selection, size=None, **args):
186 _column.__init__(self, string=string, size=size, selection=selection, **args)
188 def get(self, cr, obj, ids, name, uid=None, context=None, values=None):
190 # copy initial values fetched previously.
192 result[value['id']] = value[name]
194 model, res_id = value[name].split(',')
195 if not obj.pool[model].exists(cr, uid, [int(res_id)], context=context):
196 result[value['id']] = False
200 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
202 # reference fields have a 'model,id'-like value, that we need to convert
204 model_name, res_id = value.split(',')
205 if model_name in obj.pool and res_id:
206 model = obj.pool[model_name]
207 names = model.name_get(cr, uid, [int(res_id)], context=context)
208 return names[0][1] if names else False
209 return tools.ustr(value)
211 # takes a string (encoded in utf8) and returns a string (encoded in utf8)
212 def _symbol_set_char(self, symb):
215 # * we need to remove the "symb==False" from the next line BUT
216 # for now too many things rely on this broken behavior
217 # * the symb==None test should be common to all data types
218 if symb is None or symb == False:
221 # we need to convert the string to a unicode object to be able
222 # to evaluate its length (and possibly truncate it) reliably
223 u_symb = tools.ustr(symb)
224 return u_symb[:self.size].encode('utf8')
229 def __init__(self, string="unknown", size=None, **args):
230 _column.__init__(self, string=string, size=size or None, **args)
231 # self._symbol_set_char defined to keep the backward compatibility
232 self._symbol_f = self._symbol_set_char = lambda x: _symbol_set_char(self, x)
233 self._symbol_set = (self._symbol_c, self._symbol_f)
244 def _symbol_set_html(self, value):
245 if value is None or value is False:
247 if not self._sanitize:
249 return html_sanitize(value)
251 def __init__(self, string='unknown', sanitize=True, **args):
252 super(html, self).__init__(string=string, **args)
253 self._sanitize = sanitize
254 # symbol_set redefinition because of sanitize specific behavior
255 self._symbol_f = self._symbol_set_html
256 self._symbol_set = (self._symbol_c, self._symbol_f)
260 class float(_column):
263 _symbol_f = lambda x: __builtin__.float(x or 0.0)
264 _symbol_set = (_symbol_c, _symbol_f)
265 _symbol_get = lambda self,x: x or 0.0
267 def __init__(self, string='unknown', digits=None, digits_compute=None, required=False, **args):
268 _column.__init__(self, string=string, required=required, **args)
270 # synopsis: digits_compute(cr) -> (precision, scale)
271 self.digits_compute = digits_compute
273 def digits_change(self, cr):
274 if self.digits_compute:
275 self.digits = self.digits_compute(cr)
277 precision, scale = self.digits
278 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
279 precision_digits=scale),
280 precision_digits=scale))
302 """ Returns the current date in a format fit for being a
303 default value to a ``date`` field.
305 This method should be provided as is to the _defaults dict, it
306 should not be called.
308 return DT.date.today().strftime(
309 tools.DEFAULT_SERVER_DATE_FORMAT)
312 def context_today(model, cr, uid, context=None, timestamp=None):
313 """Returns the current date as seen in the client's timezone
314 in a format fit for date fields.
315 This method may be passed as value to initialize _defaults.
317 :param Model model: model (osv) for which the date value is being
318 computed - automatically passed when used in
320 :param datetime timestamp: optional datetime value to use instead of
321 the current date and time (must be a
322 datetime, regular dates can't be converted
324 :param dict context: the 'tz' key in the context should give the
325 name of the User/Client timezone (otherwise
329 today = timestamp or DT.datetime.now()
331 if context and context.get('tz'):
332 tz_name = context['tz']
334 tz_name = model.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
337 utc = pytz.timezone('UTC')
338 context_tz = pytz.timezone(tz_name)
339 utc_today = utc.localize(today, is_dst=False) # UTC = no DST
340 context_today = utc_today.astimezone(context_tz)
342 _logger.debug("failed to compute context/client-specific today date, "
343 "using the UTC value for `today`",
345 return (context_today or today).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
348 def date_to_datetime(model, cr, uid, userdate, context=None):
349 """ Convert date values expressed in user's timezone to
350 server-side UTC timestamp, assuming a default arbitrary
351 time of 12:00 AM - because a time is needed.
353 :param str userdate: date string in in user time zone
354 :return: UTC datetime string for server-side use
356 user_date = DT.datetime.strptime(userdate, tools.DEFAULT_SERVER_DATE_FORMAT)
357 if context and context.get('tz'):
358 tz_name = context['tz']
360 tz_name = model.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
362 utc = pytz.timezone('UTC')
363 context_tz = pytz.timezone(tz_name)
364 user_datetime = user_date + DT.timedelta(hours=12.0)
365 local_timestamp = context_tz.localize(user_datetime, is_dst=False)
366 user_datetime = local_timestamp.astimezone(utc)
367 return user_datetime.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
368 return user_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
371 class datetime(_column):
391 """ Returns the current datetime in a format fit for being a
392 default value to a ``datetime`` field.
394 This method should be provided as is to the _defaults dict, it
395 should not be called.
397 return DT.datetime.now().strftime(
398 tools.DEFAULT_SERVER_DATETIME_FORMAT)
401 def context_timestamp(cr, uid, timestamp, context=None):
402 """Returns the given timestamp converted to the client's timezone.
403 This method is *not* meant for use as a _defaults initializer,
404 because datetime fields are automatically converted upon
405 display on client side. For _defaults you :meth:`fields.datetime.now`
406 should be used instead.
408 :param datetime timestamp: naive datetime value (expressed in UTC)
409 to be converted to the client timezone
410 :param dict context: the 'tz' key in the context should give the
411 name of the User/Client timezone (otherwise
414 :return: timestamp converted to timezone-aware datetime in context
417 assert isinstance(timestamp, DT.datetime), 'Datetime instance expected'
418 if context and context.get('tz'):
419 tz_name = context['tz']
421 registry = openerp.modules.registry.RegistryManager.get(cr.dbname)
422 tz_name = registry.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
425 utc = pytz.timezone('UTC')
426 context_tz = pytz.timezone(tz_name)
427 utc_timestamp = utc.localize(timestamp, is_dst=False) # UTC = no DST
428 return utc_timestamp.astimezone(context_tz)
430 _logger.debug("failed to compute context/client-specific timestamp, "
431 "using the UTC value",
435 class binary(_column):
439 # Binary values may be byte strings (python 2.6 byte array), but
440 # the legacy OpenERP convention is to transfer and store binaries
441 # as base64-encoded strings. The base64 string may be provided as a
442 # unicode in some circumstances, hence the str() cast in symbol_f.
443 # This str coercion will only work for pure ASCII unicode strings,
444 # on purpose - non base64 data must be passed as a 8bit byte strings.
445 _symbol_f = lambda symb: symb and Binary(str(symb)) or None
447 _symbol_set = (_symbol_c, _symbol_f)
448 _symbol_get = lambda self, x: x and str(x)
450 _classic_read = False
453 def __init__(self, string='unknown', filters=None, **args):
454 _column.__init__(self, string=string, **args)
455 self.filters = filters
457 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
470 # If client is requesting only the size of the field, we return it instead
471 # of the content. Presumably a separate request will be done to read the actual
472 # content if it's needed at some point.
473 # TODO: after 6.0 we should consider returning a dict with size and content instead of
474 # having an implicit convention for the value
475 if val and context.get('bin_size_%s' % name, context.get('bin_size')):
476 res[i] = tools.human_size(long(val))
481 class selection(_column):
484 def __init__(self, selection, string='unknown', **args):
485 _column.__init__(self, string=string, **args)
486 self.selection = selection
489 def reify(cls, cr, uid, model, field, context=None):
490 """ Munges the field's ``selection`` attribute as necessary to get
491 something useable out of it: calls it if it's a function, applies
492 translations to labels if it's not.
494 A callable ``selection`` is considered translated on its own.
496 :param orm.Model model:
497 :param _column field:
499 if callable(field.selection):
500 return field.selection(model, cr, uid, context)
502 if not (context and 'lang' in context):
503 return field.selection
505 # field_to_dict isn't given a field name, only a field object, we
506 # need to get the name back in order to perform the translation lookup
508 name for name, column in model._columns.iteritems()
511 translation_filter = "%s,%s" % (model._name, field_name)
512 translate = functools.partial(
513 model.pool['ir.translation']._get_source,
514 cr, uid, translation_filter, 'selection', context['lang'])
517 (value, translate(label))
518 for value, label in field.selection
521 # ---------------------------------------------------------
523 # ---------------------------------------------------------
526 # Values: (0, 0, { fields }) create
527 # (1, ID, { fields }) update
528 # (2, ID) remove (delete)
529 # (3, ID) unlink one (target id or target of relation)
531 # (5) unlink all (only valid for one2many)
534 class many2one(_column):
535 _classic_read = False
536 _classic_write = True
539 _symbol_f = lambda x: x or None
540 _symbol_set = (_symbol_c, _symbol_f)
542 def __init__(self, obj, string='unknown', auto_join=False, **args):
543 _column.__init__(self, string=string, **args)
545 self._auto_join = auto_join
547 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
555 res[r['id']] = r[name]
557 res.setdefault(id, '')
558 obj = obj.pool[self._obj]
560 # build a dictionary of the form {'id_of_distant_resource': name_of_distant_resource}
561 # we use uid=1 because the visibility of a many2one field value (just id and name)
562 # must be the access right of the parent form and not the linked object itself.
563 records = dict(obj.name_get(cr, SUPERUSER_ID,
564 list(set([x for x in res.values() if x and isinstance(x, (int,long))])),
567 if res[id] in records:
568 res[id] = (res[id], records[res[id]])
573 def set(self, cr, obj_src, id, field, values, user=None, context=None):
576 obj = obj_src.pool[self._obj]
577 self._table = obj._table
578 if type(values) == type([]):
581 id_new = obj.create(cr, act[2])
582 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (id_new, id))
584 obj.write(cr, [act[1]], act[2], context=context)
586 cr.execute('delete from '+self._table+' where id=%s', (act[1],))
587 elif act[0] == 3 or act[0] == 5:
588 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
590 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (act[1], id))
593 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (values, id))
595 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
597 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
598 return obj.pool[self._obj].search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context)
602 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
603 return value[1] if isinstance(value, tuple) else tools.ustr(value)
606 class one2many(_column):
607 _classic_read = False
608 _classic_write = False
612 def __init__(self, obj, fields_id, string='unknown', limit=None, auto_join=False, **args):
613 _column.__init__(self, string=string, **args)
615 self._fields_id = fields_id
617 self._auto_join = auto_join
618 #one2many can't be used as condition for defaults
619 assert(self.change_default != True)
621 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
625 context = context.copy()
626 context.update(self._context)
634 domain = self._domain(obj) if callable(self._domain) else self._domain
635 model = obj.pool[self._obj]
636 ids2 = model.search(cr, user, domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
638 for r in model._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
639 if r[self._fields_id] in res:
640 res[r[self._fields_id]].append(r['id'])
645 def set(self, cr, obj, id, field, values, user=None, context=None):
650 context = context.copy()
651 context.update(self._context)
652 context['no_store_function'] = True
655 obj = obj.pool[self._obj]
659 act[2][self._fields_id] = id
660 id_new = obj.create(cr, user, act[2], context=context)
661 result += obj._store_get_values(cr, user, [id_new], act[2].keys(), context)
663 obj.write(cr, user, [act[1]], act[2], context=context)
665 obj.unlink(cr, user, [act[1]], context=context)
667 reverse_rel = obj._all_columns.get(self._fields_id)
668 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
669 # if the model has on delete cascade, just delete the row
670 if reverse_rel.column.ondelete == "cascade":
671 obj.unlink(cr, user, [act[1]], context=context)
673 cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],))
675 # table of the field (parent_model in case of inherit)
676 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
677 field_table = obj.pool[field_model]._table
678 cr.execute("select 1 from {0} where id=%s and {1}=%s".format(field_table, self._fields_id), (act[1], id))
679 if not cr.fetchone():
680 # Must use write() to recompute parent_store structure if needed and check access rules
681 obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
683 reverse_rel = obj._all_columns.get(self._fields_id)
684 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
685 # if the o2m has a static domain we must respect it when unlinking
686 domain = self._domain(obj) if callable(self._domain) else self._domain
687 extra_domain = domain or []
688 ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
689 # If the model has cascade deletion, we delete the rows because it is the intended behavior,
690 # otherwise we only nullify the reverse foreign key column.
691 if reverse_rel.column.ondelete == "cascade":
692 obj.unlink(cr, user, ids_to_unlink, context=context)
694 obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context)
696 # Must use write() to recompute parent_store structure if needed
697 obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
699 cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
700 ids3 = map(lambda x:x[0], cr.fetchall())
701 obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
704 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
705 domain = self._domain(obj) if callable(self._domain) else self._domain
706 return obj.pool[self._obj].name_search(cr, uid, value, domain, operator, context=context,limit=limit)
710 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
711 raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)')
714 # Values: (0, 0, { fields }) create
715 # (1, ID, { fields }) update (write fields to ID)
716 # (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete)
717 # (3, ID) unlink (delete the relationship between the two objects but does not delete ID)
718 # (4, ID) link (add a relationship)
720 # (6, ?, ids) set a list of links
722 class many2many(_column):
723 """Encapsulates the logic of a many-to-many bidirectional relationship, handling the
724 low-level details of the intermediary relationship table transparently.
725 A many-to-many relationship is always symmetrical, and can be declared and accessed
726 from either endpoint model.
727 If ``rel`` (relationship table name), ``id1`` (source foreign key column name)
728 or id2 (destination foreign key column name) are not specified, the system will
729 provide default values. This will by default only allow one single symmetrical
730 many-to-many relationship between the source and destination model.
731 For multiple many-to-many relationship between the same models and for
732 relationships where source and destination models are the same, ``rel``, ``id1``
733 and ``id2`` should be specified explicitly.
735 :param str obj: destination model
736 :param str rel: optional name of the intermediary relationship table. If not specified,
737 a canonical name will be derived based on the alphabetically-ordered
738 model names of the source and destination (in the form: ``amodel_bmodel_rel``).
739 Automatic naming is not possible when the source and destination are
740 the same, for obvious ambiguity reasons.
741 :param str id1: optional name for the column holding the foreign key to the current
742 model in the relationship table. If not specified, a canonical name
743 will be derived based on the model name (in the form: `src_model_id`).
744 :param str id2: optional name for the column holding the foreign key to the destination
745 model in the relationship table. If not specified, a canonical name
746 will be derived based on the model name (in the form: `dest_model_id`)
747 :param str string: field label
749 _classic_read = False
750 _classic_write = False
754 def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
757 _column.__init__(self, string=string, **args)
759 if rel and '.' in rel:
760 raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\
761 'You used %s, which is not a valid SQL table name.')% (string,rel))
767 def _sql_names(self, source_model):
768 """Return the SQL names defining the structure of the m2m relationship table
770 :return: (m2m_table, local_col, dest_col) where m2m_table is the table name,
771 local_col is the name of the column holding the current model's FK, and
772 dest_col is the name of the column holding the destination model's FK, and
774 tbl, col1, col2 = self._rel, self._id1, self._id2
775 if not all((tbl, col1, col2)):
776 # the default table name is based on the stable alphabetical order of tables
777 dest_model = source_model.pool[self._obj]
778 tables = tuple(sorted([source_model._table, dest_model._table]))
780 assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\
781 'is not possible when source and destination models are '\
783 tbl = '%s_%s_rel' % tables
785 col1 = '%s_id' % source_model._table
787 col2 = '%s_id' % dest_model._table
788 return tbl, col1, col2
790 def _get_query_and_where_params(self, cr, model, ids, values, where_params):
791 """ Extracted from ``get`` to facilitate fine-tuning of the generated
793 query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
794 FROM %(rel)s, %(from_c)s \
795 WHERE %(rel)s.%(id1)s IN %%s \
796 AND %(rel)s.%(id2)s = %(tbl)s.id \
802 return query, where_params
804 def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
816 "Specifying offset at a many2many.get() is deprecated and may"
817 " produce unpredictable results.")
818 obj = model.pool[self._obj]
819 rel, id1, id2 = self._sql_names(model)
821 # static domains are lists, and are evaluated both here and on client-side, while string
822 # domains supposed by dynamic and evaluated on client-side only (thus ignored here)
823 # FIXME: make this distinction explicit in API!
824 domain = isinstance(self._domain, list) and self._domain or []
826 wquery = obj._where_calc(cr, user, domain, context=context)
827 obj._apply_ir_rules(cr, user, wquery, 'read', context=context)
828 from_c, where_c, where_params = wquery.get_sql()
830 where_c = ' AND ' + where_c
832 order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0])
835 if self._limit is not None:
836 limit_str = ' LIMIT %d' % self._limit
838 query, where_params = self._get_query_and_where_params(cr, model, ids, {'rel': rel,
845 'order_by': order_by,
849 cr.execute(query, [tuple(ids),] + where_params)
850 for r in cr.fetchall():
851 res[r[1]].append(r[0])
854 def set(self, cr, model, id, name, values, user=None, context=None):
859 rel, id1, id2 = self._sql_names(model)
860 obj = model.pool[self._obj]
862 if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
865 idnew = obj.create(cr, user, act[2], context=context)
866 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew))
868 obj.write(cr, user, [act[1]], act[2], context=context)
870 obj.unlink(cr, user, [act[1]], context=context)
872 cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1]))
874 # following queries are in the same transaction - so should be relatively safe
875 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1]))
876 if not cr.fetchone():
877 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1]))
879 cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,))
882 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
884 d1 = ' and ' + ' and '.join(d1)
887 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)
889 for act_nbr in act[2]:
890 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr))
893 # TODO: use a name_search
895 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
896 return obj.pool[self._obj].search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
899 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
900 raise NotImplementedError('Many2Many columns should not be used as record name (_rec_name)')
903 def get_nice_size(value):
905 if isinstance(value, (int,long)):
907 elif value: # this is supposed to be a string
909 return tools.human_size(size)
911 # See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
912 # and http://bugs.python.org/issue10066
913 invalid_xml_low_bytes = re.compile(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
915 def sanitize_binary_value(value):
916 # binary fields should be 7-bit ASCII base64-encoded data,
917 # but we do additional sanity checks to make sure the values
918 # are not something else that won't pass via XML-RPC
919 if isinstance(value, (xmlrpclib.Binary, tuple, list, dict)):
920 # these builtin types are meant to pass untouched
923 # Handle invalid bytes values that will cause problems
924 # for XML-RPC. See for more info:
925 # - http://bugs.python.org/issue10066
926 # - http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
928 # Coercing to unicode would normally allow it to properly pass via
929 # XML-RPC, transparently encoded as UTF-8 by xmlrpclib.
930 # (this works for _any_ byte values, thanks to the fallback
931 # to latin-1 passthrough encoding when decoding to unicode)
932 value = tools.ustr(value)
934 # Due to Python bug #10066 this could still yield invalid XML
935 # bytes, specifically in the low byte range, that will crash
936 # the decoding side: [\x00-\x08\x0b-\x0c\x0e-\x1f]
937 # So check for low bytes values, and if any, perform
938 # base64 encoding - not very smart or useful, but this is
939 # our last resort to avoid crashing the request.
940 if invalid_xml_low_bytes.search(value):
941 # b64-encode after restoring the pure bytes with latin-1
942 # passthrough encoding
943 value = base64.b64encode(value.encode('latin-1'))
948 # ---------------------------------------------------------
950 # ---------------------------------------------------------
951 class function(_column):
953 A field whose value is computed by a function (rather
954 than being read from the database).
956 :param fnct: the callable that will compute the field value.
957 :param arg: arbitrary value to be passed to ``fnct`` when computing the value.
958 :param fnct_inv: the callable that will allow writing values in that field
959 (if not provided, the field is read-only).
960 :param fnct_inv_arg: arbitrary value to be passed to ``fnct_inv`` when
962 :param str type: type of the field simulated by the function field
963 :param fnct_search: the callable that allows searching on the field
964 (if not provided, search will not return any result).
965 :param store: store computed value in database
966 (see :ref:`The *store* parameter <field-function-store>`).
967 :type store: True or dict specifying triggers for field computation
968 :param multi: name of batch for batch computation of function fields.
969 All fields with the same batch name will be computed by
970 a single function call. This changes the signature of the
973 .. _field-function-fnct: The ``fnct`` parameter
975 .. rubric:: The ``fnct`` parameter
977 The callable implementing the function field must have the following signature:
979 .. function:: fnct(model, cr, uid, ids, field_name(s), arg, context)
981 Implements the function field.
983 :param orm model: model to which the field belongs (should be ``self`` for
985 :param field_name(s): name of the field to compute, or if ``multi`` is provided,
986 list of field names to compute.
987 :type field_name(s): str | [str]
988 :param arg: arbitrary value passed when declaring the function field
990 :return: mapping of ``ids`` to computed values, or if multi is provided,
991 to a map of field_names to computed values
993 The values in the returned dictionary must be of the type specified by the type
994 argument in the field declaration.
996 Here is an example with a simple function ``char`` function field::
999 def compute(self, cr, uid, ids, field_name, arg, context):
1003 _columns['my_char'] = fields.function(compute, type='char', size=50)
1005 # when called with ``ids=[1,2,3]``, ``compute`` could return:
1009 3: False # null values should be returned explicitly too
1012 If ``multi`` is set, then ``field_name`` is replaced by ``field_names``: a list
1013 of the field names that should be computed. Each value in the returned
1014 dictionary must then be a dictionary mapping field names to values.
1016 Here is an example where two function fields (``name`` and ``age``)
1017 are both computed by a single function field::
1020 def compute(self, cr, uid, ids, field_names, arg, context):
1024 _columns['name'] = fields.function(compute_person_data, type='char',\
1025 size=50, multi='person_data')
1026 _columns[''age'] = fields.function(compute_person_data, type='integer',\
1027 multi='person_data')
1029 # when called with ``ids=[1,2,3]``, ``compute_person_data`` could return:
1031 1: {'name': 'Bob', 'age': 23},
1032 2: {'name': 'Sally', 'age': 19},
1033 3: {'name': 'unknown', 'age': False}
1036 .. _field-function-fnct-inv:
1038 .. rubric:: The ``fnct_inv`` parameter
1040 This callable implements the write operation for the function field
1041 and must have the following signature:
1043 .. function:: fnct_inv(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context)
1045 Callable that implements the ``write`` operation for the function field.
1047 :param orm model: model to which the field belongs (should be ``self`` for
1049 :param int id: the identifier of the object to write on
1050 :param str field_name: name of the field to set
1051 :param fnct_inv_arg: arbitrary value passed when declaring the function field
1054 When writing values for a function field, the ``multi`` parameter is ignored.
1056 .. _field-function-fnct-search:
1058 .. rubric:: The ``fnct_search`` parameter
1060 This callable implements the search operation for the function field
1061 and must have the following signature:
1063 .. function:: fnct_search(model, cr, uid, model_again, field_name, criterion, context)
1065 Callable that implements the ``search`` operation for the function field by expanding
1066 a search criterion based on the function field into a new domain based only on
1067 columns that are stored in the database.
1069 :param orm model: model to which the field belongs (should be ``self`` for
1071 :param orm model_again: same value as ``model`` (seriously! this is for backwards
1073 :param str field_name: name of the field to search on
1074 :param list criterion: domain component specifying the search criterion on the field.
1076 :return: domain to use instead of ``criterion`` when performing the search.
1077 This new domain must be based only on columns stored in the database, as it
1078 will be used directly without any translation.
1080 The returned value must be a domain, that is, a list of the form [(field_name, operator, operand)].
1081 The most generic way to implement ``fnct_search`` is to directly search for the records that
1082 match the given ``criterion``, and return their ``ids`` wrapped in a domain, such as
1083 ``[('id','in',[1,3,5])]``.
1085 .. _field-function-store:
1087 .. rubric:: The ``store`` parameter
1089 The ``store`` parameter allows caching the result of the field computation in the
1090 database, and defining the triggers that will invalidate that cache and force a
1091 recomputation of the function field.
1092 When not provided, the field is computed every time its value is read.
1093 The value of ``store`` may be either ``True`` (to recompute the field value whenever
1094 any field in the same record is modified), or a dictionary specifying a more
1095 flexible set of recomputation triggers.
1097 A trigger specification is a dictionary that maps the names of the models that
1098 will trigger the computation, to a tuple describing the trigger rule, in the
1102 'trigger_model': (mapping_function,
1103 ['trigger_field1', 'trigger_field2'],
1107 A trigger rule is defined by a 3-item tuple where:
1109 * The ``mapping_function`` is defined as follows:
1111 .. function:: mapping_function(trigger_model, cr, uid, trigger_ids, context)
1113 Callable that maps record ids of a trigger model to ids of the
1114 corresponding records in the source model (whose field values
1115 need to be recomputed).
1117 :param orm model: trigger_model
1118 :param list trigger_ids: ids of the records of trigger_model that were
1121 :return: list of ids of the source model whose function field values
1122 need to be recomputed
1124 * The second item is a list of the fields who should act as triggers for
1125 the computation. If an empty list is given, all fields will act as triggers.
1126 * The last item is the priority, used to order the triggers when processing them
1127 after any write operation on a model that has function field triggers. The
1128 default priority is 10.
1130 In fact, setting store = True is the same as using the following trigger dict::
1133 'model_itself': (lambda self, cr, uid, ids, context: ids,
1139 _classic_read = False
1140 _classic_write = False
1146 # multi: compute several fields in one call
1148 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):
1149 _column.__init__(self, **args)
1152 self._fnct_inv = fnct_inv
1155 if 'relation' in args:
1156 self._obj = args['relation']
1158 self.digits = args.get('digits', (16,2))
1159 self.digits_compute = args.get('digits_compute', None)
1161 self._fnct_inv_arg = fnct_inv_arg
1165 self._fnct_search = fnct_search
1168 if not fnct_search and not store:
1169 self.selectable = False
1172 if self._type != 'many2one':
1173 # m2o fields need to return tuples with name_get, not just foreign keys
1174 self._classic_read = True
1175 self._classic_write = True
1177 self._symbol_get=lambda x:x and str(x)
1179 self._prefetch = True
1182 self._symbol_c = float._symbol_c
1183 self._symbol_f = float._symbol_f
1184 self._symbol_set = float._symbol_set
1186 if type == 'boolean':
1187 self._symbol_c = boolean._symbol_c
1188 self._symbol_f = boolean._symbol_f
1189 self._symbol_set = boolean._symbol_set
1191 if type == 'integer':
1192 self._symbol_c = integer._symbol_c
1193 self._symbol_f = integer._symbol_f
1194 self._symbol_set = integer._symbol_set
1197 self._symbol_c = char._symbol_c
1198 self._symbol_f = lambda x: _symbol_set_char(self, x)
1199 self._symbol_set = (self._symbol_c, self._symbol_f)
1201 def digits_change(self, cr):
1202 if self._type == 'float':
1203 if self.digits_compute:
1204 self.digits = self.digits_compute(cr)
1206 precision, scale = self.digits
1207 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
1208 precision_digits=scale),
1209 precision_digits=scale))
1211 def search(self, cr, uid, obj, name, args, context=None):
1212 if not self._fnct_search:
1213 #CHECKME: should raise an exception
1215 return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
1217 def postprocess(self, cr, uid, obj, field, value=None, context=None):
1218 return self._postprocess_batch(cr, uid, obj, field, {0: value}, context=context)[0]
1220 def _postprocess_batch(self, cr, uid, obj, field, values, context=None):
1227 field_type = obj._columns[field]._type
1228 new_values = dict(values)
1230 if field_type == "integer":
1231 # integer/long values greater than 2^31-1 are not supported
1232 # in pure XMLRPC, so we have to pass them as floats :-(
1233 # This is not needed for stored fields and non-functional integer
1234 # fields, as their values are constrained by the database backend
1235 # to the same 32bits signed int limit.
1236 for rid, value in values.iteritems():
1237 if value and value > xmlrpclib.MAXINT:
1238 new_values[rid] = __builtin__.float(value)
1240 elif field_type == 'binary':
1241 if context.get('bin_size'):
1242 # client requests only the size of binary fields
1243 for rid, value in values.iteritems():
1245 new_values[rid] = get_nice_size(value)
1246 elif not context.get('bin_raw'):
1247 for rid, value in values.iteritems():
1249 new_values[rid] = sanitize_binary_value(value)
1251 elif field_type == "many2one" and hasattr(obj._columns[field], 'relation'):
1252 # make the result a tuple if it is not already one
1253 if all(isinstance(value, (int, long)) for value in values.values() if value):
1254 obj_model = obj.pool[obj._columns[field].relation]
1255 ids = [i for i in values.values() if i]
1256 dict_names = dict(obj_model.name_get(cr, SUPERUSER_ID, ids, context))
1257 for rid, value in values.iteritems():
1259 new_values[rid] = (value, dict_names[value])
1263 def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
1265 # if we already have a value, don't recompute it.
1266 # This happen if case of stored many2one fields
1267 if values and not multi and name in values[0]:
1268 result = {v['id']: v[name] for v in values}
1269 elif values and multi and all(n in values[0] for n in name):
1270 result = {v['id']: dict((n, v[n]) for n in name) for v in values}
1272 result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
1275 for rid, values in result.iteritems():
1276 for f, v in values.iteritems():
1279 swap.setdefault(f, {})[rid] = v
1281 for field, values in swap.iteritems():
1282 new_values = self._postprocess_batch(cr, uid, obj, field, values, context)
1283 for rid, value in new_values.iteritems():
1284 result[rid][field] = value
1287 result = self._postprocess_batch(cr, uid, obj, name, result, context)
1291 def set(self, cr, obj, id, name, value, user=None, context=None):
1295 self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
1298 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
1299 # Function fields are supposed to emulate a basic field type,
1300 # so they can delegate to the basic type for record name rendering
1301 return globals()[field._type]._as_display_name(field, cr, uid, obj, value, context=context)
1303 # ---------------------------------------------------------
1305 # ---------------------------------------------------------
1307 class related(function):
1308 """Field that points to some data inside another field of the current record.
1313 'foo_id': fields.many2one('my.foo', 'Foo'),
1314 'bar': fields.related('foo_id', 'frol', type='char', string='Frol of Foo'),
1318 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1319 # assume self._arg = ('foo', 'bar', 'baz')
1320 # domain = [(name, op, val)] => search [('foo.bar.baz', op, val)]
1321 field = '.'.join(self._arg)
1322 return map(lambda x: (field, x[1], x[2]), domain)
1324 def _fnct_write(self,obj,cr, uid, ids, field_name, values, args, context=None):
1325 if isinstance(ids, (int, long)):
1327 for record in obj.browse(cr, uid, ids, context=context):
1328 # traverse all fields except the last one
1329 for field in self.arg[:-1]:
1330 record = record[field] or False
1333 elif isinstance(record, list):
1334 # record is the result of a one2many or many2many field
1337 # write on the last field
1338 record.write({self.arg[-1]: values})
1340 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1342 for record in obj.browse(cr, SUPERUSER_ID, ids, context=context):
1344 for field in self.arg:
1345 if isinstance(value, list):
1347 value = value[field] or False
1350 res[record.id] = value
1352 if self._type == 'many2one':
1353 # res[id] is a browse_record or False; convert it to (id, name) or False.
1354 # Perform name_get as root, as seeing the name of a related object depends on
1355 # access right of source document, not target, so user may not have access.
1356 value_ids = list(set(value.id for value in res.itervalues() if value))
1357 value_name = dict(obj.pool[self._obj].name_get(cr, SUPERUSER_ID, value_ids, context=context))
1358 res = dict((id, value and (value.id, value_name[value.id])) for id, value in res.iteritems())
1360 elif self._type in ('one2many', 'many2many'):
1361 # res[id] is a list of browse_record or False; convert it to a list of ids
1362 res = dict((id, value and map(int, value) or []) for id, value in res.iteritems())
1366 def __init__(self, *arg, **args):
1368 self._relations = []
1369 super(related, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1370 if self.store is True:
1371 # TODO: improve here to change self.store = {...} according to related objects
1375 class sparse(function):
1377 def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
1379 + For a many2many field, a list of tuples is expected.
1380 Here is the list of tuple that are accepted, with the corresponding semantics ::
1382 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1383 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1384 (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)
1385 (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)
1386 (4, ID) link to existing record with id = ID (adds a relationship)
1387 (5) unlink all (like using (3,ID) for all linked records)
1388 (6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
1391 [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
1393 + For a one2many field, a lits of tuples is expected.
1394 Here is the list of tuple that are accepted, with the corresponding semantics ::
1396 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1397 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1398 (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)
1401 [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
1404 if self._type == 'many2many':
1405 assert value[0][0] == 6, 'Unsupported m2m value for sparse field: %s' % value
1408 elif self._type == 'one2many':
1411 relation_obj = obj.pool[self.relation]
1413 assert vals[0] in (0,1,2), 'Unsupported o2m value for sparse field: %s' % vals
1415 read_value.append(relation_obj.create(cr, uid, vals[2], context=context))
1417 relation_obj.write(cr, uid, vals[1], vals[2], context=context)
1419 relation_obj.unlink(cr, uid, vals[1], context=context)
1420 read_value.remove(vals[1])
1425 def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
1426 if not type(ids) == list:
1428 records = obj.browse(cr, uid, ids, context=context)
1429 for record in records:
1430 # grab serialized value as object - already deserialized
1431 serialized = getattr(record, self.serialization_field)
1433 # simply delete the key to unset it.
1434 serialized.pop(field_name, None)
1436 serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context)
1437 obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context)
1440 def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None):
1442 records = obj.browse(cr, uid, ids, context=context)
1443 for record in records:
1444 # grab serialized value as object - already deserialized
1445 serialized = getattr(record, self.serialization_field)
1446 results[record.id] = {}
1447 for field_name in field_names:
1448 field_type = obj._columns[field_name]._type
1449 value = serialized.get(field_name, False)
1450 if field_type in ('one2many','many2many'):
1453 # filter out deleted records as superuser
1454 relation_obj = obj.pool[obj._columns[field_name].relation]
1455 value = relation_obj.exists(cr, openerp.SUPERUSER_ID, value)
1456 if type(value) in (int,long) and field_type == 'many2one':
1457 relation_obj = obj.pool[obj._columns[field_name].relation]
1458 # check for deleted record as superuser
1459 if not relation_obj.exists(cr, openerp.SUPERUSER_ID, [value]):
1461 results[record.id][field_name] = value
1464 def __init__(self, serialization_field, **kwargs):
1465 self.serialization_field = serialization_field
1466 super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
1470 # ---------------------------------------------------------
1472 # ---------------------------------------------------------
1474 class dummy(function):
1475 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1478 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1481 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1484 def __init__(self, *arg, **args):
1486 self._relations = []
1487 super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1489 # ---------------------------------------------------------
1491 # ---------------------------------------------------------
1493 class serialized(_column):
1494 """ A field able to store an arbitrary python data structure.
1496 Note: only plain components allowed.
1499 def _symbol_set_struct(val):
1500 return simplejson.dumps(val)
1502 def _symbol_get_struct(self, val):
1503 return simplejson.loads(val or '{}')
1506 _type = 'serialized'
1509 _symbol_f = _symbol_set_struct
1510 _symbol_set = (_symbol_c, _symbol_f)
1511 _symbol_get = _symbol_get_struct
1513 # TODO: review completly this class for speed improvement
1514 class property(function):
1516 def _get_default(self, obj, cr, uid, prop_name, context=None):
1517 return self._get_defaults(obj, cr, uid, [prop_name], context=None)[prop_name]
1519 def _get_defaults(self, obj, cr, uid, prop_names, context=None):
1520 """Get the default values for ``prop_names´´ property fields (result of ir.property.get() function for res_id = False).
1522 :param list of string prop_names: list of name of property fields for those we want the default value
1523 :return: map of property field names to their default value
1526 prop = obj.pool.get('ir.property')
1528 for prop_name in prop_names:
1529 res[prop_name] = prop.get(cr, uid, prop_name, obj._name, context=context)
1532 def _get_by_id(self, obj, cr, uid, prop_name, ids, context=None):
1533 prop = obj.pool.get('ir.property')
1534 vids = [obj._name + ',' + str(oid) for oid in ids]
1535 domain = [('fields_id.model', '=', obj._name), ('fields_id.name', 'in', prop_name)]
1536 if context and context.get('company_id'):
1537 domain += [('company_id', '=', context.get('company_id'))]
1539 domain = [('res_id', 'in', vids)] + domain
1540 return prop.search(cr, uid, domain, context=context)
1542 # TODO: to rewrite more clean
1543 def _fnct_write(self, obj, cr, uid, id, prop_name, id_val, obj_dest, context=None):
1547 def_id = self._field_get(cr, uid, obj._name, prop_name)
1548 company = obj.pool.get('res.company')
1549 cid = company._company_default_get(cr, uid, obj._name, def_id, context=context)
1550 # TODO for trunk: add new parameter company_id to _get_by_id method
1551 context_company = dict(context, company_id=cid)
1552 nids = self._get_by_id(obj, cr, uid, [prop_name], [id], context_company)
1554 cr.execute('DELETE FROM ir_property WHERE id IN %s', (tuple(nids),))
1556 default_val = self._get_default(obj, cr, uid, prop_name, context)
1558 property_create = False
1559 if isinstance(default_val, (openerp.osv.orm.browse_record,
1560 openerp.osv.orm.browse_null)):
1561 if default_val.id != id_val:
1562 property_create = True
1563 elif default_val != id_val:
1564 property_create = True
1567 propdef = obj.pool.get('ir.model.fields').browse(cr, uid, def_id,
1569 prop = obj.pool.get('ir.property')
1570 return prop.create(cr, uid, {
1571 'name': propdef.name,
1573 'res_id': obj._name+','+str(id),
1575 'fields_id': def_id,
1580 def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
1581 prop = obj.pool.get('ir.property')
1582 # get the default values (for res_id = False) for the property fields
1583 default_val = self._get_defaults(obj, cr, uid, prop_names, context)
1585 # build the dictionary that will be returned
1588 res[id] = default_val.copy()
1590 for prop_name in prop_names:
1591 property_field = obj._all_columns.get(prop_name).column
1592 property_destination_obj = property_field._obj if property_field._type == 'many2one' else False
1593 # If the property field is a m2o field, we will append the id of the value to name_get_ids
1594 # in order to make a name_get in batch for all the ids needed.
1597 # get the result of ir.property.get() for this res_id and save it in res if it's existing
1598 obj_reference = obj._name + ',' + str(id)
1599 value = prop.get(cr, uid, prop_name, obj._name, res_id=obj_reference, context=context)
1601 res[id][prop_name] = value
1602 # Check existence as root (as seeing the name of a related
1603 # object depends on access right of source document,
1604 # not target, so user may not have access) in order to avoid
1605 # pointing on an unexisting record.
1606 if property_destination_obj:
1607 if res[id][prop_name] and obj.pool[property_destination_obj].exists(cr, SUPERUSER_ID, res[id][prop_name].id):
1608 name_get_ids[id] = res[id][prop_name].id
1610 res[id][prop_name] = False
1611 if property_destination_obj:
1612 # name_get as root (as seeing the name of a related
1613 # object depends on access right of source document,
1614 # not target, so user may not have access.)
1615 name_get_values = dict(obj.pool[property_destination_obj].name_get(cr, SUPERUSER_ID, name_get_ids.values(), context=context))
1616 # the property field is a m2o, we need to return a tuple with (id, name)
1617 for k, v in name_get_ids.iteritems():
1618 if res[k][prop_name]:
1619 res[k][prop_name] = (v , name_get_values.get(v))
1622 def _field_get(self, cr, uid, model_name, prop):
1623 if not self.field_id.get(cr.dbname):
1624 cr.execute('SELECT id \
1625 FROM ir_model_fields \
1626 WHERE name=%s AND model=%s', (prop, model_name))
1628 self.field_id[cr.dbname] = res and res[0]
1629 return self.field_id[cr.dbname]
1632 def __init__(self, **args):
1634 if 'view_load' in args:
1635 _logger.warning("view_load attribute is deprecated on ir.fields. Args: %r", args)
1636 obj = 'relation' in args and args['relation'] or ''
1637 function.__init__(self, self._fnct_read, False, self._fnct_write, obj=obj, multi='properties', **args)
1643 def field_to_dict(model, cr, user, field, context=None):
1644 """ Return a dictionary representation of a field.
1646 The string, help, and selection attributes (if any) are untranslated. This
1647 representation is the one returned by fields_get() (fields_get() will do
1652 res = {'type': field._type}
1653 # some attributes for m2m/function field are added as debug info only
1654 if isinstance(field, function):
1655 res['function'] = field._fnct and field._fnct.func_name or False
1656 res['store'] = field.store
1657 if isinstance(field.store, dict):
1658 res['store'] = str(field.store)
1659 res['fnct_search'] = field._fnct_search and field._fnct_search.func_name or False
1660 res['fnct_inv'] = field._fnct_inv and field._fnct_inv.func_name or False
1661 res['fnct_inv_arg'] = field._fnct_inv_arg or False
1662 if isinstance(field, many2many):
1663 (table, col1, col2) = field._sql_names(model)
1664 res['m2m_join_columns'] = [col1, col2]
1665 res['m2m_join_table'] = table
1666 for arg in ('string', 'readonly', 'states', 'size', 'group_operator', 'required',
1667 'change_default', 'translate', 'help', 'select', 'selectable', 'groups',
1668 'deprecated', 'digits', 'invisible', 'filters'):
1669 if getattr(field, arg, None):
1670 res[arg] = getattr(field, arg)
1672 if hasattr(field, 'selection'):
1673 res['selection'] = selection.reify(cr, user, model, field, context=context)
1674 if res['type'] in ('one2many', 'many2many', 'many2one'):
1675 res['relation'] = field._obj
1676 res['domain'] = field._domain(model) if callable(field._domain) else field._domain
1677 res['context'] = field._context
1679 if isinstance(field, one2many):
1680 res['relation_field'] = field._fields_id
1685 class column_info(object):
1686 """ Struct containing details about an osv column, either one local to
1687 its model, or one inherited via _inherits.
1693 .. attribute:: column
1695 column instance, subclass of :class:`_column`
1697 .. attribute:: parent_model
1699 if the column is inherited, name of the model that contains it,
1700 ``None`` for local columns.
1702 .. attribute:: parent_column
1704 the name of the column containing the m2o relationship to the
1705 parent model that contains this column, ``None`` for local columns.
1707 .. attribute:: original_parent
1709 if the column is inherited, name of the original parent model that
1710 contains it i.e in case of multilevel inheritance, ``None`` for
1713 def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
1715 self.column = column
1716 self.parent_model = parent_model
1717 self.parent_column = parent_column
1718 self.original_parent = original_parent
1721 return '%s(%s, %s, %s, %s, %s)' % (
1722 self.__class__.__name__, self.name, self.column,
1723 self.parent_model, self.parent_column, self.original_parent)
1725 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: