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
49 _logger = logging.getLogger(__name__)
51 def _symbol_set(symb):
52 if symb == None or symb == False:
54 elif isinstance(symb, unicode):
55 return symb.encode('utf-8')
59 class _column(object):
60 """ Base of all fields, a database column
62 An instance of this object is a *description* of a database column. It will
63 not hold any data, but only provide the methods to manipulate data of an
64 ORM record or even prepare/update the database to hold such a field of data.
74 _symbol_f = _symbol_set
75 _symbol_set = (_symbol_c, _symbol_f)
78 # used to hide a certain field type in the list of field types
81 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):
84 The 'manual' keyword argument specifies if the field is a custom one.
85 It corresponds to the 'state' column in ir_model_fields.
92 self.states = states or {}
94 self.readonly = readonly
95 self.required = required
97 self.help = args.get('help', '')
98 self.priority = priority
99 self.change_default = change_default
100 self.ondelete = ondelete.lower() if ondelete else None # defaults to 'set null' in ORM
101 self.translate = translate
102 self._domain = domain
103 self._context = context
109 self.selectable = True
110 self.group_operator = args.get('group_operator', False)
111 self.groups = False # CSV list of ext IDs of groups that can access this field
114 setattr(self, a, args[a])
119 def set(self, cr, obj, id, name, value, user=None, context=None):
120 cr.execute('update '+obj._table+' set '+name+'='+self._symbol_set[0]+' where id=%s', (self._symbol_set[1](value), id))
122 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
123 raise Exception(_('undefined get method !'))
125 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
126 ids = obj.search(cr, uid, args+self._domain+[(name, 'ilike', value)], offset, limit, context=context)
127 res = obj.read(cr, uid, ids, [name], context=context)
128 return [x[name] for x in res]
131 # ---------------------------------------------------------
133 # ---------------------------------------------------------
134 class boolean(_column):
137 _symbol_f = lambda x: x and 'True' or 'False'
138 _symbol_set = (_symbol_c, _symbol_f)
140 def __init__(self, string='unknown', required=False, **args):
141 super(boolean, self).__init__(string=string, required=required, **args)
144 "required=True is deprecated: making a boolean field"
145 " `required` has no effect, as NULL values are "
146 "automatically turned into False.")
148 class integer(_column):
151 _symbol_f = lambda x: int(x or 0)
152 _symbol_set = (_symbol_c, _symbol_f)
153 _symbol_get = lambda self,x: x or 0
155 def __init__(self, string='unknown', required=False, **args):
156 super(integer, self).__init__(string=string, required=required, **args)
159 "required=True is deprecated: making an integer field"
160 " `required` has no effect, as NULL values are "
161 "automatically turned into 0.")
163 class reference(_column):
165 _classic_read = False # post-process to handle missing target
167 def __init__(self, string, selection, size, **args):
168 _column.__init__(self, string=string, size=size, selection=selection, **args)
170 def get(self, cr, obj, ids, name, uid=None, context=None, values=None):
172 # copy initial values fetched previously.
174 result[value['id']] = value[name]
176 model, res_id = value[name].split(',')
177 if not obj.pool.get(model).exists(cr, uid, [int(res_id)], context=context):
178 result[value['id']] = False
184 def __init__(self, string, size, **args):
185 _column.__init__(self, string=string, size=size, **args)
186 self._symbol_set = (self._symbol_c, self._symbol_set_char)
188 # takes a string (encoded in utf8) and returns a string (encoded in utf8)
189 def _symbol_set_char(self, symb):
191 # * we need to remove the "symb==False" from the next line BUT
192 # for now too many things rely on this broken behavior
193 # * the symb==None test should be common to all data types
194 if symb == None or symb == False:
197 # we need to convert the string to a unicode object to be able
198 # to evaluate its length (and possibly truncate it) reliably
199 u_symb = tools.ustr(symb)
201 return u_symb[:self.size].encode('utf8')
209 class float(_column):
212 _symbol_f = lambda x: __builtin__.float(x or 0.0)
213 _symbol_set = (_symbol_c, _symbol_f)
214 _symbol_get = lambda self,x: x or 0.0
216 def __init__(self, string='unknown', digits=None, digits_compute=None, required=False, **args):
217 _column.__init__(self, string=string, required=required, **args)
219 # synopsis: digits_compute(cr) -> (precision, scale)
220 self.digits_compute = digits_compute
223 "required=True is deprecated: making a float field"
224 " `required` has no effect, as NULL values are "
225 "automatically turned into 0.0.")
227 def digits_change(self, cr):
228 if self.digits_compute:
229 self.digits = self.digits_compute(cr)
231 precision, scale = self.digits
232 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
233 precision_digits=scale),
234 precision_digits=scale))
241 """ Returns the current date in a format fit for being a
242 default value to a ``date`` field.
244 This method should be provided as is to the _defaults dict, it
245 should not be called.
247 return DT.date.today().strftime(
248 tools.DEFAULT_SERVER_DATE_FORMAT)
251 def context_today(model, cr, uid, context=None, timestamp=None):
252 """Returns the current date as seen in the client's timezone
253 in a format fit for date fields.
254 This method may be passed as value to initialize _defaults.
256 :param Model model: model (osv) for which the date value is being
257 computed - technical field, currently ignored,
258 automatically passed when used in _defaults.
259 :param datetime timestamp: optional datetime value to use instead of
260 the current date and time (must be a
261 datetime, regular dates can't be converted
263 :param dict context: the 'tz' key in the context should give the
264 name of the User/Client timezone (otherwise
268 today = timestamp or DT.datetime.now()
270 if context and context.get('tz'):
272 utc = pytz.timezone('UTC')
273 context_tz = pytz.timezone(context['tz'])
274 utc_today = utc.localize(today, is_dst=False) # UTC = no DST
275 context_today = utc_today.astimezone(context_tz)
277 _logger.debug("failed to compute context/client-specific today date, "
278 "using the UTC value for `today`",
280 return (context_today or today).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
282 class datetime(_column):
286 """ Returns the current datetime in a format fit for being a
287 default value to a ``datetime`` field.
289 This method should be provided as is to the _defaults dict, it
290 should not be called.
292 return DT.datetime.now().strftime(
293 tools.DEFAULT_SERVER_DATETIME_FORMAT)
296 def context_timestamp(cr, uid, timestamp, context=None):
297 """Returns the given timestamp converted to the client's timezone.
298 This method is *not* meant for use as a _defaults initializer,
299 because datetime fields are automatically converted upon
300 display on client side. For _defaults you :meth:`fields.datetime.now`
301 should be used instead.
303 :param datetime timestamp: naive datetime value (expressed in UTC)
304 to be converted to the client timezone
305 :param dict context: the 'tz' key in the context should give the
306 name of the User/Client timezone (otherwise
309 :return: timestamp converted to timezone-aware datetime in context
312 assert isinstance(timestamp, DT.datetime), 'Datetime instance expected'
313 if context and context.get('tz'):
315 utc = pytz.timezone('UTC')
316 context_tz = pytz.timezone(context['tz'])
317 utc_timestamp = utc.localize(timestamp, is_dst=False) # UTC = no DST
318 return utc_timestamp.astimezone(context_tz)
320 _logger.debug("failed to compute context/client-specific timestamp, "
321 "using the UTC value",
325 class binary(_column):
329 # Binary values may be byte strings (python 2.6 byte array), but
330 # the legacy OpenERP convention is to transfer and store binaries
331 # as base64-encoded strings. The base64 string may be provided as a
332 # unicode in some circumstances, hence the str() cast in symbol_f.
333 # This str coercion will only work for pure ASCII unicode strings,
334 # on purpose - non base64 data must be passed as a 8bit byte strings.
335 _symbol_f = lambda symb: symb and Binary(str(symb)) or None
337 _symbol_set = (_symbol_c, _symbol_f)
338 _symbol_get = lambda self, x: x and str(x)
340 _classic_read = False
343 def __init__(self, string='unknown', filters=None, **args):
344 _column.__init__(self, string=string, **args)
345 self.filters = filters
347 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
360 # If client is requesting only the size of the field, we return it instead
361 # of the content. Presumably a separate request will be done to read the actual
362 # content if it's needed at some point.
363 # TODO: after 6.0 we should consider returning a dict with size and content instead of
364 # having an implicit convention for the value
365 if val and context.get('bin_size_%s' % name, context.get('bin_size')):
366 res[i] = tools.human_size(long(val))
371 class selection(_column):
374 def __init__(self, selection, string='unknown', **args):
375 _column.__init__(self, string=string, **args)
376 self.selection = selection
378 # ---------------------------------------------------------
380 # ---------------------------------------------------------
383 # Values: (0, 0, { fields }) create
384 # (1, ID, { fields }) update
385 # (2, ID) remove (delete)
386 # (3, ID) unlink one (target id or target of relation)
388 # (5) unlink all (only valid for one2many)
391 class many2one(_column):
392 _classic_read = False
393 _classic_write = True
396 _symbol_f = lambda x: x or None
397 _symbol_set = (_symbol_c, _symbol_f)
399 def __init__(self, obj, string='unknown', **args):
400 _column.__init__(self, string=string, **args)
403 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
411 res[r['id']] = r[name]
413 res.setdefault(id, '')
414 obj = obj.pool.get(self._obj)
416 # build a dictionary of the form {'id_of_distant_resource': name_of_distant_resource}
417 # we use uid=1 because the visibility of a many2one field value (just id and name)
418 # must be the access right of the parent form and not the linked object itself.
419 records = dict(obj.name_get(cr, 1,
420 list(set([x for x in res.values() if isinstance(x, (int,long))])),
423 if res[id] in records:
424 res[id] = (res[id], records[res[id]])
429 def set(self, cr, obj_src, id, field, values, user=None, context=None):
432 obj = obj_src.pool.get(self._obj)
433 self._table = obj_src.pool.get(self._obj)._table
434 if type(values) == type([]):
437 id_new = obj.create(cr, act[2])
438 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (id_new, id))
440 obj.write(cr, [act[1]], act[2], context=context)
442 cr.execute('delete from '+self._table+' where id=%s', (act[1],))
443 elif act[0] == 3 or act[0] == 5:
444 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
446 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (act[1], id))
449 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (values, id))
451 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
453 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
454 return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context)
457 class one2many(_column):
458 _classic_read = False
459 _classic_write = False
463 def __init__(self, obj, fields_id, string='unknown', limit=None, **args):
464 _column.__init__(self, string=string, **args)
466 self._fields_id = fields_id
468 #one2many can't be used as condition for defaults
469 assert(self.change_default != True)
471 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
475 context = context.copy()
476 context.update(self._context)
484 ids2 = obj.pool.get(self._obj).search(cr, user, self._domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
485 for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
486 if r[self._fields_id] in res:
487 res[r[self._fields_id]].append(r['id'])
490 def set(self, cr, obj, id, field, values, user=None, context=None):
495 context = context.copy()
496 context.update(self._context)
497 context['no_store_function'] = True
500 _table = obj.pool.get(self._obj)._table
501 obj = obj.pool.get(self._obj)
504 act[2][self._fields_id] = id
505 id_new = obj.create(cr, user, act[2], context=context)
506 result += obj._store_get_values(cr, user, [id_new], act[2].keys(), context)
508 obj.write(cr, user, [act[1]], act[2], context=context)
510 obj.unlink(cr, user, [act[1]], context=context)
512 reverse_rel = obj._all_columns.get(self._fields_id)
513 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
514 # if the model has on delete cascade, just delete the row
515 if reverse_rel.column.ondelete == "cascade":
516 obj.unlink(cr, user, [act[1]], context=context)
518 cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],))
520 # Must use write() to recompute parent_store structure if needed
521 obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
523 reverse_rel = obj._all_columns.get(self._fields_id)
524 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
525 # if the o2m has a static domain we must respect it when unlinking
526 extra_domain = self._domain if isinstance(getattr(self, '_domain', None), list) else []
527 ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
528 # If the model has cascade deletion, we delete the rows because it is the intended behavior,
529 # otherwise we only nullify the reverse foreign key column.
530 if reverse_rel.column.ondelete == "cascade":
531 obj.unlink(cr, user, ids_to_unlink, context=context)
533 obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context)
535 # Must use write() to recompute parent_store structure if needed
536 obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
538 cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
539 ids3 = map(lambda x:x[0], cr.fetchall())
540 obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
543 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
544 return obj.pool.get(self._obj).name_search(cr, uid, value, self._domain, operator, context=context,limit=limit)
548 # Values: (0, 0, { fields }) create
549 # (1, ID, { fields }) update (write fields to ID)
550 # (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete)
551 # (3, ID) unlink (delete the relationship between the two objects but does not delete ID)
552 # (4, ID) link (add a relationship)
554 # (6, ?, ids) set a list of links
556 class many2many(_column):
557 """Encapsulates the logic of a many-to-many bidirectional relationship, handling the
558 low-level details of the intermediary relationship table transparently.
559 A many-to-many relationship is always symmetrical, and can be declared and accessed
560 from either endpoint model.
561 If ``rel`` (relationship table name), ``id1`` (source foreign key column name)
562 or id2 (destination foreign key column name) are not specified, the system will
563 provide default values. This will by default only allow one single symmetrical
564 many-to-many relationship between the source and destination model.
565 For multiple many-to-many relationship between the same models and for
566 relationships where source and destination models are the same, ``rel``, ``id1``
567 and ``id2`` should be specified explicitly.
569 :param str obj: destination model
570 :param str rel: optional name of the intermediary relationship table. If not specified,
571 a canonical name will be derived based on the alphabetically-ordered
572 model names of the source and destination (in the form: ``amodel_bmodel_rel``).
573 Automatic naming is not possible when the source and destination are
574 the same, for obvious ambiguity reasons.
575 :param str id1: optional name for the column holding the foreign key to the current
576 model in the relationship table. If not specified, a canonical name
577 will be derived based on the model name (in the form: `src_model_id`).
578 :param str id2: optional name for the column holding the foreign key to the destination
579 model in the relationship table. If not specified, a canonical name
580 will be derived based on the model name (in the form: `dest_model_id`)
581 :param str string: field label
583 _classic_read = False
584 _classic_write = False
588 def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
591 _column.__init__(self, string=string, **args)
593 if rel and '.' in rel:
594 raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\
595 'You used %s, which is not a valid SQL table name.')% (string,rel))
601 def _sql_names(self, source_model):
602 """Return the SQL names defining the structure of the m2m relationship table
604 :return: (m2m_table, local_col, dest_col) where m2m_table is the table name,
605 local_col is the name of the column holding the current model's FK, and
606 dest_col is the name of the column holding the destination model's FK, and
608 tbl, col1, col2 = self._rel, self._id1, self._id2
609 if not all((tbl, col1, col2)):
610 # the default table name is based on the stable alphabetical order of tables
611 dest_model = source_model.pool.get(self._obj)
612 tables = tuple(sorted([source_model._table, dest_model._table]))
614 assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\
615 'is not possible when source and destination models are '\
617 tbl = '%s_%s_rel' % tables
619 col1 = '%s_id' % source_model._table
621 col2 = '%s_id' % dest_model._table
622 return (tbl, col1, col2)
624 def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
636 "Specifying offset at a many2many.get() is deprecated and may"
637 " produce unpredictable results.")
638 obj = model.pool.get(self._obj)
639 rel, id1, id2 = self._sql_names(model)
641 # static domains are lists, and are evaluated both here and on client-side, while string
642 # domains supposed by dynamic and evaluated on client-side only (thus ignored here)
643 # FIXME: make this distinction explicit in API!
644 domain = isinstance(self._domain, list) and self._domain or []
646 wquery = obj._where_calc(cr, user, domain, context=context)
647 obj._apply_ir_rules(cr, user, wquery, 'read', context=context)
648 from_c, where_c, where_params = wquery.get_sql()
650 where_c = ' AND ' + where_c
652 if offset or self._limit:
653 order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0])
658 if self._limit is not None:
659 limit_str = ' LIMIT %d' % self._limit
661 query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
662 FROM %(rel)s, %(from_c)s \
663 WHERE %(rel)s.%(id1)s IN %%s \
664 AND %(rel)s.%(id2)s = %(tbl)s.id \
676 'order_by': order_by,
679 cr.execute(query, [tuple(ids),] + where_params)
680 for r in cr.fetchall():
681 res[r[1]].append(r[0])
684 def set(self, cr, model, id, name, values, user=None, context=None):
689 rel, id1, id2 = self._sql_names(model)
690 obj = model.pool.get(self._obj)
692 if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
695 idnew = obj.create(cr, user, act[2], context=context)
696 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew))
698 obj.write(cr, user, [act[1]], act[2], context=context)
700 obj.unlink(cr, user, [act[1]], context=context)
702 cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1]))
704 # following queries are in the same transaction - so should be relatively safe
705 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1]))
706 if not cr.fetchone():
707 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1]))
709 cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,))
712 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
714 d1 = ' and ' + ' and '.join(d1)
717 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)
719 for act_nbr in act[2]:
720 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr))
723 # TODO: use a name_search
725 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
726 return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
729 def get_nice_size(value):
731 if isinstance(value, (int,long)):
733 elif value: # this is supposed to be a string
735 return tools.human_size(size)
737 # See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
738 # and http://bugs.python.org/issue10066
739 invalid_xml_low_bytes = re.compile(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
741 def sanitize_binary_value(value):
742 # binary fields should be 7-bit ASCII base64-encoded data,
743 # but we do additional sanity checks to make sure the values
744 # are not something else that won't pass via XML-RPC
745 if isinstance(value, (xmlrpclib.Binary, tuple, list, dict)):
746 # these builtin types are meant to pass untouched
749 # Handle invalid bytes values that will cause problems
750 # for XML-RPC. See for more info:
751 # - http://bugs.python.org/issue10066
752 # - http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
754 # Coercing to unicode would normally allow it to properly pass via
755 # XML-RPC, transparently encoded as UTF-8 by xmlrpclib.
756 # (this works for _any_ byte values, thanks to the fallback
757 # to latin-1 passthrough encoding when decoding to unicode)
758 value = tools.ustr(value)
760 # Due to Python bug #10066 this could still yield invalid XML
761 # bytes, specifically in the low byte range, that will crash
762 # the decoding side: [\x00-\x08\x0b-\x0c\x0e-\x1f]
763 # So check for low bytes values, and if any, perform
764 # base64 encoding - not very smart or useful, but this is
765 # our last resort to avoid crashing the request.
766 if invalid_xml_low_bytes.search(value):
767 # b64-encode after restoring the pure bytes with latin-1
768 # passthrough encoding
769 value = base64.b64encode(value.encode('latin-1'))
774 # ---------------------------------------------------------
776 # ---------------------------------------------------------
777 class function(_column):
779 A field whose value is computed by a function (rather
780 than being read from the database).
782 :param fnct: the callable that will compute the field value.
783 :param arg: arbitrary value to be passed to ``fnct`` when computing the value.
784 :param fnct_inv: the callable that will allow writing values in that field
785 (if not provided, the field is read-only).
786 :param fnct_inv_arg: arbitrary value to be passed to ``fnct_inv`` when
788 :param str type: type of the field simulated by the function field
789 :param fnct_search: the callable that allows searching on the field
790 (if not provided, search will not return any result).
791 :param store: store computed value in database
792 (see :ref:`The *store* parameter <field-function-store>`).
793 :type store: True or dict specifying triggers for field computation
794 :param multi: name of batch for batch computation of function fields.
795 All fields with the same batch name will be computed by
796 a single function call. This changes the signature of the
799 .. _field-function-fnct: The ``fnct`` parameter
801 .. rubric:: The ``fnct`` parameter
803 The callable implementing the function field must have the following signature:
805 .. function:: fnct(model, cr, uid, ids, field_name(s), arg, context)
807 Implements the function field.
809 :param orm model: model to which the field belongs (should be ``self`` for
811 :param field_name(s): name of the field to compute, or if ``multi`` is provided,
812 list of field names to compute.
813 :type field_name(s): str | [str]
814 :param arg: arbitrary value passed when declaring the function field
816 :return: mapping of ``ids`` to computed values, or if multi is provided,
817 to a map of field_names to computed values
819 The values in the returned dictionary must be of the type specified by the type
820 argument in the field declaration.
822 Here is an example with a simple function ``char`` function field::
825 def compute(self, cr, uid, ids, field_name, arg, context):
829 _columns['my_char'] = fields.function(compute, type='char', size=50)
831 # when called with ``ids=[1,2,3]``, ``compute`` could return:
835 3: False # null values should be returned explicitly too
838 If ``multi`` is set, then ``field_name`` is replaced by ``field_names``: a list
839 of the field names that should be computed. Each value in the returned
840 dictionary must then be a dictionary mapping field names to values.
842 Here is an example where two function fields (``name`` and ``age``)
843 are both computed by a single function field::
846 def compute(self, cr, uid, ids, field_names, arg, context):
850 _columns['name'] = fields.function(compute_person_data, type='char',\
851 size=50, multi='person_data')
852 _columns[''age'] = fields.function(compute_person_data, type='integer',\
855 # when called with ``ids=[1,2,3]``, ``compute_person_data`` could return:
857 1: {'name': 'Bob', 'age': 23},
858 2: {'name': 'Sally', 'age': 19},
859 3: {'name': 'unknown', 'age': False}
862 .. _field-function-fnct-inv:
864 .. rubric:: The ``fnct_inv`` parameter
866 This callable implements the write operation for the function field
867 and must have the following signature:
869 .. function:: fnct_inv(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context)
871 Callable that implements the ``write`` operation for the function field.
873 :param orm model: model to which the field belongs (should be ``self`` for
875 :param int id: the identifier of the object to write on
876 :param str field_name: name of the field to set
877 :param fnct_inv_arg: arbitrary value passed when declaring the function field
880 When writing values for a function field, the ``multi`` parameter is ignored.
882 .. _field-function-fnct-search:
884 .. rubric:: The ``fnct_search`` parameter
886 This callable implements the search operation for the function field
887 and must have the following signature:
889 .. function:: fnct_search(model, cr, uid, model_again, field_name, criterion, context)
891 Callable that implements the ``search`` operation for the function field by expanding
892 a search criterion based on the function field into a new domain based only on
893 columns that are stored in the database.
895 :param orm model: model to which the field belongs (should be ``self`` for
897 :param orm model_again: same value as ``model`` (seriously! this is for backwards
899 :param str field_name: name of the field to search on
900 :param list criterion: domain component specifying the search criterion on the field.
902 :return: domain to use instead of ``criterion`` when performing the search.
903 This new domain must be based only on columns stored in the database, as it
904 will be used directly without any translation.
906 The returned value must be a domain, that is, a list of the form [(field_name, operator, operand)].
907 The most generic way to implement ``fnct_search`` is to directly search for the records that
908 match the given ``criterion``, and return their ``ids`` wrapped in a domain, such as
909 ``[('id','in',[1,3,5])]``.
911 .. _field-function-store:
913 .. rubric:: The ``store`` parameter
915 The ``store`` parameter allows caching the result of the field computation in the
916 database, and defining the triggers that will invalidate that cache and force a
917 recomputation of the function field.
918 When not provided, the field is computed every time its value is read.
919 The value of ``store`` may be either ``True`` (to recompute the field value whenever
920 any field in the same record is modified), or a dictionary specifying a more
921 flexible set of recomputation triggers.
923 A trigger specification is a dictionary that maps the names of the models that
924 will trigger the computation, to a tuple describing the trigger rule, in the
928 'trigger_model': (mapping_function,
929 ['trigger_field1', 'trigger_field2'],
933 A trigger rule is defined by a 3-item tuple where:
935 * The ``mapping_function`` is defined as follows:
937 .. function:: mapping_function(trigger_model, cr, uid, trigger_ids, context)
939 Callable that maps record ids of a trigger model to ids of the
940 corresponding records in the source model (whose field values
941 need to be recomputed).
943 :param orm model: trigger_model
944 :param list trigger_ids: ids of the records of trigger_model that were
947 :return: list of ids of the source model whose function field values
948 need to be recomputed
950 * The second item is a list of the fields who should act as triggers for
951 the computation. If an empty list is given, all fields will act as triggers.
952 * The last item is the priority, used to order the triggers when processing them
953 after any write operation on a model that has function field triggers. The
954 default priority is 10.
956 In fact, setting store = True is the same as using the following trigger dict::
959 'model_itself': (lambda self, cr, uid, ids, context: ids,
965 _classic_read = False
966 _classic_write = False
972 # multi: compute several fields in one call
974 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):
975 _column.__init__(self, **args)
978 self._fnct_inv = fnct_inv
981 if 'relation' in args:
982 self._obj = args['relation']
984 self.digits = args.get('digits', (16,2))
985 self.digits_compute = args.get('digits_compute', None)
987 self._fnct_inv_arg = fnct_inv_arg
991 self._fnct_search = fnct_search
994 if not fnct_search and not store:
995 self.selectable = False
998 if self._type != 'many2one':
999 # m2o fields need to return tuples with name_get, not just foreign keys
1000 self._classic_read = True
1001 self._classic_write = True
1003 self._symbol_get=lambda x:x and str(x)
1006 self._symbol_c = float._symbol_c
1007 self._symbol_f = float._symbol_f
1008 self._symbol_set = float._symbol_set
1010 if type == 'boolean':
1011 self._symbol_c = boolean._symbol_c
1012 self._symbol_f = boolean._symbol_f
1013 self._symbol_set = boolean._symbol_set
1015 if type == 'integer':
1016 self._symbol_c = integer._symbol_c
1017 self._symbol_f = integer._symbol_f
1018 self._symbol_set = integer._symbol_set
1020 def digits_change(self, cr):
1021 if self._type == 'float':
1022 if self.digits_compute:
1023 self.digits = self.digits_compute(cr)
1025 precision, scale = self.digits
1026 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
1027 precision_digits=scale),
1028 precision_digits=scale))
1030 def search(self, cr, uid, obj, name, args, context=None):
1031 if not self._fnct_search:
1032 #CHECKME: should raise an exception
1034 return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
1036 def postprocess(self, cr, uid, obj, field, value=None, context=None):
1040 field_type = obj._columns[field]._type
1041 if field_type == "many2one":
1042 # make the result a tuple if it is not already one
1043 if isinstance(value, (int,long)) and hasattr(obj._columns[field], 'relation'):
1044 obj_model = obj.pool.get(obj._columns[field].relation)
1045 dict_names = dict(obj_model.name_get(cr, uid, [value], context))
1046 result = (value, dict_names[value])
1048 if field_type == 'binary':
1049 if context.get('bin_size'):
1050 # client requests only the size of binary fields
1051 result = get_nice_size(value)
1052 elif not context.get('bin_raw'):
1053 result = sanitize_binary_value(value)
1055 if field_type == "integer" and value > xmlrpclib.MAXINT:
1056 # integer/long values greater than 2^31-1 are not supported
1057 # in pure XMLRPC, so we have to pass them as floats :-(
1058 # This is not needed for stored fields and non-functional integer
1059 # fields, as their values are constrained by the database backend
1060 # to the same 32bits signed int limit.
1061 result = __builtin__.float(value)
1064 def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
1065 result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
1067 if self._multi and id in result:
1068 for field, value in result[id].iteritems():
1070 result[id][field] = self.postprocess(cr, uid, obj, field, value, context)
1071 elif result.get(id):
1072 result[id] = self.postprocess(cr, uid, obj, name, result[id], context)
1075 def set(self, cr, obj, id, name, value, user=None, context=None):
1079 self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
1081 # ---------------------------------------------------------
1083 # ---------------------------------------------------------
1085 class related(function):
1086 """Field that points to some data inside another field of the current record.
1091 'foo_id': fields.many2one('my.foo', 'Foo'),
1092 'bar': fields.related('foo_id', 'frol', type='char', string='Frol of Foo'),
1096 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1097 self._field_get2(cr, uid, obj, context)
1098 i = len(self._arg)-1
1101 if type(sarg) in [type([]), type( (1,) )]:
1102 where = [(self._arg[i], 'in', sarg)]
1104 where = [(self._arg[i], '=', sarg)]
1106 where = map(lambda x: (self._arg[i],x[1], x[2]), domain)
1108 sarg = obj.pool.get(self._relations[i]['object']).search(cr, uid, where, context=context)
1110 return [(self._arg[0], 'in', sarg)]
1112 def _fnct_write(self,obj,cr, uid, ids, field_name, values, args, context=None):
1113 self._field_get2(cr, uid, obj, context=context)
1114 if type(ids) != type([]):
1116 objlst = obj.browse(cr, uid, ids)
1120 for i in range(len(self.arg)):
1121 if not t_data: break
1122 field_detail = self._relations[i]
1123 if not t_data[self.arg[i]]:
1124 if self._type not in ('one2many', 'many2many'):
1127 elif field_detail['type'] in ('one2many', 'many2many'):
1128 if self._type != "many2one":
1130 t_data = t_data[self.arg[i]][0]
1135 t_data = t_data[self.arg[i]]
1137 model = obj.pool.get(self._relations[-1]['object'])
1138 model.write(cr, uid, [t_id], {args[-1]: values}, context=context)
1140 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1141 self._field_get2(cr, uid, obj, context)
1142 if not ids: return {}
1143 relation = obj._name
1144 if self._type in ('one2many', 'many2many'):
1145 res = dict([(i, []) for i in ids])
1147 res = {}.fromkeys(ids, False)
1149 objlst = obj.browse(cr, 1, ids, context=context)
1154 relation = obj._name
1155 for i in range(len(self.arg)):
1156 field_detail = self._relations[i]
1157 relation = field_detail['object']
1159 if not t_data[self.arg[i]]:
1165 if field_detail['type'] in ('one2many', 'many2many') and i != len(self.arg) - 1:
1166 t_data = t_data[self.arg[i]][0]
1168 t_data = t_data[self.arg[i]]
1169 if type(t_data) == type(objlst[0]):
1170 res[data.id] = t_data.id
1172 res[data.id] = t_data
1173 if self._type=='many2one':
1174 ids = filter(None, res.values())
1176 # name_get as root, as seeing the name of a related
1177 # object depends on access right of source document,
1178 # not target, so user may not have access.
1179 ng = dict(obj.pool.get(self._obj).name_get(cr, 1, ids, context=context))
1182 res[r] = (res[r], ng[res[r]])
1183 elif self._type in ('one2many', 'many2many'):
1186 res[r] = [x.id for x in res[r]]
1189 def __init__(self, *arg, **args):
1191 self._relations = []
1192 super(related, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1193 if self.store is True:
1194 # TODO: improve here to change self.store = {...} according to related objects
1197 def _field_get2(self, cr, uid, obj, context=None):
1201 obj_name = obj._name
1202 for i in range(len(self._arg)):
1203 f = obj.pool.get(obj_name).fields_get(cr, uid, [self._arg[i]], context=context)[self._arg[i]]
1209 if f.get('relation',False):
1210 obj_name = f['relation']
1211 result[-1]['relation'] = f['relation']
1212 self._relations = result
1215 class sparse(function):
1217 def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
1219 + For a many2many field, a list of tuples is expected.
1220 Here is the list of tuple that are accepted, with the corresponding semantics ::
1222 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1223 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1224 (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)
1225 (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)
1226 (4, ID) link to existing record with id = ID (adds a relationship)
1227 (5) unlink all (like using (3,ID) for all linked records)
1228 (6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
1231 [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
1233 + For a one2many field, a lits of tuples is expected.
1234 Here is the list of tuple that are accepted, with the corresponding semantics ::
1236 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1237 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1238 (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)
1241 [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
1244 if self._type == 'many2many':
1245 assert value[0][0] == 6, 'Unsupported m2m value for sparse field: %s' % value
1248 elif self._type == 'one2many':
1251 relation_obj = obj.pool.get(self.relation)
1253 assert vals[0] in (0,1,2), 'Unsupported o2m value for sparse field: %s' % vals
1255 read_value.append(relation_obj.create(cr, uid, vals[2], context=context))
1257 relation_obj.write(cr, uid, vals[1], vals[2], context=context)
1259 relation_obj.unlink(cr, uid, vals[1], context=context)
1260 read_value.remove(vals[1])
1265 def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
1266 if not type(ids) == list:
1268 records = obj.browse(cr, uid, ids, context=context)
1269 for record in records:
1270 # grab serialized value as object - already deserialized
1271 serialized = getattr(record, self.serialization_field)
1273 # simply delete the key to unset it.
1274 serialized.pop(field_name, None)
1276 serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context)
1277 obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context)
1280 def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None):
1282 records = obj.browse(cr, uid, ids, context=context)
1283 for record in records:
1284 # grab serialized value as object - already deserialized
1285 serialized = getattr(record, self.serialization_field)
1286 results[record.id] = {}
1287 for field_name in field_names:
1288 field_type = obj._columns[field_name]._type
1289 value = serialized.get(field_name, False)
1290 if field_type in ('one2many','many2many'):
1293 # filter out deleted records as superuser
1294 relation_obj = obj.pool.get(obj._columns[field_name].relation)
1295 value = relation_obj.exists(cr, openerp.SUPERUSER_ID, value)
1296 if type(value) in (int,long) and field_type == 'many2one':
1297 relation_obj = obj.pool.get(obj._columns[field_name].relation)
1298 # check for deleted record as superuser
1299 if not relation_obj.exists(cr, openerp.SUPERUSER_ID, [value]):
1301 results[record.id][field_name] = value
1304 def __init__(self, serialization_field, **kwargs):
1305 self.serialization_field = serialization_field
1306 return super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
1310 # ---------------------------------------------------------
1312 # ---------------------------------------------------------
1314 class dummy(function):
1315 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1318 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1321 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1324 def __init__(self, *arg, **args):
1326 self._relations = []
1327 super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=None, **args)
1329 # ---------------------------------------------------------
1331 # ---------------------------------------------------------
1333 class serialized(_column):
1334 """ A field able to store an arbitrary python data structure.
1336 Note: only plain components allowed.
1339 def _symbol_set_struct(val):
1340 return simplejson.dumps(val)
1342 def _symbol_get_struct(self, val):
1343 return simplejson.loads(val or '{}')
1346 _type = 'serialized'
1349 _symbol_f = _symbol_set_struct
1350 _symbol_set = (_symbol_c, _symbol_f)
1351 _symbol_get = _symbol_get_struct
1353 # TODO: review completly this class for speed improvement
1354 class property(function):
1356 def _get_default(self, obj, cr, uid, prop_name, context=None):
1357 return self._get_defaults(obj, cr, uid, [prop_name], context=None)[prop_name]
1359 def _get_defaults(self, obj, cr, uid, prop_names, context=None):
1360 """Get the default values for ``prop_names´´ property fields (result of ir.property.get() function for res_id = False).
1362 :param list of string prop_names: list of name of property fields for those we want the default value
1363 :return: map of property field names to their default value
1366 prop = obj.pool.get('ir.property')
1368 for prop_name in prop_names:
1369 res[prop_name] = prop.get(cr, uid, prop_name, obj._name, context=context)
1372 def _get_by_id(self, obj, cr, uid, prop_name, ids, context=None):
1373 prop = obj.pool.get('ir.property')
1374 vids = [obj._name + ',' + str(oid) for oid in ids]
1376 domain = [('fields_id.model', '=', obj._name), ('fields_id.name', 'in', prop_name)]
1377 #domain = prop._get_domain(cr, uid, prop_name, obj._name, context)
1379 domain = [('res_id', 'in', vids)] + domain
1380 return prop.search(cr, uid, domain, context=context)
1382 # TODO: to rewrite more clean
1383 def _fnct_write(self, obj, cr, uid, id, prop_name, id_val, obj_dest, context=None):
1387 nids = self._get_by_id(obj, cr, uid, [prop_name], [id], context)
1389 cr.execute('DELETE FROM ir_property WHERE id IN %s', (tuple(nids),))
1391 default_val = self._get_default(obj, cr, uid, prop_name, context)
1393 property_create = False
1394 if isinstance(default_val, openerp.osv.orm.browse_record):
1395 if default_val.id != id_val:
1396 property_create = True
1397 elif default_val != id_val:
1398 property_create = True
1401 def_id = self._field_get(cr, uid, obj._name, prop_name)
1402 company = obj.pool.get('res.company')
1403 cid = company._company_default_get(cr, uid, obj._name, def_id,
1405 propdef = obj.pool.get('ir.model.fields').browse(cr, uid, def_id,
1407 prop = obj.pool.get('ir.property')
1408 return prop.create(cr, uid, {
1409 'name': propdef.name,
1411 'res_id': obj._name+','+str(id),
1413 'fields_id': def_id,
1418 def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
1419 prop = obj.pool.get('ir.property')
1420 # get the default values (for res_id = False) for the property fields
1421 default_val = self._get_defaults(obj, cr, uid, prop_names, context)
1423 # build the dictionary that will be returned
1426 res[id] = default_val.copy()
1428 for prop_name in prop_names:
1429 property_field = obj._all_columns.get(prop_name).column
1430 property_destination_obj = property_field._obj if property_field._type == 'many2one' else False
1431 # If the property field is a m2o field, we will append the id of the value to name_get_ids
1432 # in order to make a name_get in batch for all the ids needed.
1435 # get the result of ir.property.get() for this res_id and save it in res if it's existing
1436 obj_reference = obj._name + ',' + str(id)
1437 value = prop.get(cr, uid, prop_name, obj._name, res_id=obj_reference, context=context)
1439 res[id][prop_name] = value
1440 # Check existence as root (as seeing the name of a related
1441 # object depends on access right of source document,
1442 # not target, so user may not have access) in order to avoid
1443 # pointing on an unexisting record.
1444 if property_destination_obj:
1445 if res[id][prop_name] and obj.pool.get(property_destination_obj).exists(cr, 1, res[id][prop_name].id):
1446 name_get_ids[id] = res[id][prop_name].id
1448 res[id][prop_name] = False
1449 if property_destination_obj:
1450 # name_get as root (as seeing the name of a related
1451 # object depends on access right of source document,
1452 # not target, so user may not have access.)
1453 name_get_values = dict(obj.pool.get(property_destination_obj).name_get(cr, 1, name_get_ids.values(), context=context))
1454 # the property field is a m2o, we need to return a tuple with (id, name)
1455 for k, v in name_get_ids.iteritems():
1456 if res[k][prop_name]:
1457 res[k][prop_name] = (v , name_get_values.get(v))
1460 def _field_get(self, cr, uid, model_name, prop):
1461 if not self.field_id.get(cr.dbname):
1462 cr.execute('SELECT id \
1463 FROM ir_model_fields \
1464 WHERE name=%s AND model=%s', (prop, model_name))
1466 self.field_id[cr.dbname] = res and res[0]
1467 return self.field_id[cr.dbname]
1469 def __init__(self, obj_prop, **args):
1470 # TODO remove obj_prop parameter (use many2one type)
1472 function.__init__(self, self._fnct_read, False, self._fnct_write,
1473 obj_prop, multi='properties', **args)
1479 def field_to_dict(model, cr, user, field, context=None):
1480 """ Return a dictionary representation of a field.
1482 The string, help, and selection attributes (if any) are untranslated. This
1483 representation is the one returned by fields_get() (fields_get() will do
1488 res = {'type': field._type}
1489 # This additional attributes for M2M and function field is added
1490 # because we need to display tooltip with this additional information
1491 # when client is started in debug mode.
1492 if isinstance(field, function):
1493 res['function'] = field._fnct and field._fnct.func_name or False
1494 res['store'] = field.store
1495 if isinstance(field.store, dict):
1496 res['store'] = str(field.store)
1497 res['fnct_search'] = field._fnct_search and field._fnct_search.func_name or False
1498 res['fnct_inv'] = field._fnct_inv and field._fnct_inv.func_name or False
1499 res['fnct_inv_arg'] = field._fnct_inv_arg or False
1500 res['func_obj'] = field._obj or False
1501 if isinstance(field, many2many):
1502 (table, col1, col2) = field._sql_names(model)
1503 res['related_columns'] = [col1, col2]
1504 res['third_table'] = table
1505 for arg in ('string', 'readonly', 'states', 'size', 'required', 'group_operator',
1506 'change_default', 'translate', 'help', 'select', 'selectable', 'groups'):
1507 if getattr(field, arg):
1508 res[arg] = getattr(field, arg)
1509 for arg in ('digits', 'invisible', 'filters'):
1510 if getattr(field, arg, None):
1511 res[arg] = getattr(field, arg)
1514 res['string'] = field.string
1516 res['help'] = field.help
1518 if hasattr(field, 'selection'):
1519 if isinstance(field.selection, (tuple, list)):
1520 res['selection'] = field.selection
1522 # call the 'dynamic selection' function
1523 res['selection'] = field.selection(model, cr, user, context)
1524 if res['type'] in ('one2many', 'many2many', 'many2one'):
1525 res['relation'] = field._obj
1526 res['domain'] = field._domain
1527 res['context'] = field._context
1529 if isinstance(field, one2many):
1530 res['relation_field'] = field._fields_id
1535 class column_info(object):
1536 """Struct containing details about an osv column, either one local to
1537 its model, or one inherited via _inherits.
1539 :attr name: name of the column
1540 :attr column: column instance, subclass of osv.fields._column
1541 :attr parent_model: if the column is inherited, name of the model
1542 that contains it, None for local columns.
1543 :attr parent_column: the name of the column containing the m2o
1544 relationship to the parent model that contains
1545 this column, None for local columns.
1546 :attr original_parent: if the column is inherited, name of the original
1547 parent model that contains it i.e in case of multilevel
1548 inheritence, None for local columns.
1550 def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
1552 self.column = column
1553 self.parent_model = parent_model
1554 self.parent_column = parent_column
1555 self.original_parent = original_parent
1557 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: