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)
517 ids2 = obj.pool.get(self._obj).search(cr, user, self._domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
518 for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
519 if r[self._fields_id] in res:
520 res[r[self._fields_id]].append(r['id'])
523 def set(self, cr, obj, id, field, values, user=None, context=None):
528 context = context.copy()
529 context.update(self._context)
530 context['no_store_function'] = True
533 _table = obj.pool.get(self._obj)._table
534 obj = obj.pool.get(self._obj)
537 act[2][self._fields_id] = id
538 id_new = obj.create(cr, user, act[2], context=context)
539 result += obj._store_get_values(cr, user, [id_new], act[2].keys(), context)
541 obj.write(cr, user, [act[1]], act[2], context=context)
543 obj.unlink(cr, user, [act[1]], context=context)
545 reverse_rel = obj._all_columns.get(self._fields_id)
546 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
547 # if the model has on delete cascade, just delete the row
548 if reverse_rel.column.ondelete == "cascade":
549 obj.unlink(cr, user, [act[1]], context=context)
551 cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],))
553 # Must use write() to recompute parent_store structure if needed
554 obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
556 reverse_rel = obj._all_columns.get(self._fields_id)
557 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
558 # if the o2m has a static domain we must respect it when unlinking
559 extra_domain = self._domain if isinstance(getattr(self, '_domain', None), list) else []
560 ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
561 # If the model has cascade deletion, we delete the rows because it is the intended behavior,
562 # otherwise we only nullify the reverse foreign key column.
563 if reverse_rel.column.ondelete == "cascade":
564 obj.unlink(cr, user, ids_to_unlink, context=context)
566 obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context)
568 # Must use write() to recompute parent_store structure if needed
569 obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
571 cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
572 ids3 = map(lambda x:x[0], cr.fetchall())
573 obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
576 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
577 return obj.pool.get(self._obj).name_search(cr, uid, value, self._domain, operator, context=context,limit=limit)
581 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
582 raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)')
585 # Values: (0, 0, { fields }) create
586 # (1, ID, { fields }) update (write fields to ID)
587 # (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete)
588 # (3, ID) unlink (delete the relationship between the two objects but does not delete ID)
589 # (4, ID) link (add a relationship)
591 # (6, ?, ids) set a list of links
593 class many2many(_column):
594 """Encapsulates the logic of a many-to-many bidirectional relationship, handling the
595 low-level details of the intermediary relationship table transparently.
596 A many-to-many relationship is always symmetrical, and can be declared and accessed
597 from either endpoint model.
598 If ``rel`` (relationship table name), ``id1`` (source foreign key column name)
599 or id2 (destination foreign key column name) are not specified, the system will
600 provide default values. This will by default only allow one single symmetrical
601 many-to-many relationship between the source and destination model.
602 For multiple many-to-many relationship between the same models and for
603 relationships where source and destination models are the same, ``rel``, ``id1``
604 and ``id2`` should be specified explicitly.
606 :param str obj: destination model
607 :param str rel: optional name of the intermediary relationship table. If not specified,
608 a canonical name will be derived based on the alphabetically-ordered
609 model names of the source and destination (in the form: ``amodel_bmodel_rel``).
610 Automatic naming is not possible when the source and destination are
611 the same, for obvious ambiguity reasons.
612 :param str id1: optional name for the column holding the foreign key to the current
613 model in the relationship table. If not specified, a canonical name
614 will be derived based on the model name (in the form: `src_model_id`).
615 :param str id2: optional name for the column holding the foreign key to the destination
616 model in the relationship table. If not specified, a canonical name
617 will be derived based on the model name (in the form: `dest_model_id`)
618 :param str string: field label
620 _classic_read = False
621 _classic_write = False
625 def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
628 _column.__init__(self, string=string, **args)
630 if rel and '.' in rel:
631 raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\
632 'You used %s, which is not a valid SQL table name.')% (string,rel))
638 def _sql_names(self, source_model):
639 """Return the SQL names defining the structure of the m2m relationship table
641 :return: (m2m_table, local_col, dest_col) where m2m_table is the table name,
642 local_col is the name of the column holding the current model's FK, and
643 dest_col is the name of the column holding the destination model's FK, and
645 tbl, col1, col2 = self._rel, self._id1, self._id2
646 if not all((tbl, col1, col2)):
647 # the default table name is based on the stable alphabetical order of tables
648 dest_model = source_model.pool.get(self._obj)
649 tables = tuple(sorted([source_model._table, dest_model._table]))
651 assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\
652 'is not possible when source and destination models are '\
654 tbl = '%s_%s_rel' % tables
656 col1 = '%s_id' % source_model._table
658 col2 = '%s_id' % dest_model._table
659 return (tbl, col1, col2)
661 def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
673 "Specifying offset at a many2many.get() is deprecated and may"
674 " produce unpredictable results.")
675 obj = model.pool.get(self._obj)
676 rel, id1, id2 = self._sql_names(model)
678 # static domains are lists, and are evaluated both here and on client-side, while string
679 # domains supposed by dynamic and evaluated on client-side only (thus ignored here)
680 # FIXME: make this distinction explicit in API!
681 domain = isinstance(self._domain, list) and self._domain or []
683 wquery = obj._where_calc(cr, user, domain, context=context)
684 obj._apply_ir_rules(cr, user, wquery, 'read', context=context)
685 from_c, where_c, where_params = wquery.get_sql()
687 where_c = ' AND ' + where_c
689 if offset or self._limit:
690 order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0])
695 if self._limit is not None:
696 limit_str = ' LIMIT %d' % self._limit
698 query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
699 FROM %(rel)s, %(from_c)s \
700 WHERE %(rel)s.%(id1)s IN %%s \
701 AND %(rel)s.%(id2)s = %(tbl)s.id \
713 'order_by': order_by,
716 cr.execute(query, [tuple(ids),] + where_params)
717 for r in cr.fetchall():
718 res[r[1]].append(r[0])
721 def set(self, cr, model, id, name, values, user=None, context=None):
726 rel, id1, id2 = self._sql_names(model)
727 obj = model.pool.get(self._obj)
729 if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
732 idnew = obj.create(cr, user, act[2], context=context)
733 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew))
735 obj.write(cr, user, [act[1]], act[2], context=context)
737 obj.unlink(cr, user, [act[1]], context=context)
739 cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1]))
741 # following queries are in the same transaction - so should be relatively safe
742 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1]))
743 if not cr.fetchone():
744 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1]))
746 cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,))
749 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
751 d1 = ' and ' + ' and '.join(d1)
754 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)
756 for act_nbr in act[2]:
757 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr))
760 # TODO: use a name_search
762 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
763 return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
766 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
767 raise NotImplementedError('Many2Many columns should not be used as record name (_rec_name)')
770 def get_nice_size(value):
772 if isinstance(value, (int,long)):
774 elif value: # this is supposed to be a string
776 return tools.human_size(size)
778 # See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
779 # and http://bugs.python.org/issue10066
780 invalid_xml_low_bytes = re.compile(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
782 def sanitize_binary_value(value):
783 # binary fields should be 7-bit ASCII base64-encoded data,
784 # but we do additional sanity checks to make sure the values
785 # are not something else that won't pass via XML-RPC
786 if isinstance(value, (xmlrpclib.Binary, tuple, list, dict)):
787 # these builtin types are meant to pass untouched
790 # Handle invalid bytes values that will cause problems
791 # for XML-RPC. See for more info:
792 # - http://bugs.python.org/issue10066
793 # - http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
795 # Coercing to unicode would normally allow it to properly pass via
796 # XML-RPC, transparently encoded as UTF-8 by xmlrpclib.
797 # (this works for _any_ byte values, thanks to the fallback
798 # to latin-1 passthrough encoding when decoding to unicode)
799 value = tools.ustr(value)
801 # Due to Python bug #10066 this could still yield invalid XML
802 # bytes, specifically in the low byte range, that will crash
803 # the decoding side: [\x00-\x08\x0b-\x0c\x0e-\x1f]
804 # So check for low bytes values, and if any, perform
805 # base64 encoding - not very smart or useful, but this is
806 # our last resort to avoid crashing the request.
807 if invalid_xml_low_bytes.search(value):
808 # b64-encode after restoring the pure bytes with latin-1
809 # passthrough encoding
810 value = base64.b64encode(value.encode('latin-1'))
815 # ---------------------------------------------------------
817 # ---------------------------------------------------------
818 class function(_column):
820 A field whose value is computed by a function (rather
821 than being read from the database).
823 :param fnct: the callable that will compute the field value.
824 :param arg: arbitrary value to be passed to ``fnct`` when computing the value.
825 :param fnct_inv: the callable that will allow writing values in that field
826 (if not provided, the field is read-only).
827 :param fnct_inv_arg: arbitrary value to be passed to ``fnct_inv`` when
829 :param str type: type of the field simulated by the function field
830 :param fnct_search: the callable that allows searching on the field
831 (if not provided, search will not return any result).
832 :param store: store computed value in database
833 (see :ref:`The *store* parameter <field-function-store>`).
834 :type store: True or dict specifying triggers for field computation
835 :param multi: name of batch for batch computation of function fields.
836 All fields with the same batch name will be computed by
837 a single function call. This changes the signature of the
840 .. _field-function-fnct: The ``fnct`` parameter
842 .. rubric:: The ``fnct`` parameter
844 The callable implementing the function field must have the following signature:
846 .. function:: fnct(model, cr, uid, ids, field_name(s), arg, context)
848 Implements the function field.
850 :param orm model: model to which the field belongs (should be ``self`` for
852 :param field_name(s): name of the field to compute, or if ``multi`` is provided,
853 list of field names to compute.
854 :type field_name(s): str | [str]
855 :param arg: arbitrary value passed when declaring the function field
857 :return: mapping of ``ids`` to computed values, or if multi is provided,
858 to a map of field_names to computed values
860 The values in the returned dictionary must be of the type specified by the type
861 argument in the field declaration.
863 Here is an example with a simple function ``char`` function field::
866 def compute(self, cr, uid, ids, field_name, arg, context):
870 _columns['my_char'] = fields.function(compute, type='char', size=50)
872 # when called with ``ids=[1,2,3]``, ``compute`` could return:
876 3: False # null values should be returned explicitly too
879 If ``multi`` is set, then ``field_name`` is replaced by ``field_names``: a list
880 of the field names that should be computed. Each value in the returned
881 dictionary must then be a dictionary mapping field names to values.
883 Here is an example where two function fields (``name`` and ``age``)
884 are both computed by a single function field::
887 def compute(self, cr, uid, ids, field_names, arg, context):
891 _columns['name'] = fields.function(compute_person_data, type='char',\
892 size=50, multi='person_data')
893 _columns[''age'] = fields.function(compute_person_data, type='integer',\
896 # when called with ``ids=[1,2,3]``, ``compute_person_data`` could return:
898 1: {'name': 'Bob', 'age': 23},
899 2: {'name': 'Sally', 'age': 19},
900 3: {'name': 'unknown', 'age': False}
903 .. _field-function-fnct-inv:
905 .. rubric:: The ``fnct_inv`` parameter
907 This callable implements the write operation for the function field
908 and must have the following signature:
910 .. function:: fnct_inv(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context)
912 Callable that implements the ``write`` operation for the function field.
914 :param orm model: model to which the field belongs (should be ``self`` for
916 :param int id: the identifier of the object to write on
917 :param str field_name: name of the field to set
918 :param fnct_inv_arg: arbitrary value passed when declaring the function field
921 When writing values for a function field, the ``multi`` parameter is ignored.
923 .. _field-function-fnct-search:
925 .. rubric:: The ``fnct_search`` parameter
927 This callable implements the search operation for the function field
928 and must have the following signature:
930 .. function:: fnct_search(model, cr, uid, model_again, field_name, criterion, context)
932 Callable that implements the ``search`` operation for the function field by expanding
933 a search criterion based on the function field into a new domain based only on
934 columns that are stored in the database.
936 :param orm model: model to which the field belongs (should be ``self`` for
938 :param orm model_again: same value as ``model`` (seriously! this is for backwards
940 :param str field_name: name of the field to search on
941 :param list criterion: domain component specifying the search criterion on the field.
943 :return: domain to use instead of ``criterion`` when performing the search.
944 This new domain must be based only on columns stored in the database, as it
945 will be used directly without any translation.
947 The returned value must be a domain, that is, a list of the form [(field_name, operator, operand)].
948 The most generic way to implement ``fnct_search`` is to directly search for the records that
949 match the given ``criterion``, and return their ``ids`` wrapped in a domain, such as
950 ``[('id','in',[1,3,5])]``.
952 .. _field-function-store:
954 .. rubric:: The ``store`` parameter
956 The ``store`` parameter allows caching the result of the field computation in the
957 database, and defining the triggers that will invalidate that cache and force a
958 recomputation of the function field.
959 When not provided, the field is computed every time its value is read.
960 The value of ``store`` may be either ``True`` (to recompute the field value whenever
961 any field in the same record is modified), or a dictionary specifying a more
962 flexible set of recomputation triggers.
964 A trigger specification is a dictionary that maps the names of the models that
965 will trigger the computation, to a tuple describing the trigger rule, in the
969 'trigger_model': (mapping_function,
970 ['trigger_field1', 'trigger_field2'],
974 A trigger rule is defined by a 3-item tuple where:
976 * The ``mapping_function`` is defined as follows:
978 .. function:: mapping_function(trigger_model, cr, uid, trigger_ids, context)
980 Callable that maps record ids of a trigger model to ids of the
981 corresponding records in the source model (whose field values
982 need to be recomputed).
984 :param orm model: trigger_model
985 :param list trigger_ids: ids of the records of trigger_model that were
988 :return: list of ids of the source model whose function field values
989 need to be recomputed
991 * The second item is a list of the fields who should act as triggers for
992 the computation. If an empty list is given, all fields will act as triggers.
993 * The last item is the priority, used to order the triggers when processing them
994 after any write operation on a model that has function field triggers. The
995 default priority is 10.
997 In fact, setting store = True is the same as using the following trigger dict::
1000 'model_itself': (lambda self, cr, uid, ids, context: ids,
1006 _classic_read = False
1007 _classic_write = False
1013 # multi: compute several fields in one call
1015 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):
1016 _column.__init__(self, **args)
1019 self._fnct_inv = fnct_inv
1022 if 'relation' in args:
1023 self._obj = args['relation']
1025 self.digits = args.get('digits', (16,2))
1026 self.digits_compute = args.get('digits_compute', None)
1028 self._fnct_inv_arg = fnct_inv_arg
1032 self._fnct_search = fnct_search
1035 if not fnct_search and not store:
1036 self.selectable = False
1039 if self._type != 'many2one':
1040 # m2o fields need to return tuples with name_get, not just foreign keys
1041 self._classic_read = True
1042 self._classic_write = True
1044 self._symbol_get=lambda x:x and str(x)
1047 self._symbol_c = float._symbol_c
1048 self._symbol_f = float._symbol_f
1049 self._symbol_set = float._symbol_set
1051 if type == 'boolean':
1052 self._symbol_c = boolean._symbol_c
1053 self._symbol_f = boolean._symbol_f
1054 self._symbol_set = boolean._symbol_set
1056 if type == 'integer':
1057 self._symbol_c = integer._symbol_c
1058 self._symbol_f = integer._symbol_f
1059 self._symbol_set = integer._symbol_set
1061 def digits_change(self, cr):
1062 if self._type == 'float':
1063 if self.digits_compute:
1064 self.digits = self.digits_compute(cr)
1066 precision, scale = self.digits
1067 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
1068 precision_digits=scale),
1069 precision_digits=scale))
1071 def search(self, cr, uid, obj, name, args, context=None):
1072 if not self._fnct_search:
1073 #CHECKME: should raise an exception
1075 return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
1077 def postprocess(self, cr, uid, obj, field, value=None, context=None):
1081 field_type = obj._columns[field]._type
1082 if field_type == "many2one":
1083 # make the result a tuple if it is not already one
1084 if isinstance(value, (int,long)) and hasattr(obj._columns[field], 'relation'):
1085 obj_model = obj.pool.get(obj._columns[field].relation)
1086 dict_names = dict(obj_model.name_get(cr, uid, [value], context))
1087 result = (value, dict_names[value])
1089 if field_type == 'binary':
1090 if context.get('bin_size'):
1091 # client requests only the size of binary fields
1092 result = get_nice_size(value)
1093 elif not context.get('bin_raw'):
1094 result = sanitize_binary_value(value)
1096 if field_type == "integer" and value > xmlrpclib.MAXINT:
1097 # integer/long values greater than 2^31-1 are not supported
1098 # in pure XMLRPC, so we have to pass them as floats :-(
1099 # This is not needed for stored fields and non-functional integer
1100 # fields, as their values are constrained by the database backend
1101 # to the same 32bits signed int limit.
1102 result = __builtin__.float(value)
1105 def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
1106 result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
1108 if self._multi and id in result:
1109 for field, value in result[id].iteritems():
1111 result[id][field] = self.postprocess(cr, uid, obj, field, value, context)
1112 elif result.get(id):
1113 result[id] = self.postprocess(cr, uid, obj, name, result[id], context)
1116 def set(self, cr, obj, id, name, value, user=None, context=None):
1120 self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
1123 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
1124 # Function fields are supposed to emulate a basic field type,
1125 # so they can delegate to the basic type for record name rendering
1126 return globals()[field._type]._as_display_name(field, cr, uid, obj, value, context=context)
1128 # ---------------------------------------------------------
1130 # ---------------------------------------------------------
1132 class related(function):
1133 """Field that points to some data inside another field of the current record.
1138 'foo_id': fields.many2one('my.foo', 'Foo'),
1139 'bar': fields.related('foo_id', 'frol', type='char', string='Frol of Foo'),
1143 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1144 self._field_get2(cr, uid, obj, context)
1145 i = len(self._arg)-1
1148 if type(sarg) in [type([]), type( (1,) )]:
1149 where = [(self._arg[i], 'in', sarg)]
1151 where = [(self._arg[i], '=', sarg)]
1153 where = map(lambda x: (self._arg[i],x[1], x[2]), domain)
1155 sarg = obj.pool.get(self._relations[i]['object']).search(cr, uid, where, context=context)
1157 return [(self._arg[0], 'in', sarg)]
1159 def _fnct_write(self,obj,cr, uid, ids, field_name, values, args, context=None):
1160 self._field_get2(cr, uid, obj, context=context)
1161 if type(ids) != type([]):
1163 objlst = obj.browse(cr, uid, ids)
1167 for i in range(len(self.arg)):
1168 if not t_data: break
1169 field_detail = self._relations[i]
1170 if not t_data[self.arg[i]]:
1171 if self._type not in ('one2many', 'many2many'):
1174 elif field_detail['type'] in ('one2many', 'many2many'):
1175 if self._type != "many2one":
1177 t_data = t_data[self.arg[i]][0]
1182 t_data = t_data[self.arg[i]]
1184 model = obj.pool.get(self._relations[-1]['object'])
1185 model.write(cr, uid, [t_id], {args[-1]: values}, context=context)
1187 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1188 self._field_get2(cr, uid, obj, context)
1189 if not ids: return {}
1190 relation = obj._name
1191 if self._type in ('one2many', 'many2many'):
1192 res = dict([(i, []) for i in ids])
1194 res = {}.fromkeys(ids, False)
1196 objlst = obj.browse(cr, 1, ids, context=context)
1201 relation = obj._name
1202 for i in range(len(self.arg)):
1203 field_detail = self._relations[i]
1204 relation = field_detail['object']
1206 if not t_data[self.arg[i]]:
1212 if field_detail['type'] in ('one2many', 'many2many') and i != len(self.arg) - 1:
1213 t_data = t_data[self.arg[i]][0]
1215 t_data = t_data[self.arg[i]]
1216 if type(t_data) == type(objlst[0]):
1217 res[data.id] = t_data.id
1219 res[data.id] = t_data
1220 if self._type=='many2one':
1221 ids = filter(None, res.values())
1223 # name_get as root, as seeing the name of a related
1224 # object depends on access right of source document,
1225 # not target, so user may not have access.
1226 ng = dict(obj.pool.get(self._obj).name_get(cr, 1, ids, context=context))
1229 res[r] = (res[r], ng[res[r]])
1230 elif self._type in ('one2many', 'many2many'):
1233 res[r] = [x.id for x in res[r]]
1236 def __init__(self, *arg, **args):
1238 self._relations = []
1239 super(related, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1240 if self.store is True:
1241 # TODO: improve here to change self.store = {...} according to related objects
1244 def _field_get2(self, cr, uid, obj, context=None):
1248 obj_name = obj._name
1249 for i in range(len(self._arg)):
1250 f = obj.pool.get(obj_name).fields_get(cr, uid, [self._arg[i]], context=context)[self._arg[i]]
1256 if f.get('relation',False):
1257 obj_name = f['relation']
1258 result[-1]['relation'] = f['relation']
1259 self._relations = result
1261 class sparse(function):
1263 def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
1265 + For a many2many field, a list of tuples is expected.
1266 Here is the list of tuple that are accepted, with the corresponding semantics ::
1268 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1269 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1270 (2, ID) remove and delete the linked record with id = ID (calls unlink on ID, that will delete the object completely, and the link to it as well)
1271 (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)
1272 (4, ID) link to existing record with id = ID (adds a relationship)
1273 (5) unlink all (like using (3,ID) for all linked records)
1274 (6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
1277 [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
1279 + For a one2many field, a lits of tuples is expected.
1280 Here is the list of tuple that are accepted, with the corresponding semantics ::
1282 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1283 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1284 (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 [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
1290 if self._type == 'many2many':
1291 assert value[0][0] == 6, 'Unsupported m2m value for sparse field: %s' % value
1294 elif self._type == 'one2many':
1297 relation_obj = obj.pool.get(self.relation)
1299 assert vals[0] in (0,1,2), 'Unsupported o2m value for sparse field: %s' % vals
1301 read_value.append(relation_obj.create(cr, uid, vals[2], context=context))
1303 relation_obj.write(cr, uid, vals[1], vals[2], context=context)
1305 relation_obj.unlink(cr, uid, vals[1], context=context)
1306 read_value.remove(vals[1])
1311 def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
1312 if not type(ids) == list:
1314 records = obj.browse(cr, uid, ids, context=context)
1315 for record in records:
1316 # grab serialized value as object - already deserialized
1317 serialized = getattr(record, self.serialization_field)
1319 # simply delete the key to unset it.
1320 serialized.pop(field_name, None)
1322 serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context)
1323 obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context)
1326 def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None):
1328 records = obj.browse(cr, uid, ids, context=context)
1329 for record in records:
1330 # grab serialized value as object - already deserialized
1331 serialized = getattr(record, self.serialization_field)
1332 results[record.id] = {}
1333 for field_name in field_names:
1334 field_type = obj._columns[field_name]._type
1335 value = serialized.get(field_name, False)
1336 if field_type in ('one2many','many2many'):
1339 # filter out deleted records as superuser
1340 relation_obj = obj.pool.get(obj._columns[field_name].relation)
1341 value = relation_obj.exists(cr, openerp.SUPERUSER_ID, value)
1342 if type(value) in (int,long) and field_type == 'many2one':
1343 relation_obj = obj.pool.get(obj._columns[field_name].relation)
1344 # check for deleted record as superuser
1345 if not relation_obj.exists(cr, openerp.SUPERUSER_ID, [value]):
1347 results[record.id][field_name] = value
1350 def __init__(self, serialization_field, **kwargs):
1351 self.serialization_field = serialization_field
1352 return super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
1356 # ---------------------------------------------------------
1358 # ---------------------------------------------------------
1360 class dummy(function):
1361 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1364 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1367 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1370 def __init__(self, *arg, **args):
1372 self._relations = []
1373 super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=None, **args)
1375 # ---------------------------------------------------------
1377 # ---------------------------------------------------------
1379 class serialized(_column):
1380 """ A field able to store an arbitrary python data structure.
1382 Note: only plain components allowed.
1385 def _symbol_set_struct(val):
1386 return simplejson.dumps(val)
1388 def _symbol_get_struct(self, val):
1389 return simplejson.loads(val or '{}')
1392 _type = 'serialized'
1395 _symbol_f = _symbol_set_struct
1396 _symbol_set = (_symbol_c, _symbol_f)
1397 _symbol_get = _symbol_get_struct
1399 # TODO: review completly this class for speed improvement
1400 class property(function):
1402 def _get_default(self, obj, cr, uid, prop_name, context=None):
1403 return self._get_defaults(obj, cr, uid, [prop_name], context=None)[prop_name]
1405 def _get_defaults(self, obj, cr, uid, prop_names, context=None):
1406 """Get the default values for ``prop_names´´ property fields (result of ir.property.get() function for res_id = False).
1408 :param list of string prop_names: list of name of property fields for those we want the default value
1409 :return: map of property field names to their default value
1412 prop = obj.pool.get('ir.property')
1414 for prop_name in prop_names:
1415 res[prop_name] = prop.get(cr, uid, prop_name, obj._name, context=context)
1418 def _get_by_id(self, obj, cr, uid, prop_name, ids, context=None):
1419 prop = obj.pool.get('ir.property')
1420 vids = [obj._name + ',' + str(oid) for oid in ids]
1422 domain = [('fields_id.model', '=', obj._name), ('fields_id.name', 'in', prop_name)]
1423 #domain = prop._get_domain(cr, uid, prop_name, obj._name, context)
1425 domain = [('res_id', 'in', vids)] + domain
1426 return prop.search(cr, uid, domain, context=context)
1428 # TODO: to rewrite more clean
1429 def _fnct_write(self, obj, cr, uid, id, prop_name, id_val, obj_dest, context=None):
1433 nids = self._get_by_id(obj, cr, uid, [prop_name], [id], context)
1435 cr.execute('DELETE FROM ir_property WHERE id IN %s', (tuple(nids),))
1437 default_val = self._get_default(obj, cr, uid, prop_name, context)
1439 property_create = False
1440 if isinstance(default_val, openerp.osv.orm.browse_record):
1441 if default_val.id != id_val:
1442 property_create = True
1443 elif default_val != id_val:
1444 property_create = True
1447 def_id = self._field_get(cr, uid, obj._name, prop_name)
1448 company = obj.pool.get('res.company')
1449 cid = company._company_default_get(cr, uid, obj._name, def_id,
1451 propdef = obj.pool.get('ir.model.fields').browse(cr, uid, def_id,
1453 prop = obj.pool.get('ir.property')
1454 return prop.create(cr, uid, {
1455 'name': propdef.name,
1457 'res_id': obj._name+','+str(id),
1459 'fields_id': def_id,
1464 def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
1465 prop = obj.pool.get('ir.property')
1466 # get the default values (for res_id = False) for the property fields
1467 default_val = self._get_defaults(obj, cr, uid, prop_names, context)
1469 # build the dictionary that will be returned
1472 res[id] = default_val.copy()
1474 for prop_name in prop_names:
1475 property_field = obj._all_columns.get(prop_name).column
1476 property_destination_obj = property_field._obj if property_field._type == 'many2one' else False
1477 # If the property field is a m2o field, we will append the id of the value to name_get_ids
1478 # in order to make a name_get in batch for all the ids needed.
1481 # get the result of ir.property.get() for this res_id and save it in res if it's existing
1482 obj_reference = obj._name + ',' + str(id)
1483 value = prop.get(cr, uid, prop_name, obj._name, res_id=obj_reference, context=context)
1485 res[id][prop_name] = value
1486 # Check existence as root (as seeing the name of a related
1487 # object depends on access right of source document,
1488 # not target, so user may not have access) in order to avoid
1489 # pointing on an unexisting record.
1490 if property_destination_obj:
1491 if res[id][prop_name] and obj.pool.get(property_destination_obj).exists(cr, 1, res[id][prop_name].id):
1492 name_get_ids[id] = res[id][prop_name].id
1494 res[id][prop_name] = False
1495 if property_destination_obj:
1496 # name_get as root (as seeing the name of a related
1497 # object depends on access right of source document,
1498 # not target, so user may not have access.)
1499 name_get_values = dict(obj.pool.get(property_destination_obj).name_get(cr, 1, name_get_ids.values(), context=context))
1500 # the property field is a m2o, we need to return a tuple with (id, name)
1501 for k, v in name_get_ids.iteritems():
1502 if res[k][prop_name]:
1503 res[k][prop_name] = (v , name_get_values.get(v))
1506 def _field_get(self, cr, uid, model_name, prop):
1507 if not self.field_id.get(cr.dbname):
1508 cr.execute('SELECT id \
1509 FROM ir_model_fields \
1510 WHERE name=%s AND model=%s', (prop, model_name))
1512 self.field_id[cr.dbname] = res and res[0]
1513 return self.field_id[cr.dbname]
1515 def __init__(self, obj_prop, **args):
1516 # TODO remove obj_prop parameter (use many2one type)
1518 function.__init__(self, self._fnct_read, False, self._fnct_write,
1519 obj_prop, multi='properties', **args)
1525 def field_to_dict(model, cr, user, field, context=None):
1526 """ Return a dictionary representation of a field.
1528 The string, help, and selection attributes (if any) are untranslated. This
1529 representation is the one returned by fields_get() (fields_get() will do
1534 res = {'type': field._type}
1535 # This additional attributes for M2M and function field is added
1536 # because we need to display tooltip with this additional information
1537 # when client is started in debug mode.
1538 if isinstance(field, function):
1539 res['function'] = field._fnct and field._fnct.func_name or False
1540 res['store'] = field.store
1541 if isinstance(field.store, dict):
1542 res['store'] = str(field.store)
1543 res['fnct_search'] = field._fnct_search and field._fnct_search.func_name or False
1544 res['fnct_inv'] = field._fnct_inv and field._fnct_inv.func_name or False
1545 res['fnct_inv_arg'] = field._fnct_inv_arg or False
1546 res['func_obj'] = field._obj or False
1547 if isinstance(field, many2many):
1548 (table, col1, col2) = field._sql_names(model)
1549 res['related_columns'] = [col1, col2]
1550 res['third_table'] = table
1551 for arg in ('string', 'readonly', 'states', 'size', 'required', 'group_operator',
1552 'change_default', 'translate', 'help', 'select', 'selectable', 'groups'):
1553 if getattr(field, arg):
1554 res[arg] = getattr(field, arg)
1555 for arg in ('digits', 'invisible', 'filters'):
1556 if getattr(field, arg, None):
1557 res[arg] = getattr(field, arg)
1560 res['string'] = field.string
1562 res['help'] = field.help
1564 if hasattr(field, 'selection'):
1565 if isinstance(field.selection, (tuple, list)):
1566 res['selection'] = field.selection
1568 # call the 'dynamic selection' function
1569 res['selection'] = field.selection(model, cr, user, context)
1570 if res['type'] in ('one2many', 'many2many', 'many2one'):
1571 res['relation'] = field._obj
1572 res['domain'] = field._domain
1573 res['context'] = field._context
1575 if isinstance(field, one2many):
1576 res['relation_field'] = field._fields_id
1581 class column_info(object):
1582 """Struct containing details about an osv column, either one local to
1583 its model, or one inherited via _inherits.
1585 :attr name: name of the column
1586 :attr column: column instance, subclass of osv.fields._column
1587 :attr parent_model: if the column is inherited, name of the model
1588 that contains it, None for local columns.
1589 :attr parent_column: the name of the column containing the m2o
1590 relationship to the parent model that contains
1591 this column, None for local columns.
1592 :attr original_parent: if the column is inherited, name of the original
1593 parent model that contains it i.e in case of multilevel
1594 inheritence, None for local columns.
1596 def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
1598 self.column = column
1599 self.parent_model = parent_model
1600 self.parent_column = parent_column
1601 self.original_parent = original_parent
1603 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: