[IMP] models: prefetch fields with groups (those to which user has access)
[odoo/odoo.git] / openerp / osv / fields.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 """ Fields:
23       - simple
24       - relations (one2many, many2one, many2many)
25       - function
26
27     Fields Attributes:
28         * _classic_read: is a classic sql fields
29         * _type   : field type
30         * _auto_join: for one2many and many2one fields, tells whether select
31             queries will join the relational table instead of replacing the
32             field condition by an equivalent-one based on a search.
33         * readonly
34         * required
35         * size
36 """
37
38 import base64
39 import datetime as DT
40 import functools
41 import logging
42 import pytz
43 import re
44 import xmlrpclib
45 from psycopg2 import Binary
46
47 import openerp
48 import openerp.tools as tools
49 from openerp.tools.translate import _
50 from openerp.tools import float_round, float_repr
51 from openerp.tools import html_sanitize
52 import simplejson
53 from openerp import SUPERUSER_ID
54
55 _logger = logging.getLogger(__name__)
56
57 def _symbol_set(symb):
58     if symb is None or symb == False:
59         return None
60     elif isinstance(symb, unicode):
61         return symb.encode('utf-8')
62     return str(symb)
63
64
65 class _column(object):
66     """ Base of all fields, a database column
67
68         An instance of this object is a *description* of a database column. It will
69         not hold any data, but only provide the methods to manipulate data of an
70         ORM record or even prepare/update the database to hold such a field of data.
71     """
72     _classic_read = True
73     _classic_write = True
74     _auto_join = False
75     _prefetch = True
76     _properties = False
77     _type = 'unknown'
78     _obj = None
79     _multi = False
80     _symbol_c = '%s'
81     _symbol_f = _symbol_set
82     _symbol_set = (_symbol_c, _symbol_f)
83     _symbol_get = None
84     _deprecated = False
85     copy = True # whether the field is copied by BaseModel.copy()
86
87     def __init__(self, string='unknown', required=False, readonly=False, domain=None, context=None, states=None, priority=0, change_default=False, size=None, ondelete=None, translate=False, select=False, manual=False, **args):
88         """
89
90         The 'manual' keyword argument specifies if the field is a custom one.
91         It corresponds to the 'state' column in ir_model_fields.
92
93         """
94         if domain is None:
95             domain = []
96         if context is None:
97             context = {}
98         self.states = states or {}
99         self.string = string
100         self.readonly = readonly
101         self.required = required
102         self.size = size
103         self.help = args.get('help', '')
104         self.priority = priority
105         self.change_default = change_default
106         self.ondelete = ondelete.lower() if ondelete else None # defaults to 'set null' in ORM
107         self.translate = translate
108         self._domain = domain
109         self._context = context
110         self.write = False
111         self.read = False
112         self.select = select
113         self.manual = manual
114         self.selectable = True
115         self.group_operator = args.get('group_operator', False)
116         self.groups = False  # CSV list of ext IDs of groups that can access this field
117         self.deprecated = False # Optional deprecation warning
118         for a in args:
119             setattr(self, a, args[a])
120
121         # prefetch only if self._classic_write, not self.groups, and not
122         # self.deprecated
123         if not self._classic_write or self.deprecated:
124             self._prefetch = False
125
126     def to_field(self):
127         """ convert column `self` to a new-style field """
128         from openerp.fields import Field
129         return Field.by_type[self._type](**self.to_field_args())
130
131     def to_field_args(self):
132         """ return a dictionary with all the arguments to pass to the field """
133         items = [
134             ('_origin', self),                  # field interfaces self
135             ('copy', self.copy),
136             ('index', self.select),
137             ('manual', self.manual),
138             ('string', self.string),
139             ('help', self.help),
140             ('readonly', self.readonly),
141             ('required', self.required),
142             ('states', self.states),
143             ('groups', self.groups),
144             ('size', self.size),
145             ('ondelete', self.ondelete),
146             ('translate', self.translate),
147             ('domain', self._domain),
148             ('context', self._context),
149             ('change_default', self.change_default),
150             ('deprecated', self.deprecated),
151         ]
152         return dict(item for item in items if item[1])
153
154     def restart(self):
155         pass
156
157     def set(self, cr, obj, id, name, value, user=None, context=None):
158         cr.execute('update '+obj._table+' set '+name+'='+self._symbol_set[0]+' where id=%s', (self._symbol_set[1](value), id))
159
160     def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
161         raise Exception(_('undefined get method !'))
162
163     def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
164         ids = obj.search(cr, uid, args+self._domain+[(name, 'ilike', value)], offset, limit, context=context)
165         res = obj.read(cr, uid, ids, [name], context=context)
166         return [x[name] for x in res]
167
168     def as_display_name(self, cr, uid, obj, value, context=None):
169         """Converts a field value to a suitable string representation for a record,
170            e.g. when this field is used as ``rec_name``.
171
172            :param obj: the ``BaseModel`` instance this column belongs to 
173            :param value: a proper value as returned by :py:meth:`~openerp.orm.osv.BaseModel.read`
174                          for this column
175         """
176         # delegated to class method, so a column type A can delegate
177         # to a column type B. 
178         return self._as_display_name(self, cr, uid, obj, value, context=None)
179
180     @classmethod
181     def _as_display_name(cls, field, cr, uid, obj, value, context=None):
182         # This needs to be a class method, in case a column type A as to delegate
183         # to a column type B.
184         return tools.ustr(value)
185
186 # ---------------------------------------------------------
187 # Simple fields
188 # ---------------------------------------------------------
189 class boolean(_column):
190     _type = 'boolean'
191     _symbol_c = '%s'
192     _symbol_f = bool
193     _symbol_set = (_symbol_c, _symbol_f)
194
195     def __init__(self, string='unknown', required=False, **args):
196         super(boolean, self).__init__(string=string, required=required, **args)
197         if required:
198             _logger.debug(
199                 "required=True is deprecated: making a boolean field"
200                 " `required` has no effect, as NULL values are "
201                 "automatically turned into False. args: %r",args)
202
203 class integer(_column):
204     _type = 'integer'
205     _symbol_c = '%s'
206     _symbol_f = lambda x: int(x or 0)
207     _symbol_set = (_symbol_c, _symbol_f)
208     _symbol_get = lambda self,x: x or 0
209
210     def __init__(self, string='unknown', required=False, **args):
211         super(integer, self).__init__(string=string, required=required, **args)
212
213 class reference(_column):
214     _type = 'reference'
215     _classic_read = False # post-process to handle missing target
216
217     def __init__(self, string, selection, size=None, **args):
218         if callable(selection):
219             from openerp import api
220             selection = api.expected(api.cr_uid_context, selection)
221         _column.__init__(self, string=string, size=size, selection=selection, **args)
222
223     def to_field_args(self):
224         args = super(reference, self).to_field_args()
225         args['selection'] = self.selection
226         return args
227
228     def get(self, cr, obj, ids, name, uid=None, context=None, values=None):
229         result = {}
230         # copy initial values fetched previously.
231         for value in values:
232             result[value['id']] = value[name]
233             if value[name]:
234                 model, res_id = value[name].split(',')
235                 if not obj.pool[model].exists(cr, uid, [int(res_id)], context=context):
236                     result[value['id']] = False
237         return result
238
239     @classmethod
240     def _as_display_name(cls, field, cr, uid, obj, value, context=None):
241         if value:
242             # reference fields have a 'model,id'-like value, that we need to convert
243             # to a real name
244             model_name, res_id = value.split(',')
245             if model_name in obj.pool and res_id:
246                 model = obj.pool[model_name]
247                 names = model.name_get(cr, uid, [int(res_id)], context=context)
248                 return names[0][1] if names else False
249         return tools.ustr(value)
250
251 # takes a string (encoded in utf8) and returns a string (encoded in utf8)
252 def _symbol_set_char(self, symb):
253
254     #TODO:
255     # * we need to remove the "symb==False" from the next line BUT
256     #   for now too many things rely on this broken behavior
257     # * the symb==None test should be common to all data types
258     if symb is None or symb == False:
259         return None
260
261     # we need to convert the string to a unicode object to be able
262     # to evaluate its length (and possibly truncate it) reliably
263     u_symb = tools.ustr(symb)
264     return u_symb[:self.size].encode('utf8')
265
266 class char(_column):
267     _type = 'char'
268
269     def __init__(self, string="unknown", size=None, **args):
270         _column.__init__(self, string=string, size=size or None, **args)
271         # self._symbol_set_char defined to keep the backward compatibility
272         self._symbol_f = self._symbol_set_char = lambda x: _symbol_set_char(self, x)
273         self._symbol_set = (self._symbol_c, self._symbol_f)
274
275 class text(_column):
276     _type = 'text'
277
278
279 class html(text):
280     _type = 'html'
281     _symbol_c = '%s'
282
283     def _symbol_set_html(self, value):
284         if value is None or value is False:
285             return None
286         if not self._sanitize:
287             return value
288         return html_sanitize(value)
289
290     def __init__(self, string='unknown', sanitize=True, **args):
291         super(html, self).__init__(string=string, **args)
292         self._sanitize = sanitize
293         # symbol_set redefinition because of sanitize specific behavior
294         self._symbol_f = self._symbol_set_html
295         self._symbol_set = (self._symbol_c, self._symbol_f)
296
297     def to_field_args(self):
298         args = super(html, self).to_field_args()
299         args['sanitize'] = self._sanitize
300         return args
301
302 import __builtin__
303
304 class float(_column):
305     _type = 'float'
306     _symbol_c = '%s'
307     _symbol_f = lambda x: __builtin__.float(x or 0.0)
308     _symbol_set = (_symbol_c, _symbol_f)
309     _symbol_get = lambda self,x: x or 0.0
310
311     def __init__(self, string='unknown', digits=None, digits_compute=None, required=False, **args):
312         _column.__init__(self, string=string, required=required, **args)
313         self.digits = digits
314         # synopsis: digits_compute(cr) ->  (precision, scale)
315         self.digits_compute = digits_compute
316
317     def to_field_args(self):
318         args = super(float, self).to_field_args()
319         args['digits'] = self.digits_compute or self.digits
320         return args
321
322     def digits_change(self, cr):
323         if self.digits_compute:
324             self.digits = self.digits_compute(cr)
325         if self.digits:
326             precision, scale = self.digits
327             self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
328                                                                        precision_digits=scale),
329                                                            precision_digits=scale))
330
331 class date(_column):
332     _type = 'date'
333
334     MONTHS = [
335         ('01', 'January'),
336         ('02', 'February'),
337         ('03', 'March'),
338         ('04', 'April'),
339         ('05', 'May'),
340         ('06', 'June'),
341         ('07', 'July'),
342         ('08', 'August'),
343         ('09', 'September'),
344         ('10', 'October'),
345         ('11', 'November'),
346         ('12', 'December')
347     ]
348
349     @staticmethod
350     def today(*args):
351         """ Returns the current date in a format fit for being a
352         default value to a ``date`` field.
353
354         This method should be provided as is to the _defaults dict, it
355         should not be called.
356         """
357         return DT.date.today().strftime(
358             tools.DEFAULT_SERVER_DATE_FORMAT)
359
360     @staticmethod
361     def context_today(model, cr, uid, context=None, timestamp=None):
362         """Returns the current date as seen in the client's timezone
363            in a format fit for date fields.
364            This method may be passed as value to initialize _defaults.
365
366            :param Model model: model (osv) for which the date value is being
367                                computed - automatically passed when used in
368                                 _defaults.
369            :param datetime timestamp: optional datetime value to use instead of
370                                       the current date and time (must be a
371                                       datetime, regular dates can't be converted
372                                       between timezones.)
373            :param dict context: the 'tz' key in the context should give the
374                                 name of the User/Client timezone (otherwise
375                                 UTC is used)
376            :rtype: str 
377         """
378         today = timestamp or DT.datetime.now()
379         context_today = None
380         if context and context.get('tz'):
381             tz_name = context['tz']  
382         else:
383             user = model.pool['res.users'].browse(cr, SUPERUSER_ID, uid)
384             tz_name = user.tz
385         if tz_name:
386             try:
387                 utc = pytz.timezone('UTC')
388                 context_tz = pytz.timezone(tz_name)
389                 utc_today = utc.localize(today, is_dst=False) # UTC = no DST
390                 context_today = utc_today.astimezone(context_tz)
391             except Exception:
392                 _logger.debug("failed to compute context/client-specific today date, "
393                               "using the UTC value for `today`",
394                               exc_info=True)
395         return (context_today or today).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
396
397     @staticmethod
398     def date_to_datetime(model, cr, uid, userdate, context=None):
399         """ Convert date values expressed in user's timezone to
400         server-side UTC timestamp, assuming a default arbitrary
401         time of 12:00 AM - because a time is needed.
402
403         :param str userdate: date string in in user time zone
404         :return: UTC datetime string for server-side use
405         """
406         user_date = DT.datetime.strptime(userdate, tools.DEFAULT_SERVER_DATE_FORMAT)
407         if context and context.get('tz'):
408             tz_name = context['tz']
409         else:
410             tz_name = model.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
411         if tz_name:
412             utc = pytz.timezone('UTC')
413             context_tz = pytz.timezone(tz_name)
414             user_datetime = user_date + DT.timedelta(hours=12.0)
415             local_timestamp = context_tz.localize(user_datetime, is_dst=False)
416             user_datetime = local_timestamp.astimezone(utc)
417             return user_datetime.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
418         return user_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
419
420
421 class datetime(_column):
422     _type = 'datetime'
423
424     MONTHS = [
425         ('01', 'January'),
426         ('02', 'February'),
427         ('03', 'March'),
428         ('04', 'April'),
429         ('05', 'May'),
430         ('06', 'June'),
431         ('07', 'July'),
432         ('08', 'August'),
433         ('09', 'September'),
434         ('10', 'October'),
435         ('11', 'November'),
436         ('12', 'December')
437     ]
438
439     @staticmethod
440     def now(*args):
441         """ Returns the current datetime in a format fit for being a
442         default value to a ``datetime`` field.
443
444         This method should be provided as is to the _defaults dict, it
445         should not be called.
446         """
447         return DT.datetime.now().strftime(
448             tools.DEFAULT_SERVER_DATETIME_FORMAT)
449
450     @staticmethod
451     def context_timestamp(cr, uid, timestamp, context=None):
452         """Returns the given timestamp converted to the client's timezone.
453            This method is *not* meant for use as a _defaults initializer,
454            because datetime fields are automatically converted upon
455            display on client side. For _defaults you :meth:`fields.datetime.now`
456            should be used instead.
457
458            :param datetime timestamp: naive datetime value (expressed in UTC)
459                                       to be converted to the client timezone
460            :param dict context: the 'tz' key in the context should give the
461                                 name of the User/Client timezone (otherwise
462                                 UTC is used)
463            :rtype: datetime
464            :return: timestamp converted to timezone-aware datetime in context
465                     timezone
466         """
467         assert isinstance(timestamp, DT.datetime), 'Datetime instance expected'
468         if context and context.get('tz'):
469             tz_name = context['tz']  
470         else:
471             registry = openerp.modules.registry.RegistryManager.get(cr.dbname)
472             user = registry['res.users'].browse(cr, SUPERUSER_ID, uid)
473             tz_name = user.tz
474         utc_timestamp = pytz.utc.localize(timestamp, is_dst=False) # UTC = no DST
475         if tz_name:
476             try:
477                 context_tz = pytz.timezone(tz_name)
478                 return utc_timestamp.astimezone(context_tz)
479             except Exception:
480                 _logger.debug("failed to compute context/client-specific timestamp, "
481                               "using the UTC value",
482                               exc_info=True)
483         return utc_timestamp
484
485 class binary(_column):
486     _type = 'binary'
487     _symbol_c = '%s'
488
489     # Binary values may be byte strings (python 2.6 byte array), but
490     # the legacy OpenERP convention is to transfer and store binaries
491     # as base64-encoded strings. The base64 string may be provided as a
492     # unicode in some circumstances, hence the str() cast in symbol_f.
493     # This str coercion will only work for pure ASCII unicode strings,
494     # on purpose - non base64 data must be passed as a 8bit byte strings.
495     _symbol_f = lambda symb: symb and Binary(str(symb)) or None
496
497     _symbol_set = (_symbol_c, _symbol_f)
498     _symbol_get = lambda self, x: x and str(x)
499
500     _classic_read = False
501     _prefetch = False
502
503     def __init__(self, string='unknown', filters=None, **args):
504         _column.__init__(self, string=string, **args)
505         self.filters = filters
506
507     def get(self, cr, obj, ids, name, user=None, context=None, values=None):
508         if not context:
509             context = {}
510         if not values:
511             values = []
512         res = {}
513         for i in ids:
514             val = None
515             for v in values:
516                 if v['id'] == i:
517                     val = v[name]
518                     break
519
520             # If client is requesting only the size of the field, we return it instead
521             # of the content. Presumably a separate request will be done to read the actual
522             # content if it's needed at some point.
523             # TODO: after 6.0 we should consider returning a dict with size and content instead of
524             #       having an implicit convention for the value
525             if val and context.get('bin_size_%s' % name, context.get('bin_size')):
526                 res[i] = tools.human_size(long(val))
527             else:
528                 res[i] = val
529         return res
530
531 class selection(_column):
532     _type = 'selection'
533
534     def __init__(self, selection, string='unknown', **args):
535         if callable(selection):
536             from openerp import api
537             selection = api.expected(api.cr_uid_context, selection)
538         _column.__init__(self, string=string, **args)
539         self.selection = selection
540
541     def to_field_args(self):
542         args = super(selection, self).to_field_args()
543         args['selection'] = self.selection
544         return args
545
546     @classmethod
547     def reify(cls, cr, uid, model, field, context=None):
548         """ Munges the field's ``selection`` attribute as necessary to get
549         something useable out of it: calls it if it's a function, applies
550         translations to labels if it's not.
551
552         A callable ``selection`` is considered translated on its own.
553
554         :param orm.Model model:
555         :param _column field:
556         """
557         if callable(field.selection):
558             return field.selection(model, cr, uid, context)
559
560         if not (context and 'lang' in context):
561             return field.selection
562
563         # field_to_dict isn't given a field name, only a field object, we
564         # need to get the name back in order to perform the translation lookup
565         field_name = next(
566             name for name, column in model._columns.iteritems()
567             if column == field)
568
569         translation_filter = "%s,%s" % (model._name, field_name)
570         translate = functools.partial(
571             model.pool['ir.translation']._get_source,
572             cr, uid, translation_filter, 'selection', context['lang'])
573
574         return [
575             (value, translate(label))
576             for value, label in field.selection
577         ]
578
579 # ---------------------------------------------------------
580 # Relationals fields
581 # ---------------------------------------------------------
582
583 #
584 # Values: (0, 0,  { fields })    create
585 #         (1, ID, { fields })    update
586 #         (2, ID)                remove (delete)
587 #         (3, ID)                unlink one (target id or target of relation)
588 #         (4, ID)                link
589 #         (5)                    unlink all (only valid for one2many)
590 #
591
592 class many2one(_column):
593     _classic_read = False
594     _classic_write = True
595     _type = 'many2one'
596     _symbol_c = '%s'
597     _symbol_f = lambda x: x or None
598     _symbol_set = (_symbol_c, _symbol_f)
599
600     def __init__(self, obj, string='unknown', auto_join=False, **args):
601         _column.__init__(self, string=string, **args)
602         self._obj = obj
603         self._auto_join = auto_join
604
605     def to_field_args(self):
606         args = super(many2one, self).to_field_args()
607         args['comodel_name'] = self._obj
608         args['auto_join'] = self._auto_join
609         return args
610
611     def set(self, cr, obj_src, id, field, values, user=None, context=None):
612         if not context:
613             context = {}
614         obj = obj_src.pool[self._obj]
615         self._table = obj._table
616         if type(values) == type([]):
617             for act in values:
618                 if act[0] == 0:
619                     id_new = obj.create(cr, act[2])
620                     cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (id_new, id))
621                 elif act[0] == 1:
622                     obj.write(cr, [act[1]], act[2], context=context)
623                 elif act[0] == 2:
624                     cr.execute('delete from '+self._table+' where id=%s', (act[1],))
625                 elif act[0] == 3 or act[0] == 5:
626                     cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
627                 elif act[0] == 4:
628                     cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (act[1], id))
629         else:
630             if values:
631                 cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (values, id))
632             else:
633                 cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,))
634
635     def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
636         return obj.pool[self._obj].search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context)
637
638     @classmethod
639     def _as_display_name(cls, field, cr, uid, obj, value, context=None):
640         return value[1] if isinstance(value, tuple) else tools.ustr(value) 
641
642
643 class one2many(_column):
644     _classic_read = False
645     _classic_write = False
646     _prefetch = False
647     _type = 'one2many'
648
649     # one2many columns are not copied by default
650     copy = False
651
652     def __init__(self, obj, fields_id, string='unknown', limit=None, auto_join=False, **args):
653         _column.__init__(self, string=string, **args)
654         self._obj = obj
655         self._fields_id = fields_id
656         self._limit = limit
657         self._auto_join = auto_join
658         #one2many can't be used as condition for defaults
659         assert(self.change_default != True)
660
661     def to_field_args(self):
662         args = super(one2many, self).to_field_args()
663         args['comodel_name'] = self._obj
664         args['inverse_name'] = self._fields_id
665         args['auto_join'] = self._auto_join
666         args['limit'] = self._limit
667         return args
668
669     def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
670         if self._context:
671             context = dict(context or {})
672             context.update(self._context)
673
674         comodel = obj.pool[self._obj].browse(cr, user, [], context)
675         inverse = self._fields_id
676         domain = self._domain(obj) if callable(self._domain) else self._domain
677         domain = domain + [(inverse, 'in', ids)]
678
679         records = comodel.search(domain, limit=self._limit)
680         record_ids = map(int, records)
681
682         res = dict((id, []) for id in ids)
683         if record_ids:
684             cr.execute('SELECT id, %(inverse)s \
685                        FROM %(rel)s \
686                        WHERE id in %%s ' % {
687                         'inverse': inverse,
688                         'rel': comodel._table,
689                     }, (tuple(record_ids),))
690             record_value_id = dict(cr.fetchall())
691             # match the result per id, preserving the order
692             for record in records:
693                 key = record_value_id[record.id]
694                 res[key].append(record.id)
695
696         return res
697
698     def set(self, cr, obj, id, field, values, user=None, context=None):
699         result = []
700         context = dict(context or {})
701         context.update(self._context)
702         context['recompute'] = False    # recomputation is done by outer create/write
703         if not values:
704             return
705         obj = obj.pool[self._obj]
706         _table = obj._table
707         for act in values:
708             if act[0] == 0:
709                 act[2][self._fields_id] = id
710                 id_new = obj.create(cr, user, act[2], context=context)
711                 result += obj._store_get_values(cr, user, [id_new], act[2].keys(), context)
712             elif act[0] == 1:
713                 obj.write(cr, user, [act[1]], act[2], context=context)
714             elif act[0] == 2:
715                 obj.unlink(cr, user, [act[1]], context=context)
716             elif act[0] == 3:
717                 reverse_rel = obj._all_columns.get(self._fields_id)
718                 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
719                 # if the model has on delete cascade, just delete the row
720                 if reverse_rel.column.ondelete == "cascade":
721                     obj.unlink(cr, user, [act[1]], context=context)
722                 else:
723                     cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],))
724             elif act[0] == 4:
725                 # table of the field (parent_model in case of inherit)
726                 field_model = self._fields_id in obj.pool[self._obj]._columns and self._obj or obj.pool[self._obj]._all_columns[self._fields_id].parent_model
727                 field_table = obj.pool[field_model]._table
728                 cr.execute("select 1 from {0} where id=%s and {1}=%s".format(field_table, self._fields_id), (act[1], id))
729                 if not cr.fetchone():
730                     # Must use write() to recompute parent_store structure if needed and check access rules
731                     obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
732             elif act[0] == 5:
733                 reverse_rel = obj._all_columns.get(self._fields_id)
734                 assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
735                 # if the o2m has a static domain we must respect it when unlinking
736                 domain = self._domain(obj) if callable(self._domain) else self._domain
737                 extra_domain = domain or []
738                 ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
739                 # If the model has cascade deletion, we delete the rows because it is the intended behavior,
740                 # otherwise we only nullify the reverse foreign key column.
741                 if reverse_rel.column.ondelete == "cascade":
742                     obj.unlink(cr, user, ids_to_unlink, context=context)
743                 else:
744                     obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context)
745             elif act[0] == 6:
746                 # Must use write() to recompute parent_store structure if needed
747                 obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
748                 ids2 = act[2] or [0]
749                 cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
750                 ids3 = map(lambda x:x[0], cr.fetchall())
751                 obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
752         return result
753
754     def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
755         domain = self._domain(obj) if callable(self._domain) else self._domain
756         return obj.pool[self._obj].name_search(cr, uid, value, domain, operator, context=context,limit=limit)
757
758     @classmethod
759     def _as_display_name(cls, field, cr, uid, obj, value, context=None):
760         raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)') 
761
762 #
763 # Values: (0, 0,  { fields })    create
764 #         (1, ID, { fields })    update (write fields to ID)
765 #         (2, ID)                remove (calls unlink on ID, that will also delete the relationship because of the ondelete)
766 #         (3, ID)                unlink (delete the relationship between the two objects but does not delete ID)
767 #         (4, ID)                link (add a relationship)
768 #         (5, ID)                unlink all
769 #         (6, ?, ids)            set a list of links
770 #
771 class many2many(_column):
772     """Encapsulates the logic of a many-to-many bidirectional relationship, handling the
773        low-level details of the intermediary relationship table transparently.
774        A many-to-many relationship is always symmetrical, and can be declared and accessed
775        from either endpoint model.
776        If ``rel`` (relationship table name), ``id1`` (source foreign key column name)
777        or id2 (destination foreign key column name) are not specified, the system will
778        provide default values. This will by default only allow one single symmetrical
779        many-to-many relationship between the source and destination model.
780        For multiple many-to-many relationship between the same models and for
781        relationships where source and destination models are the same, ``rel``, ``id1``
782        and ``id2`` should be specified explicitly.
783
784        :param str obj: destination model
785        :param str rel: optional name of the intermediary relationship table. If not specified,
786                        a canonical name will be derived based on the alphabetically-ordered
787                        model names of the source and destination (in the form: ``amodel_bmodel_rel``).
788                        Automatic naming is not possible when the source and destination are
789                        the same, for obvious ambiguity reasons.
790        :param str id1: optional name for the column holding the foreign key to the current
791                        model in the relationship table. If not specified, a canonical name
792                        will be derived based on the model name (in the form: `src_model_id`).
793        :param str id2: optional name for the column holding the foreign key to the destination
794                        model in the relationship table. If not specified, a canonical name
795                        will be derived based on the model name (in the form: `dest_model_id`)
796        :param str string: field label
797     """
798     _classic_read = False
799     _classic_write = False
800     _prefetch = False
801     _type = 'many2many'
802
803     def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
804         """
805         """
806         _column.__init__(self, string=string, **args)
807         self._obj = obj
808         if rel and '.' in rel:
809             raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\
810                 'You used %s, which is not a valid SQL table name.')% (string,rel))
811         self._rel = rel
812         self._id1 = id1
813         self._id2 = id2
814         self._limit = limit
815
816     def to_field_args(self):
817         args = super(many2many, self).to_field_args()
818         args['comodel_name'] = self._obj
819         args['relation'] = self._rel
820         args['column1'] = self._id1
821         args['column2'] = self._id2
822         args['limit'] = self._limit
823         return args
824
825     def _sql_names(self, source_model):
826         """Return the SQL names defining the structure of the m2m relationship table
827
828             :return: (m2m_table, local_col, dest_col) where m2m_table is the table name,
829                      local_col is the name of the column holding the current model's FK, and
830                      dest_col is the name of the column holding the destination model's FK, and
831         """
832         tbl, col1, col2 = self._rel, self._id1, self._id2
833         if not all((tbl, col1, col2)):
834             # the default table name is based on the stable alphabetical order of tables
835             dest_model = source_model.pool[self._obj]
836             tables = tuple(sorted([source_model._table, dest_model._table]))
837             if not tbl:
838                 assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\
839                                                'is not possible when source and destination models are '\
840                                                'the same'
841                 tbl = '%s_%s_rel' % tables
842             if not col1:
843                 col1 = '%s_id' % source_model._table
844             if not col2:
845                 col2 = '%s_id' % dest_model._table
846         return tbl, col1, col2
847
848     def _get_query_and_where_params(self, cr, model, ids, values, where_params):
849         """ Extracted from ``get`` to facilitate fine-tuning of the generated
850             query. """
851         query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
852                    FROM %(rel)s, %(from_c)s \
853                   WHERE %(rel)s.%(id1)s IN %%s \
854                     AND %(rel)s.%(id2)s = %(tbl)s.id \
855                  %(where_c)s  \
856                  %(order_by)s \
857                  %(limit)s \
858                  OFFSET %(offset)d' \
859                  % values
860         return query, where_params
861
862     def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
863         if not context:
864             context = {}
865         if not values:
866             values = {}
867         res = {}
868         if not ids:
869             return res
870         for id in ids:
871             res[id] = []
872         if offset:
873             _logger.warning(
874                 "Specifying offset at a many2many.get() is deprecated and may"
875                 " produce unpredictable results.")
876         obj = model.pool[self._obj]
877         rel, id1, id2 = self._sql_names(model)
878
879         # static domains are lists, and are evaluated both here and on client-side, while string
880         # domains supposed by dynamic and evaluated on client-side only (thus ignored here)
881         # FIXME: make this distinction explicit in API!
882         domain = isinstance(self._domain, list) and self._domain or []
883
884         wquery = obj._where_calc(cr, user, domain, context=context)
885         obj._apply_ir_rules(cr, user, wquery, 'read', context=context)
886         from_c, where_c, where_params = wquery.get_sql()
887         if where_c:
888             where_c = ' AND ' + where_c
889
890         order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0])
891
892         limit_str = ''
893         if self._limit is not None:
894             limit_str = ' LIMIT %d' % self._limit
895
896         query, where_params = self._get_query_and_where_params(cr, model, ids, {'rel': rel,
897                'from_c': from_c,
898                'tbl': obj._table,
899                'id1': id1,
900                'id2': id2,
901                'where_c': where_c,
902                'limit': limit_str,
903                'order_by': order_by,
904                'offset': offset,
905                 }, where_params)
906
907         cr.execute(query, [tuple(ids),] + where_params)
908         for r in cr.fetchall():
909             res[r[1]].append(r[0])
910         return res
911
912     def set(self, cr, model, id, name, values, user=None, context=None):
913         if not context:
914             context = {}
915         if not values:
916             return
917         rel, id1, id2 = self._sql_names(model)
918         obj = model.pool[self._obj]
919         for act in values:
920             if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
921                 continue
922             if act[0] == 0:
923                 idnew = obj.create(cr, user, act[2], context=context)
924                 cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew))
925             elif act[0] == 1:
926                 obj.write(cr, user, [act[1]], act[2], context=context)
927             elif act[0] == 2:
928                 obj.unlink(cr, user, [act[1]], context=context)
929             elif act[0] == 3:
930                 cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1]))
931             elif act[0] == 4:
932                 # following queries are in the same transaction - so should be relatively safe
933                 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1]))
934                 if not cr.fetchone():
935                     cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1]))
936             elif act[0] == 5:
937                 cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,))
938             elif act[0] == 6:
939
940                 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
941                 if d1:
942                     d1 = ' and ' + ' and '.join(d1)
943                 else:
944                     d1 = ''
945                 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)
946
947                 for act_nbr in act[2]:
948                     cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr))
949
950     #
951     # TODO: use a name_search
952     #
953     def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
954         return obj.pool[self._obj].search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
955
956     @classmethod
957     def _as_display_name(cls, field, cr, uid, obj, value, context=None):
958         raise NotImplementedError('Many2Many columns should not be used as record name (_rec_name)') 
959
960
961 def get_nice_size(value):
962     size = 0
963     if isinstance(value, (int,long)):
964         size = value
965     elif value: # this is supposed to be a string
966         size = len(value)
967     return tools.human_size(size)
968
969 # See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
970 # and http://bugs.python.org/issue10066
971 invalid_xml_low_bytes = re.compile(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
972
973 def sanitize_binary_value(value):
974     # binary fields should be 7-bit ASCII base64-encoded data,
975     # but we do additional sanity checks to make sure the values
976     # are not something else that won't pass via XML-RPC
977     if isinstance(value, (xmlrpclib.Binary, tuple, list, dict)):
978         # these builtin types are meant to pass untouched
979         return value
980
981     # Handle invalid bytes values that will cause problems
982     # for XML-RPC. See for more info:
983     #  - http://bugs.python.org/issue10066
984     #  - http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
985
986     # Coercing to unicode would normally allow it to properly pass via
987     # XML-RPC, transparently encoded as UTF-8 by xmlrpclib.
988     # (this works for _any_ byte values, thanks to the fallback
989     #  to latin-1 passthrough encoding when decoding to unicode)
990     value = tools.ustr(value)
991
992     # Due to Python bug #10066 this could still yield invalid XML
993     # bytes, specifically in the low byte range, that will crash
994     # the decoding side: [\x00-\x08\x0b-\x0c\x0e-\x1f]
995     # So check for low bytes values, and if any, perform
996     # base64 encoding - not very smart or useful, but this is
997     # our last resort to avoid crashing the request.
998     if invalid_xml_low_bytes.search(value):
999         # b64-encode after restoring the pure bytes with latin-1
1000         # passthrough encoding
1001         value = base64.b64encode(value.encode('latin-1'))
1002
1003     return value
1004
1005
1006 # ---------------------------------------------------------
1007 # Function fields
1008 # ---------------------------------------------------------
1009 class function(_column):
1010     """
1011     A field whose value is computed by a function (rather
1012     than being read from the database).
1013
1014     :param fnct: the callable that will compute the field value.
1015     :param arg: arbitrary value to be passed to ``fnct`` when computing the value.
1016     :param fnct_inv: the callable that will allow writing values in that field
1017                      (if not provided, the field is read-only).
1018     :param fnct_inv_arg: arbitrary value to be passed to ``fnct_inv`` when
1019                          writing a value.
1020     :param str type: type of the field simulated by the function field
1021     :param fnct_search: the callable that allows searching on the field
1022                         (if not provided, search will not return any result).
1023     :param store: store computed value in database
1024                   (see :ref:`The *store* parameter <field-function-store>`).
1025     :type store: True or dict specifying triggers for field computation
1026     :param multi: name of batch for batch computation of function fields.
1027                   All fields with the same batch name will be computed by
1028                   a single function call. This changes the signature of the
1029                   ``fnct`` callable.
1030
1031     .. _field-function-fnct: The ``fnct`` parameter
1032
1033     .. rubric:: The ``fnct`` parameter
1034
1035     The callable implementing the function field must have the following signature:
1036
1037     .. function:: fnct(model, cr, uid, ids, field_name(s), arg, context)
1038
1039         Implements the function field.
1040
1041         :param orm model: model to which the field belongs (should be ``self`` for
1042                           a model method)
1043         :param field_name(s): name of the field to compute, or if ``multi`` is provided,
1044                               list of field names to compute.
1045         :type field_name(s): str | [str]
1046         :param arg: arbitrary value passed when declaring the function field
1047         :rtype: dict
1048         :return: mapping of ``ids`` to computed values, or if multi is provided,
1049                  to a map of field_names to computed values
1050
1051     The values in the returned dictionary must be of the type specified by the type
1052     argument in the field declaration.
1053
1054     Here is an example with a simple function ``char`` function field::
1055
1056         # declarations
1057         def compute(self, cr, uid, ids, field_name, arg, context):
1058             result = {}
1059             # ...
1060             return result
1061         _columns['my_char'] = fields.function(compute, type='char', size=50)
1062
1063         # when called with ``ids=[1,2,3]``, ``compute`` could return:
1064         {
1065             1: 'foo',
1066             2: 'bar',
1067             3: False # null values should be returned explicitly too
1068         }
1069
1070     If ``multi`` is set, then ``field_name`` is replaced by ``field_names``: a list
1071     of the field names that should be computed. Each value in the returned
1072     dictionary must then be a dictionary mapping field names to values.
1073
1074     Here is an example where two function fields (``name`` and ``age``)
1075     are both computed by a single function field::
1076
1077         # declarations
1078         def compute(self, cr, uid, ids, field_names, arg, context):
1079             result = {}
1080             # ...
1081             return result
1082         _columns['name'] = fields.function(compute_person_data, type='char',\
1083                                            size=50, multi='person_data')
1084         _columns[''age'] = fields.function(compute_person_data, type='integer',\
1085                                            multi='person_data')
1086
1087         # when called with ``ids=[1,2,3]``, ``compute_person_data`` could return:
1088         {
1089             1: {'name': 'Bob', 'age': 23},
1090             2: {'name': 'Sally', 'age': 19},
1091             3: {'name': 'unknown', 'age': False}
1092         }
1093
1094     .. _field-function-fnct-inv:
1095
1096     .. rubric:: The ``fnct_inv`` parameter
1097
1098     This callable implements the write operation for the function field
1099     and must have the following signature:
1100
1101     .. function:: fnct_inv(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context)
1102
1103         Callable that implements the ``write`` operation for the function field.
1104
1105         :param orm model: model to which the field belongs (should be ``self`` for
1106                           a model method)
1107         :param int id: the identifier of the object to write on
1108         :param str field_name: name of the field to set
1109         :param fnct_inv_arg: arbitrary value passed when declaring the function field
1110         :return: True
1111
1112     When writing values for a function field, the ``multi`` parameter is ignored.
1113
1114     .. _field-function-fnct-search:
1115
1116     .. rubric:: The ``fnct_search`` parameter
1117
1118     This callable implements the search operation for the function field
1119     and must have the following signature:
1120
1121     .. function:: fnct_search(model, cr, uid, model_again, field_name, criterion, context)
1122
1123         Callable that implements the ``search`` operation for the function field by expanding
1124         a search criterion based on the function field into a new domain based only on
1125         columns that are stored in the database.
1126
1127         :param orm model: model to which the field belongs (should be ``self`` for
1128                           a model method)
1129         :param orm model_again: same value as ``model`` (seriously! this is for backwards
1130                                 compatibility)
1131         :param str field_name: name of the field to search on
1132         :param list criterion: domain component specifying the search criterion on the field.
1133         :rtype: list
1134         :return: domain to use instead of ``criterion`` when performing the search.
1135                  This new domain must be based only on columns stored in the database, as it
1136                  will be used directly without any translation.
1137
1138         The returned value must be a domain, that is, a list of the form [(field_name, operator, operand)].
1139         The most generic way to implement ``fnct_search`` is to directly search for the records that
1140         match the given ``criterion``, and return their ``ids`` wrapped in a domain, such as
1141         ``[('id','in',[1,3,5])]``.
1142
1143     .. _field-function-store:
1144
1145     .. rubric:: The ``store`` parameter
1146
1147     The ``store`` parameter allows caching the result of the field computation in the
1148     database, and defining the triggers that will invalidate that cache and force a
1149     recomputation of the function field.
1150     When not provided, the field is computed every time its value is read.
1151     The value of ``store`` may be either ``True`` (to recompute the field value whenever
1152     any field in the same record is modified), or a dictionary specifying a more
1153     flexible set of recomputation triggers.
1154
1155     A trigger specification is a dictionary that maps the names of the models that
1156     will trigger the computation, to a tuple describing the trigger rule, in the
1157     following form::
1158
1159         store = {
1160             'trigger_model': (mapping_function,
1161                               ['trigger_field1', 'trigger_field2'],
1162                               priority),
1163         }
1164
1165     A trigger rule is defined by a 3-item tuple where:
1166
1167         * The ``mapping_function`` is defined as follows:
1168
1169             .. function:: mapping_function(trigger_model, cr, uid, trigger_ids, context)
1170
1171                 Callable that maps record ids of a trigger model to ids of the
1172                 corresponding records in the source model (whose field values
1173                 need to be recomputed).
1174
1175                 :param orm model: trigger_model
1176                 :param list trigger_ids: ids of the records of trigger_model that were
1177                                          modified
1178                 :rtype: list
1179                 :return: list of ids of the source model whose function field values
1180                          need to be recomputed
1181
1182         * The second item is a list of the fields who should act as triggers for
1183           the computation. If an empty list is given, all fields will act as triggers.
1184         * The last item is the priority, used to order the triggers when processing them
1185           after any write operation on a model that has function field triggers. The
1186           default priority is 10.
1187
1188     In fact, setting store = True is the same as using the following trigger dict::
1189
1190         store = {
1191               'model_itself': (lambda self, cr, uid, ids, context: ids,
1192                                [],
1193                                10)
1194         }
1195
1196     """
1197     _classic_read = False
1198     _classic_write = False
1199     _prefetch = False
1200     _type = 'function'
1201     _properties = True
1202
1203     # function fields are not copied by default
1204     copy = False
1205
1206 #
1207 # multi: compute several fields in one call
1208 #
1209     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):
1210         _column.__init__(self, **args)
1211         self._obj = obj
1212         self._fnct = fnct
1213         self._fnct_inv = fnct_inv
1214         self._arg = arg
1215         self._multi = multi
1216         if 'relation' in args:
1217             self._obj = args['relation']
1218
1219         self.digits = args.get('digits', (16,2))
1220         self.digits_compute = args.get('digits_compute', None)
1221         if callable(args.get('selection')):
1222             from openerp import api
1223             self.selection = api.expected(api.cr_uid_context, args['selection'])
1224
1225         self._fnct_inv_arg = fnct_inv_arg
1226         if not fnct_inv:
1227             self.readonly = 1
1228         self._type = type
1229         self._fnct_search = fnct_search
1230         self.store = store
1231
1232         if not fnct_search and not store:
1233             self.selectable = False
1234
1235         if store:
1236             if self._type != 'many2one':
1237                 # m2o fields need to return tuples with name_get, not just foreign keys
1238                 self._classic_read = True
1239             self._classic_write = True
1240             if type=='binary':
1241                 self._symbol_get=lambda x:x and str(x)
1242             else:
1243                 self._prefetch = True
1244
1245         if type == 'char':
1246             self._symbol_c = char._symbol_c
1247             self._symbol_f = lambda x: _symbol_set_char(self, x)
1248             self._symbol_set = (self._symbol_c, self._symbol_f)
1249         else:
1250             type_class = globals().get(type)
1251             if type_class is not None:
1252                 self._symbol_c = type_class._symbol_c
1253                 self._symbol_f = type_class._symbol_f
1254                 self._symbol_set = type_class._symbol_set
1255
1256     def to_field_args(self):
1257         args = super(function, self).to_field_args()
1258         if self._type in ('float',):
1259             args['digits'] = self.digits_compute or self.digits
1260         elif self._type in ('selection', 'reference'):
1261             args['selection'] = self.selection
1262         elif self._type in ('many2one', 'one2many', 'many2many'):
1263             args['comodel_name'] = self._obj
1264         return args
1265
1266     def digits_change(self, cr):
1267         if self._type == 'float':
1268             if self.digits_compute:
1269                 self.digits = self.digits_compute(cr)
1270             if self.digits:
1271                 precision, scale = self.digits
1272                 self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
1273                                                                            precision_digits=scale),
1274                                                                precision_digits=scale))
1275
1276     def search(self, cr, uid, obj, name, args, context=None):
1277         if not self._fnct_search:
1278             #CHECKME: should raise an exception
1279             return []
1280         return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
1281
1282     def postprocess(self, cr, uid, obj, field, value=None, context=None):
1283         return self._postprocess_batch(cr, uid, obj, field, {0: value}, context=context)[0]
1284
1285     def _postprocess_batch(self, cr, uid, obj, field, values, context=None):
1286         if not values:
1287             return values
1288
1289         if context is None:
1290             context = {}
1291
1292         field_type = obj._columns[field]._type
1293         new_values = dict(values)
1294
1295         if field_type == 'binary':
1296             if context.get('bin_size'):
1297                 # client requests only the size of binary fields
1298                 for rid, value in values.iteritems():
1299                     if value:
1300                         new_values[rid] = get_nice_size(value)
1301             elif not context.get('bin_raw'):
1302                 for rid, value in values.iteritems():
1303                     if value:
1304                         new_values[rid] = sanitize_binary_value(value)
1305
1306         return new_values
1307
1308     def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
1309         multi = self._multi
1310         # if we already have a value, don't recompute it.
1311         # This happen if case of stored many2one fields
1312         if values and not multi and name in values[0]:
1313             result = dict((v['id'], v[name]) for v in values)
1314         elif values and multi and all(n in values[0] for n in name):
1315             result = dict((v['id'], dict((n, v[n]) for n in name)) for v in values)
1316         else:
1317             result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
1318         if multi:
1319             swap = {}
1320             for rid, values in result.iteritems():
1321                 for f, v in values.iteritems():
1322                     if f not in name:
1323                         continue
1324                     swap.setdefault(f, {})[rid] = v
1325
1326             for field, values in swap.iteritems():
1327                 new_values = self._postprocess_batch(cr, uid, obj, field, values, context)
1328                 for rid, value in new_values.iteritems():
1329                     result[rid][field] = value
1330
1331         else:
1332             result = self._postprocess_batch(cr, uid, obj, name, result, context)
1333
1334         return result
1335
1336     def set(self, cr, obj, id, name, value, user=None, context=None):
1337         if not context:
1338             context = {}
1339         if self._fnct_inv:
1340             self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
1341
1342     @classmethod
1343     def _as_display_name(cls, field, cr, uid, obj, value, context=None):
1344         # Function fields are supposed to emulate a basic field type,
1345         # so they can delegate to the basic type for record name rendering
1346         return globals()[field._type]._as_display_name(field, cr, uid, obj, value, context=context)
1347
1348 # ---------------------------------------------------------
1349 # Related fields
1350 # ---------------------------------------------------------
1351
1352 class related(function):
1353     """Field that points to some data inside another field of the current record.
1354
1355     Example::
1356
1357        _columns = {
1358            'foo_id': fields.many2one('my.foo', 'Foo'),
1359            'bar': fields.related('foo_id', 'frol', type='char', string='Frol of Foo'),
1360         }
1361     """
1362
1363     def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1364         # assume self._arg = ('foo', 'bar', 'baz')
1365         # domain = [(name, op, val)]   =>   search [('foo.bar.baz', op, val)]
1366         field = '.'.join(self._arg)
1367         return map(lambda x: (field, x[1], x[2]), domain)
1368
1369     def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1370         if isinstance(ids, (int, long)):
1371             ids = [ids]
1372         for instance in obj.browse(cr, uid, ids, context=context):
1373             # traverse all fields except the last one
1374             for field in self.arg[:-1]:
1375                 instance = instance[field][:1]
1376             if instance:
1377                 # write on the last field of the target record
1378                 instance.write({self.arg[-1]: values})
1379
1380     def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1381         res = {}
1382         for record in obj.browse(cr, SUPERUSER_ID, ids, context=context):
1383             value = record
1384             # traverse all fields except the last one
1385             for field in self.arg[:-1]:
1386                 value = value[field][:1]
1387             # read the last field on the target record
1388             res[record.id] = value[self.arg[-1]]
1389
1390         if self._type == 'many2one':
1391             # res[id] is a recordset; convert it to (id, name) or False.
1392             # Perform name_get as root, as seeing the name of a related object depends on
1393             # access right of source document, not target, so user may not have access.
1394             value_ids = list(set(value.id for value in res.itervalues() if value))
1395             value_name = dict(obj.pool[self._obj].name_get(cr, SUPERUSER_ID, value_ids, context=context))
1396             res = dict((id, bool(value) and (value.id, value_name[value.id])) for id, value in res.iteritems())
1397
1398         elif self._type in ('one2many', 'many2many'):
1399             # res[id] is a recordset; convert it to a list of ids
1400             res = dict((id, value.ids) for id, value in res.iteritems())
1401
1402         return res
1403
1404     def __init__(self, *arg, **args):
1405         self.arg = arg
1406         self._relations = []
1407         super(related, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1408         if self.store is True:
1409             # TODO: improve here to change self.store = {...} according to related objects
1410             pass
1411
1412
1413 class sparse(function):   
1414
1415     def convert_value(self, obj, cr, uid, record, value, read_value, context=None):        
1416         """
1417             + For a many2many field, a list of tuples is expected.
1418               Here is the list of tuple that are accepted, with the corresponding semantics ::
1419
1420                  (0, 0,  { values })    link to a new record that needs to be created with the given values dictionary
1421                  (1, ID, { values })    update the linked record with id = ID (write *values* on it)
1422                  (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)
1423                  (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)
1424                  (4, ID)                link to existing record with id = ID (adds a relationship)
1425                  (5)                    unlink all (like using (3,ID) for all linked records)
1426                  (6, 0, [IDs])          replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
1427
1428                  Example:
1429                     [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
1430
1431             + For a one2many field, a lits of tuples is expected.
1432               Here is the list of tuple that are accepted, with the corresponding semantics ::
1433
1434                  (0, 0,  { values })    link to a new record that needs to be created with the given values dictionary
1435                  (1, ID, { values })    update the linked record with id = ID (write *values* on it)
1436                  (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)
1437
1438                  Example:
1439                     [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
1440         """
1441
1442         if self._type == 'many2many':
1443             assert value[0][0] == 6, 'Unsupported m2m value for sparse field: %s' % value
1444             return value[0][2]
1445
1446         elif self._type == 'one2many':
1447             if not read_value:
1448                 read_value = []
1449             relation_obj = obj.pool[self.relation]
1450             for vals in value:
1451                 assert vals[0] in (0,1,2), 'Unsupported o2m value for sparse field: %s' % vals
1452                 if vals[0] == 0:
1453                     read_value.append(relation_obj.create(cr, uid, vals[2], context=context))
1454                 elif vals[0] == 1:
1455                     relation_obj.write(cr, uid, vals[1], vals[2], context=context)
1456                 elif vals[0] == 2:
1457                     relation_obj.unlink(cr, uid, vals[1], context=context)
1458                     read_value.remove(vals[1])
1459             return read_value
1460         return value
1461
1462
1463     def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
1464         if not type(ids) == list:
1465             ids = [ids]
1466         records = obj.browse(cr, uid, ids, context=context)
1467         for record in records:
1468             # grab serialized value as object - already deserialized
1469             serialized = getattr(record, self.serialization_field)
1470             if value is None:
1471                 # simply delete the key to unset it.
1472                 serialized.pop(field_name, None)
1473             else: 
1474                 serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context)
1475             obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context)
1476         return True
1477
1478     def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None):
1479         results = {}
1480         records = obj.browse(cr, uid, ids, context=context)
1481         for record in records:
1482             # grab serialized value as object - already deserialized
1483             serialized = getattr(record, self.serialization_field)
1484             results[record.id] = {}
1485             for field_name in field_names:
1486                 field_type = obj._columns[field_name]._type
1487                 value = serialized.get(field_name, False)
1488                 if field_type in ('one2many','many2many'):
1489                     value = value or []
1490                     if value:
1491                         # filter out deleted records as superuser
1492                         relation_obj = obj.pool[obj._columns[field_name].relation]
1493                         value = relation_obj.exists(cr, openerp.SUPERUSER_ID, value)
1494                 if type(value) in (int,long) and field_type == 'many2one':
1495                     relation_obj = obj.pool[obj._columns[field_name].relation]
1496                     # check for deleted record as superuser
1497                     if not relation_obj.exists(cr, openerp.SUPERUSER_ID, [value]):
1498                         value = False
1499                 results[record.id][field_name] = value
1500         return results
1501
1502     def __init__(self, serialization_field, **kwargs):
1503         self.serialization_field = serialization_field
1504         super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
1505      
1506
1507
1508 # ---------------------------------------------------------
1509 # Dummy fields
1510 # ---------------------------------------------------------
1511
1512 class dummy(function):
1513     def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
1514         return []
1515
1516     def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1517         return False
1518
1519     def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1520         return {}
1521
1522     def __init__(self, *arg, **args):
1523         self.arg = arg
1524         self._relations = []
1525         super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
1526
1527 # ---------------------------------------------------------
1528 # Serialized fields
1529 # ---------------------------------------------------------
1530
1531 class serialized(_column):
1532     """ A field able to store an arbitrary python data structure.
1533     
1534         Note: only plain components allowed.
1535     """
1536     
1537     def _symbol_set_struct(val):
1538         return simplejson.dumps(val)
1539
1540     def _symbol_get_struct(self, val):
1541         return simplejson.loads(val or '{}')
1542     
1543     _prefetch = False
1544     _type = 'serialized'
1545
1546     _symbol_c = '%s'
1547     _symbol_f = _symbol_set_struct
1548     _symbol_set = (_symbol_c, _symbol_f)
1549     _symbol_get = _symbol_get_struct
1550
1551 # TODO: review completly this class for speed improvement
1552 class property(function):
1553
1554     def to_field_args(self):
1555         args = super(property, self).to_field_args()
1556         args['company_dependent'] = True
1557         return args
1558
1559     def _fnct_search(self, tobj, cr, uid, obj, name, domain, context=None):
1560         ir_property = obj.pool['ir.property']
1561         result = []
1562         for field, operator, value in domain:
1563             result += ir_property.search_multi(cr, uid, name, tobj._name, operator, value, context=context)
1564         return result
1565
1566     def _fnct_write(self, obj, cr, uid, id, prop_name, value, obj_dest, context=None):
1567         ir_property = obj.pool['ir.property']
1568         ir_property.set_multi(cr, uid, prop_name, obj._name, {id: value}, context=context)
1569         return True
1570
1571     def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
1572         ir_property = obj.pool['ir.property']
1573
1574         res = {id: {} for id in ids}
1575         for prop_name in prop_names:
1576             column = obj._all_columns[prop_name].column
1577             values = ir_property.get_multi(cr, uid, prop_name, obj._name, ids, context=context)
1578             if column._type == 'many2one':
1579                 # name_get the non-null values as SUPERUSER_ID
1580                 vals = sum(set(filter(None, values.itervalues())),
1581                            obj.pool[column._obj].browse(cr, uid, [], context=context))
1582                 vals_name = dict(vals.sudo().name_get()) if vals else {}
1583                 for id, value in values.iteritems():
1584                     ng = False
1585                     if value and value.id in vals_name:
1586                         ng = value.id, vals_name[value.id]
1587                     res[id][prop_name] = ng
1588             else:
1589                 for id, value in values.iteritems():
1590                     res[id][prop_name] = value
1591
1592         return res
1593
1594     def __init__(self, **args):
1595         if 'view_load' in args:
1596             _logger.warning("view_load attribute is deprecated on ir.fields. Args: %r", args)
1597         args = dict(args)
1598         args['obj'] = args.pop('relation', '') or args.get('obj', '')
1599         super(property, self).__init__(
1600             fnct=self._fnct_read,
1601             fnct_inv=self._fnct_write,
1602             fnct_search=self._fnct_search,
1603             multi='properties',
1604             **args
1605         )
1606
1607
1608 class column_info(object):
1609     """ Struct containing details about an osv column, either one local to
1610         its model, or one inherited via _inherits.
1611
1612         .. attribute:: name
1613
1614             name of the column
1615
1616         .. attribute:: column
1617
1618             column instance, subclass of :class:`_column`
1619
1620         .. attribute:: parent_model
1621
1622             if the column is inherited, name of the model that contains it,
1623             ``None`` for local columns.
1624
1625         .. attribute:: parent_column
1626
1627             the name of the column containing the m2o relationship to the
1628             parent model that contains this column, ``None`` for local columns.
1629
1630         .. attribute:: original_parent
1631
1632             if the column is inherited, name of the original parent model that
1633             contains it i.e in case of multilevel inheritance, ``None`` for
1634             local columns.
1635     """
1636     def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
1637         self.name = name
1638         self.column = column
1639         self.parent_model = parent_model
1640         self.parent_column = parent_column
1641         self.original_parent = original_parent
1642
1643     def __str__(self):
1644         return '%s(%s, %s, %s, %s, %s)' % (
1645             self.__class__.__name__, self.name, self.column,
1646             self.parent_model, self.parent_column, self.original_parent)
1647
1648
1649 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: