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)
113 setattr(self, a, args[a])
118 def set(self, cr, obj, id, name, value, user=None, context=None):
119 cr.execute('update '+obj._table+' set '+name+'='+self._symbol_set[0]+' where id=%s', (self._symbol_set[1](value), id))
121 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
122 raise Exception(_('undefined get method !'))
124 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
125 ids = obj.search(cr, uid, args+self._domain+[(name, 'ilike', value)], offset, limit, context=context)
126 res = obj.read(cr, uid, ids, [name], context=context)
127 return [x[name] for x in res]
130 # ---------------------------------------------------------
132 # ---------------------------------------------------------
133 class boolean(_column):
136 _symbol_f = lambda x: x and 'True' or 'False'
137 _symbol_set = (_symbol_c, _symbol_f)
139 def __init__(self, string='unknown', required=False, **args):
140 super(boolean, self).__init__(string=string, required=required, **args)
143 "required=True is deprecated: making a boolean field"
144 " `required` has no effect, as NULL values are "
145 "automatically turned into False.")
147 class integer(_column):
150 _symbol_f = lambda x: int(x or 0)
151 _symbol_set = (_symbol_c, _symbol_f)
152 _symbol_get = lambda self,x: x or 0
154 def __init__(self, string='unknown', required=False, **args):
155 super(integer, self).__init__(string=string, required=required, **args)
158 "required=True is deprecated: making an integer field"
159 " `required` has no effect, as NULL values are "
160 "automatically turned into 0.")
162 class reference(_column):
164 _classic_read = False # post-process to handle missing target
166 def __init__(self, string, selection, size, **args):
167 _column.__init__(self, string=string, size=size, selection=selection, **args)
169 def get(self, cr, obj, ids, name, uid=None, context=None, values=None):
171 # copy initial values fetched previously.
173 result[value['id']] = value[name]
175 model, res_id = value[name].split(',')
176 if not obj.pool.get(model).exists(cr, uid, [int(res_id)], context=context):
177 result[value['id']] = False
183 def __init__(self, string, size, **args):
184 _column.__init__(self, string=string, size=size, **args)
185 self._symbol_set = (self._symbol_c, self._symbol_set_char)
187 # takes a string (encoded in utf8) and returns a string (encoded in utf8)
188 def _symbol_set_char(self, symb):
190 # * we need to remove the "symb==False" from the next line BUT
191 # for now too many things rely on this broken behavior
192 # * the symb==None test should be common to all data types
193 if symb == None or symb == False:
196 # we need to convert the string to a unicode object to be able
197 # to evaluate its length (and possibly truncate it) reliably
198 u_symb = tools.ustr(symb)
200 return u_symb[:self.size].encode('utf8')
208 class float(_column):
211 _symbol_f = lambda x: __builtin__.float(x or 0.0)
212 _symbol_set = (_symbol_c, _symbol_f)
213 _symbol_get = lambda self,x: x or 0.0
215 def __init__(self, string='unknown', digits=None, digits_compute=None, required=False, **args):
216 _column.__init__(self, string=string, required=required, **args)
218 # synopsis: digits_compute(cr) -> (precision, scale)
219 self.digits_compute = digits_compute
222 "required=True is deprecated: making a float field"
223 " `required` has no effect, as NULL values are "
224 "automatically turned into 0.0.")
226 def digits_change(self, cr):
227 if self.digits_compute:
228 self.digits = self.digits_compute(cr)
230 precision, scale = self.digits
231 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
232 precision_digits=scale),
233 precision_digits=scale))
240 """ Returns the current date in a format fit for being a
241 default value to a ``date`` field.
243 This method should be provided as is to the _defaults dict, it
244 should not be called.
246 return DT.date.today().strftime(
247 tools.DEFAULT_SERVER_DATE_FORMAT)
250 def context_today(model, cr, uid, context=None, timestamp=None):
251 """Returns the current date as seen in the client's timezone
252 in a format fit for date fields.
253 This method may be passed as value to initialize _defaults.
255 :param Model model: model (osv) for which the date value is being
256 computed - technical field, currently ignored,
257 automatically passed when used in _defaults.
258 :param datetime timestamp: optional datetime value to use instead of
259 the current date and time (must be a
260 datetime, regular dates can't be converted
262 :param dict context: the 'tz' key in the context should give the
263 name of the User/Client timezone (otherwise
267 today = timestamp or DT.datetime.now()
269 if context and context.get('tz'):
271 utc = pytz.timezone('UTC')
272 context_tz = pytz.timezone(context['tz'])
273 utc_today = utc.localize(today, is_dst=False) # UTC = no DST
274 context_today = utc_today.astimezone(context_tz)
276 _logger.debug("failed to compute context/client-specific today date, "
277 "using the UTC value for `today`",
279 return (context_today or today).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
281 class datetime(_column):
285 """ Returns the current datetime in a format fit for being a
286 default value to a ``datetime`` field.
288 This method should be provided as is to the _defaults dict, it
289 should not be called.
291 return DT.datetime.now().strftime(
292 tools.DEFAULT_SERVER_DATETIME_FORMAT)
295 def context_timestamp(cr, uid, timestamp, context=None):
296 """Returns the given timestamp converted to the client's timezone.
297 This method is *not* meant for use as a _defaults initializer,
298 because datetime fields are automatically converted upon
299 display on client side. For _defaults you :meth:`fields.datetime.now`
300 should be used instead.
302 :param datetime timestamp: naive datetime value (expressed in UTC)
303 to be converted to the client timezone
304 :param dict context: the 'tz' key in the context should give the
305 name of the User/Client timezone (otherwise
308 :return: timestamp converted to timezone-aware datetime in context
311 assert isinstance(timestamp, DT.datetime), 'Datetime instance expected'
312 if context and context.get('tz'):
314 utc = pytz.timezone('UTC')
315 context_tz = pytz.timezone(context['tz'])
316 utc_timestamp = utc.localize(timestamp, is_dst=False) # UTC = no DST
317 return utc_timestamp.astimezone(context_tz)
319 _logger.debug("failed to compute context/client-specific timestamp, "
320 "using the UTC value",
324 class binary(_column):
328 # Binary values may be byte strings (python 2.6 byte array), but
329 # the legacy OpenERP convention is to transfer and store binaries
330 # as base64-encoded strings. The base64 string may be provided as a
331 # unicode in some circumstances, hence the str() cast in symbol_f.
332 # This str coercion will only work for pure ASCII unicode strings,
333 # on purpose - non base64 data must be passed as a 8bit byte strings.
334 _symbol_f = lambda symb: symb and Binary(str(symb)) or None
336 _symbol_set = (_symbol_c, _symbol_f)
337 _symbol_get = lambda self, x: x and str(x)
339 _classic_read = False
342 def __init__(self, string='unknown', filters=None, **args):
343 _column.__init__(self, string=string, **args)
344 self.filters = filters
346 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
359 # If client is requesting only the size of the field, we return it instead
360 # of the content. Presumably a separate request will be done to read the actual
361 # content if it's needed at some point.
362 # TODO: after 6.0 we should consider returning a dict with size and content instead of
363 # having an implicit convention for the value
364 if val and context.get('bin_size_%s' % name, context.get('bin_size')):
365 res[i] = tools.human_size(long(val))
370 class selection(_column):
373 def __init__(self, selection, string='unknown', **args):
374 _column.__init__(self, string=string, **args)
375 self.selection = selection
377 # ---------------------------------------------------------
379 # ---------------------------------------------------------
382 # Values: (0, 0, { fields }) create
383 # (1, ID, { fields }) update
384 # (2, ID) remove (delete)
385 # (3, ID) unlink one (target id or target of relation)
387 # (5) unlink all (only valid for one2many)
390 class many2one(_column):
391 _classic_read = False
392 _classic_write = True
395 _symbol_f = lambda x: x or None
396 _symbol_set = (_symbol_c, _symbol_f)
398 def __init__(self, obj, string='unknown', **args):
399 _column.__init__(self, string=string, **args)
402 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
410 res[r['id']] = r[name]
412 res.setdefault(id, '')
413 obj = obj.pool.get(self._obj)
415 # build a dictionary of the form {'id_of_distant_resource': name_of_distant_resource}
416 # we use uid=1 because the visibility of a many2one field value (just id and name)
417 # must be the access right of the parent form and not the linked object itself.
418 records = dict(obj.name_get(cr, 1,
419 list(set([x for x in res.values() if isinstance(x, (int,long))])),
422 if res[id] in records:
423 res[id] = (res[id], records[res[id]])
428 def set(self, cr, obj_src, id, field, values, user=None, context=None):
431 obj = obj_src.pool.get(self._obj)
432 self._table = obj_src.pool.get(self._obj)._table
433 if type(values) == type([]):
436 id_new = obj.create(cr, act[2])
437 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (id_new, id))
439 obj.write(cr, [act[1]], act[2], context=context)
441 cr.execute('delete from '+self._table+' where id=%s', (act[1],))
442 elif act[0] == 3 or act[0] == 5:
443 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
445 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (act[1], id))
448 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (values, id))
450 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
452 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
453 return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context)
456 class one2many(_column):
457 _classic_read = False
458 _classic_write = False
462 def __init__(self, obj, fields_id, string='unknown', limit=None, **args):
463 _column.__init__(self, string=string, **args)
465 self._fields_id = fields_id
467 #one2many can't be used as condition for defaults
468 assert(self.change_default != True)
470 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
474 context = context.copy()
475 context.update(self._context)
483 ids2 = obj.pool.get(self._obj).search(cr, user, self._domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
484 for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
485 if r[self._fields_id] in res:
486 res[r[self._fields_id]].append(r['id'])
489 def set(self, cr, obj, id, field, values, user=None, context=None):
494 context = context.copy()
495 context.update(self._context)
496 context['no_store_function'] = True
499 _table = obj.pool.get(self._obj)._table
500 obj = obj.pool.get(self._obj)
503 act[2][self._fields_id] = id
504 id_new = obj.create(cr, user, act[2], context=context)
505 result += obj._store_get_values(cr, user, [id_new], act[2].keys(), context)
507 obj.write(cr, user, [act[1]], act[2], context=context)
509 obj.unlink(cr, user, [act[1]], context=context)
511 reverse_rel = obj._all_columns.get(self._fields_id)
512 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
513 # if the model has on delete cascade, just delete the row
514 if reverse_rel.column.ondelete == "cascade":
515 obj.unlink(cr, user, [act[1]], context=context)
517 cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],))
519 # Must use write() to recompute parent_store structure if needed
520 obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
522 reverse_rel = obj._all_columns.get(self._fields_id)
523 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
524 # if the o2m has a static domain we must respect it when unlinking
525 extra_domain = self._domain if isinstance(getattr(self, '_domain', None), list) else []
526 ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
527 # If the model has cascade deletion, we delete the rows because it is the intended behavior,
528 # otherwise we only nullify the reverse foreign key column.
529 if reverse_rel.column.ondelete == "cascade":
530 obj.unlink(cr, user, ids_to_unlink, context=context)
532 obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context)
534 # Must use write() to recompute parent_store structure if needed
535 obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
537 cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
538 ids3 = map(lambda x:x[0], cr.fetchall())
539 obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
542 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
543 return obj.pool.get(self._obj).name_search(cr, uid, value, self._domain, operator, context=context,limit=limit)
547 # Values: (0, 0, { fields }) create
548 # (1, ID, { fields }) update (write fields to ID)
549 # (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete)
550 # (3, ID) unlink (delete the relationship between the two objects but does not delete ID)
551 # (4, ID) link (add a relationship)
553 # (6, ?, ids) set a list of links
555 class many2many(_column):
556 """Encapsulates the logic of a many-to-many bidirectional relationship, handling the
557 low-level details of the intermediary relationship table transparently.
558 A many-to-many relationship is always symmetrical, and can be declared and accessed
559 from either endpoint model.
560 If ``rel`` (relationship table name), ``id1`` (source foreign key column name)
561 or id2 (destination foreign key column name) are not specified, the system will
562 provide default values. This will by default only allow one single symmetrical
563 many-to-many relationship between the source and destination model.
564 For multiple many-to-many relationship between the same models and for
565 relationships where source and destination models are the same, ``rel``, ``id1``
566 and ``id2`` should be specified explicitly.
568 :param str obj: destination model
569 :param str rel: optional name of the intermediary relationship table. If not specified,
570 a canonical name will be derived based on the alphabetically-ordered
571 model names of the source and destination (in the form: ``amodel_bmodel_rel``).
572 Automatic naming is not possible when the source and destination are
573 the same, for obvious ambiguity reasons.
574 :param str id1: optional name for the column holding the foreign key to the current
575 model in the relationship table. If not specified, a canonical name
576 will be derived based on the model name (in the form: `src_model_id`).
577 :param str id2: optional name for the column holding the foreign key to the destination
578 model in the relationship table. If not specified, a canonical name
579 will be derived based on the model name (in the form: `dest_model_id`)
580 :param str string: field label
582 _classic_read = False
583 _classic_write = False
587 def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
590 _column.__init__(self, string=string, **args)
592 if rel and '.' in rel:
593 raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\
594 'You used %s, which is not a valid SQL table name.')% (string,rel))
600 def _sql_names(self, source_model):
601 """Return the SQL names defining the structure of the m2m relationship table
603 :return: (m2m_table, local_col, dest_col) where m2m_table is the table name,
604 local_col is the name of the column holding the current model's FK, and
605 dest_col is the name of the column holding the destination model's FK, and
607 tbl, col1, col2 = self._rel, self._id1, self._id2
608 if not all((tbl, col1, col2)):
609 # the default table name is based on the stable alphabetical order of tables
610 dest_model = source_model.pool.get(self._obj)
611 tables = tuple(sorted([source_model._table, dest_model._table]))
613 assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\
614 'is not possible when source and destination models are '\
616 tbl = '%s_%s_rel' % tables
618 col1 = '%s_id' % source_model._table
620 col2 = '%s_id' % dest_model._table
621 return (tbl, col1, col2)
623 def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
635 "Specifying offset at a many2many.get() is deprecated and may"
636 " produce unpredictable results.")
637 obj = model.pool.get(self._obj)
638 rel, id1, id2 = self._sql_names(model)
640 # static domains are lists, and are evaluated both here and on client-side, while string
641 # domains supposed by dynamic and evaluated on client-side only (thus ignored here)
642 # FIXME: make this distinction explicit in API!
643 domain = isinstance(self._domain, list) and self._domain or []
645 wquery = obj._where_calc(cr, user, domain, context=context)
646 obj._apply_ir_rules(cr, user, wquery, 'read', context=context)
647 from_c, where_c, where_params = wquery.get_sql()
649 where_c = ' AND ' + where_c
651 if offset or self._limit:
652 order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0])
657 if self._limit is not None:
658 limit_str = ' LIMIT %d' % self._limit
660 query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
661 FROM %(rel)s, %(from_c)s \
662 WHERE %(rel)s.%(id1)s IN %%s \
663 AND %(rel)s.%(id2)s = %(tbl)s.id \
675 'order_by': order_by,
678 cr.execute(query, [tuple(ids),] + where_params)
679 for r in cr.fetchall():
680 res[r[1]].append(r[0])
683 def set(self, cr, model, id, name, values, user=None, context=None):
688 rel, id1, id2 = self._sql_names(model)
689 obj = model.pool.get(self._obj)
691 if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
694 idnew = obj.create(cr, user, act[2], context=context)
695 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew))
697 obj.write(cr, user, [act[1]], act[2], context=context)
699 obj.unlink(cr, user, [act[1]], context=context)
701 cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1]))
703 # following queries are in the same transaction - so should be relatively safe
704 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1]))
705 if not cr.fetchone():
706 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1]))
708 cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,))
711 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
713 d1 = ' and ' + ' and '.join(d1)
716 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)
718 for act_nbr in act[2]:
719 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr))
722 # TODO: use a name_search
724 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
725 return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
728 def get_nice_size(value):
730 if isinstance(value, (int,long)):
732 elif value: # this is supposed to be a string
734 return tools.human_size(size)
736 # See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
737 # and http://bugs.python.org/issue10066
738 invalid_xml_low_bytes = re.compile(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
740 def sanitize_binary_value(value):
741 # binary fields should be 7-bit ASCII base64-encoded data,
742 # but we do additional sanity checks to make sure the values
743 # are not something else that won't pass via XML-RPC
744 if isinstance(value, (xmlrpclib.Binary, tuple, list, dict)):
745 # these builtin types are meant to pass untouched
748 # Handle invalid bytes values that will cause problems
749 # for XML-RPC. See for more info:
750 # - http://bugs.python.org/issue10066
751 # - http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
753 # Coercing to unicode would normally allow it to properly pass via
754 # XML-RPC, transparently encoded as UTF-8 by xmlrpclib.
755 # (this works for _any_ byte values, thanks to the fallback
756 # to latin-1 passthrough encoding when decoding to unicode)
757 value = tools.ustr(value)
759 # Due to Python bug #10066 this could still yield invalid XML
760 # bytes, specifically in the low byte range, that will crash
761 # the decoding side: [\x00-\x08\x0b-\x0c\x0e-\x1f]
762 # So check for low bytes values, and if any, perform
763 # base64 encoding - not very smart or useful, but this is
764 # our last resort to avoid crashing the request.
765 if invalid_xml_low_bytes.search(value):
766 # b64-encode after restoring the pure bytes with latin-1
767 # passthrough encoding
768 value = base64.b64encode(value.encode('latin-1'))
773 # ---------------------------------------------------------
775 # ---------------------------------------------------------
776 class function(_column):
778 A field whose value is computed by a function (rather
779 than being read from the database).
781 :param fnct: the callable that will compute the field value.
782 :param arg: arbitrary value to be passed to ``fnct`` when computing the value.
783 :param fnct_inv: the callable that will allow writing values in that field
784 (if not provided, the field is read-only).
785 :param fnct_inv_arg: arbitrary value to be passed to ``fnct_inv`` when
787 :param str type: type of the field simulated by the function field
788 :param fnct_search: the callable that allows searching on the field
789 (if not provided, search will not return any result).
790 :param store: store computed value in database
791 (see :ref:`The *store* parameter <field-function-store>`).
792 :type store: True or dict specifying triggers for field computation
793 :param multi: name of batch for batch computation of function fields.
794 All fields with the same batch name will be computed by
795 a single function call. This changes the signature of the
798 .. _field-function-fnct: The ``fnct`` parameter
800 .. rubric:: The ``fnct`` parameter
802 The callable implementing the function field must have the following signature:
804 .. function:: fnct(model, cr, uid, ids, field_name(s), arg, context)
806 Implements the function field.
808 :param orm model: model to which the field belongs (should be ``self`` for
810 :param field_name(s): name of the field to compute, or if ``multi`` is provided,
811 list of field names to compute.
812 :type field_name(s): str | [str]
813 :param arg: arbitrary value passed when declaring the function field
815 :return: mapping of ``ids`` to computed values, or if multi is provided,
816 to a map of field_names to computed values
818 The values in the returned dictionary must be of the type specified by the type
819 argument in the field declaration.
821 Here is an example with a simple function ``char`` function field::
824 def compute(self, cr, uid, ids, field_name, arg, context):
828 _columns['my_char'] = fields.function(compute, type='char', size=50)
830 # when called with ``ids=[1,2,3]``, ``compute`` could return:
834 3: False # null values should be returned explicitly too
837 If ``multi`` is set, then ``field_name`` is replaced by ``field_names``: a list
838 of the field names that should be computed. Each value in the returned
839 dictionary must then be a dictionary mapping field names to values.
841 Here is an example where two function fields (``name`` and ``age``)
842 are both computed by a single function field::
845 def compute(self, cr, uid, ids, field_names, arg, context):
849 _columns['name'] = fields.function(compute_person_data, type='char',\
850 size=50, multi='person_data')
851 _columns[''age'] = fields.function(compute_person_data, type='integer',\
854 # when called with ``ids=[1,2,3]``, ``compute_person_data`` could return:
856 1: {'name': 'Bob', 'age': 23},
857 2: {'name': 'Sally', 'age': 19},
858 3: {'name': 'unknown', 'age': False}
861 .. _field-function-fnct-inv:
863 .. rubric:: The ``fnct_inv`` parameter
865 This callable implements the write operation for the function field
866 and must have the following signature:
868 .. function:: fnct_inv(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context)
870 Callable that implements the ``write`` operation for the function field.
872 :param orm model: model to which the field belongs (should be ``self`` for
874 :param int id: the identifier of the object to write on
875 :param str field_name: name of the field to set
876 :param fnct_inv_arg: arbitrary value passed when declaring the function field
879 When writing values for a function field, the ``multi`` parameter is ignored.
881 .. _field-function-fnct-search:
883 .. rubric:: The ``fnct_search`` parameter
885 This callable implements the search operation for the function field
886 and must have the following signature:
888 .. function:: fnct_search(model, cr, uid, model_again, field_name, criterion, context)
890 Callable that implements the ``search`` operation for the function field by expanding
891 a search criterion based on the function field into a new domain based only on
892 columns that are stored in the database.
894 :param orm model: model to which the field belongs (should be ``self`` for
896 :param orm model_again: same value as ``model`` (seriously! this is for backwards
898 :param str field_name: name of the field to search on
899 :param list criterion: domain component specifying the search criterion on the field.
901 :return: domain to use instead of ``criterion`` when performing the search.
902 This new domain must be based only on columns stored in the database, as it
903 will be used directly without any translation.
905 The returned value must be a domain, that is, a list of the form [(field_name, operator, operand)].
906 The most generic way to implement ``fnct_search`` is to directly search for the records that
907 match the given ``criterion``, and return their ``ids`` wrapped in a domain, such as
908 ``[('id','in',[1,3,5])]``.
910 .. _field-function-store:
912 .. rubric:: The ``store`` parameter
914 The ``store`` parameter allows caching the result of the field computation in the
915 database, and defining the triggers that will invalidate that cache and force a
916 recomputation of the function field.
917 When not provided, the field is computed every time its value is read.
918 The value of ``store`` may be either ``True`` (to recompute the field value whenever
919 any field in the same record is modified), or a dictionary specifying a more
920 flexible set of recomputation triggers.
922 A trigger specification is a dictionary that maps the names of the models that
923 will trigger the computation, to a tuple describing the trigger rule, in the
927 'trigger_model': (mapping_function,
928 ['trigger_field1', 'trigger_field2'],
932 A trigger rule is defined by a 3-item tuple where:
934 * The ``mapping_function`` is defined as follows:
936 .. function:: mapping_function(trigger_model, cr, uid, trigger_ids, context)
938 Callable that maps record ids of a trigger model to ids of the
939 corresponding records in the source model (whose field values
940 need to be recomputed).
942 :param orm model: trigger_model
943 :param list trigger_ids: ids of the records of trigger_model that were
946 :return: list of ids of the source model whose function field values
947 need to be recomputed
949 * The second item is a list of the fields who should act as triggers for
950 the computation. If an empty list is given, all fields will act as triggers.
951 * The last item is the priority, used to order the triggers when processing them
952 after any write operation on a model that has function field triggers. The
953 default priority is 10.
955 In fact, setting store = True is the same as using the following trigger dict::
958 'model_itself': (lambda self, cr, uid, ids, context: ids,
964 _classic_read = False
965 _classic_write = False
971 # multi: compute several fields in one call
973 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):
974 _column.__init__(self, **args)
977 self._fnct_inv = fnct_inv
980 if 'relation' in args:
981 self._obj = args['relation']
983 self.digits = args.get('digits', (16,2))
984 self.digits_compute = args.get('digits_compute', None)
986 self._fnct_inv_arg = fnct_inv_arg
990 self._fnct_search = fnct_search
993 if not fnct_search and not store:
994 self.selectable = False
997 if self._type != 'many2one':
998 # m2o fields need to return tuples with name_get, not just foreign keys
999 self._classic_read = True
1000 self._classic_write = True
1002 self._symbol_get=lambda x:x and str(x)
1005 self._symbol_c = float._symbol_c
1006 self._symbol_f = float._symbol_f
1007 self._symbol_set = float._symbol_set
1009 if type == 'boolean':
1010 self._symbol_c = boolean._symbol_c
1011 self._symbol_f = boolean._symbol_f
1012 self._symbol_set = boolean._symbol_set
1014 if type == 'integer':
1015 self._symbol_c = integer._symbol_c
1016 self._symbol_f = integer._symbol_f
1017 self._symbol_set = integer._symbol_set
1019 def digits_change(self, cr):
1020 if self._type == 'float':
1021 if self.digits_compute:
1022 self.digits = self.digits_compute(cr)
1024 precision, scale = self.digits
1025 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
1026 precision_digits=scale),
1027 precision_digits=scale))
1029 def search(self, cr, uid, obj, name, args, context=None):
1030 if not self._fnct_search:
1031 #CHECKME: should raise an exception
1033 return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
1035 def postprocess(self, cr, uid, obj, field, value=None, context=None):
1039 field_type = obj._columns[field]._type
1040 if field_type == "many2one":
1041 # make the result a tuple if it is not already one
1042 if isinstance(value, (int,long)) and hasattr(obj._columns[field], 'relation'):
1043 obj_model = obj.pool.get(obj._columns[field].relation)
1044 dict_names = dict(obj_model.name_get(cr, uid, [value], context))
1045 result = (value, dict_names[value])
1047 if field_type == 'binary':
1048 if context.get('bin_size'):
1049 # client requests only the size of binary fields
1050 result = get_nice_size(value)
1051 elif not context.get('bin_raw'):
1052 result = sanitize_binary_value(value)
1054 if field_type == "integer" and value > xmlrpclib.MAXINT:
1055 # integer/long values greater than 2^31-1 are not supported
1056 # in pure XMLRPC, so we have to pass them as floats :-(
1057 # This is not needed for stored fields and non-functional integer
1058 # fields, as their values are constrained by the database backend
1059 # to the same 32bits signed int limit.
1060 result = float(value)
1063 def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
1064 result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
1066 if self._multi and id in result:
1067 for field, value in result[id].iteritems():
1069 result[id][field] = self.postprocess(cr, uid, obj, field, value, context)
1070 elif result.get(id):
1071 result[id] = self.postprocess(cr, uid, obj, name, result[id], context)
1074 def set(self, cr, obj, id, name, value, user=None, context=None):
1078 self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
1080 # ---------------------------------------------------------
1082 # ---------------------------------------------------------
1084 class related(function):
1085 """Field that points to some data inside another field of the current record.
1090 'foo_id': fields.many2one('my.foo', 'Foo'),
1091 'bar': fields.related('foo_id', 'frol', type='char', string='Frol of Foo'),
1095 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1096 self._field_get2(cr, uid, obj, context)
1097 i = len(self._arg)-1
1100 if type(sarg) in [type([]), type( (1,) )]:
1101 where = [(self._arg[i], 'in', sarg)]
1103 where = [(self._arg[i], '=', sarg)]
1105 where = map(lambda x: (self._arg[i],x[1], x[2]), domain)
1107 sarg = obj.pool.get(self._relations[i]['object']).search(cr, uid, where, context=context)
1109 return [(self._arg[0], 'in', sarg)]
1111 def _fnct_write(self,obj,cr, uid, ids, field_name, values, args, context=None):
1112 self._field_get2(cr, uid, obj, context=context)
1113 if type(ids) != type([]):
1115 objlst = obj.browse(cr, uid, ids)
1119 for i in range(len(self.arg)):
1120 if not t_data: break
1121 field_detail = self._relations[i]
1122 if not t_data[self.arg[i]]:
1123 if self._type not in ('one2many', 'many2many'):
1126 elif field_detail['type'] in ('one2many', 'many2many'):
1127 if self._type != "many2one":
1129 t_data = t_data[self.arg[i]][0]
1134 t_data = t_data[self.arg[i]]
1136 model = obj.pool.get(self._relations[-1]['object'])
1137 model.write(cr, uid, [t_id], {args[-1]: values}, context=context)
1139 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1140 self._field_get2(cr, uid, obj, context)
1141 if not ids: return {}
1142 relation = obj._name
1143 if self._type in ('one2many', 'many2many'):
1144 res = dict([(i, []) for i in ids])
1146 res = {}.fromkeys(ids, False)
1148 objlst = obj.browse(cr, 1, ids, context=context)
1153 relation = obj._name
1154 for i in range(len(self.arg)):
1155 field_detail = self._relations[i]
1156 relation = field_detail['object']
1158 if not t_data[self.arg[i]]:
1164 if field_detail['type'] in ('one2many', 'many2many') and i != len(self.arg) - 1:
1165 t_data = t_data[self.arg[i]][0]
1167 t_data = t_data[self.arg[i]]
1168 if type(t_data) == type(objlst[0]):
1169 res[data.id] = t_data.id
1171 res[data.id] = t_data
1172 if self._type=='many2one':
1173 ids = filter(None, res.values())
1175 # name_get as root, as seeing the name of a related
1176 # object depends on access right of source document,
1177 # not target, so user may not have access.
1178 ng = dict(obj.pool.get(self._obj).name_get(cr, 1, ids, context=context))
1181 res[r] = (res[r], ng[res[r]])
1182 elif self._type in ('one2many', 'many2many'):
1185 res[r] = [x.id for x in res[r]]
1188 def __init__(self, *arg, **args):
1190 self._relations = []
1191 super(related, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1192 if self.store is True:
1193 # TODO: improve here to change self.store = {...} according to related objects
1196 def _field_get2(self, cr, uid, obj, context=None):
1200 obj_name = obj._name
1201 for i in range(len(self._arg)):
1202 f = obj.pool.get(obj_name).fields_get(cr, uid, [self._arg[i]], context=context)[self._arg[i]]
1208 if f.get('relation',False):
1209 obj_name = f['relation']
1210 result[-1]['relation'] = f['relation']
1211 self._relations = result
1214 class sparse(function):
1216 def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
1218 + For a many2many field, a list of tuples is expected.
1219 Here is the list of tuple that are accepted, with the corresponding semantics ::
1221 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1222 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1223 (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)
1224 (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)
1225 (4, ID) link to existing record with id = ID (adds a relationship)
1226 (5) unlink all (like using (3,ID) for all linked records)
1227 (6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
1230 [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
1232 + For a one2many field, a lits of tuples is expected.
1233 Here is the list of tuple that are accepted, with the corresponding semantics ::
1235 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
1236 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
1237 (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)
1240 [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
1243 if self._type == 'many2many':
1244 assert value[0][0] == 6, 'Unsupported m2m value for sparse field: %s' % value
1247 elif self._type == 'one2many':
1250 relation_obj = obj.pool.get(self.relation)
1252 assert vals[0] in (0,1,2), 'Unsupported o2m value for sparse field: %s' % vals
1254 read_value.append(relation_obj.create(cr, uid, vals[2], context=context))
1256 relation_obj.write(cr, uid, vals[1], vals[2], context=context)
1258 relation_obj.unlink(cr, uid, vals[1], context=context)
1259 read_value.remove(vals[1])
1264 def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
1265 if not type(ids) == list:
1267 records = obj.browse(cr, uid, ids, context=context)
1268 for record in records:
1269 # grab serialized value as object - already deserialized
1270 serialized = getattr(record, self.serialization_field)
1272 # simply delete the key to unset it.
1273 serialized.pop(field_name, None)
1275 serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context)
1276 obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context)
1279 def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None):
1281 records = obj.browse(cr, uid, ids, context=context)
1282 for record in records:
1283 # grab serialized value as object - already deserialized
1284 serialized = getattr(record, self.serialization_field)
1285 results[record.id] = {}
1286 for field_name in field_names:
1287 field_type = obj._columns[field_name]._type
1288 value = serialized.get(field_name, False)
1289 if field_type in ('one2many','many2many'):
1292 # filter out deleted records as superuser
1293 relation_obj = obj.pool.get(obj._columns[field_name].relation)
1294 value = relation_obj.exists(cr, openerp.SUPERUSER_ID, value)
1295 if type(value) in (int,long) and field_type == 'many2one':
1296 relation_obj = obj.pool.get(obj._columns[field_name].relation)
1297 # check for deleted record as superuser
1298 if not relation_obj.exists(cr, openerp.SUPERUSER_ID, [value]):
1300 results[record.id][field_name] = value
1303 def __init__(self, serialization_field, **kwargs):
1304 self.serialization_field = serialization_field
1305 return super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
1309 # ---------------------------------------------------------
1311 # ---------------------------------------------------------
1313 class dummy(function):
1314 def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1317 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1320 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1323 def __init__(self, *arg, **args):
1325 self._relations = []
1326 super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=None, **args)
1328 # ---------------------------------------------------------
1330 # ---------------------------------------------------------
1332 class serialized(_column):
1333 """ A field able to store an arbitrary python data structure.
1335 Note: only plain components allowed.
1338 def _symbol_set_struct(val):
1339 return simplejson.dumps(val)
1341 def _symbol_get_struct(self, val):
1342 return simplejson.loads(val or '{}')
1345 _type = 'serialized'
1348 _symbol_f = _symbol_set_struct
1349 _symbol_set = (_symbol_c, _symbol_f)
1350 _symbol_get = _symbol_get_struct
1352 # TODO: review completly this class for speed improvement
1353 class property(function):
1355 def _get_default(self, obj, cr, uid, prop_name, context=None):
1356 return self._get_defaults(obj, cr, uid, [prop_name], context=None)[prop_name]
1358 def _get_defaults(self, obj, cr, uid, prop_names, context=None):
1359 """Get the default values for ``prop_names´´ property fields (result of ir.property.get() function for res_id = False).
1361 :param list of string prop_names: list of name of property fields for those we want the default value
1362 :return: map of property field names to their default value
1365 prop = obj.pool.get('ir.property')
1367 for prop_name in prop_names:
1368 res[prop_name] = prop.get(cr, uid, prop_name, obj._name, context=context)
1371 def _get_by_id(self, obj, cr, uid, prop_name, ids, context=None):
1372 prop = obj.pool.get('ir.property')
1373 vids = [obj._name + ',' + str(oid) for oid in ids]
1375 domain = [('fields_id.model', '=', obj._name), ('fields_id.name', 'in', prop_name)]
1376 #domain = prop._get_domain(cr, uid, prop_name, obj._name, context)
1378 domain = [('res_id', 'in', vids)] + domain
1379 return prop.search(cr, uid, domain, context=context)
1381 # TODO: to rewrite more clean
1382 def _fnct_write(self, obj, cr, uid, id, prop_name, id_val, obj_dest, context=None):
1386 nids = self._get_by_id(obj, cr, uid, [prop_name], [id], context)
1388 cr.execute('DELETE FROM ir_property WHERE id IN %s', (tuple(nids),))
1390 default_val = self._get_default(obj, cr, uid, prop_name, context)
1392 property_create = False
1393 if isinstance(default_val, openerp.osv.orm.browse_record):
1394 if default_val.id != id_val:
1395 property_create = True
1396 elif default_val != id_val:
1397 property_create = True
1400 def_id = self._field_get(cr, uid, obj._name, prop_name)
1401 company = obj.pool.get('res.company')
1402 cid = company._company_default_get(cr, uid, obj._name, def_id,
1404 propdef = obj.pool.get('ir.model.fields').browse(cr, uid, def_id,
1406 prop = obj.pool.get('ir.property')
1407 return prop.create(cr, uid, {
1408 'name': propdef.name,
1410 'res_id': obj._name+','+str(id),
1412 'fields_id': def_id,
1417 def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
1418 prop = obj.pool.get('ir.property')
1419 # get the default values (for res_id = False) for the property fields
1420 default_val = self._get_defaults(obj, cr, uid, prop_names, context)
1422 # build the dictionary that will be returned
1425 res[id] = default_val.copy()
1427 for prop_name in prop_names:
1428 property_field = obj._all_columns.get(prop_name).column
1429 property_destination_obj = property_field._obj if property_field._type == 'many2one' else False
1430 # If the property field is a m2o field, we will append the id of the value to name_get_ids
1431 # in order to make a name_get in batch for all the ids needed.
1434 # get the result of ir.property.get() for this res_id and save it in res if it's existing
1435 obj_reference = obj._name + ',' + str(id)
1436 value = prop.get(cr, uid, prop_name, obj._name, res_id=obj_reference, context=context)
1438 res[id][prop_name] = value
1439 # Check existence as root (as seeing the name of a related
1440 # object depends on access right of source document,
1441 # not target, so user may not have access) in order to avoid
1442 # pointing on an unexisting record.
1443 if property_destination_obj:
1444 if res[id][prop_name] and obj.pool.get(property_destination_obj).exists(cr, 1, res[id][prop_name].id):
1445 name_get_ids[id] = res[id][prop_name].id
1447 res[id][prop_name] = False
1448 if property_destination_obj:
1449 # name_get as root (as seeing the name of a related
1450 # object depends on access right of source document,
1451 # not target, so user may not have access.)
1452 name_get_values = dict(obj.pool.get(property_destination_obj).name_get(cr, 1, name_get_ids.values(), context=context))
1453 # the property field is a m2o, we need to return a tuple with (id, name)
1454 for k, v in name_get_ids.iteritems():
1455 if res[k][prop_name]:
1456 res[k][prop_name] = (v , name_get_values.get(v))
1459 def _field_get(self, cr, uid, model_name, prop):
1460 if not self.field_id.get(cr.dbname):
1461 cr.execute('SELECT id \
1462 FROM ir_model_fields \
1463 WHERE name=%s AND model=%s', (prop, model_name))
1465 self.field_id[cr.dbname] = res and res[0]
1466 return self.field_id[cr.dbname]
1468 def __init__(self, obj_prop, **args):
1469 # TODO remove obj_prop parameter (use many2one type)
1471 function.__init__(self, self._fnct_read, False, self._fnct_write,
1472 obj_prop, multi='properties', **args)
1478 def field_to_dict(model, cr, user, field, context=None):
1479 """ Return a dictionary representation of a field.
1481 The string, help, and selection attributes (if any) are untranslated. This
1482 representation is the one returned by fields_get() (fields_get() will do
1487 res = {'type': field._type}
1488 # This additional attributes for M2M and function field is added
1489 # because we need to display tooltip with this additional information
1490 # when client is started in debug mode.
1491 if isinstance(field, function):
1492 res['function'] = field._fnct and field._fnct.func_name or False
1493 res['store'] = field.store
1494 if isinstance(field.store, dict):
1495 res['store'] = str(field.store)
1496 res['fnct_search'] = field._fnct_search and field._fnct_search.func_name or False
1497 res['fnct_inv'] = field._fnct_inv and field._fnct_inv.func_name or False
1498 res['fnct_inv_arg'] = field._fnct_inv_arg or False
1499 res['func_obj'] = field._obj or False
1500 if isinstance(field, many2many):
1501 (table, col1, col2) = field._sql_names(model)
1502 res['related_columns'] = [col1, col2]
1503 res['third_table'] = table
1504 for arg in ('string', 'readonly', 'states', 'size', 'required', 'group_operator',
1505 'change_default', 'translate', 'help', 'select', 'selectable'):
1506 if getattr(field, arg):
1507 res[arg] = getattr(field, arg)
1508 for arg in ('digits', 'invisible', 'filters'):
1509 if getattr(field, arg, None):
1510 res[arg] = getattr(field, arg)
1513 res['string'] = field.string
1515 res['help'] = field.help
1517 if hasattr(field, 'selection'):
1518 if isinstance(field.selection, (tuple, list)):
1519 res['selection'] = field.selection
1521 # call the 'dynamic selection' function
1522 res['selection'] = field.selection(model, cr, user, context)
1523 if res['type'] in ('one2many', 'many2many', 'many2one'):
1524 res['relation'] = field._obj
1525 res['domain'] = field._domain
1526 res['context'] = field._context
1528 if isinstance(field, one2many):
1529 res['relation_field'] = field._fields_id
1534 class column_info(object):
1535 """Struct containing details about an osv column, either one local to
1536 its model, or one inherited via _inherits.
1538 :attr name: name of the column
1539 :attr column: column instance, subclass of osv.fields._column
1540 :attr parent_model: if the column is inherited, name of the model
1541 that contains it, None for local columns.
1542 :attr parent_column: the name of the column containing the m2o
1543 relationship to the parent model that contains
1544 this column, None for local columns.
1545 :attr original_parent: if the column is inherited, name of the original
1546 parent model that contains it i.e in case of multilevel
1547 inheritence, None for local columns.
1549 def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
1551 self.column = column
1552 self.parent_model = parent_model
1553 self.parent_column = parent_column
1554 self.original_parent = original_parent
1556 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: