1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
24 - relations (one2many, many2one, many2many)
28 * _classic_read: is a classic sql fields
30 * _auto_join: for one2many and many2one fields, tells whether select
31 queries will join the relational table instead of replacing the
32 field condition by an equivalent-one based on a search.
44 from psycopg2 import Binary
47 import openerp.tools as tools
48 from openerp.tools.translate import _
49 from openerp.tools import float_round, float_repr
50 from openerp.tools import html_sanitize
52 from openerp import SUPERUSER_ID
54 _logger = logging.getLogger(__name__)
56 def _symbol_set(symb):
57 if symb is None or symb == False:
59 elif isinstance(symb, unicode):
60 return symb.encode('utf-8')
64 class _column(object):
65 """ Base of all fields, a database column
67 An instance of this object is a *description* of a database column. It will
68 not hold any data, but only provide the methods to manipulate data of an
69 ORM record or even prepare/update the database to hold such a field of data.
80 _symbol_f = _symbol_set
81 _symbol_set = (_symbol_c, _symbol_f)
84 # used to hide a certain field type in the list of field types
87 def __init__(self, string='unknown', required=False, readonly=False, domain=None, context=None, states=None, priority=0, change_default=False, size=None, ondelete=None, translate=False, select=False, manual=False, **args):
90 The 'manual' keyword argument specifies if the field is a custom one.
91 It corresponds to the 'state' column in ir_model_fields.
98 self.states = states or {}
100 self.readonly = readonly
101 self.required = required
103 self.help = args.get('help', '')
104 self.priority = priority
105 self.change_default = change_default
106 self.ondelete = ondelete.lower() if ondelete else None # defaults to 'set null' in ORM
107 self.translate = translate
108 self._domain = domain
109 self._context = context
115 self.selectable = True
116 self.group_operator = args.get('group_operator', False)
117 self.groups = False # CSV list of ext IDs of groups that can access this field
118 self.deprecated = False # Optional deprecation warning
120 setattr(self, a, args[a])
125 def set(self, cr, obj, id, name, value, user=None, context=None):
126 cr.execute('update '+obj._table+' set '+name+'='+self._symbol_set[0]+' where id=%s', (self._symbol_set[1](value), id))
128 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
129 raise Exception(_('undefined get method !'))
131 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
132 ids = obj.search(cr, uid, args+self._domain+[(name, 'ilike', value)], offset, limit, context=context)
133 res = obj.read(cr, uid, ids, [name], context=context)
134 return [x[name] for x in res]
136 def as_display_name(self, cr, uid, obj, value, context=None):
137 """Converts a field value to a suitable string representation for a record,
138 e.g. when this field is used as ``rec_name``.
140 :param obj: the ``BaseModel`` instance this column belongs to
141 :param value: a proper value as returned by :py:meth:`~openerp.orm.osv.BaseModel.read`
144 # delegated to class method, so a column type A can delegate
145 # to a column type B.
146 return self._as_display_name(self, cr, uid, obj, value, context=None)
149 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
150 # This needs to be a class method, in case a column type A as to delegate
151 # to a column type B.
152 return tools.ustr(value)
154 # ---------------------------------------------------------
156 # ---------------------------------------------------------
157 class boolean(_column):
160 _symbol_f = lambda x: x and 'True' or 'False'
161 _symbol_set = (_symbol_c, _symbol_f)
163 def __init__(self, string='unknown', required=False, **args):
164 super(boolean, self).__init__(string=string, required=required, **args)
167 "required=True is deprecated: making a boolean field"
168 " `required` has no effect, as NULL values are "
169 "automatically turned into False. args: %r",args)
171 class integer(_column):
174 _symbol_f = lambda x: int(x or 0)
175 _symbol_set = (_symbol_c, _symbol_f)
176 _symbol_get = lambda self,x: x or 0
178 def __init__(self, string='unknown', required=False, **args):
179 super(integer, self).__init__(string=string, required=required, **args)
181 class reference(_column):
183 _classic_read = False # post-process to handle missing target
185 def __init__(self, string, selection, size, **args):
186 _column.__init__(self, string=string, size=size, selection=selection, **args)
188 def get(self, cr, obj, ids, name, uid=None, context=None, values=None):
190 # copy initial values fetched previously.
192 result[value['id']] = value[name]
194 model, res_id = value[name].split(',')
195 if not obj.pool.get(model).exists(cr, uid, [int(res_id)], context=context):
196 result[value['id']] = False
200 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
202 # reference fields have a 'model,id'-like value, that we need to convert
204 model_name, res_id = value.split(',')
205 model = obj.pool.get(model_name)
207 return model.name_get(cr, uid, [int(res_id)], context=context)[0][1]
208 return tools.ustr(value)
210 # takes a string (encoded in utf8) and returns a string (encoded in utf8)
211 def _symbol_set_char(self, symb):
214 # * we need to remove the "symb==False" from the next line BUT
215 # for now too many things rely on this broken behavior
216 # * the symb==None test should be common to all data types
217 if symb is None or symb == False:
220 # we need to convert the string to a unicode object to be able
221 # to evaluate its length (and possibly truncate it) reliably
222 u_symb = tools.ustr(symb)
223 return u_symb[:self.size].encode('utf8')
228 def __init__(self, string="unknown", size=None, **args):
229 _column.__init__(self, string=string, size=size or None, **args)
230 # self._symbol_set_char defined to keep the backward compatibility
231 self._symbol_f = self._symbol_set_char = lambda x: _symbol_set_char(self, x)
232 self._symbol_set = (self._symbol_c, self._symbol_f)
242 if x is None or x == False:
244 return html_sanitize(x)
246 _symbol_set = (_symbol_c, _symbol_f)
250 class float(_column):
253 _symbol_f = lambda x: __builtin__.float(x or 0.0)
254 _symbol_set = (_symbol_c, _symbol_f)
255 _symbol_get = lambda self,x: x or 0.0
257 def __init__(self, string='unknown', digits=None, digits_compute=None, required=False, **args):
258 _column.__init__(self, string=string, required=required, **args)
260 # synopsis: digits_compute(cr) -> (precision, scale)
261 self.digits_compute = digits_compute
263 def digits_change(self, cr):
264 if self.digits_compute:
265 self.digits = self.digits_compute(cr)
267 precision, scale = self.digits
268 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
269 precision_digits=scale),
270 precision_digits=scale))
277 """ Returns the current date in a format fit for being a
278 default value to a ``date`` field.
280 This method should be provided as is to the _defaults dict, it
281 should not be called.
283 return DT.date.today().strftime(
284 tools.DEFAULT_SERVER_DATE_FORMAT)
287 def context_today(model, cr, uid, context=None, timestamp=None):
288 """Returns the current date as seen in the client's timezone
289 in a format fit for date fields.
290 This method may be passed as value to initialize _defaults.
292 :param Model model: model (osv) for which the date value is being
293 computed - automatically passed when used in
295 :param datetime timestamp: optional datetime value to use instead of
296 the current date and time (must be a
297 datetime, regular dates can't be converted
299 :param dict context: the 'tz' key in the context should give the
300 name of the User/Client timezone (otherwise
304 today = timestamp or DT.datetime.now()
306 if context and context.get('tz'):
307 tz_name = context['tz']
309 tz_name = model.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
312 utc = pytz.timezone('UTC')
313 context_tz = pytz.timezone(tz_name)
314 utc_today = utc.localize(today, is_dst=False) # UTC = no DST
315 context_today = utc_today.astimezone(context_tz)
317 _logger.debug("failed to compute context/client-specific today date, "
318 "using the UTC value for `today`",
320 return (context_today or today).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
322 class datetime(_column):
326 """ Returns the current datetime in a format fit for being a
327 default value to a ``datetime`` field.
329 This method should be provided as is to the _defaults dict, it
330 should not be called.
332 return DT.datetime.now().strftime(
333 tools.DEFAULT_SERVER_DATETIME_FORMAT)
336 def context_timestamp(cr, uid, timestamp, context=None):
337 """Returns the given timestamp converted to the client's timezone.
338 This method is *not* meant for use as a _defaults initializer,
339 because datetime fields are automatically converted upon
340 display on client side. For _defaults you :meth:`fields.datetime.now`
341 should be used instead.
343 :param datetime timestamp: naive datetime value (expressed in UTC)
344 to be converted to the client timezone
345 :param dict context: the 'tz' key in the context should give the
346 name of the User/Client timezone (otherwise
349 :return: timestamp converted to timezone-aware datetime in context
352 assert isinstance(timestamp, DT.datetime), 'Datetime instance expected'
353 if context and context.get('tz'):
354 tz_name = context['tz']
356 registry = openerp.modules.registry.RegistryManager.get(cr.dbname)
357 tz_name = registry.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
360 utc = pytz.timezone('UTC')
361 context_tz = pytz.timezone(tz_name)
362 utc_timestamp = utc.localize(timestamp, is_dst=False) # UTC = no DST
363 return utc_timestamp.astimezone(context_tz)
365 _logger.debug("failed to compute context/client-specific timestamp, "
366 "using the UTC value",
370 class binary(_column):
374 # Binary values may be byte strings (python 2.6 byte array), but
375 # the legacy OpenERP convention is to transfer and store binaries
376 # as base64-encoded strings. The base64 string may be provided as a
377 # unicode in some circumstances, hence the str() cast in symbol_f.
378 # This str coercion will only work for pure ASCII unicode strings,
379 # on purpose - non base64 data must be passed as a 8bit byte strings.
380 _symbol_f = lambda symb: symb and Binary(str(symb)) or None
382 _symbol_set = (_symbol_c, _symbol_f)
383 _symbol_get = lambda self, x: x and str(x)
385 _classic_read = False
388 def __init__(self, string='unknown', filters=None, **args):
389 _column.__init__(self, string=string, **args)
390 self.filters = filters
392 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
405 # If client is requesting only the size of the field, we return it instead
406 # of the content. Presumably a separate request will be done to read the actual
407 # content if it's needed at some point.
408 # TODO: after 6.0 we should consider returning a dict with size and content instead of
409 # having an implicit convention for the value
410 if val and context.get('bin_size_%s' % name, context.get('bin_size')):
411 res[i] = tools.human_size(long(val))
416 class selection(_column):
419 def __init__(self, selection, string='unknown', **args):
420 _column.__init__(self, string=string, **args)
421 self.selection = selection
423 # ---------------------------------------------------------
425 # ---------------------------------------------------------
428 # Values: (0, 0, { fields }) create
429 # (1, ID, { fields }) update
430 # (2, ID) remove (delete)
431 # (3, ID) unlink one (target id or target of relation)
433 # (5) unlink all (only valid for one2many)
436 class many2one(_column):
437 _classic_read = False
438 _classic_write = True
441 _symbol_f = lambda x: x or None
442 _symbol_set = (_symbol_c, _symbol_f)
444 def __init__(self, obj, string='unknown', auto_join=False, **args):
445 _column.__init__(self, string=string, **args)
447 self._auto_join = auto_join
449 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
457 res[r['id']] = r[name]
459 res.setdefault(id, '')
460 obj = obj.pool.get(self._obj)
462 # build a dictionary of the form {'id_of_distant_resource': name_of_distant_resource}
463 # we use uid=1 because the visibility of a many2one field value (just id and name)
464 # must be the access right of the parent form and not the linked object itself.
465 records = dict(obj.name_get(cr, SUPERUSER_ID,
466 list(set([x for x in res.values() if isinstance(x, (int,long))])),
469 if res[id] in records:
470 res[id] = (res[id], records[res[id]])
475 def set(self, cr, obj_src, id, field, values, user=None, context=None):
478 obj = obj_src.pool.get(self._obj)
479 self._table = obj_src.pool.get(self._obj)._table
480 if type(values) == type([]):
483 id_new = obj.create(cr, act[2])
484 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (id_new, id))
486 obj.write(cr, [act[1]], act[2], context=context)
488 cr.execute('delete from '+self._table+' where id=%s', (act[1],))
489 elif act[0] == 3 or act[0] == 5:
490 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
492 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (act[1], id))
495 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (values, id))
497 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
499 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
500 return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context)
504 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
505 return value[1] if isinstance(value, tuple) else tools.ustr(value)
508 class one2many(_column):
509 _classic_read = False
510 _classic_write = False
514 def __init__(self, obj, fields_id, string='unknown', limit=None, auto_join=False, **args):
515 _column.__init__(self, string=string, **args)
517 self._fields_id = fields_id
519 self._auto_join = auto_join
520 #one2many can't be used as condition for defaults
521 assert(self.change_default != True)
523 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
527 context = context.copy()
528 context.update(self._context)
536 domain = self._domain(obj) if callable(self._domain) else self._domain
537 ids2 = obj.pool.get(self._obj).search(cr, user, domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
538 for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
539 if r[self._fields_id] in res:
540 res[r[self._fields_id]].append(r['id'])
543 def set(self, cr, obj, id, field, values, user=None, context=None):
548 context = context.copy()
549 context.update(self._context)
550 context['no_store_function'] = True
553 _table = obj.pool.get(self._obj)._table
554 obj = obj.pool.get(self._obj)
557 act[2][self._fields_id] = id
558 id_new = obj.create(cr, user, act[2], context=context)
559 result += obj._store_get_values(cr, user, [id_new], act[2].keys(), context)
561 obj.write(cr, user, [act[1]], act[2], context=context)
563 obj.unlink(cr, user, [act[1]], context=context)
565 reverse_rel = obj._all_columns.get(self._fields_id)
566 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
567 # if the model has on delete cascade, just delete the row
568 if reverse_rel.column.ondelete == "cascade":
569 obj.unlink(cr, user, [act[1]], context=context)
571 cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],))
573 # Must use write() to recompute parent_store structure if needed
574 obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
576 reverse_rel = obj._all_columns.get(self._fields_id)
577 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
578 # if the o2m has a static domain we must respect it when unlinking
579 domain = self._domain(obj) if callable(self._domain) else self._domain
580 extra_domain = domain or []
581 ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
582 # If the model has cascade deletion, we delete the rows because it is the intended behavior,
583 # otherwise we only nullify the reverse foreign key column.
584 if reverse_rel.column.ondelete == "cascade":
585 obj.unlink(cr, user, ids_to_unlink, context=context)
587 obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context)
589 # Must use write() to recompute parent_store structure if needed
590 obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
592 cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
593 ids3 = map(lambda x:x[0], cr.fetchall())
594 obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
597 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
598 domain = self._domain(obj) if callable(self._domain) else self._domain
599 return obj.pool.get(self._obj).name_search(cr, uid, value, domain, operator, context=context,limit=limit)
603 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
604 raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)')
607 # Values: (0, 0, { fields }) create
608 # (1, ID, { fields }) update (write fields to ID)
609 # (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete)
610 # (3, ID) unlink (delete the relationship between the two objects but does not delete ID)
611 # (4, ID) link (add a relationship)
613 # (6, ?, ids) set a list of links
615 class many2many(_column):
616 """Encapsulates the logic of a many-to-many bidirectional relationship, handling the
617 low-level details of the intermediary relationship table transparently.
618 A many-to-many relationship is always symmetrical, and can be declared and accessed
619 from either endpoint model.
620 If ``rel`` (relationship table name), ``id1`` (source foreign key column name)
621 or id2 (destination foreign key column name) are not specified, the system will
622 provide default values. This will by default only allow one single symmetrical
623 many-to-many relationship between the source and destination model.
624 For multiple many-to-many relationship between the same models and for
625 relationships where source and destination models are the same, ``rel``, ``id1``
626 and ``id2`` should be specified explicitly.
628 :param str obj: destination model
629 :param str rel: optional name of the intermediary relationship table. If not specified,
630 a canonical name will be derived based on the alphabetically-ordered
631 model names of the source and destination (in the form: ``amodel_bmodel_rel``).
632 Automatic naming is not possible when the source and destination are
633 the same, for obvious ambiguity reasons.
634 :param str id1: optional name for the column holding the foreign key to the current
635 model in the relationship table. If not specified, a canonical name
636 will be derived based on the model name (in the form: `src_model_id`).
637 :param str id2: optional name for the column holding the foreign key to the destination
638 model in the relationship table. If not specified, a canonical name
639 will be derived based on the model name (in the form: `dest_model_id`)
640 :param str string: field label
642 _classic_read = False
643 _classic_write = False
647 def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
650 _column.__init__(self, string=string, **args)
652 if rel and '.' in rel:
653 raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\
654 'You used %s, which is not a valid SQL table name.')% (string,rel))
660 def _sql_names(self, source_model):
661 """Return the SQL names defining the structure of the m2m relationship table
663 :return: (m2m_table, local_col, dest_col) where m2m_table is the table name,
664 local_col is the name of the column holding the current model's FK, and
665 dest_col is the name of the column holding the destination model's FK, and
667 tbl, col1, col2 = self._rel, self._id1, self._id2
668 if not all((tbl, col1, col2)):
669 # the default table name is based on the stable alphabetical order of tables
670 dest_model = source_model.pool.get(self._obj)
671 tables = tuple(sorted([source_model._table, dest_model._table]))
673 assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\
674 'is not possible when source and destination models are '\
676 tbl = '%s_%s_rel' % tables
678 col1 = '%s_id' % source_model._table
680 col2 = '%s_id' % dest_model._table
681 return tbl, col1, col2
683 def _get_query_and_where_params(self, cr, model, ids, values, where_params):
684 """ Extracted from ``get`` to facilitate fine-tuning of the generated
686 query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
687 FROM %(rel)s, %(from_c)s \
688 WHERE %(rel)s.%(id1)s IN %%s \
689 AND %(rel)s.%(id2)s = %(tbl)s.id \
695 return query, where_params
697 def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
709 "Specifying offset at a many2many.get() is deprecated and may"
710 " produce unpredictable results.")
711 obj = model.pool.get(self._obj)
712 rel, id1, id2 = self._sql_names(model)
714 # static domains are lists, and are evaluated both here and on client-side, while string
715 # domains supposed by dynamic and evaluated on client-side only (thus ignored here)
716 # FIXME: make this distinction explicit in API!
717 domain = isinstance(self._domain, list) and self._domain or []
719 wquery = obj._where_calc(cr, user, domain, context=context)
720 obj._apply_ir_rules(cr, user, wquery, 'read', context=context)
721 from_c, where_c, where_params = wquery.get_sql()
723 where_c = ' AND ' + where_c
725 order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0])
728 if self._limit is not None:
729 limit_str = ' LIMIT %d' % self._limit
731 query, where_params = self._get_query_and_where_params(cr, model, ids, {'rel': rel,
738 'order_by': order_by,
742 cr.execute(query, [tuple(ids),] + where_params)
743 for r in cr.fetchall():
744 res[r[1]].append(r[0])
747 def set(self, cr, model, id, name, values, user=None, context=None):
752 rel, id1, id2 = self._sql_names(model)
753 obj = model.pool.get(self._obj)
755 if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
758 idnew = obj.create(cr, user, act[2], context=context)
759 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew))
761 obj.write(cr, user, [act[1]], act[2], context=context)
763 obj.unlink(cr, user, [act[1]], context=context)
765 cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1]))
767 # following queries are in the same transaction - so should be relatively safe
768 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1]))
769 if not cr.fetchone():
770 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1]))
772 cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,))
775 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
777 d1 = ' and ' + ' and '.join(d1)
780 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)
782 for act_nbr in act[2]:
783 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr))
786 # TODO: use a name_search
788 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
789 return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
792 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
793 raise NotImplementedError('Many2Many columns should not be used as record name (_rec_name)')
796 def get_nice_size(value):
798 if isinstance(value, (int,long)):
800 elif value: # this is supposed to be a string
802 return tools.human_size(size)
804 # See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
805 # and http://bugs.python.org/issue10066
806 invalid_xml_low_bytes = re.compile(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
808 def sanitize_binary_value(value):
809 # binary fields should be 7-bit ASCII base64-encoded data,
810 # but we do additional sanity checks to make sure the values
811 # are not something else that won't pass via XML-RPC
812 if isinstance(value, (xmlrpclib.Binary, tuple, list, dict)):
813 # these builtin types are meant to pass untouched
816 # Handle invalid bytes values that will cause problems
817 # for XML-RPC. See for more info:
818 # - http://bugs.python.org/issue10066
819 # - http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
821 # Coercing to unicode would normally allow it to properly pass via
822 # XML-RPC, transparently encoded as UTF-8 by xmlrpclib.
823 # (this works for _any_ byte values, thanks to the fallback
824 # to latin-1 passthrough encoding when decoding to unicode)
825 value = tools.ustr(value)
827 # Due to Python bug #10066 this could still yield invalid XML
828 # bytes, specifically in the low byte range, that will crash
829 # the decoding side: [\x00-\x08\x0b-\x0c\x0e-\x1f]
830 # So check for low bytes values, and if any, perform
831 # base64 encoding - not very smart or useful, but this is
832 # our last resort to avoid crashing the request.
833 if invalid_xml_low_bytes.search(value):
834 # b64-encode after restoring the pure bytes with latin-1
835 # passthrough encoding
836 value = base64.b64encode(value.encode('latin-1'))
841 # ---------------------------------------------------------
843 # ---------------------------------------------------------
844 class function(_column):
846 A field whose value is computed by a function (rather
847 than being read from the database).
849 :param fnct: the callable that will compute the field value.
850 :param arg: arbitrary value to be passed to ``fnct`` when computing the value.
851 :param fnct_inv: the callable that will allow writing values in that field
852 (if not provided, the field is read-only).
853 :param fnct_inv_arg: arbitrary value to be passed to ``fnct_inv`` when
855 :param str type: type of the field simulated by the function field
856 :param fnct_search: the callable that allows searching on the field
857 (if not provided, search will not return any result).
858 :param store: store computed value in database
859 (see :ref:`The *store* parameter <field-function-store>`).
860 :type store: True or dict specifying triggers for field computation
861 :param multi: name of batch for batch computation of function fields.
862 All fields with the same batch name will be computed by
863 a single function call. This changes the signature of the
866 .. _field-function-fnct: The ``fnct`` parameter
868 .. rubric:: The ``fnct`` parameter
870 The callable implementing the function field must have the following signature:
872 .. function:: fnct(model, cr, uid, ids, field_name(s), arg, context)
874 Implements the function field.
876 :param orm model: model to which the field belongs (should be ``self`` for
878 :param field_name(s): name of the field to compute, or if ``multi`` is provided,
879 list of field names to compute.
880 :type field_name(s): str | [str]
881 :param arg: arbitrary value passed when declaring the function field
883 :return: mapping of ``ids`` to computed values, or if multi is provided,
884 to a map of field_names to computed values
886 The values in the returned dictionary must be of the type specified by the type
887 argument in the field declaration.
889 Here is an example with a simple function ``char`` function field::
892 def compute(self, cr, uid, ids, field_name, arg, context):
896 _columns['my_char'] = fields.function(compute, type='char', size=50)
898 # when called with ``ids=[1,2,3]``, ``compute`` could return:
902 3: False # null values should be returned explicitly too
905 If ``multi`` is set, then ``field_name`` is replaced by ``field_names``: a list
906 of the field names that should be computed. Each value in the returned
907 dictionary must then be a dictionary mapping field names to values.
909 Here is an example where two function fields (``name`` and ``age``)
910 are both computed by a single function field::
913 def compute(self, cr, uid, ids, field_names, arg, context):
917 _columns['name'] = fields.function(compute_person_data, type='char',\
918 size=50, multi='person_data')
919 _columns[''age'] = fields.function(compute_person_data, type='integer',\
922 # when called with ``ids=[1,2,3]``, ``compute_person_data`` could return:
924 1: {'name': 'Bob', 'age': 23},
925 2: {'name': 'Sally', 'age': 19},
926 3: {'name': 'unknown', 'age': False}
929 .. _field-function-fnct-inv:
931 .. rubric:: The ``fnct_inv`` parameter
933 This callable implements the write operation for the function field
934 and must have the following signature:
936 .. function:: fnct_inv(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context)
938 Callable that implements the ``write`` operation for the function field.
940 :param orm model: model to which the field belongs (should be ``self`` for
942 :param int id: the identifier of the object to write on
943 :param str field_name: name of the field to set
944 :param fnct_inv_arg: arbitrary value passed when declaring the function field
947 When writing values for a function field, the ``multi`` parameter is ignored.
949 .. _field-function-fnct-search:
951 .. rubric:: The ``fnct_search`` parameter
953 This callable implements the search operation for the function field
954 and must have the following signature:
956 .. function:: fnct_search(model, cr, uid, model_again, field_name, criterion, context)
958 Callable that implements the ``search`` operation for the function field by expanding
959 a search criterion based on the function field into a new domain based only on
960 columns that are stored in the database.
962 :param orm model: model to which the field belongs (should be ``self`` for
964 :param orm model_again: same value as ``model`` (seriously! this is for backwards
966 :param str field_name: name of the field to search on
967 :param list criterion: domain component specifying the search criterion on the field.
969 :return: domain to use instead of ``criterion`` when performing the search.
970 This new domain must be based only on columns stored in the database, as it
971 will be used directly without any translation.
973 The returned value must be a domain, that is, a list of the form [(field_name, operator, operand)].
974 The most generic way to implement ``fnct_search`` is to directly search for the records that
975 match the given ``criterion``, and return their ``ids`` wrapped in a domain, such as
976 ``[('id','in',[1,3,5])]``.
978 .. _field-function-store:
980 .. rubric:: The ``store`` parameter
982 The ``store`` parameter allows caching the result of the field computation in the
983 database, and defining the triggers that will invalidate that cache and force a
984 recomputation of the function field.
985 When not provided, the field is computed every time its value is read.
986 The value of ``store`` may be either ``True`` (to recompute the field value whenever
987 any field in the same record is modified), or a dictionary specifying a more
988 flexible set of recomputation triggers.
990 A trigger specification is a dictionary that maps the names of the models that
991 will trigger the computation, to a tuple describing the trigger rule, in the
995 'trigger_model': (mapping_function,
996 ['trigger_field1', 'trigger_field2'],
1000 A trigger rule is defined by a 3-item tuple where:
1002 * The ``mapping_function`` is defined as follows:
1004 .. function:: mapping_function(trigger_model, cr, uid, trigger_ids, context)
1006 Callable that maps record ids of a trigger model to ids of the
1007 corresponding records in the source model (whose field values
1008 need to be recomputed).
1010 :param orm model: trigger_model
1011 :param list trigger_ids: ids of the records of trigger_model that were
1014 :return: list of ids of the source model whose function field values
1015 need to be recomputed
1017 * The second item is a list of the fields who should act as triggers for
1018 the computation. If an empty list is given, all fields will act as triggers.
1019 * The last item is the priority, used to order the triggers when processing them
1020 after any write operation on a model that has function field triggers. The
1021 default priority is 10.
1023 In fact, setting store = True is the same as using the following trigger dict::
1026 'model_itself': (lambda self, cr, uid, ids, context: ids,
1032 _classic_read = False
1033 _classic_write = False
1039 # multi: compute several fields in one call
1041 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):
1042 _column.__init__(self, **args)
1045 self._fnct_inv = fnct_inv
1048 if 'relation' in args:
1049 self._obj = args['relation']
1051 self.digits = args.get('digits', (16,2))
1052 self.digits_compute = args.get('digits_compute', None)
1054 self._fnct_inv_arg = fnct_inv_arg
1058 self._fnct_search = fnct_search
1061 if not fnct_search and not store:
1062 self.selectable = False
1065 if self._type != 'many2one':
1066 # m2o fields need to return tuples with name_get, not just foreign keys
1067 self._classic_read = True
1068 self._classic_write = True
1070 self._symbol_get=lambda x:x and str(x)
1072 self._prefetch = True
1075 self._symbol_c = float._symbol_c
1076 self._symbol_f = float._symbol_f
1077 self._symbol_set = float._symbol_set
1079 if type == 'boolean':
1080 self._symbol_c = boolean._symbol_c
1081 self._symbol_f = boolean._symbol_f
1082 self._symbol_set = boolean._symbol_set
1084 if type == 'integer':
1085 self._symbol_c = integer._symbol_c
1086 self._symbol_f = integer._symbol_f
1087 self._symbol_set = integer._symbol_set
1090 self._symbol_c = char._symbol_c
1091 self._symbol_f = lambda x: _symbol_set_char(self, x)
1092 self._symbol_set = (self._symbol_c, self._symbol_f)
1094 def digits_change(self, cr):
1095 if self._type == 'float':
1096 if self.digits_compute:
1097 self.digits = self.digits_compute(cr)
1099 precision, scale = self.digits
1100 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
1101 precision_digits=scale),
1102 precision_digits=scale))
1104 def search(self, cr, uid, obj, name, args, context=None):
1105 if not self._fnct_search:
1106 #CHECKME: should raise an exception
1108 return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
1110 def postprocess(self, cr, uid, obj, field, value=None, context=None):
1114 field_type = obj._columns[field]._type
1115 if field_type == "many2one":
1116 # make the result a tuple if it is not already one
1117 if isinstance(value, (int,long)) and hasattr(obj._columns[field], 'relation'):
1118 obj_model = obj.pool.get(obj._columns[field].relation)
1119 dict_names = dict(obj_model.name_get(cr, uid, [value], context))
1120 result = (value, dict_names[value])
1122 if field_type == 'binary':
1123 if context.get('bin_size'):
1124 # client requests only the size of binary fields
1125 result = get_nice_size(value)
1126 elif not context.get('bin_raw'):
1127 result = sanitize_binary_value(value)
1129 if field_type == "integer" and value > xmlrpclib.MAXINT:
1130 # integer/long values greater than 2^31-1 are not supported
1131 # in pure XMLRPC, so we have to pass them as floats :-(
1132 # This is not needed for stored fields and non-functional integer
1133 # fields, as their values are constrained by the database backend
1134 # to the same 32bits signed int limit.
1135 result = __builtin__.float(value)
1138 def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
1139 result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
1141 if self._multi and id in result:
1142 for field, value in result[id].iteritems():
1144 result[id][field] = self.postprocess(cr, uid, obj, field, value, context)
1145 elif result.get(id):
1146 result[id] = self.postprocess(cr, uid, obj, name, result[id], context)
1149 def set(self, cr, obj, id, name, value, user=None, context=None):
1153 self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
1156 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
1157 # Function fields are supposed to emulate a basic field type,
1158 # so they can delegate to the basic type for record name rendering
1159 return globals()[field._type]._as_display_name(field, cr, uid, obj, value, context=context)
1161 # ---------------------------------------------------------
1163 # ---------------------------------------------------------
1165 class related(function):
1166 """Field that points to some data inside another field of the current record.
1171 'foo_id': fields.many2one('my.foo', 'Foo'),
1172 'bar': fields.related('foo_id', 'frol', type='char', string='Frol of Foo'),
1176 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1177 # assume self._arg = ('foo', 'bar', 'baz')
1178 # domain = [(name, op, val)] => search [('foo.bar.baz', op, val)]
1179 field = '.'.join(self._arg)
1180 return map(lambda x: (field, x[1], x[2]), domain)
1182 def _fnct_write(self,obj,cr, uid, ids, field_name, values, args, context=None):
1183 if isinstance(ids, (int, long)):
1185 for record in obj.browse(cr, uid, ids, context=context):
1186 # traverse all fields except the last one
1187 for field in self.arg[:-1]:
1188 record = record[field] or False
1191 elif isinstance(record, list):
1192 # record is the result of a one2many or many2many field
1195 # write on the last field
1196 record.write({self.arg[-1]: values})
1198 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1200 for record in obj.browse(cr, SUPERUSER_ID, ids, context=context):
1202 for field in self.arg:
1203 if isinstance(value, list):
1205 value = value[field] or False
1208 res[record.id] = value
1210 if self._type == 'many2one':
1211 # res[id] is a browse_record or False; convert it to (id, name) or False.
1212 # Perform name_get as root, as seeing the name of a related object depends on
1213 # access right of source document, not target, so user may not have access.
1214 value_ids = list(set(value.id for value in res.itervalues() if value))
1215 value_name = dict(obj.pool.get(self._obj).name_get(cr, SUPERUSER_ID, value_ids, context=context))
1216 res = dict((id, value and (value.id, value_name[value.id])) for id, value in res.iteritems())
1218 elif self._type in ('one2many', 'many2many'):
1219 # res[id] is a list of browse_record or False; convert it to a list of ids
1220 res = dict((id, value and map(int, value) or []) for id, value in res.iteritems())
1224 def __init__(self, *arg, **args):
1226 self._relations = []
1227 super(related, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1228 if self.store is True:
1229 # TODO: improve here to change self.store = {...} according to related objects
1233 class sparse(function):
1235 def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
1237 + For a many2many field, a list of tuples is expected.
1238 Here is the list of tuple that are accepted, with the corresponding semantics ::
1240 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1241 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1242 (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)
1243 (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)
1244 (4, ID) link to existing record with id = ID (adds a relationship)
1245 (5) unlink all (like using (3,ID) for all linked records)
1246 (6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
1249 [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
1251 + For a one2many field, a lits of tuples is expected.
1252 Here is the list of tuple that are accepted, with the corresponding semantics ::
1254 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1255 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1256 (2, ID) remove and delete the linked record with id = ID (calls unlink on ID, that will delete the object completely, and the link to it as well)
1259 [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
1262 if self._type == 'many2many':
1263 assert value[0][0] == 6, 'Unsupported m2m value for sparse field: %s' % value
1266 elif self._type == 'one2many':
1269 relation_obj = obj.pool.get(self.relation)
1271 assert vals[0] in (0,1,2), 'Unsupported o2m value for sparse field: %s' % vals
1273 read_value.append(relation_obj.create(cr, uid, vals[2], context=context))
1275 relation_obj.write(cr, uid, vals[1], vals[2], context=context)
1277 relation_obj.unlink(cr, uid, vals[1], context=context)
1278 read_value.remove(vals[1])
1283 def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
1284 if not type(ids) == list:
1286 records = obj.browse(cr, uid, ids, context=context)
1287 for record in records:
1288 # grab serialized value as object - already deserialized
1289 serialized = getattr(record, self.serialization_field)
1291 # simply delete the key to unset it.
1292 serialized.pop(field_name, None)
1294 serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context)
1295 obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context)
1298 def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None):
1300 records = obj.browse(cr, uid, ids, context=context)
1301 for record in records:
1302 # grab serialized value as object - already deserialized
1303 serialized = getattr(record, self.serialization_field)
1304 results[record.id] = {}
1305 for field_name in field_names:
1306 field_type = obj._columns[field_name]._type
1307 value = serialized.get(field_name, False)
1308 if field_type in ('one2many','many2many'):
1311 # filter out deleted records as superuser
1312 relation_obj = obj.pool.get(obj._columns[field_name].relation)
1313 value = relation_obj.exists(cr, openerp.SUPERUSER_ID, value)
1314 if type(value) in (int,long) and field_type == 'many2one':
1315 relation_obj = obj.pool.get(obj._columns[field_name].relation)
1316 # check for deleted record as superuser
1317 if not relation_obj.exists(cr, openerp.SUPERUSER_ID, [value]):
1319 results[record.id][field_name] = value
1322 def __init__(self, serialization_field, **kwargs):
1323 self.serialization_field = serialization_field
1324 super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
1328 # ---------------------------------------------------------
1330 # ---------------------------------------------------------
1332 class dummy(function):
1333 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1336 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1339 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1342 def __init__(self, *arg, **args):
1344 self._relations = []
1345 super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=None, **args)
1347 # ---------------------------------------------------------
1349 # ---------------------------------------------------------
1351 class serialized(_column):
1352 """ A field able to store an arbitrary python data structure.
1354 Note: only plain components allowed.
1357 def _symbol_set_struct(val):
1358 return simplejson.dumps(val)
1360 def _symbol_get_struct(self, val):
1361 return simplejson.loads(val or '{}')
1364 _type = 'serialized'
1367 _symbol_f = _symbol_set_struct
1368 _symbol_set = (_symbol_c, _symbol_f)
1369 _symbol_get = _symbol_get_struct
1371 # TODO: review completly this class for speed improvement
1372 class property(function):
1374 def _get_default(self, obj, cr, uid, prop_name, context=None):
1375 return self._get_defaults(obj, cr, uid, [prop_name], context=None)[prop_name]
1377 def _get_defaults(self, obj, cr, uid, prop_names, context=None):
1378 """Get the default values for ``prop_names´´ property fields (result of ir.property.get() function for res_id = False).
1380 :param list of string prop_names: list of name of property fields for those we want the default value
1381 :return: map of property field names to their default value
1384 prop = obj.pool.get('ir.property')
1386 for prop_name in prop_names:
1387 res[prop_name] = prop.get(cr, uid, prop_name, obj._name, context=context)
1390 def _get_by_id(self, obj, cr, uid, prop_name, ids, context=None):
1391 prop = obj.pool.get('ir.property')
1392 vids = [obj._name + ',' + str(oid) for oid in ids]
1394 domain = [('fields_id.model', '=', obj._name), ('fields_id.name', 'in', prop_name)]
1395 #domain = prop._get_domain(cr, uid, prop_name, obj._name, context)
1397 domain = [('res_id', 'in', vids)] + domain
1398 return prop.search(cr, uid, domain, context=context)
1400 # TODO: to rewrite more clean
1401 def _fnct_write(self, obj, cr, uid, id, prop_name, id_val, obj_dest, context=None):
1405 nids = self._get_by_id(obj, cr, uid, [prop_name], [id], context)
1407 cr.execute('DELETE FROM ir_property WHERE id IN %s', (tuple(nids),))
1409 default_val = self._get_default(obj, cr, uid, prop_name, context)
1411 property_create = False
1412 if isinstance(default_val, openerp.osv.orm.browse_record):
1413 if default_val.id != id_val:
1414 property_create = True
1415 elif default_val != id_val:
1416 property_create = True
1419 def_id = self._field_get(cr, uid, obj._name, prop_name)
1420 company = obj.pool.get('res.company')
1421 cid = company._company_default_get(cr, uid, obj._name, def_id,
1423 propdef = obj.pool.get('ir.model.fields').browse(cr, uid, def_id,
1425 prop = obj.pool.get('ir.property')
1426 return prop.create(cr, uid, {
1427 'name': propdef.name,
1429 'res_id': obj._name+','+str(id),
1431 'fields_id': def_id,
1436 def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
1437 prop = obj.pool.get('ir.property')
1438 # get the default values (for res_id = False) for the property fields
1439 default_val = self._get_defaults(obj, cr, uid, prop_names, context)
1441 # build the dictionary that will be returned
1444 res[id] = default_val.copy()
1446 for prop_name in prop_names:
1447 property_field = obj._all_columns.get(prop_name).column
1448 property_destination_obj = property_field._obj if property_field._type == 'many2one' else False
1449 # If the property field is a m2o field, we will append the id of the value to name_get_ids
1450 # in order to make a name_get in batch for all the ids needed.
1453 # get the result of ir.property.get() for this res_id and save it in res if it's existing
1454 obj_reference = obj._name + ',' + str(id)
1455 value = prop.get(cr, uid, prop_name, obj._name, res_id=obj_reference, context=context)
1457 res[id][prop_name] = value
1458 # Check existence as root (as seeing the name of a related
1459 # object depends on access right of source document,
1460 # not target, so user may not have access) in order to avoid
1461 # pointing on an unexisting record.
1462 if property_destination_obj:
1463 if res[id][prop_name] and obj.pool.get(property_destination_obj).exists(cr, SUPERUSER_ID, res[id][prop_name].id):
1464 name_get_ids[id] = res[id][prop_name].id
1466 res[id][prop_name] = False
1467 if property_destination_obj:
1468 # name_get as root (as seeing the name of a related
1469 # object depends on access right of source document,
1470 # not target, so user may not have access.)
1471 name_get_values = dict(obj.pool.get(property_destination_obj).name_get(cr, SUPERUSER_ID, name_get_ids.values(), context=context))
1472 # the property field is a m2o, we need to return a tuple with (id, name)
1473 for k, v in name_get_ids.iteritems():
1474 if res[k][prop_name]:
1475 res[k][prop_name] = (v , name_get_values.get(v))
1478 def _field_get(self, cr, uid, model_name, prop):
1479 if not self.field_id.get(cr.dbname):
1480 cr.execute('SELECT id \
1481 FROM ir_model_fields \
1482 WHERE name=%s AND model=%s', (prop, model_name))
1484 self.field_id[cr.dbname] = res and res[0]
1485 return self.field_id[cr.dbname]
1487 def __init__(self, obj_prop, **args):
1488 # TODO remove obj_prop parameter (use many2one type)
1490 function.__init__(self, self._fnct_read, False, self._fnct_write,
1491 obj_prop, multi='properties', **args)
1497 def field_to_dict(model, cr, user, field, context=None):
1498 """ Return a dictionary representation of a field.
1500 The string, help, and selection attributes (if any) are untranslated. This
1501 representation is the one returned by fields_get() (fields_get() will do
1506 res = {'type': field._type}
1507 # some attributes for m2m/function field are added as debug info only
1508 if isinstance(field, function):
1509 res['function'] = field._fnct and field._fnct.func_name or False
1510 res['store'] = field.store
1511 if isinstance(field.store, dict):
1512 res['store'] = str(field.store)
1513 res['fnct_search'] = field._fnct_search and field._fnct_search.func_name or False
1514 res['fnct_inv'] = field._fnct_inv and field._fnct_inv.func_name or False
1515 res['fnct_inv_arg'] = field._fnct_inv_arg or False
1516 if isinstance(field, many2many):
1517 (table, col1, col2) = field._sql_names(model)
1518 res['m2m_join_columns'] = [col1, col2]
1519 res['m2m_join_table'] = table
1520 for arg in ('string', 'readonly', 'states', 'size', 'group_operator', 'required',
1521 'change_default', 'translate', 'help', 'select', 'selectable', 'groups',
1522 'deprecated', 'digits', 'invisible', 'filters'):
1523 if getattr(field, arg, None):
1524 res[arg] = getattr(field, arg)
1526 if hasattr(field, 'selection'):
1527 if isinstance(field.selection, (tuple, list)):
1528 res['selection'] = field.selection
1530 # call the 'dynamic selection' function
1531 res['selection'] = field.selection(model, cr, user, context)
1532 if res['type'] in ('one2many', 'many2many', 'many2one'):
1533 res['relation'] = field._obj
1534 res['domain'] = field._domain(model) if callable(field._domain) else field._domain
1535 res['context'] = field._context
1537 if isinstance(field, one2many):
1538 res['relation_field'] = field._fields_id
1543 class column_info(object):
1544 """ Struct containing details about an osv column, either one local to
1545 its model, or one inherited via _inherits.
1551 .. attribute:: column
1553 column instance, subclass of :class:`_column`
1555 .. attribute:: parent_model
1557 if the column is inherited, name of the model that contains it,
1558 ``None`` for local columns.
1560 .. attribute:: parent_column
1562 the name of the column containing the m2o relationship to the
1563 parent model that contains this column, ``None`` for local columns.
1565 .. attribute:: original_parent
1567 if the column is inherited, name of the original parent model that
1568 contains it i.e in case of multilevel inheritance, ``None`` for
1571 def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
1573 self.column = column
1574 self.parent_model = parent_model
1575 self.parent_column = parent_column
1576 self.original_parent = original_parent
1579 return '%s(%s, %s, %s, %s, %s)' % (
1580 self.__class__.__name__, self.name, self.column,
1581 self.parent_model, self.parent_column, self.original_parent)
1583 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: