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
41 from psycopg2 import Binary
44 import openerp.tools as tools
45 from openerp.tools.translate import _
46 from openerp.tools import float_round, float_repr
48 from openerp.tools.html_sanitize import html_sanitize
50 _logger = logging.getLogger(__name__)
52 def _symbol_set(symb):
53 if symb is None or symb == False:
55 elif isinstance(symb, unicode):
56 return symb.encode('utf-8')
60 class _column(object):
61 """ Base of all fields, a database column
63 An instance of this object is a *description* of a database column. It will
64 not hold any data, but only provide the methods to manipulate data of an
65 ORM record or even prepare/update the database to hold such a field of data.
75 _symbol_f = _symbol_set
76 _symbol_set = (_symbol_c, _symbol_f)
79 # used to hide a certain field type in the list of field types
82 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):
85 The 'manual' keyword argument specifies if the field is a custom one.
86 It corresponds to the 'state' column in ir_model_fields.
93 self.states = states or {}
95 self.readonly = readonly
96 self.required = required
98 self.help = args.get('help', '')
99 self.priority = priority
100 self.change_default = change_default
101 self.ondelete = ondelete.lower() if ondelete else None # defaults to 'set null' in ORM
102 self.translate = translate
103 self._domain = domain
104 self._context = context
110 self.selectable = True
111 self.group_operator = args.get('group_operator', False)
112 self.groups = False # CSV list of ext IDs of groups that can access this field
113 self.deprecated = False # Optional deprecation warning
116 setattr(self, a, args[a])
121 def set(self, cr, obj, id, name, value, user=None, context=None):
122 cr.execute('update '+obj._table+' set '+name+'='+self._symbol_set[0]+' where id=%s', (self._symbol_set[1](value), id))
124 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
125 raise Exception(_('undefined get method !'))
127 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
128 ids = obj.search(cr, uid, args+self._domain+[(name, 'ilike', value)], offset, limit, context=context)
129 res = obj.read(cr, uid, ids, [name], context=context)
130 return [x[name] for x in res]
132 def as_display_name(self, cr, uid, obj, value, context=None):
133 """Converts a field value to a suitable string representation for a record,
134 e.g. when this field is used as ``rec_name``.
136 :param obj: the ``BaseModel`` instance this column belongs to
137 :param value: a proper value as returned by :py:meth:`~openerp.orm.osv.BaseModel.read`
140 # delegated to class method, so a column type A can delegate
141 # to a column type B.
142 return self._as_display_name(self, cr, uid, obj, value, context=None)
145 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
146 # This needs to be a class method, in case a column type A as to delegate
147 # to a column type B.
148 return tools.ustr(value)
150 # ---------------------------------------------------------
152 # ---------------------------------------------------------
153 class boolean(_column):
156 _symbol_f = lambda x: x and 'True' or 'False'
157 _symbol_set = (_symbol_c, _symbol_f)
159 def __init__(self, string='unknown', required=False, **args):
160 super(boolean, self).__init__(string=string, required=required, **args)
163 "required=True is deprecated: making a boolean field"
164 " `required` has no effect, as NULL values are "
165 "automatically turned into False.")
167 class integer(_column):
170 _symbol_f = lambda x: int(x or 0)
171 _symbol_set = (_symbol_c, _symbol_f)
172 _symbol_get = lambda self,x: x or 0
174 def __init__(self, string='unknown', required=False, **args):
175 super(integer, self).__init__(string=string, required=required, **args)
177 class reference(_column):
179 _classic_read = False # post-process to handle missing target
181 def __init__(self, string, selection, size, **args):
182 _column.__init__(self, string=string, size=size, selection=selection, **args)
184 def get(self, cr, obj, ids, name, uid=None, context=None, values=None):
186 # copy initial values fetched previously.
188 result[value['id']] = value[name]
190 model, res_id = value[name].split(',')
191 if not obj.pool.get(model).exists(cr, uid, [int(res_id)], context=context):
192 result[value['id']] = False
196 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
198 # reference fields have a 'model,id'-like value, that we need to convert
200 model_name, res_id = value.split(',')
201 model = obj.pool.get(model_name)
203 return model.name_get(cr, uid, [int(res_id)], context=context)[0][1]
204 return tools.ustr(value)
209 def __init__(self, string="unknown", size=None, **args):
210 _column.__init__(self, string=string, size=size or None, **args)
211 self._symbol_set = (self._symbol_c, self._symbol_set_char)
213 # takes a string (encoded in utf8) and returns a string (encoded in utf8)
214 def _symbol_set_char(self, symb):
216 # * we need to remove the "symb==False" from the next line BUT
217 # for now too many things rely on this broken behavior
218 # * the symb==None test should be common to all data types
219 if symb is None or symb == False:
222 # we need to convert the string to a unicode object to be able
223 # to evaluate its length (and possibly truncate it) reliably
224 u_symb = tools.ustr(symb)
226 return u_symb[:self.size].encode('utf8')
236 return html_sanitize(x)
238 _symbol_set = (_symbol_c, _symbol_f)
242 class float(_column):
245 _symbol_f = lambda x: __builtin__.float(x or 0.0)
246 _symbol_set = (_symbol_c, _symbol_f)
247 _symbol_get = lambda self,x: x or 0.0
249 def __init__(self, string='unknown', digits=None, digits_compute=None, required=False, **args):
250 _column.__init__(self, string=string, required=required, **args)
252 # synopsis: digits_compute(cr) -> (precision, scale)
253 self.digits_compute = digits_compute
255 def digits_change(self, cr):
256 if self.digits_compute:
257 self.digits = self.digits_compute(cr)
259 precision, scale = self.digits
260 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
261 precision_digits=scale),
262 precision_digits=scale))
269 """ Returns the current date in a format fit for being a
270 default value to a ``date`` field.
272 This method should be provided as is to the _defaults dict, it
273 should not be called.
275 return DT.date.today().strftime(
276 tools.DEFAULT_SERVER_DATE_FORMAT)
279 def context_today(model, cr, uid, context=None, timestamp=None):
280 """Returns the current date as seen in the client's timezone
281 in a format fit for date fields.
282 This method may be passed as value to initialize _defaults.
284 :param Model model: model (osv) for which the date value is being
285 computed - technical field, currently ignored,
286 automatically passed when used in _defaults.
287 :param datetime timestamp: optional datetime value to use instead of
288 the current date and time (must be a
289 datetime, regular dates can't be converted
291 :param dict context: the 'tz' key in the context should give the
292 name of the User/Client timezone (otherwise
296 today = timestamp or DT.datetime.now()
298 if context and context.get('tz'):
300 utc = pytz.timezone('UTC')
301 context_tz = pytz.timezone(context['tz'])
302 utc_today = utc.localize(today, is_dst=False) # UTC = no DST
303 context_today = utc_today.astimezone(context_tz)
305 _logger.debug("failed to compute context/client-specific today date, "
306 "using the UTC value for `today`",
308 return (context_today or today).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
310 class datetime(_column):
314 """ Returns the current datetime in a format fit for being a
315 default value to a ``datetime`` field.
317 This method should be provided as is to the _defaults dict, it
318 should not be called.
320 return DT.datetime.now().strftime(
321 tools.DEFAULT_SERVER_DATETIME_FORMAT)
324 def context_timestamp(cr, uid, timestamp, context=None):
325 """Returns the given timestamp converted to the client's timezone.
326 This method is *not* meant for use as a _defaults initializer,
327 because datetime fields are automatically converted upon
328 display on client side. For _defaults you :meth:`fields.datetime.now`
329 should be used instead.
331 :param datetime timestamp: naive datetime value (expressed in UTC)
332 to be converted to the client timezone
333 :param dict context: the 'tz' key in the context should give the
334 name of the User/Client timezone (otherwise
337 :return: timestamp converted to timezone-aware datetime in context
340 assert isinstance(timestamp, DT.datetime), 'Datetime instance expected'
341 if context and context.get('tz'):
343 utc = pytz.timezone('UTC')
344 context_tz = pytz.timezone(context['tz'])
345 utc_timestamp = utc.localize(timestamp, is_dst=False) # UTC = no DST
346 return utc_timestamp.astimezone(context_tz)
348 _logger.debug("failed to compute context/client-specific timestamp, "
349 "using the UTC value",
353 class binary(_column):
357 # Binary values may be byte strings (python 2.6 byte array), but
358 # the legacy OpenERP convention is to transfer and store binaries
359 # as base64-encoded strings. The base64 string may be provided as a
360 # unicode in some circumstances, hence the str() cast in symbol_f.
361 # This str coercion will only work for pure ASCII unicode strings,
362 # on purpose - non base64 data must be passed as a 8bit byte strings.
363 _symbol_f = lambda symb: symb and Binary(str(symb)) or None
365 _symbol_set = (_symbol_c, _symbol_f)
366 _symbol_get = lambda self, x: x and str(x)
368 _classic_read = False
371 def __init__(self, string='unknown', filters=None, **args):
372 _column.__init__(self, string=string, **args)
373 self.filters = filters
375 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
388 # If client is requesting only the size of the field, we return it instead
389 # of the content. Presumably a separate request will be done to read the actual
390 # content if it's needed at some point.
391 # TODO: after 6.0 we should consider returning a dict with size and content instead of
392 # having an implicit convention for the value
393 if val and context.get('bin_size_%s' % name, context.get('bin_size')):
394 res[i] = tools.human_size(long(val))
399 class selection(_column):
402 def __init__(self, selection, string='unknown', **args):
403 _column.__init__(self, string=string, **args)
404 self.selection = selection
406 # ---------------------------------------------------------
408 # ---------------------------------------------------------
411 # Values: (0, 0, { fields }) create
412 # (1, ID, { fields }) update
413 # (2, ID) remove (delete)
414 # (3, ID) unlink one (target id or target of relation)
416 # (5) unlink all (only valid for one2many)
419 class many2one(_column):
420 _classic_read = False
421 _classic_write = True
424 _symbol_f = lambda x: x or None
425 _symbol_set = (_symbol_c, _symbol_f)
427 def __init__(self, obj, string='unknown', **args):
428 _column.__init__(self, string=string, **args)
431 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
439 res[r['id']] = r[name]
441 res.setdefault(id, '')
442 obj = obj.pool.get(self._obj)
444 # build a dictionary of the form {'id_of_distant_resource': name_of_distant_resource}
445 # we use uid=1 because the visibility of a many2one field value (just id and name)
446 # must be the access right of the parent form and not the linked object itself.
447 records = dict(obj.name_get(cr, 1,
448 list(set([x for x in res.values() if isinstance(x, (int,long))])),
451 if res[id] in records:
452 res[id] = (res[id], records[res[id]])
457 def set(self, cr, obj_src, id, field, values, user=None, context=None):
460 obj = obj_src.pool.get(self._obj)
461 self._table = obj_src.pool.get(self._obj)._table
462 if type(values) == type([]):
465 id_new = obj.create(cr, act[2])
466 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (id_new, id))
468 obj.write(cr, [act[1]], act[2], context=context)
470 cr.execute('delete from '+self._table+' where id=%s', (act[1],))
471 elif act[0] == 3 or act[0] == 5:
472 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
474 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (act[1], id))
477 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (values, id))
479 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
481 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
482 return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context)
486 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
487 return value[1] if isinstance(value, tuple) else tools.ustr(value)
490 class one2many(_column):
491 _classic_read = False
492 _classic_write = False
496 def __init__(self, obj, fields_id, string='unknown', limit=None, **args):
497 _column.__init__(self, string=string, **args)
499 self._fields_id = fields_id
501 #one2many can't be used as condition for defaults
502 assert(self.change_default != True)
504 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
508 context = context.copy()
509 context.update(self._context)
518 if isinstance(self._domain, type(lambda: None)):
519 dom = self._domain(obj)
520 ids2 = obj.pool.get(self._obj).search(cr, user, dom + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
521 for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
522 if r[self._fields_id] in res:
523 res[r[self._fields_id]].append(r['id'])
526 def set(self, cr, obj, id, field, values, user=None, context=None):
531 context = context.copy()
532 context.update(self._context)
533 context['no_store_function'] = True
536 _table = obj.pool.get(self._obj)._table
537 obj = obj.pool.get(self._obj)
540 act[2][self._fields_id] = id
541 id_new = obj.create(cr, user, act[2], context=context)
542 result += obj._store_get_values(cr, user, [id_new], act[2].keys(), context)
544 obj.write(cr, user, [act[1]], act[2], context=context)
546 obj.unlink(cr, user, [act[1]], context=context)
548 reverse_rel = obj._all_columns.get(self._fields_id)
549 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
550 # if the model has on delete cascade, just delete the row
551 if reverse_rel.column.ondelete == "cascade":
552 obj.unlink(cr, user, [act[1]], context=context)
554 cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],))
556 # Must use write() to recompute parent_store structure if needed
557 obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
559 reverse_rel = obj._all_columns.get(self._fields_id)
560 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
561 # if the o2m has a static domain we must respect it when unlinking
563 if isinstance(self._domain, type(lambda: None)):
564 dom = self._domain(obj)
565 extra_domain = dom or []
566 ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
567 # If the model has cascade deletion, we delete the rows because it is the intended behavior,
568 # otherwise we only nullify the reverse foreign key column.
569 if reverse_rel.column.ondelete == "cascade":
570 obj.unlink(cr, user, ids_to_unlink, context=context)
572 obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context)
574 # Must use write() to recompute parent_store structure if needed
575 obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
577 cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
578 ids3 = map(lambda x:x[0], cr.fetchall())
579 obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
582 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
584 if isinstance(self._domain, type(lambda: None)):
585 dom = self._domain(obj)
586 return obj.pool.get(self._obj).name_search(cr, uid, value, dom, operator, context=context,limit=limit)
590 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
591 raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)')
594 # Values: (0, 0, { fields }) create
595 # (1, ID, { fields }) update (write fields to ID)
596 # (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete)
597 # (3, ID) unlink (delete the relationship between the two objects but does not delete ID)
598 # (4, ID) link (add a relationship)
600 # (6, ?, ids) set a list of links
602 class many2many(_column):
603 """Encapsulates the logic of a many-to-many bidirectional relationship, handling the
604 low-level details of the intermediary relationship table transparently.
605 A many-to-many relationship is always symmetrical, and can be declared and accessed
606 from either endpoint model.
607 If ``rel`` (relationship table name), ``id1`` (source foreign key column name)
608 or id2 (destination foreign key column name) are not specified, the system will
609 provide default values. This will by default only allow one single symmetrical
610 many-to-many relationship between the source and destination model.
611 For multiple many-to-many relationship between the same models and for
612 relationships where source and destination models are the same, ``rel``, ``id1``
613 and ``id2`` should be specified explicitly.
615 :param str obj: destination model
616 :param str rel: optional name of the intermediary relationship table. If not specified,
617 a canonical name will be derived based on the alphabetically-ordered
618 model names of the source and destination (in the form: ``amodel_bmodel_rel``).
619 Automatic naming is not possible when the source and destination are
620 the same, for obvious ambiguity reasons.
621 :param str id1: optional name for the column holding the foreign key to the current
622 model in the relationship table. If not specified, a canonical name
623 will be derived based on the model name (in the form: `src_model_id`).
624 :param str id2: optional name for the column holding the foreign key to the destination
625 model in the relationship table. If not specified, a canonical name
626 will be derived based on the model name (in the form: `dest_model_id`)
627 :param str string: field label
629 _classic_read = False
630 _classic_write = False
634 def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
637 _column.__init__(self, string=string, **args)
639 if rel and '.' in rel:
640 raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\
641 'You used %s, which is not a valid SQL table name.')% (string,rel))
647 def _sql_names(self, source_model):
648 """Return the SQL names defining the structure of the m2m relationship table
650 :return: (m2m_table, local_col, dest_col) where m2m_table is the table name,
651 local_col is the name of the column holding the current model's FK, and
652 dest_col is the name of the column holding the destination model's FK, and
654 tbl, col1, col2 = self._rel, self._id1, self._id2
655 if not all((tbl, col1, col2)):
656 # the default table name is based on the stable alphabetical order of tables
657 dest_model = source_model.pool.get(self._obj)
658 tables = tuple(sorted([source_model._table, dest_model._table]))
660 assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\
661 'is not possible when source and destination models are '\
663 tbl = '%s_%s_rel' % tables
665 col1 = '%s_id' % source_model._table
667 col2 = '%s_id' % dest_model._table
668 return (tbl, col1, col2)
670 def _get_query_and_where_params(self, cr, model, ids, values, where_params):
671 """ Extracted from ``get`` to facilitate fine-tuning of the generated
673 query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
674 FROM %(rel)s, %(from_c)s \
675 WHERE %(rel)s.%(id1)s IN %%s \
676 AND %(rel)s.%(id2)s = %(tbl)s.id \
682 return query, where_params
684 def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
696 "Specifying offset at a many2many.get() is deprecated and may"
697 " produce unpredictable results.")
698 obj = model.pool.get(self._obj)
699 rel, id1, id2 = self._sql_names(model)
701 # static domains are lists, and are evaluated both here and on client-side, while string
702 # domains supposed by dynamic and evaluated on client-side only (thus ignored here)
703 # FIXME: make this distinction explicit in API!
704 domain = isinstance(self._domain, list) and self._domain or []
706 wquery = obj._where_calc(cr, user, domain, context=context)
707 obj._apply_ir_rules(cr, user, wquery, 'read', context=context)
708 from_c, where_c, where_params = wquery.get_sql()
710 where_c = ' AND ' + where_c
712 if offset or self._limit:
713 order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0])
718 if self._limit is not None:
719 limit_str = ' LIMIT %d' % self._limit
721 query, where_params = self._get_query_and_where_params(cr, model, ids, {'rel': rel,
728 'order_by': order_by,
732 cr.execute(query, [tuple(ids),] + where_params)
733 for r in cr.fetchall():
734 res[r[1]].append(r[0])
737 def set(self, cr, model, id, name, values, user=None, context=None):
742 rel, id1, id2 = self._sql_names(model)
743 obj = model.pool.get(self._obj)
745 if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
748 idnew = obj.create(cr, user, act[2], context=context)
749 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew))
751 obj.write(cr, user, [act[1]], act[2], context=context)
753 obj.unlink(cr, user, [act[1]], context=context)
755 cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1]))
757 # following queries are in the same transaction - so should be relatively safe
758 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1]))
759 if not cr.fetchone():
760 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1]))
762 cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,))
765 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
767 d1 = ' and ' + ' and '.join(d1)
770 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)
772 for act_nbr in act[2]:
773 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr))
776 # TODO: use a name_search
778 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
779 return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
782 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
783 raise NotImplementedError('Many2Many columns should not be used as record name (_rec_name)')
786 def get_nice_size(value):
788 if isinstance(value, (int,long)):
790 elif value: # this is supposed to be a string
792 return tools.human_size(size)
794 # See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
795 # and http://bugs.python.org/issue10066
796 invalid_xml_low_bytes = re.compile(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
798 def sanitize_binary_value(value):
799 # binary fields should be 7-bit ASCII base64-encoded data,
800 # but we do additional sanity checks to make sure the values
801 # are not something else that won't pass via XML-RPC
802 if isinstance(value, (xmlrpclib.Binary, tuple, list, dict)):
803 # these builtin types are meant to pass untouched
806 # Handle invalid bytes values that will cause problems
807 # for XML-RPC. See for more info:
808 # - http://bugs.python.org/issue10066
809 # - http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
811 # Coercing to unicode would normally allow it to properly pass via
812 # XML-RPC, transparently encoded as UTF-8 by xmlrpclib.
813 # (this works for _any_ byte values, thanks to the fallback
814 # to latin-1 passthrough encoding when decoding to unicode)
815 value = tools.ustr(value)
817 # Due to Python bug #10066 this could still yield invalid XML
818 # bytes, specifically in the low byte range, that will crash
819 # the decoding side: [\x00-\x08\x0b-\x0c\x0e-\x1f]
820 # So check for low bytes values, and if any, perform
821 # base64 encoding - not very smart or useful, but this is
822 # our last resort to avoid crashing the request.
823 if invalid_xml_low_bytes.search(value):
824 # b64-encode after restoring the pure bytes with latin-1
825 # passthrough encoding
826 value = base64.b64encode(value.encode('latin-1'))
831 # ---------------------------------------------------------
833 # ---------------------------------------------------------
834 class function(_column):
836 A field whose value is computed by a function (rather
837 than being read from the database).
839 :param fnct: the callable that will compute the field value.
840 :param arg: arbitrary value to be passed to ``fnct`` when computing the value.
841 :param fnct_inv: the callable that will allow writing values in that field
842 (if not provided, the field is read-only).
843 :param fnct_inv_arg: arbitrary value to be passed to ``fnct_inv`` when
845 :param str type: type of the field simulated by the function field
846 :param fnct_search: the callable that allows searching on the field
847 (if not provided, search will not return any result).
848 :param store: store computed value in database
849 (see :ref:`The *store* parameter <field-function-store>`).
850 :type store: True or dict specifying triggers for field computation
851 :param multi: name of batch for batch computation of function fields.
852 All fields with the same batch name will be computed by
853 a single function call. This changes the signature of the
856 .. _field-function-fnct: The ``fnct`` parameter
858 .. rubric:: The ``fnct`` parameter
860 The callable implementing the function field must have the following signature:
862 .. function:: fnct(model, cr, uid, ids, field_name(s), arg, context)
864 Implements the function field.
866 :param orm model: model to which the field belongs (should be ``self`` for
868 :param field_name(s): name of the field to compute, or if ``multi`` is provided,
869 list of field names to compute.
870 :type field_name(s): str | [str]
871 :param arg: arbitrary value passed when declaring the function field
873 :return: mapping of ``ids`` to computed values, or if multi is provided,
874 to a map of field_names to computed values
876 The values in the returned dictionary must be of the type specified by the type
877 argument in the field declaration.
879 Here is an example with a simple function ``char`` function field::
882 def compute(self, cr, uid, ids, field_name, arg, context):
886 _columns['my_char'] = fields.function(compute, type='char', size=50)
888 # when called with ``ids=[1,2,3]``, ``compute`` could return:
892 3: False # null values should be returned explicitly too
895 If ``multi`` is set, then ``field_name`` is replaced by ``field_names``: a list
896 of the field names that should be computed. Each value in the returned
897 dictionary must then be a dictionary mapping field names to values.
899 Here is an example where two function fields (``name`` and ``age``)
900 are both computed by a single function field::
903 def compute(self, cr, uid, ids, field_names, arg, context):
907 _columns['name'] = fields.function(compute_person_data, type='char',\
908 size=50, multi='person_data')
909 _columns[''age'] = fields.function(compute_person_data, type='integer',\
912 # when called with ``ids=[1,2,3]``, ``compute_person_data`` could return:
914 1: {'name': 'Bob', 'age': 23},
915 2: {'name': 'Sally', 'age': 19},
916 3: {'name': 'unknown', 'age': False}
919 .. _field-function-fnct-inv:
921 .. rubric:: The ``fnct_inv`` parameter
923 This callable implements the write operation for the function field
924 and must have the following signature:
926 .. function:: fnct_inv(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context)
928 Callable that implements the ``write`` operation for the function field.
930 :param orm model: model to which the field belongs (should be ``self`` for
932 :param int id: the identifier of the object to write on
933 :param str field_name: name of the field to set
934 :param fnct_inv_arg: arbitrary value passed when declaring the function field
937 When writing values for a function field, the ``multi`` parameter is ignored.
939 .. _field-function-fnct-search:
941 .. rubric:: The ``fnct_search`` parameter
943 This callable implements the search operation for the function field
944 and must have the following signature:
946 .. function:: fnct_search(model, cr, uid, model_again, field_name, criterion, context)
948 Callable that implements the ``search`` operation for the function field by expanding
949 a search criterion based on the function field into a new domain based only on
950 columns that are stored in the database.
952 :param orm model: model to which the field belongs (should be ``self`` for
954 :param orm model_again: same value as ``model`` (seriously! this is for backwards
956 :param str field_name: name of the field to search on
957 :param list criterion: domain component specifying the search criterion on the field.
959 :return: domain to use instead of ``criterion`` when performing the search.
960 This new domain must be based only on columns stored in the database, as it
961 will be used directly without any translation.
963 The returned value must be a domain, that is, a list of the form [(field_name, operator, operand)].
964 The most generic way to implement ``fnct_search`` is to directly search for the records that
965 match the given ``criterion``, and return their ``ids`` wrapped in a domain, such as
966 ``[('id','in',[1,3,5])]``.
968 .. _field-function-store:
970 .. rubric:: The ``store`` parameter
972 The ``store`` parameter allows caching the result of the field computation in the
973 database, and defining the triggers that will invalidate that cache and force a
974 recomputation of the function field.
975 When not provided, the field is computed every time its value is read.
976 The value of ``store`` may be either ``True`` (to recompute the field value whenever
977 any field in the same record is modified), or a dictionary specifying a more
978 flexible set of recomputation triggers.
980 A trigger specification is a dictionary that maps the names of the models that
981 will trigger the computation, to a tuple describing the trigger rule, in the
985 'trigger_model': (mapping_function,
986 ['trigger_field1', 'trigger_field2'],
990 A trigger rule is defined by a 3-item tuple where:
992 * The ``mapping_function`` is defined as follows:
994 .. function:: mapping_function(trigger_model, cr, uid, trigger_ids, context)
996 Callable that maps record ids of a trigger model to ids of the
997 corresponding records in the source model (whose field values
998 need to be recomputed).
1000 :param orm model: trigger_model
1001 :param list trigger_ids: ids of the records of trigger_model that were
1004 :return: list of ids of the source model whose function field values
1005 need to be recomputed
1007 * The second item is a list of the fields who should act as triggers for
1008 the computation. If an empty list is given, all fields will act as triggers.
1009 * The last item is the priority, used to order the triggers when processing them
1010 after any write operation on a model that has function field triggers. The
1011 default priority is 10.
1013 In fact, setting store = True is the same as using the following trigger dict::
1016 'model_itself': (lambda self, cr, uid, ids, context: ids,
1022 _classic_read = False
1023 _classic_write = False
1029 # multi: compute several fields in one call
1031 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):
1032 _column.__init__(self, **args)
1035 self._fnct_inv = fnct_inv
1038 if 'relation' in args:
1039 self._obj = args['relation']
1041 self.digits = args.get('digits', (16,2))
1042 self.digits_compute = args.get('digits_compute', None)
1044 self._fnct_inv_arg = fnct_inv_arg
1048 self._fnct_search = fnct_search
1051 if not fnct_search and not store:
1052 self.selectable = False
1055 if self._type != 'many2one':
1056 # m2o fields need to return tuples with name_get, not just foreign keys
1057 self._classic_read = True
1058 self._classic_write = True
1060 self._symbol_get=lambda x:x and str(x)
1063 self._symbol_c = float._symbol_c
1064 self._symbol_f = float._symbol_f
1065 self._symbol_set = float._symbol_set
1067 if type == 'boolean':
1068 self._symbol_c = boolean._symbol_c
1069 self._symbol_f = boolean._symbol_f
1070 self._symbol_set = boolean._symbol_set
1072 if type == 'integer':
1073 self._symbol_c = integer._symbol_c
1074 self._symbol_f = integer._symbol_f
1075 self._symbol_set = integer._symbol_set
1077 def digits_change(self, cr):
1078 if self._type == 'float':
1079 if self.digits_compute:
1080 self.digits = self.digits_compute(cr)
1082 precision, scale = self.digits
1083 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
1084 precision_digits=scale),
1085 precision_digits=scale))
1087 def search(self, cr, uid, obj, name, args, context=None):
1088 if not self._fnct_search:
1089 #CHECKME: should raise an exception
1091 return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
1093 def postprocess(self, cr, uid, obj, field, value=None, context=None):
1097 field_type = obj._columns[field]._type
1098 if field_type == "many2one":
1099 # make the result a tuple if it is not already one
1100 if isinstance(value, (int,long)) and hasattr(obj._columns[field], 'relation'):
1101 obj_model = obj.pool.get(obj._columns[field].relation)
1102 dict_names = dict(obj_model.name_get(cr, uid, [value], context))
1103 result = (value, dict_names[value])
1105 if field_type == 'binary':
1106 if context.get('bin_size'):
1107 # client requests only the size of binary fields
1108 result = get_nice_size(value)
1109 elif not context.get('bin_raw'):
1110 result = sanitize_binary_value(value)
1112 if field_type == "integer" and value > xmlrpclib.MAXINT:
1113 # integer/long values greater than 2^31-1 are not supported
1114 # in pure XMLRPC, so we have to pass them as floats :-(
1115 # This is not needed for stored fields and non-functional integer
1116 # fields, as their values are constrained by the database backend
1117 # to the same 32bits signed int limit.
1118 result = __builtin__.float(value)
1121 def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
1122 result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
1124 if self._multi and id in result:
1125 for field, value in result[id].iteritems():
1127 result[id][field] = self.postprocess(cr, uid, obj, field, value, context)
1128 elif result.get(id):
1129 result[id] = self.postprocess(cr, uid, obj, name, result[id], context)
1132 def set(self, cr, obj, id, name, value, user=None, context=None):
1136 self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
1139 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
1140 # Function fields are supposed to emulate a basic field type,
1141 # so they can delegate to the basic type for record name rendering
1142 return globals()[field._type]._as_display_name(field, cr, uid, obj, value, context=context)
1144 # ---------------------------------------------------------
1146 # ---------------------------------------------------------
1148 class related(function):
1149 """Field that points to some data inside another field of the current record.
1154 'foo_id': fields.many2one('my.foo', 'Foo'),
1155 'bar': fields.related('foo_id', 'frol', type='char', string='Frol of Foo'),
1159 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1160 self._field_get2(cr, uid, obj, context)
1161 i = len(self._arg)-1
1164 if type(sarg) in [type([]), type( (1,) )]:
1165 where = [(self._arg[i], 'in', sarg)]
1167 where = [(self._arg[i], '=', sarg)]
1169 where = map(lambda x: (self._arg[i],x[1], x[2]), domain)
1171 sarg = obj.pool.get(self._relations[i]['object']).search(cr, uid, where, context=context)
1173 return [(self._arg[0], 'in', sarg)]
1175 def _fnct_write(self,obj,cr, uid, ids, field_name, values, args, context=None):
1176 self._field_get2(cr, uid, obj, context=context)
1177 if type(ids) != type([]):
1179 objlst = obj.browse(cr, uid, ids)
1183 for i in range(len(self.arg)):
1184 if not t_data: break
1185 field_detail = self._relations[i]
1186 if not t_data[self.arg[i]]:
1187 if self._type not in ('one2many', 'many2many'):
1190 elif field_detail['type'] in ('one2many', 'many2many'):
1191 if self._type != "many2one":
1193 t_data = t_data[self.arg[i]][0]
1198 t_data = t_data[self.arg[i]]
1200 model = obj.pool.get(self._relations[-1]['object'])
1201 model.write(cr, uid, [t_id], {args[-1]: values}, context=context)
1203 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1204 self._field_get2(cr, uid, obj, context)
1205 if not ids: return {}
1206 relation = obj._name
1207 if self._type in ('one2many', 'many2many'):
1208 res = dict([(i, []) for i in ids])
1210 res = {}.fromkeys(ids, False)
1212 objlst = obj.browse(cr, 1, ids, context=context)
1217 relation = obj._name
1218 for i in range(len(self.arg)):
1219 field_detail = self._relations[i]
1220 relation = field_detail['object']
1222 if not t_data[self.arg[i]]:
1228 if field_detail['type'] in ('one2many', 'many2many') and i != len(self.arg) - 1:
1229 t_data = t_data[self.arg[i]][0]
1231 t_data = t_data[self.arg[i]]
1232 if type(t_data) == type(objlst[0]):
1233 res[data.id] = t_data.id
1235 res[data.id] = t_data
1236 if self._type=='many2one':
1237 ids = filter(None, res.values())
1239 # name_get as root, as seeing the name of a related
1240 # object depends on access right of source document,
1241 # not target, so user may not have access.
1242 ng = dict(obj.pool.get(self._obj).name_get(cr, 1, ids, context=context))
1245 res[r] = (res[r], ng[res[r]])
1246 elif self._type in ('one2many', 'many2many'):
1249 res[r] = [x.id for x in res[r]]
1252 def __init__(self, *arg, **args):
1254 self._relations = []
1255 super(related, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1256 if self.store is True:
1257 # TODO: improve here to change self.store = {...} according to related objects
1260 def _field_get2(self, cr, uid, obj, context=None):
1264 obj_name = obj._name
1265 for i in range(len(self._arg)):
1266 f = obj.pool.get(obj_name).fields_get(cr, uid, [self._arg[i]], context=context)[self._arg[i]]
1272 if f.get('relation',False):
1273 obj_name = f['relation']
1274 result[-1]['relation'] = f['relation']
1275 self._relations = result
1277 class sparse(function):
1279 def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
1281 + For a many2many field, a list of tuples is expected.
1282 Here is the list of tuple that are accepted, with the corresponding semantics ::
1284 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1285 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1286 (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)
1287 (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)
1288 (4, ID) link to existing record with id = ID (adds a relationship)
1289 (5) unlink all (like using (3,ID) for all linked records)
1290 (6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
1293 [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
1295 + For a one2many field, a lits of tuples is expected.
1296 Here is the list of tuple that are accepted, with the corresponding semantics ::
1298 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1299 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1300 (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)
1303 [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
1306 if self._type == 'many2many':
1307 assert value[0][0] == 6, 'Unsupported m2m value for sparse field: %s' % value
1310 elif self._type == 'one2many':
1313 relation_obj = obj.pool.get(self.relation)
1315 assert vals[0] in (0,1,2), 'Unsupported o2m value for sparse field: %s' % vals
1317 read_value.append(relation_obj.create(cr, uid, vals[2], context=context))
1319 relation_obj.write(cr, uid, vals[1], vals[2], context=context)
1321 relation_obj.unlink(cr, uid, vals[1], context=context)
1322 read_value.remove(vals[1])
1327 def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
1328 if not type(ids) == list:
1330 records = obj.browse(cr, uid, ids, context=context)
1331 for record in records:
1332 # grab serialized value as object - already deserialized
1333 serialized = getattr(record, self.serialization_field)
1335 # simply delete the key to unset it.
1336 serialized.pop(field_name, None)
1338 serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context)
1339 obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context)
1342 def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None):
1344 records = obj.browse(cr, uid, ids, context=context)
1345 for record in records:
1346 # grab serialized value as object - already deserialized
1347 serialized = getattr(record, self.serialization_field)
1348 results[record.id] = {}
1349 for field_name in field_names:
1350 field_type = obj._columns[field_name]._type
1351 value = serialized.get(field_name, False)
1352 if field_type in ('one2many','many2many'):
1355 # filter out deleted records as superuser
1356 relation_obj = obj.pool.get(obj._columns[field_name].relation)
1357 value = relation_obj.exists(cr, openerp.SUPERUSER_ID, value)
1358 if type(value) in (int,long) and field_type == 'many2one':
1359 relation_obj = obj.pool.get(obj._columns[field_name].relation)
1360 # check for deleted record as superuser
1361 if not relation_obj.exists(cr, openerp.SUPERUSER_ID, [value]):
1363 results[record.id][field_name] = value
1366 def __init__(self, serialization_field, **kwargs):
1367 self.serialization_field = serialization_field
1368 return super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
1372 # ---------------------------------------------------------
1374 # ---------------------------------------------------------
1376 class dummy(function):
1377 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1380 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1383 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1386 def __init__(self, *arg, **args):
1388 self._relations = []
1389 super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=None, **args)
1391 # ---------------------------------------------------------
1393 # ---------------------------------------------------------
1395 class serialized(_column):
1396 """ A field able to store an arbitrary python data structure.
1398 Note: only plain components allowed.
1401 def _symbol_set_struct(val):
1402 return simplejson.dumps(val)
1404 def _symbol_get_struct(self, val):
1405 return simplejson.loads(val or '{}')
1408 _type = 'serialized'
1411 _symbol_f = _symbol_set_struct
1412 _symbol_set = (_symbol_c, _symbol_f)
1413 _symbol_get = _symbol_get_struct
1415 # TODO: review completly this class for speed improvement
1416 class property(function):
1418 def _get_default(self, obj, cr, uid, prop_name, context=None):
1419 return self._get_defaults(obj, cr, uid, [prop_name], context=None)[prop_name]
1421 def _get_defaults(self, obj, cr, uid, prop_names, context=None):
1422 """Get the default values for ``prop_names´´ property fields (result of ir.property.get() function for res_id = False).
1424 :param list of string prop_names: list of name of property fields for those we want the default value
1425 :return: map of property field names to their default value
1428 prop = obj.pool.get('ir.property')
1430 for prop_name in prop_names:
1431 res[prop_name] = prop.get(cr, uid, prop_name, obj._name, context=context)
1434 def _get_by_id(self, obj, cr, uid, prop_name, ids, context=None):
1435 prop = obj.pool.get('ir.property')
1436 vids = [obj._name + ',' + str(oid) for oid in ids]
1438 domain = [('fields_id.model', '=', obj._name), ('fields_id.name', 'in', prop_name)]
1439 #domain = prop._get_domain(cr, uid, prop_name, obj._name, context)
1441 domain = [('res_id', 'in', vids)] + domain
1442 return prop.search(cr, uid, domain, context=context)
1444 # TODO: to rewrite more clean
1445 def _fnct_write(self, obj, cr, uid, id, prop_name, id_val, obj_dest, context=None):
1449 nids = self._get_by_id(obj, cr, uid, [prop_name], [id], context)
1451 cr.execute('DELETE FROM ir_property WHERE id IN %s', (tuple(nids),))
1453 default_val = self._get_default(obj, cr, uid, prop_name, context)
1455 property_create = False
1456 if isinstance(default_val, openerp.osv.orm.browse_record):
1457 if default_val.id != id_val:
1458 property_create = True
1459 elif default_val != id_val:
1460 property_create = True
1463 def_id = self._field_get(cr, uid, obj._name, prop_name)
1464 company = obj.pool.get('res.company')
1465 cid = company._company_default_get(cr, uid, obj._name, def_id,
1467 propdef = obj.pool.get('ir.model.fields').browse(cr, uid, def_id,
1469 prop = obj.pool.get('ir.property')
1470 return prop.create(cr, uid, {
1471 'name': propdef.name,
1473 'res_id': obj._name+','+str(id),
1475 'fields_id': def_id,
1480 def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
1481 prop = obj.pool.get('ir.property')
1482 # get the default values (for res_id = False) for the property fields
1483 default_val = self._get_defaults(obj, cr, uid, prop_names, context)
1485 # build the dictionary that will be returned
1488 res[id] = default_val.copy()
1490 for prop_name in prop_names:
1491 property_field = obj._all_columns.get(prop_name).column
1492 property_destination_obj = property_field._obj if property_field._type == 'many2one' else False
1493 # If the property field is a m2o field, we will append the id of the value to name_get_ids
1494 # in order to make a name_get in batch for all the ids needed.
1497 # get the result of ir.property.get() for this res_id and save it in res if it's existing
1498 obj_reference = obj._name + ',' + str(id)
1499 value = prop.get(cr, uid, prop_name, obj._name, res_id=obj_reference, context=context)
1501 res[id][prop_name] = value
1502 # Check existence as root (as seeing the name of a related
1503 # object depends on access right of source document,
1504 # not target, so user may not have access) in order to avoid
1505 # pointing on an unexisting record.
1506 if property_destination_obj:
1507 if res[id][prop_name] and obj.pool.get(property_destination_obj).exists(cr, 1, res[id][prop_name].id):
1508 name_get_ids[id] = res[id][prop_name].id
1510 res[id][prop_name] = False
1511 if property_destination_obj:
1512 # name_get as root (as seeing the name of a related
1513 # object depends on access right of source document,
1514 # not target, so user may not have access.)
1515 name_get_values = dict(obj.pool.get(property_destination_obj).name_get(cr, 1, name_get_ids.values(), context=context))
1516 # the property field is a m2o, we need to return a tuple with (id, name)
1517 for k, v in name_get_ids.iteritems():
1518 if res[k][prop_name]:
1519 res[k][prop_name] = (v , name_get_values.get(v))
1522 def _field_get(self, cr, uid, model_name, prop):
1523 if not self.field_id.get(cr.dbname):
1524 cr.execute('SELECT id \
1525 FROM ir_model_fields \
1526 WHERE name=%s AND model=%s', (prop, model_name))
1528 self.field_id[cr.dbname] = res and res[0]
1529 return self.field_id[cr.dbname]
1531 def __init__(self, obj_prop, **args):
1532 # TODO remove obj_prop parameter (use many2one type)
1534 function.__init__(self, self._fnct_read, False, self._fnct_write,
1535 obj_prop, multi='properties', **args)
1541 def field_to_dict(model, cr, user, field, context=None):
1542 """ Return a dictionary representation of a field.
1544 The string, help, and selection attributes (if any) are untranslated. This
1545 representation is the one returned by fields_get() (fields_get() will do
1550 res = {'type': field._type}
1551 # This additional attributes for M2M and function field is added
1552 # because we need to display tooltip with this additional information
1553 # when client is started in debug mode.
1554 if isinstance(field, function):
1555 res['function'] = field._fnct and field._fnct.func_name or False
1556 # res['store'] = field.store
1557 if isinstance(field.store, dict):
1558 res['store'] = str(field.store)
1559 res['fnct_search'] = field._fnct_search and field._fnct_search.func_name or False
1560 res['fnct_inv'] = field._fnct_inv and field._fnct_inv.func_name or False
1561 res['fnct_inv_arg'] = field._fnct_inv_arg or False
1562 res['func_obj'] = field._obj or False
1563 if isinstance(field, many2many):
1564 (table, col1, col2) = field._sql_names(model)
1565 res['related_columns'] = [col1, col2]
1566 res['third_table'] = table
1567 for arg in ('string', 'readonly', 'states', 'size', 'required', 'group_operator',
1568 'change_default', 'translate', 'help', 'select', 'selectable', 'groups'):
1569 if getattr(field, arg):
1570 res[arg] = getattr(field, arg)
1571 for arg in ('digits', 'invisible', 'filters'):
1572 if getattr(field, arg, None):
1573 res[arg] = getattr(field, arg)
1576 res['string'] = field.string
1578 res['help'] = field.help
1580 if hasattr(field, 'selection'):
1581 if isinstance(field.selection, (tuple, list)):
1582 res['selection'] = field.selection
1584 # call the 'dynamic selection' function
1585 res['selection'] = field.selection(model, cr, user, context)
1586 if res['type'] in ('one2many', 'many2many', 'many2one'):
1587 res['relation'] = field._obj
1588 res['domain'] = field._domain
1589 res['context'] = field._context
1591 if isinstance(field, one2many):
1592 res['relation_field'] = field._fields_id
1597 class column_info(object):
1598 """Struct containing details about an osv column, either one local to
1599 its model, or one inherited via _inherits.
1601 :attr name: name of the column
1602 :attr column: column instance, subclass of osv.fields._column
1603 :attr parent_model: if the column is inherited, name of the model
1604 that contains it, None for local columns.
1605 :attr parent_column: the name of the column containing the m2o
1606 relationship to the parent model that contains
1607 this column, None for local columns.
1608 :attr original_parent: if the column is inherited, name of the original
1609 parent model that contains it i.e in case of multilevel
1610 inheritence, None for local columns.
1612 def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
1614 self.column = column
1615 self.parent_model = parent_model
1616 self.parent_column = parent_column
1617 self.original_parent = original_parent
1619 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: