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 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 cr.execute("select 1 from {0} where id=%s and {1}=%s".format(_table, self._fields_id), (act[1], id))
575 if not cr.fetchone():
576 # Must use write() to recompute parent_store structure if needed and check access rules
577 obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
579 reverse_rel = obj._all_columns.get(self._fields_id)
580 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
581 # if the o2m has a static domain we must respect it when unlinking
582 domain = self._domain(obj) if callable(self._domain) else self._domain
583 extra_domain = domain or []
584 ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
585 # If the model has cascade deletion, we delete the rows because it is the intended behavior,
586 # otherwise we only nullify the reverse foreign key column.
587 if reverse_rel.column.ondelete == "cascade":
588 obj.unlink(cr, user, ids_to_unlink, context=context)
590 obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context)
592 # Must use write() to recompute parent_store structure if needed
593 obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
595 cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
596 ids3 = map(lambda x:x[0], cr.fetchall())
597 obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
600 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
601 domain = self._domain(obj) if callable(self._domain) else self._domain
602 return obj.pool.get(self._obj).name_search(cr, uid, value, domain, operator, context=context,limit=limit)
606 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
607 raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)')
610 # Values: (0, 0, { fields }) create
611 # (1, ID, { fields }) update (write fields to ID)
612 # (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete)
613 # (3, ID) unlink (delete the relationship between the two objects but does not delete ID)
614 # (4, ID) link (add a relationship)
616 # (6, ?, ids) set a list of links
618 class many2many(_column):
619 """Encapsulates the logic of a many-to-many bidirectional relationship, handling the
620 low-level details of the intermediary relationship table transparently.
621 A many-to-many relationship is always symmetrical, and can be declared and accessed
622 from either endpoint model.
623 If ``rel`` (relationship table name), ``id1`` (source foreign key column name)
624 or id2 (destination foreign key column name) are not specified, the system will
625 provide default values. This will by default only allow one single symmetrical
626 many-to-many relationship between the source and destination model.
627 For multiple many-to-many relationship between the same models and for
628 relationships where source and destination models are the same, ``rel``, ``id1``
629 and ``id2`` should be specified explicitly.
631 :param str obj: destination model
632 :param str rel: optional name of the intermediary relationship table. If not specified,
633 a canonical name will be derived based on the alphabetically-ordered
634 model names of the source and destination (in the form: ``amodel_bmodel_rel``).
635 Automatic naming is not possible when the source and destination are
636 the same, for obvious ambiguity reasons.
637 :param str id1: optional name for the column holding the foreign key to the current
638 model in the relationship table. If not specified, a canonical name
639 will be derived based on the model name (in the form: `src_model_id`).
640 :param str id2: optional name for the column holding the foreign key to the destination
641 model in the relationship table. If not specified, a canonical name
642 will be derived based on the model name (in the form: `dest_model_id`)
643 :param str string: field label
645 _classic_read = False
646 _classic_write = False
650 def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
653 _column.__init__(self, string=string, **args)
655 if rel and '.' in rel:
656 raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\
657 'You used %s, which is not a valid SQL table name.')% (string,rel))
663 def _sql_names(self, source_model):
664 """Return the SQL names defining the structure of the m2m relationship table
666 :return: (m2m_table, local_col, dest_col) where m2m_table is the table name,
667 local_col is the name of the column holding the current model's FK, and
668 dest_col is the name of the column holding the destination model's FK, and
670 tbl, col1, col2 = self._rel, self._id1, self._id2
671 if not all((tbl, col1, col2)):
672 # the default table name is based on the stable alphabetical order of tables
673 dest_model = source_model.pool.get(self._obj)
674 tables = tuple(sorted([source_model._table, dest_model._table]))
676 assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\
677 'is not possible when source and destination models are '\
679 tbl = '%s_%s_rel' % tables
681 col1 = '%s_id' % source_model._table
683 col2 = '%s_id' % dest_model._table
684 return tbl, col1, col2
686 def _get_query_and_where_params(self, cr, model, ids, values, where_params):
687 """ Extracted from ``get`` to facilitate fine-tuning of the generated
689 query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
690 FROM %(rel)s, %(from_c)s \
691 WHERE %(rel)s.%(id1)s IN %%s \
692 AND %(rel)s.%(id2)s = %(tbl)s.id \
698 return query, where_params
700 def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
712 "Specifying offset at a many2many.get() is deprecated and may"
713 " produce unpredictable results.")
714 obj = model.pool.get(self._obj)
715 rel, id1, id2 = self._sql_names(model)
717 # static domains are lists, and are evaluated both here and on client-side, while string
718 # domains supposed by dynamic and evaluated on client-side only (thus ignored here)
719 # FIXME: make this distinction explicit in API!
720 domain = isinstance(self._domain, list) and self._domain or []
722 wquery = obj._where_calc(cr, user, domain, context=context)
723 obj._apply_ir_rules(cr, user, wquery, 'read', context=context)
724 from_c, where_c, where_params = wquery.get_sql()
726 where_c = ' AND ' + where_c
728 order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0])
731 if self._limit is not None:
732 limit_str = ' LIMIT %d' % self._limit
734 query, where_params = self._get_query_and_where_params(cr, model, ids, {'rel': rel,
741 'order_by': order_by,
745 cr.execute(query, [tuple(ids),] + where_params)
746 for r in cr.fetchall():
747 res[r[1]].append(r[0])
750 def set(self, cr, model, id, name, values, user=None, context=None):
755 rel, id1, id2 = self._sql_names(model)
756 obj = model.pool.get(self._obj)
758 if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
761 idnew = obj.create(cr, user, act[2], context=context)
762 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew))
764 obj.write(cr, user, [act[1]], act[2], context=context)
766 obj.unlink(cr, user, [act[1]], context=context)
768 cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1]))
770 # following queries are in the same transaction - so should be relatively safe
771 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1]))
772 if not cr.fetchone():
773 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1]))
775 cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,))
778 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
780 d1 = ' and ' + ' and '.join(d1)
783 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)
785 for act_nbr in act[2]:
786 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr))
789 # TODO: use a name_search
791 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
792 return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
795 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
796 raise NotImplementedError('Many2Many columns should not be used as record name (_rec_name)')
799 def get_nice_size(value):
801 if isinstance(value, (int,long)):
803 elif value: # this is supposed to be a string
805 return tools.human_size(size)
807 # See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
808 # and http://bugs.python.org/issue10066
809 invalid_xml_low_bytes = re.compile(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
811 def sanitize_binary_value(value):
812 # binary fields should be 7-bit ASCII base64-encoded data,
813 # but we do additional sanity checks to make sure the values
814 # are not something else that won't pass via XML-RPC
815 if isinstance(value, (xmlrpclib.Binary, tuple, list, dict)):
816 # these builtin types are meant to pass untouched
819 # Handle invalid bytes values that will cause problems
820 # for XML-RPC. See for more info:
821 # - http://bugs.python.org/issue10066
822 # - http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
824 # Coercing to unicode would normally allow it to properly pass via
825 # XML-RPC, transparently encoded as UTF-8 by xmlrpclib.
826 # (this works for _any_ byte values, thanks to the fallback
827 # to latin-1 passthrough encoding when decoding to unicode)
828 value = tools.ustr(value)
830 # Due to Python bug #10066 this could still yield invalid XML
831 # bytes, specifically in the low byte range, that will crash
832 # the decoding side: [\x00-\x08\x0b-\x0c\x0e-\x1f]
833 # So check for low bytes values, and if any, perform
834 # base64 encoding - not very smart or useful, but this is
835 # our last resort to avoid crashing the request.
836 if invalid_xml_low_bytes.search(value):
837 # b64-encode after restoring the pure bytes with latin-1
838 # passthrough encoding
839 value = base64.b64encode(value.encode('latin-1'))
844 # ---------------------------------------------------------
846 # ---------------------------------------------------------
847 class function(_column):
849 A field whose value is computed by a function (rather
850 than being read from the database).
852 :param fnct: the callable that will compute the field value.
853 :param arg: arbitrary value to be passed to ``fnct`` when computing the value.
854 :param fnct_inv: the callable that will allow writing values in that field
855 (if not provided, the field is read-only).
856 :param fnct_inv_arg: arbitrary value to be passed to ``fnct_inv`` when
858 :param str type: type of the field simulated by the function field
859 :param fnct_search: the callable that allows searching on the field
860 (if not provided, search will not return any result).
861 :param store: store computed value in database
862 (see :ref:`The *store* parameter <field-function-store>`).
863 :type store: True or dict specifying triggers for field computation
864 :param multi: name of batch for batch computation of function fields.
865 All fields with the same batch name will be computed by
866 a single function call. This changes the signature of the
869 .. _field-function-fnct: The ``fnct`` parameter
871 .. rubric:: The ``fnct`` parameter
873 The callable implementing the function field must have the following signature:
875 .. function:: fnct(model, cr, uid, ids, field_name(s), arg, context)
877 Implements the function field.
879 :param orm model: model to which the field belongs (should be ``self`` for
881 :param field_name(s): name of the field to compute, or if ``multi`` is provided,
882 list of field names to compute.
883 :type field_name(s): str | [str]
884 :param arg: arbitrary value passed when declaring the function field
886 :return: mapping of ``ids`` to computed values, or if multi is provided,
887 to a map of field_names to computed values
889 The values in the returned dictionary must be of the type specified by the type
890 argument in the field declaration.
892 Here is an example with a simple function ``char`` function field::
895 def compute(self, cr, uid, ids, field_name, arg, context):
899 _columns['my_char'] = fields.function(compute, type='char', size=50)
901 # when called with ``ids=[1,2,3]``, ``compute`` could return:
905 3: False # null values should be returned explicitly too
908 If ``multi`` is set, then ``field_name`` is replaced by ``field_names``: a list
909 of the field names that should be computed. Each value in the returned
910 dictionary must then be a dictionary mapping field names to values.
912 Here is an example where two function fields (``name`` and ``age``)
913 are both computed by a single function field::
916 def compute(self, cr, uid, ids, field_names, arg, context):
920 _columns['name'] = fields.function(compute_person_data, type='char',\
921 size=50, multi='person_data')
922 _columns[''age'] = fields.function(compute_person_data, type='integer',\
925 # when called with ``ids=[1,2,3]``, ``compute_person_data`` could return:
927 1: {'name': 'Bob', 'age': 23},
928 2: {'name': 'Sally', 'age': 19},
929 3: {'name': 'unknown', 'age': False}
932 .. _field-function-fnct-inv:
934 .. rubric:: The ``fnct_inv`` parameter
936 This callable implements the write operation for the function field
937 and must have the following signature:
939 .. function:: fnct_inv(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context)
941 Callable that implements the ``write`` operation for the function field.
943 :param orm model: model to which the field belongs (should be ``self`` for
945 :param int id: the identifier of the object to write on
946 :param str field_name: name of the field to set
947 :param fnct_inv_arg: arbitrary value passed when declaring the function field
950 When writing values for a function field, the ``multi`` parameter is ignored.
952 .. _field-function-fnct-search:
954 .. rubric:: The ``fnct_search`` parameter
956 This callable implements the search operation for the function field
957 and must have the following signature:
959 .. function:: fnct_search(model, cr, uid, model_again, field_name, criterion, context)
961 Callable that implements the ``search`` operation for the function field by expanding
962 a search criterion based on the function field into a new domain based only on
963 columns that are stored in the database.
965 :param orm model: model to which the field belongs (should be ``self`` for
967 :param orm model_again: same value as ``model`` (seriously! this is for backwards
969 :param str field_name: name of the field to search on
970 :param list criterion: domain component specifying the search criterion on the field.
972 :return: domain to use instead of ``criterion`` when performing the search.
973 This new domain must be based only on columns stored in the database, as it
974 will be used directly without any translation.
976 The returned value must be a domain, that is, a list of the form [(field_name, operator, operand)].
977 The most generic way to implement ``fnct_search`` is to directly search for the records that
978 match the given ``criterion``, and return their ``ids`` wrapped in a domain, such as
979 ``[('id','in',[1,3,5])]``.
981 .. _field-function-store:
983 .. rubric:: The ``store`` parameter
985 The ``store`` parameter allows caching the result of the field computation in the
986 database, and defining the triggers that will invalidate that cache and force a
987 recomputation of the function field.
988 When not provided, the field is computed every time its value is read.
989 The value of ``store`` may be either ``True`` (to recompute the field value whenever
990 any field in the same record is modified), or a dictionary specifying a more
991 flexible set of recomputation triggers.
993 A trigger specification is a dictionary that maps the names of the models that
994 will trigger the computation, to a tuple describing the trigger rule, in the
998 'trigger_model': (mapping_function,
999 ['trigger_field1', 'trigger_field2'],
1003 A trigger rule is defined by a 3-item tuple where:
1005 * The ``mapping_function`` is defined as follows:
1007 .. function:: mapping_function(trigger_model, cr, uid, trigger_ids, context)
1009 Callable that maps record ids of a trigger model to ids of the
1010 corresponding records in the source model (whose field values
1011 need to be recomputed).
1013 :param orm model: trigger_model
1014 :param list trigger_ids: ids of the records of trigger_model that were
1017 :return: list of ids of the source model whose function field values
1018 need to be recomputed
1020 * The second item is a list of the fields who should act as triggers for
1021 the computation. If an empty list is given, all fields will act as triggers.
1022 * The last item is the priority, used to order the triggers when processing them
1023 after any write operation on a model that has function field triggers. The
1024 default priority is 10.
1026 In fact, setting store = True is the same as using the following trigger dict::
1029 'model_itself': (lambda self, cr, uid, ids, context: ids,
1035 _classic_read = False
1036 _classic_write = False
1042 # multi: compute several fields in one call
1044 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):
1045 _column.__init__(self, **args)
1048 self._fnct_inv = fnct_inv
1051 if 'relation' in args:
1052 self._obj = args['relation']
1054 self.digits = args.get('digits', (16,2))
1055 self.digits_compute = args.get('digits_compute', None)
1057 self._fnct_inv_arg = fnct_inv_arg
1061 self._fnct_search = fnct_search
1064 if not fnct_search and not store:
1065 self.selectable = False
1068 if self._type != 'many2one':
1069 # m2o fields need to return tuples with name_get, not just foreign keys
1070 self._classic_read = True
1071 self._classic_write = True
1073 self._symbol_get=lambda x:x and str(x)
1075 self._prefetch = True
1078 self._symbol_c = float._symbol_c
1079 self._symbol_f = float._symbol_f
1080 self._symbol_set = float._symbol_set
1082 if type == 'boolean':
1083 self._symbol_c = boolean._symbol_c
1084 self._symbol_f = boolean._symbol_f
1085 self._symbol_set = boolean._symbol_set
1087 if type == 'integer':
1088 self._symbol_c = integer._symbol_c
1089 self._symbol_f = integer._symbol_f
1090 self._symbol_set = integer._symbol_set
1093 self._symbol_c = char._symbol_c
1094 self._symbol_f = lambda x: _symbol_set_char(self, x)
1095 self._symbol_set = (self._symbol_c, self._symbol_f)
1097 def digits_change(self, cr):
1098 if self._type == 'float':
1099 if self.digits_compute:
1100 self.digits = self.digits_compute(cr)
1102 precision, scale = self.digits
1103 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
1104 precision_digits=scale),
1105 precision_digits=scale))
1107 def search(self, cr, uid, obj, name, args, context=None):
1108 if not self._fnct_search:
1109 #CHECKME: should raise an exception
1111 return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
1113 def postprocess(self, cr, uid, obj, field, value=None, context=None):
1117 field_type = obj._columns[field]._type
1118 if field_type == "many2one":
1119 # make the result a tuple if it is not already one
1120 if isinstance(value, (int,long)) and hasattr(obj._columns[field], 'relation'):
1121 obj_model = obj.pool.get(obj._columns[field].relation)
1122 dict_names = dict(obj_model.name_get(cr, SUPERUSER_ID, [value], context))
1123 result = (value, dict_names[value])
1125 if field_type == 'binary':
1126 if context.get('bin_size'):
1127 # client requests only the size of binary fields
1128 result = get_nice_size(value)
1129 elif not context.get('bin_raw'):
1130 result = sanitize_binary_value(value)
1132 if field_type == "integer" and value > xmlrpclib.MAXINT:
1133 # integer/long values greater than 2^31-1 are not supported
1134 # in pure XMLRPC, so we have to pass them as floats :-(
1135 # This is not needed for stored fields and non-functional integer
1136 # fields, as their values are constrained by the database backend
1137 # to the same 32bits signed int limit.
1138 result = __builtin__.float(value)
1141 def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
1142 result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
1144 if self._multi and id in result:
1145 for field, value in result[id].iteritems():
1147 result[id][field] = self.postprocess(cr, uid, obj, field, value, context)
1148 elif result.get(id):
1149 result[id] = self.postprocess(cr, uid, obj, name, result[id], context)
1152 def set(self, cr, obj, id, name, value, user=None, context=None):
1156 self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
1159 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
1160 # Function fields are supposed to emulate a basic field type,
1161 # so they can delegate to the basic type for record name rendering
1162 return globals()[field._type]._as_display_name(field, cr, uid, obj, value, context=context)
1164 # ---------------------------------------------------------
1166 # ---------------------------------------------------------
1168 class related(function):
1169 """Field that points to some data inside another field of the current record.
1174 'foo_id': fields.many2one('my.foo', 'Foo'),
1175 'bar': fields.related('foo_id', 'frol', type='char', string='Frol of Foo'),
1179 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1180 # assume self._arg = ('foo', 'bar', 'baz')
1181 # domain = [(name, op, val)] => search [('foo.bar.baz', op, val)]
1182 field = '.'.join(self._arg)
1183 return map(lambda x: (field, x[1], x[2]), domain)
1185 def _fnct_write(self,obj,cr, uid, ids, field_name, values, args, context=None):
1186 if isinstance(ids, (int, long)):
1188 for record in obj.browse(cr, uid, ids, context=context):
1189 # traverse all fields except the last one
1190 for field in self.arg[:-1]:
1191 record = record[field] or False
1194 elif isinstance(record, list):
1195 # record is the result of a one2many or many2many field
1198 # write on the last field
1199 record.write({self.arg[-1]: values})
1201 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1203 for record in obj.browse(cr, SUPERUSER_ID, ids, context=context):
1205 for field in self.arg:
1206 if isinstance(value, list):
1208 value = value[field] or False
1211 res[record.id] = value
1213 if self._type == 'many2one':
1214 # res[id] is a browse_record or False; convert it to (id, name) or False.
1215 # Perform name_get as root, as seeing the name of a related object depends on
1216 # access right of source document, not target, so user may not have access.
1217 value_ids = list(set(value.id for value in res.itervalues() if value))
1218 value_name = dict(obj.pool.get(self._obj).name_get(cr, SUPERUSER_ID, value_ids, context=context))
1219 res = dict((id, value and (value.id, value_name[value.id])) for id, value in res.iteritems())
1221 elif self._type in ('one2many', 'many2many'):
1222 # res[id] is a list of browse_record or False; convert it to a list of ids
1223 res = dict((id, value and map(int, value) or []) for id, value in res.iteritems())
1227 def __init__(self, *arg, **args):
1229 self._relations = []
1230 super(related, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1231 if self.store is True:
1232 # TODO: improve here to change self.store = {...} according to related objects
1236 class sparse(function):
1238 def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
1240 + For a many2many field, a list of tuples is expected.
1241 Here is the list of tuple that are accepted, with the corresponding semantics ::
1243 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1244 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1245 (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)
1246 (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)
1247 (4, ID) link to existing record with id = ID (adds a relationship)
1248 (5) unlink all (like using (3,ID) for all linked records)
1249 (6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
1252 [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
1254 + For a one2many field, a lits of tuples is expected.
1255 Here is the list of tuple that are accepted, with the corresponding semantics ::
1257 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1258 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1259 (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)
1262 [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
1265 if self._type == 'many2many':
1266 assert value[0][0] == 6, 'Unsupported m2m value for sparse field: %s' % value
1269 elif self._type == 'one2many':
1272 relation_obj = obj.pool.get(self.relation)
1274 assert vals[0] in (0,1,2), 'Unsupported o2m value for sparse field: %s' % vals
1276 read_value.append(relation_obj.create(cr, uid, vals[2], context=context))
1278 relation_obj.write(cr, uid, vals[1], vals[2], context=context)
1280 relation_obj.unlink(cr, uid, vals[1], context=context)
1281 read_value.remove(vals[1])
1286 def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
1287 if not type(ids) == list:
1289 records = obj.browse(cr, uid, ids, context=context)
1290 for record in records:
1291 # grab serialized value as object - already deserialized
1292 serialized = getattr(record, self.serialization_field)
1294 # simply delete the key to unset it.
1295 serialized.pop(field_name, None)
1297 serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context)
1298 obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context)
1301 def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None):
1303 records = obj.browse(cr, uid, ids, context=context)
1304 for record in records:
1305 # grab serialized value as object - already deserialized
1306 serialized = getattr(record, self.serialization_field)
1307 results[record.id] = {}
1308 for field_name in field_names:
1309 field_type = obj._columns[field_name]._type
1310 value = serialized.get(field_name, False)
1311 if field_type in ('one2many','many2many'):
1314 # filter out deleted records as superuser
1315 relation_obj = obj.pool.get(obj._columns[field_name].relation)
1316 value = relation_obj.exists(cr, openerp.SUPERUSER_ID, value)
1317 if type(value) in (int,long) and field_type == 'many2one':
1318 relation_obj = obj.pool.get(obj._columns[field_name].relation)
1319 # check for deleted record as superuser
1320 if not relation_obj.exists(cr, openerp.SUPERUSER_ID, [value]):
1322 results[record.id][field_name] = value
1325 def __init__(self, serialization_field, **kwargs):
1326 self.serialization_field = serialization_field
1327 super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
1331 # ---------------------------------------------------------
1333 # ---------------------------------------------------------
1335 class dummy(function):
1336 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1339 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1342 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1345 def __init__(self, *arg, **args):
1347 self._relations = []
1348 super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=None, **args)
1350 # ---------------------------------------------------------
1352 # ---------------------------------------------------------
1354 class serialized(_column):
1355 """ A field able to store an arbitrary python data structure.
1357 Note: only plain components allowed.
1360 def _symbol_set_struct(val):
1361 return simplejson.dumps(val)
1363 def _symbol_get_struct(self, val):
1364 return simplejson.loads(val or '{}')
1367 _type = 'serialized'
1370 _symbol_f = _symbol_set_struct
1371 _symbol_set = (_symbol_c, _symbol_f)
1372 _symbol_get = _symbol_get_struct
1374 # TODO: review completly this class for speed improvement
1375 class property(function):
1377 def _get_default(self, obj, cr, uid, prop_name, context=None):
1378 return self._get_defaults(obj, cr, uid, [prop_name], context=None)[prop_name]
1380 def _get_defaults(self, obj, cr, uid, prop_names, context=None):
1381 """Get the default values for ``prop_names´´ property fields (result of ir.property.get() function for res_id = False).
1383 :param list of string prop_names: list of name of property fields for those we want the default value
1384 :return: map of property field names to their default value
1387 prop = obj.pool.get('ir.property')
1389 for prop_name in prop_names:
1390 res[prop_name] = prop.get(cr, uid, prop_name, obj._name, context=context)
1393 def _get_by_id(self, obj, cr, uid, prop_name, ids, context=None):
1394 prop = obj.pool.get('ir.property')
1395 vids = [obj._name + ',' + str(oid) for oid in ids]
1397 domain = [('fields_id.model', '=', obj._name), ('fields_id.name', 'in', prop_name)]
1398 #domain = prop._get_domain(cr, uid, prop_name, obj._name, context)
1400 domain = [('res_id', 'in', vids)] + domain
1401 return prop.search(cr, uid, domain, context=context)
1403 # TODO: to rewrite more clean
1404 def _fnct_write(self, obj, cr, uid, id, prop_name, id_val, obj_dest, context=None):
1408 nids = self._get_by_id(obj, cr, uid, [prop_name], [id], context)
1410 cr.execute('DELETE FROM ir_property WHERE id IN %s', (tuple(nids),))
1412 default_val = self._get_default(obj, cr, uid, prop_name, context)
1414 property_create = False
1415 if isinstance(default_val, openerp.osv.orm.browse_record):
1416 if default_val.id != id_val:
1417 property_create = True
1418 elif default_val != id_val:
1419 property_create = True
1422 def_id = self._field_get(cr, uid, obj._name, prop_name)
1423 company = obj.pool.get('res.company')
1424 cid = company._company_default_get(cr, uid, obj._name, def_id,
1426 propdef = obj.pool.get('ir.model.fields').browse(cr, uid, def_id,
1428 prop = obj.pool.get('ir.property')
1429 return prop.create(cr, uid, {
1430 'name': propdef.name,
1432 'res_id': obj._name+','+str(id),
1434 'fields_id': def_id,
1439 def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
1440 prop = obj.pool.get('ir.property')
1441 # get the default values (for res_id = False) for the property fields
1442 default_val = self._get_defaults(obj, cr, uid, prop_names, context)
1444 # build the dictionary that will be returned
1447 res[id] = default_val.copy()
1449 for prop_name in prop_names:
1450 property_field = obj._all_columns.get(prop_name).column
1451 property_destination_obj = property_field._obj if property_field._type == 'many2one' else False
1452 # If the property field is a m2o field, we will append the id of the value to name_get_ids
1453 # in order to make a name_get in batch for all the ids needed.
1456 # get the result of ir.property.get() for this res_id and save it in res if it's existing
1457 obj_reference = obj._name + ',' + str(id)
1458 value = prop.get(cr, uid, prop_name, obj._name, res_id=obj_reference, context=context)
1460 res[id][prop_name] = value
1461 # Check existence as root (as seeing the name of a related
1462 # object depends on access right of source document,
1463 # not target, so user may not have access) in order to avoid
1464 # pointing on an unexisting record.
1465 if property_destination_obj:
1466 if res[id][prop_name] and obj.pool.get(property_destination_obj).exists(cr, SUPERUSER_ID, res[id][prop_name].id):
1467 name_get_ids[id] = res[id][prop_name].id
1469 res[id][prop_name] = False
1470 if property_destination_obj:
1471 # name_get as root (as seeing the name of a related
1472 # object depends on access right of source document,
1473 # not target, so user may not have access.)
1474 name_get_values = dict(obj.pool.get(property_destination_obj).name_get(cr, SUPERUSER_ID, name_get_ids.values(), context=context))
1475 # the property field is a m2o, we need to return a tuple with (id, name)
1476 for k, v in name_get_ids.iteritems():
1477 if res[k][prop_name]:
1478 res[k][prop_name] = (v , name_get_values.get(v))
1481 def _field_get(self, cr, uid, model_name, prop):
1482 if not self.field_id.get(cr.dbname):
1483 cr.execute('SELECT id \
1484 FROM ir_model_fields \
1485 WHERE name=%s AND model=%s', (prop, model_name))
1487 self.field_id[cr.dbname] = res and res[0]
1488 return self.field_id[cr.dbname]
1490 def __init__(self, obj_prop, **args):
1491 # TODO remove obj_prop parameter (use many2one type)
1493 function.__init__(self, self._fnct_read, False, self._fnct_write,
1494 obj_prop, multi='properties', **args)
1500 def field_to_dict(model, cr, user, field, context=None):
1501 """ Return a dictionary representation of a field.
1503 The string, help, and selection attributes (if any) are untranslated. This
1504 representation is the one returned by fields_get() (fields_get() will do
1509 res = {'type': field._type}
1510 # some attributes for m2m/function field are added as debug info only
1511 if isinstance(field, function):
1512 res['function'] = field._fnct and field._fnct.func_name or False
1513 res['store'] = field.store
1514 if isinstance(field.store, dict):
1515 res['store'] = str(field.store)
1516 res['fnct_search'] = field._fnct_search and field._fnct_search.func_name or False
1517 res['fnct_inv'] = field._fnct_inv and field._fnct_inv.func_name or False
1518 res['fnct_inv_arg'] = field._fnct_inv_arg or False
1519 if isinstance(field, many2many):
1520 (table, col1, col2) = field._sql_names(model)
1521 res['m2m_join_columns'] = [col1, col2]
1522 res['m2m_join_table'] = table
1523 for arg in ('string', 'readonly', 'states', 'size', 'group_operator', 'required',
1524 'change_default', 'translate', 'help', 'select', 'selectable', 'groups',
1525 'deprecated', 'digits', 'invisible', 'filters'):
1526 if getattr(field, arg, None):
1527 res[arg] = getattr(field, arg)
1529 if hasattr(field, 'selection'):
1530 if isinstance(field.selection, (tuple, list)):
1531 res['selection'] = field.selection
1533 # call the 'dynamic selection' function
1534 res['selection'] = field.selection(model, cr, user, context)
1535 if res['type'] in ('one2many', 'many2many', 'many2one'):
1536 res['relation'] = field._obj
1537 res['domain'] = field._domain(model) if callable(field._domain) else field._domain
1538 res['context'] = field._context
1540 if isinstance(field, one2many):
1541 res['relation_field'] = field._fields_id
1546 class column_info(object):
1547 """ Struct containing details about an osv column, either one local to
1548 its model, or one inherited via _inherits.
1554 .. attribute:: column
1556 column instance, subclass of :class:`_column`
1558 .. attribute:: parent_model
1560 if the column is inherited, name of the model that contains it,
1561 ``None`` for local columns.
1563 .. attribute:: parent_column
1565 the name of the column containing the m2o relationship to the
1566 parent model that contains this column, ``None`` for local columns.
1568 .. attribute:: original_parent
1570 if the column is inherited, name of the original parent model that
1571 contains it i.e in case of multilevel inheritance, ``None`` for
1574 def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
1576 self.column = column
1577 self.parent_model = parent_model
1578 self.parent_column = parent_column
1579 self.original_parent = original_parent
1582 return '%s(%s, %s, %s, %s, %s)' % (
1583 self.__class__.__name__, self.name, self.column,
1584 self.parent_model, self.parent_column, self.original_parent)
1586 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: