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
47 from openerp.tools import html_sanitize
49 from openerp import SUPERUSER_ID
51 _logger = logging.getLogger(__name__)
53 def _symbol_set(symb):
54 if symb is None or symb == False:
56 elif isinstance(symb, unicode):
57 return symb.encode('utf-8')
61 class _column(object):
62 """ Base of all fields, a database column
64 An instance of this object is a *description* of a database column. It will
65 not hold any data, but only provide the methods to manipulate data of an
66 ORM record or even prepare/update the database to hold such a field of data.
76 _symbol_f = _symbol_set
77 _symbol_set = (_symbol_c, _symbol_f)
80 # used to hide a certain field type in the list of field types
83 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):
86 The 'manual' keyword argument specifies if the field is a custom one.
87 It corresponds to the 'state' column in ir_model_fields.
94 self.states = states or {}
96 self.readonly = readonly
97 self.required = required
99 self.help = args.get('help', '')
100 self.priority = priority
101 self.change_default = change_default
102 self.ondelete = ondelete.lower() if ondelete else None # defaults to 'set null' in ORM
103 self.translate = translate
104 self._domain = domain
105 self._context = context
111 self.selectable = True
112 self.group_operator = args.get('group_operator', False)
113 self.groups = False # CSV list of ext IDs of groups that can access this field
114 self.deprecated = False # Optional deprecation warning
117 setattr(self, a, args[a])
122 def set(self, cr, obj, id, name, value, user=None, context=None):
123 cr.execute('update '+obj._table+' set '+name+'='+self._symbol_set[0]+' where id=%s', (self._symbol_set[1](value), id))
125 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
126 raise Exception(_('undefined get method !'))
128 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
129 ids = obj.search(cr, uid, args+self._domain+[(name, 'ilike', value)], offset, limit, context=context)
130 res = obj.read(cr, uid, ids, [name], context=context)
131 return [x[name] for x in res]
133 def as_display_name(self, cr, uid, obj, value, context=None):
134 """Converts a field value to a suitable string representation for a record,
135 e.g. when this field is used as ``rec_name``.
137 :param obj: the ``BaseModel`` instance this column belongs to
138 :param value: a proper value as returned by :py:meth:`~openerp.orm.osv.BaseModel.read`
141 # delegated to class method, so a column type A can delegate
142 # to a column type B.
143 return self._as_display_name(self, cr, uid, obj, value, context=None)
146 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
147 # This needs to be a class method, in case a column type A as to delegate
148 # to a column type B.
149 return tools.ustr(value)
151 # ---------------------------------------------------------
153 # ---------------------------------------------------------
154 class boolean(_column):
157 _symbol_f = lambda x: x and 'True' or 'False'
158 _symbol_set = (_symbol_c, _symbol_f)
160 def __init__(self, string='unknown', required=False, **args):
161 super(boolean, self).__init__(string=string, required=required, **args)
164 "required=True is deprecated: making a boolean field"
165 " `required` has no effect, as NULL values are "
166 "automatically turned into False. args: %r",args)
168 class integer(_column):
171 _symbol_f = lambda x: int(x or 0)
172 _symbol_set = (_symbol_c, _symbol_f)
173 _symbol_get = lambda self,x: x or 0
175 def __init__(self, string='unknown', required=False, **args):
176 super(integer, self).__init__(string=string, required=required, **args)
178 class reference(_column):
180 _classic_read = False # post-process to handle missing target
182 def __init__(self, string, selection, size, **args):
183 _column.__init__(self, string=string, size=size, selection=selection, **args)
185 def get(self, cr, obj, ids, name, uid=None, context=None, values=None):
187 # copy initial values fetched previously.
189 result[value['id']] = value[name]
191 model, res_id = value[name].split(',')
192 if not obj.pool.get(model).exists(cr, uid, [int(res_id)], context=context):
193 result[value['id']] = False
197 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
199 # reference fields have a 'model,id'-like value, that we need to convert
201 model_name, res_id = value.split(',')
202 model = obj.pool.get(model_name)
204 return model.name_get(cr, uid, [int(res_id)], context=context)[0][1]
205 return tools.ustr(value)
210 def __init__(self, string="unknown", size=None, **args):
211 _column.__init__(self, string=string, size=size or None, **args)
212 self._symbol_set = (self._symbol_c, self._symbol_set_char)
214 # takes a string (encoded in utf8) and returns a string (encoded in utf8)
215 def _symbol_set_char(self, symb):
217 # * we need to remove the "symb==False" from the next line BUT
218 # for now too many things rely on this broken behavior
219 # * the symb==None test should be common to all data types
220 if symb is None or symb == False:
223 # we need to convert the string to a unicode object to be able
224 # to evaluate its length (and possibly truncate it) reliably
225 u_symb = tools.ustr(symb)
227 return u_symb[:self.size].encode('utf8')
237 if x is None or x == False:
239 return html_sanitize(x)
241 _symbol_set = (_symbol_c, _symbol_f)
245 class float(_column):
248 _symbol_f = lambda x: __builtin__.float(x or 0.0)
249 _symbol_set = (_symbol_c, _symbol_f)
250 _symbol_get = lambda self,x: x or 0.0
252 def __init__(self, string='unknown', digits=None, digits_compute=None, required=False, **args):
253 _column.__init__(self, string=string, required=required, **args)
255 # synopsis: digits_compute(cr) -> (precision, scale)
256 self.digits_compute = digits_compute
258 def digits_change(self, cr):
259 if self.digits_compute:
260 self.digits = self.digits_compute(cr)
262 precision, scale = self.digits
263 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
264 precision_digits=scale),
265 precision_digits=scale))
272 """ Returns the current date in a format fit for being a
273 default value to a ``date`` field.
275 This method should be provided as is to the _defaults dict, it
276 should not be called.
278 return DT.date.today().strftime(
279 tools.DEFAULT_SERVER_DATE_FORMAT)
282 def context_today(model, cr, uid, context=None, timestamp=None):
283 """Returns the current date as seen in the client's timezone
284 in a format fit for date fields.
285 This method may be passed as value to initialize _defaults.
287 :param Model model: model (osv) for which the date value is being
288 computed - technical field, currently ignored,
289 automatically passed when used in _defaults.
290 :param datetime timestamp: optional datetime value to use instead of
291 the current date and time (must be a
292 datetime, regular dates can't be converted
294 :param dict context: the 'tz' key in the context should give the
295 name of the User/Client timezone (otherwise
299 today = timestamp or DT.datetime.now()
301 if context and context.get('tz'):
303 utc = pytz.timezone('UTC')
304 context_tz = pytz.timezone(context['tz'])
305 utc_today = utc.localize(today, is_dst=False) # UTC = no DST
306 context_today = utc_today.astimezone(context_tz)
308 _logger.debug("failed to compute context/client-specific today date, "
309 "using the UTC value for `today`",
311 return (context_today or today).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
313 class datetime(_column):
317 """ Returns the current datetime in a format fit for being a
318 default value to a ``datetime`` field.
320 This method should be provided as is to the _defaults dict, it
321 should not be called.
323 return DT.datetime.now().strftime(
324 tools.DEFAULT_SERVER_DATETIME_FORMAT)
327 def context_timestamp(cr, uid, timestamp, context=None):
328 """Returns the given timestamp converted to the client's timezone.
329 This method is *not* meant for use as a _defaults initializer,
330 because datetime fields are automatically converted upon
331 display on client side. For _defaults you :meth:`fields.datetime.now`
332 should be used instead.
334 :param datetime timestamp: naive datetime value (expressed in UTC)
335 to be converted to the client timezone
336 :param dict context: the 'tz' key in the context should give the
337 name of the User/Client timezone (otherwise
340 :return: timestamp converted to timezone-aware datetime in context
343 assert isinstance(timestamp, DT.datetime), 'Datetime instance expected'
344 if context and context.get('tz'):
346 utc = pytz.timezone('UTC')
347 context_tz = pytz.timezone(context['tz'])
348 utc_timestamp = utc.localize(timestamp, is_dst=False) # UTC = no DST
349 return utc_timestamp.astimezone(context_tz)
351 _logger.debug("failed to compute context/client-specific timestamp, "
352 "using the UTC value",
356 class binary(_column):
360 # Binary values may be byte strings (python 2.6 byte array), but
361 # the legacy OpenERP convention is to transfer and store binaries
362 # as base64-encoded strings. The base64 string may be provided as a
363 # unicode in some circumstances, hence the str() cast in symbol_f.
364 # This str coercion will only work for pure ASCII unicode strings,
365 # on purpose - non base64 data must be passed as a 8bit byte strings.
366 _symbol_f = lambda symb: symb and Binary(str(symb)) or None
368 _symbol_set = (_symbol_c, _symbol_f)
369 _symbol_get = lambda self, x: x and str(x)
371 _classic_read = False
374 def __init__(self, string='unknown', filters=None, **args):
375 _column.__init__(self, string=string, **args)
376 self.filters = filters
378 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
391 # If client is requesting only the size of the field, we return it instead
392 # of the content. Presumably a separate request will be done to read the actual
393 # content if it's needed at some point.
394 # TODO: after 6.0 we should consider returning a dict with size and content instead of
395 # having an implicit convention for the value
396 if val and context.get('bin_size_%s' % name, context.get('bin_size')):
397 res[i] = tools.human_size(long(val))
402 class selection(_column):
405 def __init__(self, selection, string='unknown', **args):
406 _column.__init__(self, string=string, **args)
407 self.selection = selection
409 # ---------------------------------------------------------
411 # ---------------------------------------------------------
414 # Values: (0, 0, { fields }) create
415 # (1, ID, { fields }) update
416 # (2, ID) remove (delete)
417 # (3, ID) unlink one (target id or target of relation)
419 # (5) unlink all (only valid for one2many)
422 class many2one(_column):
423 _classic_read = False
424 _classic_write = True
427 _symbol_f = lambda x: x or None
428 _symbol_set = (_symbol_c, _symbol_f)
430 def __init__(self, obj, string='unknown', **args):
431 _column.__init__(self, string=string, **args)
434 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
442 res[r['id']] = r[name]
444 res.setdefault(id, '')
445 obj = obj.pool.get(self._obj)
447 # build a dictionary of the form {'id_of_distant_resource': name_of_distant_resource}
448 # we use uid=1 because the visibility of a many2one field value (just id and name)
449 # must be the access right of the parent form and not the linked object itself.
450 records = dict(obj.name_get(cr, SUPERUSER_ID,
451 list(set([x for x in res.values() if isinstance(x, (int,long))])),
454 if res[id] in records:
455 res[id] = (res[id], records[res[id]])
460 def set(self, cr, obj_src, id, field, values, user=None, context=None):
463 obj = obj_src.pool.get(self._obj)
464 self._table = obj_src.pool.get(self._obj)._table
465 if type(values) == type([]):
468 id_new = obj.create(cr, act[2])
469 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (id_new, id))
471 obj.write(cr, [act[1]], act[2], context=context)
473 cr.execute('delete from '+self._table+' where id=%s', (act[1],))
474 elif act[0] == 3 or act[0] == 5:
475 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
477 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (act[1], id))
480 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (values, id))
482 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
484 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
485 return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context)
489 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
490 return value[1] if isinstance(value, tuple) else tools.ustr(value)
493 class one2many(_column):
494 _classic_read = False
495 _classic_write = False
499 def __init__(self, obj, fields_id, string='unknown', limit=None, **args):
500 _column.__init__(self, string=string, **args)
502 self._fields_id = fields_id
504 #one2many can't be used as condition for defaults
505 assert(self.change_default != True)
507 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
511 context = context.copy()
512 context.update(self._context)
520 domain = self._domain(obj) if callable(self._domain) else self._domain
521 ids2 = obj.pool.get(self._obj).search(cr, user, domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
522 for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
523 if r[self._fields_id] in res:
524 res[r[self._fields_id]].append(r['id'])
527 def set(self, cr, obj, id, field, values, user=None, context=None):
532 context = context.copy()
533 context.update(self._context)
534 context['no_store_function'] = True
537 _table = obj.pool.get(self._obj)._table
538 obj = obj.pool.get(self._obj)
541 act[2][self._fields_id] = id
542 id_new = obj.create(cr, user, act[2], context=context)
543 result += obj._store_get_values(cr, user, [id_new], act[2].keys(), context)
545 obj.write(cr, user, [act[1]], act[2], context=context)
547 obj.unlink(cr, user, [act[1]], context=context)
549 reverse_rel = obj._all_columns.get(self._fields_id)
550 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
551 # if the model has on delete cascade, just delete the row
552 if reverse_rel.column.ondelete == "cascade":
553 obj.unlink(cr, user, [act[1]], context=context)
555 cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],))
557 # Must use write() to recompute parent_store structure if needed
558 obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
560 reverse_rel = obj._all_columns.get(self._fields_id)
561 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
562 # if the o2m has a static domain we must respect it when unlinking
563 domain = self._domain(obj) if callable(self._domain) else self._domain
564 extra_domain = domain or []
565 ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
566 # If the model has cascade deletion, we delete the rows because it is the intended behavior,
567 # otherwise we only nullify the reverse foreign key column.
568 if reverse_rel.column.ondelete == "cascade":
569 obj.unlink(cr, user, ids_to_unlink, context=context)
571 obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context)
573 # Must use write() to recompute parent_store structure if needed
574 obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
576 cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
577 ids3 = map(lambda x:x[0], cr.fetchall())
578 obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
581 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
582 domain = self._domain(obj) if callable(self._domain) else self._domain
583 return obj.pool.get(self._obj).name_search(cr, uid, value, domain, operator, context=context,limit=limit)
587 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
588 raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)')
591 # Values: (0, 0, { fields }) create
592 # (1, ID, { fields }) update (write fields to ID)
593 # (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete)
594 # (3, ID) unlink (delete the relationship between the two objects but does not delete ID)
595 # (4, ID) link (add a relationship)
597 # (6, ?, ids) set a list of links
599 class many2many(_column):
600 """Encapsulates the logic of a many-to-many bidirectional relationship, handling the
601 low-level details of the intermediary relationship table transparently.
602 A many-to-many relationship is always symmetrical, and can be declared and accessed
603 from either endpoint model.
604 If ``rel`` (relationship table name), ``id1`` (source foreign key column name)
605 or id2 (destination foreign key column name) are not specified, the system will
606 provide default values. This will by default only allow one single symmetrical
607 many-to-many relationship between the source and destination model.
608 For multiple many-to-many relationship between the same models and for
609 relationships where source and destination models are the same, ``rel``, ``id1``
610 and ``id2`` should be specified explicitly.
612 :param str obj: destination model
613 :param str rel: optional name of the intermediary relationship table. If not specified,
614 a canonical name will be derived based on the alphabetically-ordered
615 model names of the source and destination (in the form: ``amodel_bmodel_rel``).
616 Automatic naming is not possible when the source and destination are
617 the same, for obvious ambiguity reasons.
618 :param str id1: optional name for the column holding the foreign key to the current
619 model in the relationship table. If not specified, a canonical name
620 will be derived based on the model name (in the form: `src_model_id`).
621 :param str id2: optional name for the column holding the foreign key to the destination
622 model in the relationship table. If not specified, a canonical name
623 will be derived based on the model name (in the form: `dest_model_id`)
624 :param str string: field label
626 _classic_read = False
627 _classic_write = False
631 def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
634 _column.__init__(self, string=string, **args)
636 if rel and '.' in rel:
637 raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\
638 'You used %s, which is not a valid SQL table name.')% (string,rel))
644 def _sql_names(self, source_model):
645 """Return the SQL names defining the structure of the m2m relationship table
647 :return: (m2m_table, local_col, dest_col) where m2m_table is the table name,
648 local_col is the name of the column holding the current model's FK, and
649 dest_col is the name of the column holding the destination model's FK, and
651 tbl, col1, col2 = self._rel, self._id1, self._id2
652 if not all((tbl, col1, col2)):
653 # the default table name is based on the stable alphabetical order of tables
654 dest_model = source_model.pool.get(self._obj)
655 tables = tuple(sorted([source_model._table, dest_model._table]))
657 assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\
658 'is not possible when source and destination models are '\
660 tbl = '%s_%s_rel' % tables
662 col1 = '%s_id' % source_model._table
664 col2 = '%s_id' % dest_model._table
665 return (tbl, col1, col2)
667 def _get_query_and_where_params(self, cr, model, ids, values, where_params):
668 """ Extracted from ``get`` to facilitate fine-tuning of the generated
670 query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
671 FROM %(rel)s, %(from_c)s \
672 WHERE %(rel)s.%(id1)s IN %%s \
673 AND %(rel)s.%(id2)s = %(tbl)s.id \
679 return query, where_params
681 def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
693 "Specifying offset at a many2many.get() is deprecated and may"
694 " produce unpredictable results.")
695 obj = model.pool.get(self._obj)
696 rel, id1, id2 = self._sql_names(model)
698 # static domains are lists, and are evaluated both here and on client-side, while string
699 # domains supposed by dynamic and evaluated on client-side only (thus ignored here)
700 # FIXME: make this distinction explicit in API!
701 domain = isinstance(self._domain, list) and self._domain or []
703 wquery = obj._where_calc(cr, user, domain, context=context)
704 obj._apply_ir_rules(cr, user, wquery, 'read', context=context)
705 from_c, where_c, where_params = wquery.get_sql()
707 where_c = ' AND ' + where_c
709 order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0])
712 if self._limit is not None:
713 limit_str = ' LIMIT %d' % self._limit
715 query, where_params = self._get_query_and_where_params(cr, model, ids, {'rel': rel,
722 'order_by': order_by,
726 cr.execute(query, [tuple(ids),] + where_params)
727 for r in cr.fetchall():
728 res[r[1]].append(r[0])
731 def set(self, cr, model, id, name, values, user=None, context=None):
736 rel, id1, id2 = self._sql_names(model)
737 obj = model.pool.get(self._obj)
739 if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
742 idnew = obj.create(cr, user, act[2], context=context)
743 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew))
745 obj.write(cr, user, [act[1]], act[2], context=context)
747 obj.unlink(cr, user, [act[1]], context=context)
749 cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1]))
751 # following queries are in the same transaction - so should be relatively safe
752 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1]))
753 if not cr.fetchone():
754 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1]))
756 cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,))
759 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
761 d1 = ' and ' + ' and '.join(d1)
764 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)
766 for act_nbr in act[2]:
767 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr))
770 # TODO: use a name_search
772 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
773 return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
776 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
777 raise NotImplementedError('Many2Many columns should not be used as record name (_rec_name)')
780 def get_nice_size(value):
782 if isinstance(value, (int,long)):
784 elif value: # this is supposed to be a string
786 return tools.human_size(size)
788 # See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
789 # and http://bugs.python.org/issue10066
790 invalid_xml_low_bytes = re.compile(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
792 def sanitize_binary_value(value):
793 # binary fields should be 7-bit ASCII base64-encoded data,
794 # but we do additional sanity checks to make sure the values
795 # are not something else that won't pass via XML-RPC
796 if isinstance(value, (xmlrpclib.Binary, tuple, list, dict)):
797 # these builtin types are meant to pass untouched
800 # Handle invalid bytes values that will cause problems
801 # for XML-RPC. See for more info:
802 # - http://bugs.python.org/issue10066
803 # - http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
805 # Coercing to unicode would normally allow it to properly pass via
806 # XML-RPC, transparently encoded as UTF-8 by xmlrpclib.
807 # (this works for _any_ byte values, thanks to the fallback
808 # to latin-1 passthrough encoding when decoding to unicode)
809 value = tools.ustr(value)
811 # Due to Python bug #10066 this could still yield invalid XML
812 # bytes, specifically in the low byte range, that will crash
813 # the decoding side: [\x00-\x08\x0b-\x0c\x0e-\x1f]
814 # So check for low bytes values, and if any, perform
815 # base64 encoding - not very smart or useful, but this is
816 # our last resort to avoid crashing the request.
817 if invalid_xml_low_bytes.search(value):
818 # b64-encode after restoring the pure bytes with latin-1
819 # passthrough encoding
820 value = base64.b64encode(value.encode('latin-1'))
825 # ---------------------------------------------------------
827 # ---------------------------------------------------------
828 class function(_column):
830 A field whose value is computed by a function (rather
831 than being read from the database).
833 :param fnct: the callable that will compute the field value.
834 :param arg: arbitrary value to be passed to ``fnct`` when computing the value.
835 :param fnct_inv: the callable that will allow writing values in that field
836 (if not provided, the field is read-only).
837 :param fnct_inv_arg: arbitrary value to be passed to ``fnct_inv`` when
839 :param str type: type of the field simulated by the function field
840 :param fnct_search: the callable that allows searching on the field
841 (if not provided, search will not return any result).
842 :param store: store computed value in database
843 (see :ref:`The *store* parameter <field-function-store>`).
844 :type store: True or dict specifying triggers for field computation
845 :param multi: name of batch for batch computation of function fields.
846 All fields with the same batch name will be computed by
847 a single function call. This changes the signature of the
850 .. _field-function-fnct: The ``fnct`` parameter
852 .. rubric:: The ``fnct`` parameter
854 The callable implementing the function field must have the following signature:
856 .. function:: fnct(model, cr, uid, ids, field_name(s), arg, context)
858 Implements the function field.
860 :param orm model: model to which the field belongs (should be ``self`` for
862 :param field_name(s): name of the field to compute, or if ``multi`` is provided,
863 list of field names to compute.
864 :type field_name(s): str | [str]
865 :param arg: arbitrary value passed when declaring the function field
867 :return: mapping of ``ids`` to computed values, or if multi is provided,
868 to a map of field_names to computed values
870 The values in the returned dictionary must be of the type specified by the type
871 argument in the field declaration.
873 Here is an example with a simple function ``char`` function field::
876 def compute(self, cr, uid, ids, field_name, arg, context):
880 _columns['my_char'] = fields.function(compute, type='char', size=50)
882 # when called with ``ids=[1,2,3]``, ``compute`` could return:
886 3: False # null values should be returned explicitly too
889 If ``multi`` is set, then ``field_name`` is replaced by ``field_names``: a list
890 of the field names that should be computed. Each value in the returned
891 dictionary must then be a dictionary mapping field names to values.
893 Here is an example where two function fields (``name`` and ``age``)
894 are both computed by a single function field::
897 def compute(self, cr, uid, ids, field_names, arg, context):
901 _columns['name'] = fields.function(compute_person_data, type='char',\
902 size=50, multi='person_data')
903 _columns[''age'] = fields.function(compute_person_data, type='integer',\
906 # when called with ``ids=[1,2,3]``, ``compute_person_data`` could return:
908 1: {'name': 'Bob', 'age': 23},
909 2: {'name': 'Sally', 'age': 19},
910 3: {'name': 'unknown', 'age': False}
913 .. _field-function-fnct-inv:
915 .. rubric:: The ``fnct_inv`` parameter
917 This callable implements the write operation for the function field
918 and must have the following signature:
920 .. function:: fnct_inv(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context)
922 Callable that implements the ``write`` operation for the function field.
924 :param orm model: model to which the field belongs (should be ``self`` for
926 :param int id: the identifier of the object to write on
927 :param str field_name: name of the field to set
928 :param fnct_inv_arg: arbitrary value passed when declaring the function field
931 When writing values for a function field, the ``multi`` parameter is ignored.
933 .. _field-function-fnct-search:
935 .. rubric:: The ``fnct_search`` parameter
937 This callable implements the search operation for the function field
938 and must have the following signature:
940 .. function:: fnct_search(model, cr, uid, model_again, field_name, criterion, context)
942 Callable that implements the ``search`` operation for the function field by expanding
943 a search criterion based on the function field into a new domain based only on
944 columns that are stored in the database.
946 :param orm model: model to which the field belongs (should be ``self`` for
948 :param orm model_again: same value as ``model`` (seriously! this is for backwards
950 :param str field_name: name of the field to search on
951 :param list criterion: domain component specifying the search criterion on the field.
953 :return: domain to use instead of ``criterion`` when performing the search.
954 This new domain must be based only on columns stored in the database, as it
955 will be used directly without any translation.
957 The returned value must be a domain, that is, a list of the form [(field_name, operator, operand)].
958 The most generic way to implement ``fnct_search`` is to directly search for the records that
959 match the given ``criterion``, and return their ``ids`` wrapped in a domain, such as
960 ``[('id','in',[1,3,5])]``.
962 .. _field-function-store:
964 .. rubric:: The ``store`` parameter
966 The ``store`` parameter allows caching the result of the field computation in the
967 database, and defining the triggers that will invalidate that cache and force a
968 recomputation of the function field.
969 When not provided, the field is computed every time its value is read.
970 The value of ``store`` may be either ``True`` (to recompute the field value whenever
971 any field in the same record is modified), or a dictionary specifying a more
972 flexible set of recomputation triggers.
974 A trigger specification is a dictionary that maps the names of the models that
975 will trigger the computation, to a tuple describing the trigger rule, in the
979 'trigger_model': (mapping_function,
980 ['trigger_field1', 'trigger_field2'],
984 A trigger rule is defined by a 3-item tuple where:
986 * The ``mapping_function`` is defined as follows:
988 .. function:: mapping_function(trigger_model, cr, uid, trigger_ids, context)
990 Callable that maps record ids of a trigger model to ids of the
991 corresponding records in the source model (whose field values
992 need to be recomputed).
994 :param orm model: trigger_model
995 :param list trigger_ids: ids of the records of trigger_model that were
998 :return: list of ids of the source model whose function field values
999 need to be recomputed
1001 * The second item is a list of the fields who should act as triggers for
1002 the computation. If an empty list is given, all fields will act as triggers.
1003 * The last item is the priority, used to order the triggers when processing them
1004 after any write operation on a model that has function field triggers. The
1005 default priority is 10.
1007 In fact, setting store = True is the same as using the following trigger dict::
1010 'model_itself': (lambda self, cr, uid, ids, context: ids,
1016 _classic_read = False
1017 _classic_write = False
1023 # multi: compute several fields in one call
1025 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):
1026 _column.__init__(self, **args)
1029 self._fnct_inv = fnct_inv
1032 if 'relation' in args:
1033 self._obj = args['relation']
1035 self.digits = args.get('digits', (16,2))
1036 self.digits_compute = args.get('digits_compute', None)
1038 self._fnct_inv_arg = fnct_inv_arg
1042 self._fnct_search = fnct_search
1045 if not fnct_search and not store:
1046 self.selectable = False
1049 if self._type != 'many2one':
1050 # m2o fields need to return tuples with name_get, not just foreign keys
1051 self._classic_read = True
1052 self._classic_write = True
1054 self._symbol_get=lambda x:x and str(x)
1057 self._symbol_c = float._symbol_c
1058 self._symbol_f = float._symbol_f
1059 self._symbol_set = float._symbol_set
1061 if type == 'boolean':
1062 self._symbol_c = boolean._symbol_c
1063 self._symbol_f = boolean._symbol_f
1064 self._symbol_set = boolean._symbol_set
1066 if type == 'integer':
1067 self._symbol_c = integer._symbol_c
1068 self._symbol_f = integer._symbol_f
1069 self._symbol_set = integer._symbol_set
1071 def digits_change(self, cr):
1072 if self._type == 'float':
1073 if self.digits_compute:
1074 self.digits = self.digits_compute(cr)
1076 precision, scale = self.digits
1077 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
1078 precision_digits=scale),
1079 precision_digits=scale))
1081 def search(self, cr, uid, obj, name, args, context=None):
1082 if not self._fnct_search:
1083 #CHECKME: should raise an exception
1085 return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
1087 def postprocess(self, cr, uid, obj, field, value=None, context=None):
1091 field_type = obj._columns[field]._type
1092 if field_type == "many2one":
1093 # make the result a tuple if it is not already one
1094 if isinstance(value, (int,long)) and hasattr(obj._columns[field], 'relation'):
1095 obj_model = obj.pool.get(obj._columns[field].relation)
1096 dict_names = dict(obj_model.name_get(cr, uid, [value], context))
1097 result = (value, dict_names[value])
1099 if field_type == 'binary':
1100 if context.get('bin_size'):
1101 # client requests only the size of binary fields
1102 result = get_nice_size(value)
1103 elif not context.get('bin_raw'):
1104 result = sanitize_binary_value(value)
1106 if field_type == "integer" and value > xmlrpclib.MAXINT:
1107 # integer/long values greater than 2^31-1 are not supported
1108 # in pure XMLRPC, so we have to pass them as floats :-(
1109 # This is not needed for stored fields and non-functional integer
1110 # fields, as their values are constrained by the database backend
1111 # to the same 32bits signed int limit.
1112 result = __builtin__.float(value)
1115 def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
1116 result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
1118 if self._multi and id in result:
1119 for field, value in result[id].iteritems():
1121 result[id][field] = self.postprocess(cr, uid, obj, field, value, context)
1122 elif result.get(id):
1123 result[id] = self.postprocess(cr, uid, obj, name, result[id], context)
1126 def set(self, cr, obj, id, name, value, user=None, context=None):
1130 self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
1133 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
1134 # Function fields are supposed to emulate a basic field type,
1135 # so they can delegate to the basic type for record name rendering
1136 return globals()[field._type]._as_display_name(field, cr, uid, obj, value, context=context)
1138 # ---------------------------------------------------------
1140 # ---------------------------------------------------------
1142 class related(function):
1143 """Field that points to some data inside another field of the current record.
1148 'foo_id': fields.many2one('my.foo', 'Foo'),
1149 'bar': fields.related('foo_id', 'frol', type='char', string='Frol of Foo'),
1153 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1154 # assume self._arg = ('foo', 'bar', 'baz')
1155 # domain = [(name, op, val)] => search [('foo.bar.baz', op, val)]
1156 field = '.'.join(self._arg)
1157 return map(lambda x: (field, x[1], x[2]), domain)
1159 def _fnct_write(self,obj,cr, uid, ids, field_name, values, args, context=None):
1160 if isinstance(ids, (int, long)):
1162 for record in obj.browse(cr, uid, ids, context=context):
1163 # traverse all fields except the last one
1164 for field in self.arg[:-1]:
1165 record = record[field] or False
1168 elif isinstance(record, list):
1169 # record is the result of a one2many or many2many field
1172 # write on the last field
1173 record.write({self.arg[-1]: values})
1175 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1177 for record in obj.browse(cr, SUPERUSER_ID, ids, context=context):
1179 for field in self.arg:
1180 if isinstance(value, list):
1182 value = value[field] or False
1185 res[record.id] = value
1187 if self._type == 'many2one':
1188 # res[id] is a browse_record or False; convert it to (id, name) or False.
1189 # Perform name_get as root, as seeing the name of a related object depends on
1190 # access right of source document, not target, so user may not have access.
1191 value_ids = list(set(value.id for value in res.itervalues() if value))
1192 value_name = dict(obj.pool.get(self._obj).name_get(cr, SUPERUSER_ID, value_ids, context=context))
1193 res = dict((id, value and (value.id, value_name[value.id])) for id, value in res.iteritems())
1195 elif self._type in ('one2many', 'many2many'):
1196 # res[id] is a list of browse_record or False; convert it to a list of ids
1197 res = dict((id, value and map(int, value) or []) for id, value in res.iteritems())
1201 def __init__(self, *arg, **args):
1203 self._relations = []
1204 super(related, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1205 if self.store is True:
1206 # TODO: improve here to change self.store = {...} according to related objects
1210 class sparse(function):
1212 def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
1214 + For a many2many field, a list of tuples is expected.
1215 Here is the list of tuple that are accepted, with the corresponding semantics ::
1217 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1218 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1219 (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)
1220 (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)
1221 (4, ID) link to existing record with id = ID (adds a relationship)
1222 (5) unlink all (like using (3,ID) for all linked records)
1223 (6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
1226 [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
1228 + For a one2many field, a lits of tuples is expected.
1229 Here is the list of tuple that are accepted, with the corresponding semantics ::
1231 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1232 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1233 (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)
1236 [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
1239 if self._type == 'many2many':
1240 assert value[0][0] == 6, 'Unsupported m2m value for sparse field: %s' % value
1243 elif self._type == 'one2many':
1246 relation_obj = obj.pool.get(self.relation)
1248 assert vals[0] in (0,1,2), 'Unsupported o2m value for sparse field: %s' % vals
1250 read_value.append(relation_obj.create(cr, uid, vals[2], context=context))
1252 relation_obj.write(cr, uid, vals[1], vals[2], context=context)
1254 relation_obj.unlink(cr, uid, vals[1], context=context)
1255 read_value.remove(vals[1])
1260 def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
1261 if not type(ids) == list:
1263 records = obj.browse(cr, uid, ids, context=context)
1264 for record in records:
1265 # grab serialized value as object - already deserialized
1266 serialized = getattr(record, self.serialization_field)
1268 # simply delete the key to unset it.
1269 serialized.pop(field_name, None)
1271 serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context)
1272 obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context)
1275 def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None):
1277 records = obj.browse(cr, uid, ids, context=context)
1278 for record in records:
1279 # grab serialized value as object - already deserialized
1280 serialized = getattr(record, self.serialization_field)
1281 results[record.id] = {}
1282 for field_name in field_names:
1283 field_type = obj._columns[field_name]._type
1284 value = serialized.get(field_name, False)
1285 if field_type in ('one2many','many2many'):
1288 # filter out deleted records as superuser
1289 relation_obj = obj.pool.get(obj._columns[field_name].relation)
1290 value = relation_obj.exists(cr, openerp.SUPERUSER_ID, value)
1291 if type(value) in (int,long) and field_type == 'many2one':
1292 relation_obj = obj.pool.get(obj._columns[field_name].relation)
1293 # check for deleted record as superuser
1294 if not relation_obj.exists(cr, openerp.SUPERUSER_ID, [value]):
1296 results[record.id][field_name] = value
1299 def __init__(self, serialization_field, **kwargs):
1300 self.serialization_field = serialization_field
1301 return super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
1305 # ---------------------------------------------------------
1307 # ---------------------------------------------------------
1309 class dummy(function):
1310 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1313 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1316 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1319 def __init__(self, *arg, **args):
1321 self._relations = []
1322 super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=None, **args)
1324 # ---------------------------------------------------------
1326 # ---------------------------------------------------------
1328 class serialized(_column):
1329 """ A field able to store an arbitrary python data structure.
1331 Note: only plain components allowed.
1334 def _symbol_set_struct(val):
1335 return simplejson.dumps(val)
1337 def _symbol_get_struct(self, val):
1338 return simplejson.loads(val or '{}')
1341 _type = 'serialized'
1344 _symbol_f = _symbol_set_struct
1345 _symbol_set = (_symbol_c, _symbol_f)
1346 _symbol_get = _symbol_get_struct
1348 # TODO: review completly this class for speed improvement
1349 class property(function):
1351 def _get_default(self, obj, cr, uid, prop_name, context=None):
1352 return self._get_defaults(obj, cr, uid, [prop_name], context=None)[prop_name]
1354 def _get_defaults(self, obj, cr, uid, prop_names, context=None):
1355 """Get the default values for ``prop_names´´ property fields (result of ir.property.get() function for res_id = False).
1357 :param list of string prop_names: list of name of property fields for those we want the default value
1358 :return: map of property field names to their default value
1361 prop = obj.pool.get('ir.property')
1363 for prop_name in prop_names:
1364 res[prop_name] = prop.get(cr, uid, prop_name, obj._name, context=context)
1367 def _get_by_id(self, obj, cr, uid, prop_name, ids, context=None):
1368 prop = obj.pool.get('ir.property')
1369 vids = [obj._name + ',' + str(oid) for oid in ids]
1371 domain = [('fields_id.model', '=', obj._name), ('fields_id.name', 'in', prop_name)]
1372 #domain = prop._get_domain(cr, uid, prop_name, obj._name, context)
1374 domain = [('res_id', 'in', vids)] + domain
1375 return prop.search(cr, uid, domain, context=context)
1377 # TODO: to rewrite more clean
1378 def _fnct_write(self, obj, cr, uid, id, prop_name, id_val, obj_dest, context=None):
1382 nids = self._get_by_id(obj, cr, uid, [prop_name], [id], context)
1384 cr.execute('DELETE FROM ir_property WHERE id IN %s', (tuple(nids),))
1386 default_val = self._get_default(obj, cr, uid, prop_name, context)
1388 property_create = False
1389 if isinstance(default_val, openerp.osv.orm.browse_record):
1390 if default_val.id != id_val:
1391 property_create = True
1392 elif default_val != id_val:
1393 property_create = True
1396 def_id = self._field_get(cr, uid, obj._name, prop_name)
1397 company = obj.pool.get('res.company')
1398 cid = company._company_default_get(cr, uid, obj._name, def_id,
1400 propdef = obj.pool.get('ir.model.fields').browse(cr, uid, def_id,
1402 prop = obj.pool.get('ir.property')
1403 return prop.create(cr, uid, {
1404 'name': propdef.name,
1406 'res_id': obj._name+','+str(id),
1408 'fields_id': def_id,
1413 def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
1414 prop = obj.pool.get('ir.property')
1415 # get the default values (for res_id = False) for the property fields
1416 default_val = self._get_defaults(obj, cr, uid, prop_names, context)
1418 # build the dictionary that will be returned
1421 res[id] = default_val.copy()
1423 for prop_name in prop_names:
1424 property_field = obj._all_columns.get(prop_name).column
1425 property_destination_obj = property_field._obj if property_field._type == 'many2one' else False
1426 # If the property field is a m2o field, we will append the id of the value to name_get_ids
1427 # in order to make a name_get in batch for all the ids needed.
1430 # get the result of ir.property.get() for this res_id and save it in res if it's existing
1431 obj_reference = obj._name + ',' + str(id)
1432 value = prop.get(cr, uid, prop_name, obj._name, res_id=obj_reference, context=context)
1434 res[id][prop_name] = value
1435 # Check existence as root (as seeing the name of a related
1436 # object depends on access right of source document,
1437 # not target, so user may not have access) in order to avoid
1438 # pointing on an unexisting record.
1439 if property_destination_obj:
1440 if res[id][prop_name] and obj.pool.get(property_destination_obj).exists(cr, SUPERUSER_ID, res[id][prop_name].id):
1441 name_get_ids[id] = res[id][prop_name].id
1443 res[id][prop_name] = False
1444 if property_destination_obj:
1445 # name_get as root (as seeing the name of a related
1446 # object depends on access right of source document,
1447 # not target, so user may not have access.)
1448 name_get_values = dict(obj.pool.get(property_destination_obj).name_get(cr, SUPERUSER_ID, name_get_ids.values(), context=context))
1449 # the property field is a m2o, we need to return a tuple with (id, name)
1450 for k, v in name_get_ids.iteritems():
1451 if res[k][prop_name]:
1452 res[k][prop_name] = (v , name_get_values.get(v))
1455 def _field_get(self, cr, uid, model_name, prop):
1456 if not self.field_id.get(cr.dbname):
1457 cr.execute('SELECT id \
1458 FROM ir_model_fields \
1459 WHERE name=%s AND model=%s', (prop, model_name))
1461 self.field_id[cr.dbname] = res and res[0]
1462 return self.field_id[cr.dbname]
1464 def __init__(self, obj_prop, **args):
1465 # TODO remove obj_prop parameter (use many2one type)
1467 function.__init__(self, self._fnct_read, False, self._fnct_write,
1468 obj_prop, multi='properties', **args)
1474 def field_to_dict(model, cr, user, field, context=None):
1475 """ Return a dictionary representation of a field.
1477 The string, help, and selection attributes (if any) are untranslated. This
1478 representation is the one returned by fields_get() (fields_get() will do
1483 res = {'type': field._type}
1484 # some attributes for m2m/function field are added as debug info only
1485 if isinstance(field, function):
1486 res['function'] = field._fnct and field._fnct.func_name or False
1487 res['store'] = field.store
1488 if isinstance(field.store, dict):
1489 res['store'] = str(field.store)
1490 res['fnct_search'] = field._fnct_search and field._fnct_search.func_name or False
1491 res['fnct_inv'] = field._fnct_inv and field._fnct_inv.func_name or False
1492 res['fnct_inv_arg'] = field._fnct_inv_arg or False
1493 if isinstance(field, many2many):
1494 (table, col1, col2) = field._sql_names(model)
1495 res['m2m_join_columns'] = [col1, col2]
1496 res['m2m_join_table'] = table
1497 for arg in ('string', 'readonly', 'states', 'size', 'group_operator', 'required',
1498 'change_default', 'translate', 'help', 'select', 'selectable', 'groups',
1499 'deprecated', 'digits', 'invisible', 'filters'):
1500 if getattr(field, arg, None):
1501 res[arg] = getattr(field, arg)
1503 if hasattr(field, 'selection'):
1504 if isinstance(field.selection, (tuple, list)):
1505 res['selection'] = field.selection
1507 # call the 'dynamic selection' function
1508 res['selection'] = field.selection(model, cr, user, context)
1509 if res['type'] in ('one2many', 'many2many', 'many2one'):
1510 res['relation'] = field._obj
1511 res['domain'] = field._domain(model) if callable(field._domain) else field._domain
1512 res['context'] = field._context
1514 if isinstance(field, one2many):
1515 res['relation_field'] = field._fields_id
1520 class column_info(object):
1521 """ Struct containing details about an osv column, either one local to
1522 its model, or one inherited via _inherits.
1528 .. attribute:: column
1530 column instance, subclass of :class:`_column`
1532 .. attribute:: parent_model
1534 if the column is inherited, name of the model that contains it,
1535 ``None`` for local columns.
1537 .. attribute:: parent_column
1539 the name of the column containing the m2o relationship to the
1540 parent model that contains this column, ``None`` for local columns.
1542 .. attribute:: original_parent
1544 if the column is inherited, name of the original parent model that
1545 contains it i.e in case of multilevel inheritance, ``None`` for
1548 def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
1550 self.column = column
1551 self.parent_model = parent_model
1552 self.parent_column = parent_column
1553 self.original_parent = original_parent
1556 return '%s(%s, %s, %s, %s, %s)' % (
1557 self.__name__, self.name, self.column,
1558 self.parent_model, self.parent_column, self.original_parent)
1560 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: