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.
44 from psycopg2 import Binary
47 import openerp.tools as tools
48 from openerp.tools.translate import _
49 from openerp.tools import float_round, float_repr
50 from openerp.tools import html_sanitize
52 from openerp import SUPERUSER_ID
54 _logger = logging.getLogger(__name__)
56 def _symbol_set(symb):
57 if symb is None or symb == False:
59 elif isinstance(symb, unicode):
60 return symb.encode('utf-8')
64 class _column(object):
65 """ Base of all fields, a database column
67 An instance of this object is a *description* of a database column. It will
68 not hold any data, but only provide the methods to manipulate data of an
69 ORM record or even prepare/update the database to hold such a field of data.
80 _symbol_f = _symbol_set
81 _symbol_set = (_symbol_c, _symbol_f)
84 # used to hide a certain field type in the list of field types
87 def __init__(self, string='unknown', required=False, readonly=False, domain=None, context=None, states=None, priority=0, change_default=False, size=None, ondelete=None, translate=False, select=False, manual=False, **args):
90 The 'manual' keyword argument specifies if the field is a custom one.
91 It corresponds to the 'state' column in ir_model_fields.
98 self.states = states or {}
100 self.readonly = readonly
101 self.required = required
103 self.help = args.get('help', '')
104 self.priority = priority
105 self.change_default = change_default
106 self.ondelete = ondelete.lower() if ondelete else None # defaults to 'set null' in ORM
107 self.translate = translate
108 self._domain = domain
109 self._context = context
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, **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.get(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 model = obj.pool.get(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)
243 if x is None or x == False:
245 return html_sanitize(x)
247 _symbol_set = (_symbol_c, _symbol_f)
251 class float(_column):
254 _symbol_f = lambda x: __builtin__.float(x or 0.0)
255 _symbol_set = (_symbol_c, _symbol_f)
256 _symbol_get = lambda self,x: x or 0.0
258 def __init__(self, string='unknown', digits=None, digits_compute=None, required=False, **args):
259 _column.__init__(self, string=string, required=required, **args)
261 # synopsis: digits_compute(cr) -> (precision, scale)
262 self.digits_compute = digits_compute
264 def digits_change(self, cr):
265 if self.digits_compute:
266 self.digits = self.digits_compute(cr)
268 precision, scale = self.digits
269 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
270 precision_digits=scale),
271 precision_digits=scale))
278 """ Returns the current date in a format fit for being a
279 default value to a ``date`` field.
281 This method should be provided as is to the _defaults dict, it
282 should not be called.
284 return DT.date.today().strftime(
285 tools.DEFAULT_SERVER_DATE_FORMAT)
288 def context_today(model, cr, uid, context=None, timestamp=None):
289 """Returns the current date as seen in the client's timezone
290 in a format fit for date fields.
291 This method may be passed as value to initialize _defaults.
293 :param Model model: model (osv) for which the date value is being
294 computed - automatically passed when used in
296 :param datetime timestamp: optional datetime value to use instead of
297 the current date and time (must be a
298 datetime, regular dates can't be converted
300 :param dict context: the 'tz' key in the context should give the
301 name of the User/Client timezone (otherwise
305 today = timestamp or DT.datetime.now()
307 if context and context.get('tz'):
308 tz_name = context['tz']
310 tz_name = model.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
313 utc = pytz.timezone('UTC')
314 context_tz = pytz.timezone(tz_name)
315 utc_today = utc.localize(today, is_dst=False) # UTC = no DST
316 context_today = utc_today.astimezone(context_tz)
318 _logger.debug("failed to compute context/client-specific today date, "
319 "using the UTC value for `today`",
321 return (context_today or today).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
323 class datetime(_column):
327 """ Returns the current datetime in a format fit for being a
328 default value to a ``datetime`` field.
330 This method should be provided as is to the _defaults dict, it
331 should not be called.
333 return DT.datetime.now().strftime(
334 tools.DEFAULT_SERVER_DATETIME_FORMAT)
337 def context_timestamp(cr, uid, timestamp, context=None):
338 """Returns the given timestamp converted to the client's timezone.
339 This method is *not* meant for use as a _defaults initializer,
340 because datetime fields are automatically converted upon
341 display on client side. For _defaults you :meth:`fields.datetime.now`
342 should be used instead.
344 :param datetime timestamp: naive datetime value (expressed in UTC)
345 to be converted to the client timezone
346 :param dict context: the 'tz' key in the context should give the
347 name of the User/Client timezone (otherwise
350 :return: timestamp converted to timezone-aware datetime in context
353 assert isinstance(timestamp, DT.datetime), 'Datetime instance expected'
354 if context and context.get('tz'):
355 tz_name = context['tz']
357 registry = openerp.modules.registry.RegistryManager.get(cr.dbname)
358 tz_name = registry.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
361 utc = pytz.timezone('UTC')
362 context_tz = pytz.timezone(tz_name)
363 utc_timestamp = utc.localize(timestamp, is_dst=False) # UTC = no DST
364 return utc_timestamp.astimezone(context_tz)
366 _logger.debug("failed to compute context/client-specific timestamp, "
367 "using the UTC value",
371 class binary(_column):
375 # Binary values may be byte strings (python 2.6 byte array), but
376 # the legacy OpenERP convention is to transfer and store binaries
377 # as base64-encoded strings. The base64 string may be provided as a
378 # unicode in some circumstances, hence the str() cast in symbol_f.
379 # This str coercion will only work for pure ASCII unicode strings,
380 # on purpose - non base64 data must be passed as a 8bit byte strings.
381 _symbol_f = lambda symb: symb and Binary(str(symb)) or None
383 _symbol_set = (_symbol_c, _symbol_f)
384 _symbol_get = lambda self, x: x and str(x)
386 _classic_read = False
389 def __init__(self, string='unknown', filters=None, **args):
390 _column.__init__(self, string=string, **args)
391 self.filters = filters
393 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
406 # If client is requesting only the size of the field, we return it instead
407 # of the content. Presumably a separate request will be done to read the actual
408 # content if it's needed at some point.
409 # TODO: after 6.0 we should consider returning a dict with size and content instead of
410 # having an implicit convention for the value
411 if val and context.get('bin_size_%s' % name, context.get('bin_size')):
412 res[i] = tools.human_size(long(val))
417 class selection(_column):
420 def __init__(self, selection, string='unknown', **args):
421 _column.__init__(self, string=string, **args)
422 self.selection = selection
424 # ---------------------------------------------------------
426 # ---------------------------------------------------------
429 # Values: (0, 0, { fields }) create
430 # (1, ID, { fields }) update
431 # (2, ID) remove (delete)
432 # (3, ID) unlink one (target id or target of relation)
434 # (5) unlink all (only valid for one2many)
437 class many2one(_column):
438 _classic_read = False
439 _classic_write = True
442 _symbol_f = lambda x: x or None
443 _symbol_set = (_symbol_c, _symbol_f)
445 def __init__(self, obj, string='unknown', auto_join=False, **args):
446 _column.__init__(self, string=string, **args)
448 self._auto_join = auto_join
450 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
458 res[r['id']] = r[name]
460 res.setdefault(id, '')
461 obj = obj.pool.get(self._obj)
463 # build a dictionary of the form {'id_of_distant_resource': name_of_distant_resource}
464 # we use uid=1 because the visibility of a many2one field value (just id and name)
465 # must be the access right of the parent form and not the linked object itself.
466 records = dict(obj.name_get(cr, SUPERUSER_ID,
467 list(set([x for x in res.values() if x and isinstance(x, (int,long))])),
470 if res[id] in records:
471 res[id] = (res[id], records[res[id]])
476 def set(self, cr, obj_src, id, field, values, user=None, context=None):
479 obj = obj_src.pool.get(self._obj)
480 self._table = obj_src.pool.get(self._obj)._table
481 if type(values) == type([]):
484 id_new = obj.create(cr, act[2])
485 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (id_new, id))
487 obj.write(cr, [act[1]], act[2], context=context)
489 cr.execute('delete from '+self._table+' where id=%s', (act[1],))
490 elif act[0] == 3 or act[0] == 5:
491 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
493 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (act[1], id))
496 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (values, id))
498 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
500 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
501 return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context)
505 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
506 return value[1] if isinstance(value, tuple) else tools.ustr(value)
509 class one2many(_column):
510 _classic_read = False
511 _classic_write = False
515 def __init__(self, obj, fields_id, string='unknown', limit=None, auto_join=False, **args):
516 _column.__init__(self, string=string, **args)
518 self._fields_id = fields_id
520 self._auto_join = auto_join
521 #one2many can't be used as condition for defaults
522 assert(self.change_default != True)
524 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
528 context = context.copy()
529 context.update(self._context)
537 domain = self._domain(obj) if callable(self._domain) else self._domain
538 ids2 = obj.pool.get(self._obj).search(cr, user, domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
539 for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
540 if r[self._fields_id] in res:
541 res[r[self._fields_id]].append(r['id'])
544 def set(self, cr, obj, id, field, values, user=None, context=None):
549 context = context.copy()
550 context.update(self._context)
551 context['no_store_function'] = True
554 _table = obj.pool.get(self._obj)._table
555 obj = obj.pool.get(self._obj)
558 act[2][self._fields_id] = id
559 id_new = obj.create(cr, user, act[2], context=context)
560 result += obj._store_get_values(cr, user, [id_new], act[2].keys(), context)
562 obj.write(cr, user, [act[1]], act[2], context=context)
564 obj.unlink(cr, user, [act[1]], context=context)
566 reverse_rel = obj._all_columns.get(self._fields_id)
567 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
568 # if the model has on delete cascade, just delete the row
569 if reverse_rel.column.ondelete == "cascade":
570 obj.unlink(cr, user, [act[1]], context=context)
572 cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],))
574 # table of the field (parent_model in case of inherit)
575 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
576 field_table = obj.pool[field_model]._table
577 cr.execute("select 1 from {0} where id=%s and {1}=%s".format(field_table, self._fields_id), (act[1], id))
578 if not cr.fetchone():
579 # Must use write() to recompute parent_store structure if needed and check access rules
580 obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
582 reverse_rel = obj._all_columns.get(self._fields_id)
583 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
584 # if the o2m has a static domain we must respect it when unlinking
585 domain = self._domain(obj) if callable(self._domain) else self._domain
586 extra_domain = domain or []
587 ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
588 # If the model has cascade deletion, we delete the rows because it is the intended behavior,
589 # otherwise we only nullify the reverse foreign key column.
590 if reverse_rel.column.ondelete == "cascade":
591 obj.unlink(cr, user, ids_to_unlink, context=context)
593 obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context)
595 # Must use write() to recompute parent_store structure if needed
596 obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
598 cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
599 ids3 = map(lambda x:x[0], cr.fetchall())
600 obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
603 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
604 domain = self._domain(obj) if callable(self._domain) else self._domain
605 return obj.pool.get(self._obj).name_search(cr, uid, value, domain, operator, context=context,limit=limit)
609 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
610 raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)')
613 # Values: (0, 0, { fields }) create
614 # (1, ID, { fields }) update (write fields to ID)
615 # (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete)
616 # (3, ID) unlink (delete the relationship between the two objects but does not delete ID)
617 # (4, ID) link (add a relationship)
619 # (6, ?, ids) set a list of links
621 class many2many(_column):
622 """Encapsulates the logic of a many-to-many bidirectional relationship, handling the
623 low-level details of the intermediary relationship table transparently.
624 A many-to-many relationship is always symmetrical, and can be declared and accessed
625 from either endpoint model.
626 If ``rel`` (relationship table name), ``id1`` (source foreign key column name)
627 or id2 (destination foreign key column name) are not specified, the system will
628 provide default values. This will by default only allow one single symmetrical
629 many-to-many relationship between the source and destination model.
630 For multiple many-to-many relationship between the same models and for
631 relationships where source and destination models are the same, ``rel``, ``id1``
632 and ``id2`` should be specified explicitly.
634 :param str obj: destination model
635 :param str rel: optional name of the intermediary relationship table. If not specified,
636 a canonical name will be derived based on the alphabetically-ordered
637 model names of the source and destination (in the form: ``amodel_bmodel_rel``).
638 Automatic naming is not possible when the source and destination are
639 the same, for obvious ambiguity reasons.
640 :param str id1: optional name for the column holding the foreign key to the current
641 model in the relationship table. If not specified, a canonical name
642 will be derived based on the model name (in the form: `src_model_id`).
643 :param str id2: optional name for the column holding the foreign key to the destination
644 model in the relationship table. If not specified, a canonical name
645 will be derived based on the model name (in the form: `dest_model_id`)
646 :param str string: field label
648 _classic_read = False
649 _classic_write = False
653 def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
656 _column.__init__(self, string=string, **args)
658 if rel and '.' in rel:
659 raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\
660 'You used %s, which is not a valid SQL table name.')% (string,rel))
666 def _sql_names(self, source_model):
667 """Return the SQL names defining the structure of the m2m relationship table
669 :return: (m2m_table, local_col, dest_col) where m2m_table is the table name,
670 local_col is the name of the column holding the current model's FK, and
671 dest_col is the name of the column holding the destination model's FK, and
673 tbl, col1, col2 = self._rel, self._id1, self._id2
674 if not all((tbl, col1, col2)):
675 # the default table name is based on the stable alphabetical order of tables
676 dest_model = source_model.pool.get(self._obj)
677 tables = tuple(sorted([source_model._table, dest_model._table]))
679 assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\
680 'is not possible when source and destination models are '\
682 tbl = '%s_%s_rel' % tables
684 col1 = '%s_id' % source_model._table
686 col2 = '%s_id' % dest_model._table
687 return tbl, col1, col2
689 def _get_query_and_where_params(self, cr, model, ids, values, where_params):
690 """ Extracted from ``get`` to facilitate fine-tuning of the generated
692 query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
693 FROM %(rel)s, %(from_c)s \
694 WHERE %(rel)s.%(id1)s IN %%s \
695 AND %(rel)s.%(id2)s = %(tbl)s.id \
701 return query, where_params
703 def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
715 "Specifying offset at a many2many.get() is deprecated and may"
716 " produce unpredictable results.")
717 obj = model.pool.get(self._obj)
718 rel, id1, id2 = self._sql_names(model)
720 # static domains are lists, and are evaluated both here and on client-side, while string
721 # domains supposed by dynamic and evaluated on client-side only (thus ignored here)
722 # FIXME: make this distinction explicit in API!
723 domain = isinstance(self._domain, list) and self._domain or []
725 wquery = obj._where_calc(cr, user, domain, context=context)
726 obj._apply_ir_rules(cr, user, wquery, 'read', context=context)
727 from_c, where_c, where_params = wquery.get_sql()
729 where_c = ' AND ' + where_c
731 order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0])
734 if self._limit is not None:
735 limit_str = ' LIMIT %d' % self._limit
737 query, where_params = self._get_query_and_where_params(cr, model, ids, {'rel': rel,
744 'order_by': order_by,
748 cr.execute(query, [tuple(ids),] + where_params)
749 for r in cr.fetchall():
750 res[r[1]].append(r[0])
753 def set(self, cr, model, id, name, values, user=None, context=None):
758 rel, id1, id2 = self._sql_names(model)
759 obj = model.pool.get(self._obj)
761 if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
764 idnew = obj.create(cr, user, act[2], context=context)
765 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew))
767 obj.write(cr, user, [act[1]], act[2], context=context)
769 obj.unlink(cr, user, [act[1]], context=context)
771 cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1]))
773 # following queries are in the same transaction - so should be relatively safe
774 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1]))
775 if not cr.fetchone():
776 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1]))
778 cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,))
781 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
783 d1 = ' and ' + ' and '.join(d1)
786 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)
788 for act_nbr in act[2]:
789 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr))
792 # TODO: use a name_search
794 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
795 return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
798 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
799 raise NotImplementedError('Many2Many columns should not be used as record name (_rec_name)')
802 def get_nice_size(value):
804 if isinstance(value, (int,long)):
806 elif value: # this is supposed to be a string
808 return tools.human_size(size)
810 # See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
811 # and http://bugs.python.org/issue10066
812 invalid_xml_low_bytes = re.compile(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
814 def sanitize_binary_value(value):
815 # binary fields should be 7-bit ASCII base64-encoded data,
816 # but we do additional sanity checks to make sure the values
817 # are not something else that won't pass via XML-RPC
818 if isinstance(value, (xmlrpclib.Binary, tuple, list, dict)):
819 # these builtin types are meant to pass untouched
822 # Handle invalid bytes values that will cause problems
823 # for XML-RPC. See for more info:
824 # - http://bugs.python.org/issue10066
825 # - http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
827 # Coercing to unicode would normally allow it to properly pass via
828 # XML-RPC, transparently encoded as UTF-8 by xmlrpclib.
829 # (this works for _any_ byte values, thanks to the fallback
830 # to latin-1 passthrough encoding when decoding to unicode)
831 value = tools.ustr(value)
833 # Due to Python bug #10066 this could still yield invalid XML
834 # bytes, specifically in the low byte range, that will crash
835 # the decoding side: [\x00-\x08\x0b-\x0c\x0e-\x1f]
836 # So check for low bytes values, and if any, perform
837 # base64 encoding - not very smart or useful, but this is
838 # our last resort to avoid crashing the request.
839 if invalid_xml_low_bytes.search(value):
840 # b64-encode after restoring the pure bytes with latin-1
841 # passthrough encoding
842 value = base64.b64encode(value.encode('latin-1'))
847 # ---------------------------------------------------------
849 # ---------------------------------------------------------
850 class function(_column):
852 A field whose value is computed by a function (rather
853 than being read from the database).
855 :param fnct: the callable that will compute the field value.
856 :param arg: arbitrary value to be passed to ``fnct`` when computing the value.
857 :param fnct_inv: the callable that will allow writing values in that field
858 (if not provided, the field is read-only).
859 :param fnct_inv_arg: arbitrary value to be passed to ``fnct_inv`` when
861 :param str type: type of the field simulated by the function field
862 :param fnct_search: the callable that allows searching on the field
863 (if not provided, search will not return any result).
864 :param store: store computed value in database
865 (see :ref:`The *store* parameter <field-function-store>`).
866 :type store: True or dict specifying triggers for field computation
867 :param multi: name of batch for batch computation of function fields.
868 All fields with the same batch name will be computed by
869 a single function call. This changes the signature of the
872 .. _field-function-fnct: The ``fnct`` parameter
874 .. rubric:: The ``fnct`` parameter
876 The callable implementing the function field must have the following signature:
878 .. function:: fnct(model, cr, uid, ids, field_name(s), arg, context)
880 Implements the function field.
882 :param orm model: model to which the field belongs (should be ``self`` for
884 :param field_name(s): name of the field to compute, or if ``multi`` is provided,
885 list of field names to compute.
886 :type field_name(s): str | [str]
887 :param arg: arbitrary value passed when declaring the function field
889 :return: mapping of ``ids`` to computed values, or if multi is provided,
890 to a map of field_names to computed values
892 The values in the returned dictionary must be of the type specified by the type
893 argument in the field declaration.
895 Here is an example with a simple function ``char`` function field::
898 def compute(self, cr, uid, ids, field_name, arg, context):
902 _columns['my_char'] = fields.function(compute, type='char', size=50)
904 # when called with ``ids=[1,2,3]``, ``compute`` could return:
908 3: False # null values should be returned explicitly too
911 If ``multi`` is set, then ``field_name`` is replaced by ``field_names``: a list
912 of the field names that should be computed. Each value in the returned
913 dictionary must then be a dictionary mapping field names to values.
915 Here is an example where two function fields (``name`` and ``age``)
916 are both computed by a single function field::
919 def compute(self, cr, uid, ids, field_names, arg, context):
923 _columns['name'] = fields.function(compute_person_data, type='char',\
924 size=50, multi='person_data')
925 _columns[''age'] = fields.function(compute_person_data, type='integer',\
928 # when called with ``ids=[1,2,3]``, ``compute_person_data`` could return:
930 1: {'name': 'Bob', 'age': 23},
931 2: {'name': 'Sally', 'age': 19},
932 3: {'name': 'unknown', 'age': False}
935 .. _field-function-fnct-inv:
937 .. rubric:: The ``fnct_inv`` parameter
939 This callable implements the write operation for the function field
940 and must have the following signature:
942 .. function:: fnct_inv(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context)
944 Callable that implements the ``write`` operation for the function field.
946 :param orm model: model to which the field belongs (should be ``self`` for
948 :param int id: the identifier of the object to write on
949 :param str field_name: name of the field to set
950 :param fnct_inv_arg: arbitrary value passed when declaring the function field
953 When writing values for a function field, the ``multi`` parameter is ignored.
955 .. _field-function-fnct-search:
957 .. rubric:: The ``fnct_search`` parameter
959 This callable implements the search operation for the function field
960 and must have the following signature:
962 .. function:: fnct_search(model, cr, uid, model_again, field_name, criterion, context)
964 Callable that implements the ``search`` operation for the function field by expanding
965 a search criterion based on the function field into a new domain based only on
966 columns that are stored in the database.
968 :param orm model: model to which the field belongs (should be ``self`` for
970 :param orm model_again: same value as ``model`` (seriously! this is for backwards
972 :param str field_name: name of the field to search on
973 :param list criterion: domain component specifying the search criterion on the field.
975 :return: domain to use instead of ``criterion`` when performing the search.
976 This new domain must be based only on columns stored in the database, as it
977 will be used directly without any translation.
979 The returned value must be a domain, that is, a list of the form [(field_name, operator, operand)].
980 The most generic way to implement ``fnct_search`` is to directly search for the records that
981 match the given ``criterion``, and return their ``ids`` wrapped in a domain, such as
982 ``[('id','in',[1,3,5])]``.
984 .. _field-function-store:
986 .. rubric:: The ``store`` parameter
988 The ``store`` parameter allows caching the result of the field computation in the
989 database, and defining the triggers that will invalidate that cache and force a
990 recomputation of the function field.
991 When not provided, the field is computed every time its value is read.
992 The value of ``store`` may be either ``True`` (to recompute the field value whenever
993 any field in the same record is modified), or a dictionary specifying a more
994 flexible set of recomputation triggers.
996 A trigger specification is a dictionary that maps the names of the models that
997 will trigger the computation, to a tuple describing the trigger rule, in the
1001 'trigger_model': (mapping_function,
1002 ['trigger_field1', 'trigger_field2'],
1006 A trigger rule is defined by a 3-item tuple where:
1008 * The ``mapping_function`` is defined as follows:
1010 .. function:: mapping_function(trigger_model, cr, uid, trigger_ids, context)
1012 Callable that maps record ids of a trigger model to ids of the
1013 corresponding records in the source model (whose field values
1014 need to be recomputed).
1016 :param orm model: trigger_model
1017 :param list trigger_ids: ids of the records of trigger_model that were
1020 :return: list of ids of the source model whose function field values
1021 need to be recomputed
1023 * The second item is a list of the fields who should act as triggers for
1024 the computation. If an empty list is given, all fields will act as triggers.
1025 * The last item is the priority, used to order the triggers when processing them
1026 after any write operation on a model that has function field triggers. The
1027 default priority is 10.
1029 In fact, setting store = True is the same as using the following trigger dict::
1032 'model_itself': (lambda self, cr, uid, ids, context: ids,
1038 _classic_read = False
1039 _classic_write = False
1045 # multi: compute several fields in one call
1047 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):
1048 _column.__init__(self, **args)
1051 self._fnct_inv = fnct_inv
1054 if 'relation' in args:
1055 self._obj = args['relation']
1057 self.digits = args.get('digits', (16,2))
1058 self.digits_compute = args.get('digits_compute', None)
1060 self._fnct_inv_arg = fnct_inv_arg
1064 self._fnct_search = fnct_search
1067 if not fnct_search and not store:
1068 self.selectable = False
1071 if self._type != 'many2one':
1072 # m2o fields need to return tuples with name_get, not just foreign keys
1073 self._classic_read = True
1074 self._classic_write = True
1076 self._symbol_get=lambda x:x and str(x)
1078 self._prefetch = True
1081 self._symbol_c = float._symbol_c
1082 self._symbol_f = float._symbol_f
1083 self._symbol_set = float._symbol_set
1085 if type == 'boolean':
1086 self._symbol_c = boolean._symbol_c
1087 self._symbol_f = boolean._symbol_f
1088 self._symbol_set = boolean._symbol_set
1090 if type == 'integer':
1091 self._symbol_c = integer._symbol_c
1092 self._symbol_f = integer._symbol_f
1093 self._symbol_set = integer._symbol_set
1096 self._symbol_c = char._symbol_c
1097 self._symbol_f = lambda x: _symbol_set_char(self, x)
1098 self._symbol_set = (self._symbol_c, self._symbol_f)
1100 def digits_change(self, cr):
1101 if self._type == 'float':
1102 if self.digits_compute:
1103 self.digits = self.digits_compute(cr)
1105 precision, scale = self.digits
1106 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
1107 precision_digits=scale),
1108 precision_digits=scale))
1110 def search(self, cr, uid, obj, name, args, context=None):
1111 if not self._fnct_search:
1112 #CHECKME: should raise an exception
1114 return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
1116 def postprocess(self, cr, uid, obj, field, value=None, context=None):
1120 field_type = obj._columns[field]._type
1121 if field_type == "many2one":
1122 # make the result a tuple if it is not already one
1123 if isinstance(value, (int,long)) and hasattr(obj._columns[field], 'relation'):
1124 obj_model = obj.pool.get(obj._columns[field].relation)
1125 dict_names = dict(obj_model.name_get(cr, SUPERUSER_ID, [value], context))
1126 result = (value, dict_names[value])
1128 if field_type == 'binary':
1129 if context.get('bin_size'):
1130 # client requests only the size of binary fields
1131 result = get_nice_size(value)
1132 elif not context.get('bin_raw'):
1133 result = sanitize_binary_value(value)
1135 if field_type == "integer" and value > xmlrpclib.MAXINT:
1136 # integer/long values greater than 2^31-1 are not supported
1137 # in pure XMLRPC, so we have to pass them as floats :-(
1138 # This is not needed for stored fields and non-functional integer
1139 # fields, as their values are constrained by the database backend
1140 # to the same 32bits signed int limit.
1141 result = __builtin__.float(value)
1144 def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
1146 # if we already have a value, don't recompute it.
1147 # This happen if case of stored many2one fields
1148 if values and not multi and name in values[0]:
1149 result = {v['id']: v[name] for v in values}
1150 elif values and multi and all(n in values[0] for n in name):
1151 result = {v['id']: dict((n, v[n]) for n in name) for v in values}
1153 result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
1155 if multi and id in result:
1156 for field, value in result[id].iteritems():
1158 result[id][field] = self.postprocess(cr, uid, obj, field, value, context)
1159 elif result.get(id):
1160 result[id] = self.postprocess(cr, uid, obj, name, result[id], context)
1163 def set(self, cr, obj, id, name, value, user=None, context=None):
1167 self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
1170 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
1171 # Function fields are supposed to emulate a basic field type,
1172 # so they can delegate to the basic type for record name rendering
1173 return globals()[field._type]._as_display_name(field, cr, uid, obj, value, context=context)
1175 # ---------------------------------------------------------
1177 # ---------------------------------------------------------
1179 class related(function):
1180 """Field that points to some data inside another field of the current record.
1185 'foo_id': fields.many2one('my.foo', 'Foo'),
1186 'bar': fields.related('foo_id', 'frol', type='char', string='Frol of Foo'),
1190 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1191 # assume self._arg = ('foo', 'bar', 'baz')
1192 # domain = [(name, op, val)] => search [('foo.bar.baz', op, val)]
1193 field = '.'.join(self._arg)
1194 return map(lambda x: (field, x[1], x[2]), domain)
1196 def _fnct_write(self,obj,cr, uid, ids, field_name, values, args, context=None):
1197 if isinstance(ids, (int, long)):
1199 for record in obj.browse(cr, uid, ids, context=context):
1200 # traverse all fields except the last one
1201 for field in self.arg[:-1]:
1202 record = record[field] or False
1205 elif isinstance(record, list):
1206 # record is the result of a one2many or many2many field
1209 # write on the last field
1210 record.write({self.arg[-1]: values})
1212 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1214 for record in obj.browse(cr, SUPERUSER_ID, ids, context=context):
1216 for field in self.arg:
1217 if isinstance(value, list):
1219 value = value[field] or False
1222 res[record.id] = value
1224 if self._type == 'many2one':
1225 # res[id] is a browse_record or False; convert it to (id, name) or False.
1226 # Perform name_get as root, as seeing the name of a related object depends on
1227 # access right of source document, not target, so user may not have access.
1228 value_ids = list(set(value.id for value in res.itervalues() if value))
1229 value_name = dict(obj.pool.get(self._obj).name_get(cr, SUPERUSER_ID, value_ids, context=context))
1230 res = dict((id, value and (value.id, value_name[value.id])) for id, value in res.iteritems())
1232 elif self._type in ('one2many', 'many2many'):
1233 # res[id] is a list of browse_record or False; convert it to a list of ids
1234 res = dict((id, value and map(int, value) or []) for id, value in res.iteritems())
1238 def __init__(self, *arg, **args):
1240 self._relations = []
1241 super(related, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1242 if self.store is True:
1243 # TODO: improve here to change self.store = {...} according to related objects
1247 class sparse(function):
1249 def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
1251 + For a many2many field, a list of tuples is expected.
1252 Here is the list of tuple that are accepted, with the corresponding semantics ::
1254 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1255 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1256 (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)
1257 (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)
1258 (4, ID) link to existing record with id = ID (adds a relationship)
1259 (5) unlink all (like using (3,ID) for all linked records)
1260 (6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
1263 [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
1265 + For a one2many field, a lits of tuples is expected.
1266 Here is the list of tuple that are accepted, with the corresponding semantics ::
1268 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1269 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1270 (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)
1273 [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
1276 if self._type == 'many2many':
1277 assert value[0][0] == 6, 'Unsupported m2m value for sparse field: %s' % value
1280 elif self._type == 'one2many':
1283 relation_obj = obj.pool.get(self.relation)
1285 assert vals[0] in (0,1,2), 'Unsupported o2m value for sparse field: %s' % vals
1287 read_value.append(relation_obj.create(cr, uid, vals[2], context=context))
1289 relation_obj.write(cr, uid, vals[1], vals[2], context=context)
1291 relation_obj.unlink(cr, uid, vals[1], context=context)
1292 read_value.remove(vals[1])
1297 def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
1298 if not type(ids) == list:
1300 records = obj.browse(cr, uid, ids, context=context)
1301 for record in records:
1302 # grab serialized value as object - already deserialized
1303 serialized = getattr(record, self.serialization_field)
1305 # simply delete the key to unset it.
1306 serialized.pop(field_name, None)
1308 serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context)
1309 obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context)
1312 def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None):
1314 records = obj.browse(cr, uid, ids, context=context)
1315 for record in records:
1316 # grab serialized value as object - already deserialized
1317 serialized = getattr(record, self.serialization_field)
1318 results[record.id] = {}
1319 for field_name in field_names:
1320 field_type = obj._columns[field_name]._type
1321 value = serialized.get(field_name, False)
1322 if field_type in ('one2many','many2many'):
1325 # filter out deleted records as superuser
1326 relation_obj = obj.pool.get(obj._columns[field_name].relation)
1327 value = relation_obj.exists(cr, openerp.SUPERUSER_ID, value)
1328 if type(value) in (int,long) and field_type == 'many2one':
1329 relation_obj = obj.pool.get(obj._columns[field_name].relation)
1330 # check for deleted record as superuser
1331 if not relation_obj.exists(cr, openerp.SUPERUSER_ID, [value]):
1333 results[record.id][field_name] = value
1336 def __init__(self, serialization_field, **kwargs):
1337 self.serialization_field = serialization_field
1338 super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
1342 # ---------------------------------------------------------
1344 # ---------------------------------------------------------
1346 class dummy(function):
1347 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1350 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1353 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1356 def __init__(self, *arg, **args):
1358 self._relations = []
1359 super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=None, **args)
1361 # ---------------------------------------------------------
1363 # ---------------------------------------------------------
1365 class serialized(_column):
1366 """ A field able to store an arbitrary python data structure.
1368 Note: only plain components allowed.
1371 def _symbol_set_struct(val):
1372 return simplejson.dumps(val)
1374 def _symbol_get_struct(self, val):
1375 return simplejson.loads(val or '{}')
1378 _type = 'serialized'
1381 _symbol_f = _symbol_set_struct
1382 _symbol_set = (_symbol_c, _symbol_f)
1383 _symbol_get = _symbol_get_struct
1385 # TODO: review completly this class for speed improvement
1386 class property(function):
1388 def _get_default(self, obj, cr, uid, prop_name, context=None):
1389 return self._get_defaults(obj, cr, uid, [prop_name], context=None)[prop_name]
1391 def _get_defaults(self, obj, cr, uid, prop_names, context=None):
1392 """Get the default values for ``prop_names´´ property fields (result of ir.property.get() function for res_id = False).
1394 :param list of string prop_names: list of name of property fields for those we want the default value
1395 :return: map of property field names to their default value
1398 prop = obj.pool.get('ir.property')
1400 for prop_name in prop_names:
1401 res[prop_name] = prop.get(cr, uid, prop_name, obj._name, context=context)
1404 def _get_by_id(self, obj, cr, uid, prop_name, ids, context=None):
1405 prop = obj.pool.get('ir.property')
1406 vids = [obj._name + ',' + str(oid) for oid in ids]
1407 domain = [('fields_id.model', '=', obj._name), ('fields_id.name', 'in', prop_name)]
1408 if context and context.get('company_id'):
1409 domain += [('company_id', '=', context.get('company_id'))]
1411 domain = [('res_id', 'in', vids)] + domain
1412 return prop.search(cr, uid, domain, context=context)
1414 # TODO: to rewrite more clean
1415 def _fnct_write(self, obj, cr, uid, id, prop_name, id_val, obj_dest, context=None):
1419 def_id = self._field_get(cr, uid, obj._name, prop_name)
1420 company = obj.pool.get('res.company')
1421 cid = company._company_default_get(cr, uid, obj._name, def_id, context=context)
1422 # TODO for trunk: add new parameter company_id to _get_by_id method
1423 context_company = dict(context, company_id=cid)
1424 nids = self._get_by_id(obj, cr, uid, [prop_name], [id], context_company)
1426 cr.execute('DELETE FROM ir_property WHERE id IN %s', (tuple(nids),))
1428 default_val = self._get_default(obj, cr, uid, prop_name, context)
1430 property_create = False
1431 if isinstance(default_val, (openerp.osv.orm.browse_record,
1432 openerp.osv.orm.browse_null)):
1433 if default_val.id != id_val:
1434 property_create = True
1435 elif default_val != id_val:
1436 property_create = True
1439 propdef = obj.pool.get('ir.model.fields').browse(cr, uid, def_id,
1441 prop = obj.pool.get('ir.property')
1442 return prop.create(cr, uid, {
1443 'name': propdef.name,
1445 'res_id': obj._name+','+str(id),
1447 'fields_id': def_id,
1452 def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
1453 prop = obj.pool.get('ir.property')
1454 # get the default values (for res_id = False) for the property fields
1455 default_val = self._get_defaults(obj, cr, uid, prop_names, context)
1457 # build the dictionary that will be returned
1460 res[id] = default_val.copy()
1462 for prop_name in prop_names:
1463 property_field = obj._all_columns.get(prop_name).column
1464 property_destination_obj = property_field._obj if property_field._type == 'many2one' else False
1465 # If the property field is a m2o field, we will append the id of the value to name_get_ids
1466 # in order to make a name_get in batch for all the ids needed.
1469 # get the result of ir.property.get() for this res_id and save it in res if it's existing
1470 obj_reference = obj._name + ',' + str(id)
1471 value = prop.get(cr, uid, prop_name, obj._name, res_id=obj_reference, context=context)
1473 res[id][prop_name] = value
1474 # Check existence as root (as seeing the name of a related
1475 # object depends on access right of source document,
1476 # not target, so user may not have access) in order to avoid
1477 # pointing on an unexisting record.
1478 if property_destination_obj:
1479 if res[id][prop_name] and obj.pool.get(property_destination_obj).exists(cr, SUPERUSER_ID, res[id][prop_name].id):
1480 name_get_ids[id] = res[id][prop_name].id
1482 res[id][prop_name] = False
1483 if property_destination_obj:
1484 # name_get as root (as seeing the name of a related
1485 # object depends on access right of source document,
1486 # not target, so user may not have access.)
1487 name_get_values = dict(obj.pool.get(property_destination_obj).name_get(cr, SUPERUSER_ID, name_get_ids.values(), context=context))
1488 # the property field is a m2o, we need to return a tuple with (id, name)
1489 for k, v in name_get_ids.iteritems():
1490 if res[k][prop_name]:
1491 res[k][prop_name] = (v , name_get_values.get(v))
1494 def _field_get(self, cr, uid, model_name, prop):
1495 if not self.field_id.get(cr.dbname):
1496 cr.execute('SELECT id \
1497 FROM ir_model_fields \
1498 WHERE name=%s AND model=%s', (prop, model_name))
1500 self.field_id[cr.dbname] = res and res[0]
1501 return self.field_id[cr.dbname]
1503 def __init__(self, obj_prop, **args):
1504 # TODO remove obj_prop parameter (use many2one type)
1506 function.__init__(self, self._fnct_read, False, self._fnct_write,
1507 obj_prop, multi='properties', **args)
1513 def field_to_dict(model, cr, user, field, context=None):
1514 """ Return a dictionary representation of a field.
1516 The string, help, and selection attributes (if any) are untranslated. This
1517 representation is the one returned by fields_get() (fields_get() will do
1522 res = {'type': field._type}
1523 # some attributes for m2m/function field are added as debug info only
1524 if isinstance(field, function):
1525 res['function'] = field._fnct and field._fnct.func_name or False
1526 res['store'] = field.store
1527 if isinstance(field.store, dict):
1528 res['store'] = str(field.store)
1529 res['fnct_search'] = field._fnct_search and field._fnct_search.func_name or False
1530 res['fnct_inv'] = field._fnct_inv and field._fnct_inv.func_name or False
1531 res['fnct_inv_arg'] = field._fnct_inv_arg or False
1532 if isinstance(field, many2many):
1533 (table, col1, col2) = field._sql_names(model)
1534 res['m2m_join_columns'] = [col1, col2]
1535 res['m2m_join_table'] = table
1536 for arg in ('string', 'readonly', 'states', 'size', 'group_operator', 'required',
1537 'change_default', 'translate', 'help', 'select', 'selectable', 'groups',
1538 'deprecated', 'digits', 'invisible', 'filters'):
1539 if getattr(field, arg, None):
1540 res[arg] = getattr(field, arg)
1542 if hasattr(field, 'selection'):
1543 if isinstance(field.selection, (tuple, list)):
1544 res['selection'] = field.selection
1546 # call the 'dynamic selection' function
1547 res['selection'] = field.selection(model, cr, user, context)
1548 if res['type'] in ('one2many', 'many2many', 'many2one'):
1549 res['relation'] = field._obj
1550 res['domain'] = field._domain(model) if callable(field._domain) else field._domain
1551 res['context'] = field._context
1553 if isinstance(field, one2many):
1554 res['relation_field'] = field._fields_id
1559 class column_info(object):
1560 """ Struct containing details about an osv column, either one local to
1561 its model, or one inherited via _inherits.
1567 .. attribute:: column
1569 column instance, subclass of :class:`_column`
1571 .. attribute:: parent_model
1573 if the column is inherited, name of the model that contains it,
1574 ``None`` for local columns.
1576 .. attribute:: parent_column
1578 the name of the column containing the m2o relationship to the
1579 parent model that contains this column, ``None`` for local columns.
1581 .. attribute:: original_parent
1583 if the column is inherited, name of the original parent model that
1584 contains it i.e in case of multilevel inheritance, ``None`` for
1587 def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
1589 self.column = column
1590 self.parent_model = parent_model
1591 self.parent_column = parent_column
1592 self.original_parent = original_parent
1595 return '%s(%s, %s, %s, %s, %s)' % (
1596 self.__class__.__name__, self.name, self.column,
1597 self.parent_model, self.parent_column, self.original_parent)
1599 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: