fix
[odoo/odoo.git] / openerp / osv / orm.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 #.apidoc title: Object Relational Mapping
23 #.apidoc module-mods: member-order: bysource
24
25 """
26   Object relational mapping to database (postgresql) module
27      * Hierarchical structure
28      * Constraints consistency, validations
29      * Object meta Data depends on its status
30      * Optimised processing by complex query (multiple actions at once)
31      * Default fields value
32      * Permissions optimisation
33      * Persistant object: DB postgresql
34      * Datas conversions
35      * Multi-level caching system
36      * 2 different inheritancies
37      * Fields:
38           - classicals (varchar, integer, boolean, ...)
39           - relations (one2many, many2one, many2many)
40           - functions
41
42 """
43
44 import calendar
45 import copy
46 import datetime
47 import itertools
48 import logging
49 import operator
50 import pickle
51 import re
52 import simplejson
53 import time
54 import types
55 from lxml import etree
56
57 import fields
58 import openerp
59 import openerp.netsvc as netsvc
60 import openerp.tools as tools
61 from openerp.tools.config import config
62 from openerp.tools.safe_eval import safe_eval as eval
63 from openerp.tools.translate import _
64 from openerp import SUPERUSER_ID
65 from query import Query
66
67 _logger = logging.getLogger(__name__)
68 _schema = logging.getLogger(__name__ + '.schema')
69
70 # List of etree._Element subclasses that we choose to ignore when parsing XML.
71 from openerp.tools import SKIPPED_ELEMENT_TYPES
72
73 regex_order = re.compile('^(([a-z0-9_]+|"[a-z0-9_]+")( *desc| *asc)?( *, *|))+$', re.I)
74 regex_object_name = re.compile(r'^[a-z0-9_.]+$')
75
76 def transfer_field_to_modifiers(field, modifiers):
77     default_values = {}
78     state_exceptions = {}
79     for attr in ('invisible', 'readonly', 'required'):
80         state_exceptions[attr] = []
81         default_values[attr] = bool(field.get(attr))
82     for state, modifs in (field.get("states",{})).items():
83         for modif in modifs:
84             if default_values[modif[0]] != modif[1]:
85                 state_exceptions[modif[0]].append(state)
86
87     for attr, default_value in default_values.items():
88         if state_exceptions[attr]:
89             modifiers[attr] = [("state", "not in" if default_value else "in", state_exceptions[attr])]
90         else:
91             modifiers[attr] = default_value
92
93
94 # Don't deal with groups, it is done by check_group().
95 # Need the context to evaluate the invisible attribute on tree views.
96 # For non-tree views, the context shouldn't be given.
97 def transfer_node_to_modifiers(node, modifiers, context=None, in_tree_view=False):
98     if node.get('attrs'):
99         modifiers.update(eval(node.get('attrs')))
100
101     if node.get('states'):
102         if 'invisible' in modifiers and isinstance(modifiers['invisible'], list):
103             # TODO combine with AND or OR, use implicit AND for now.
104             modifiers['invisible'].append(('state', 'not in', node.get('states').split(',')))
105         else:
106             modifiers['invisible'] = [('state', 'not in', node.get('states').split(','))]
107
108     for a in ('invisible', 'readonly', 'required'):
109         if node.get(a):
110             v = bool(eval(node.get(a), {'context': context or {}}))
111             if in_tree_view and a == 'invisible':
112                 # Invisible in a tree view has a specific meaning, make it a
113                 # new key in the modifiers attribute.
114                 modifiers['tree_invisible'] = v
115             elif v or (a not in modifiers or not isinstance(modifiers[a], list)):
116                 # Don't set the attribute to False if a dynamic value was
117                 # provided (i.e. a domain from attrs or states).
118                 modifiers[a] = v
119
120
121 def simplify_modifiers(modifiers):
122     for a in ('invisible', 'readonly', 'required'):
123         if a in modifiers and not modifiers[a]:
124             del modifiers[a]
125
126
127 def transfer_modifiers_to_node(modifiers, node):
128     if modifiers:
129         simplify_modifiers(modifiers)
130         node.set('modifiers', simplejson.dumps(modifiers))
131
132 def setup_modifiers(node, field=None, context=None, in_tree_view=False):
133     """ Processes node attributes and field descriptors to generate
134     the ``modifiers`` node attribute and set it on the provided node.
135
136     Alters its first argument in-place.
137
138     :param node: ``field`` node from an OpenERP view
139     :type node: lxml.etree._Element
140     :param dict field: field descriptor corresponding to the provided node
141     :param dict context: execution context used to evaluate node attributes
142     :param bool in_tree_view: triggers the ``tree_invisible`` code
143                               path (separate from ``invisible``): in
144                               tree view there are two levels of
145                               invisibility, cell content (a column is
146                               present but the cell itself is not
147                               displayed) with ``invisible`` and column
148                               invisibility (the whole column is
149                               hidden) with ``tree_invisible``.
150     :returns: nothing
151     """
152     modifiers = {}
153     if field is not None:
154         transfer_field_to_modifiers(field, modifiers)
155     transfer_node_to_modifiers(
156         node, modifiers, context=context, in_tree_view=in_tree_view)
157     transfer_modifiers_to_node(modifiers, node)
158
159 def test_modifiers(what, expected):
160     modifiers = {}
161     if isinstance(what, basestring):
162         node = etree.fromstring(what)
163         transfer_node_to_modifiers(node, modifiers)
164         simplify_modifiers(modifiers)
165         json = simplejson.dumps(modifiers)
166         assert json == expected, "%s != %s" % (json, expected)
167     elif isinstance(what, dict):
168         transfer_field_to_modifiers(what, modifiers)
169         simplify_modifiers(modifiers)
170         json = simplejson.dumps(modifiers)
171         assert json == expected, "%s != %s" % (json, expected)
172
173
174 # To use this test:
175 # import openerp
176 # openerp.osv.orm.modifiers_tests()
177 def modifiers_tests():
178     test_modifiers('<field name="a"/>', '{}')
179     test_modifiers('<field name="a" invisible="1"/>', '{"invisible": true}')
180     test_modifiers('<field name="a" readonly="1"/>', '{"readonly": true}')
181     test_modifiers('<field name="a" required="1"/>', '{"required": true}')
182     test_modifiers('<field name="a" invisible="0"/>', '{}')
183     test_modifiers('<field name="a" readonly="0"/>', '{}')
184     test_modifiers('<field name="a" required="0"/>', '{}')
185     test_modifiers('<field name="a" invisible="1" required="1"/>', '{"invisible": true, "required": true}') # TODO order is not guaranteed
186     test_modifiers('<field name="a" invisible="1" required="0"/>', '{"invisible": true}')
187     test_modifiers('<field name="a" invisible="0" required="1"/>', '{"required": true}')
188     test_modifiers("""<field name="a" attrs="{'invisible': [('b', '=', 'c')]}"/>""", '{"invisible": [["b", "=", "c"]]}')
189
190     # The dictionary is supposed to be the result of fields_get().
191     test_modifiers({}, '{}')
192     test_modifiers({"invisible": True}, '{"invisible": true}')
193     test_modifiers({"invisible": False}, '{}')
194
195
196 def check_object_name(name):
197     """ Check if the given name is a valid openerp object name.
198
199         The _name attribute in osv and osv_memory object is subject to
200         some restrictions. This function returns True or False whether
201         the given name is allowed or not.
202
203         TODO: this is an approximation. The goal in this approximation
204         is to disallow uppercase characters (in some places, we quote
205         table/column names and in other not, which leads to this kind
206         of errors:
207
208             psycopg2.ProgrammingError: relation "xxx" does not exist).
209
210         The same restriction should apply to both osv and osv_memory
211         objects for consistency.
212
213     """
214     if regex_object_name.match(name) is None:
215         return False
216     return True
217
218 def raise_on_invalid_object_name(name):
219     if not check_object_name(name):
220         msg = "The _name attribute %s is not valid." % name
221         _logger.error(msg)
222         raise except_orm('ValueError', msg)
223
224 POSTGRES_CONFDELTYPES = {
225     'RESTRICT': 'r',
226     'NO ACTION': 'a',
227     'CASCADE': 'c',
228     'SET NULL': 'n',
229     'SET DEFAULT': 'd',
230 }
231
232 def intersect(la, lb):
233     return filter(lambda x: x in lb, la)
234
235 def fix_import_export_id_paths(fieldname):
236     """
237     Fixes the id fields in import and exports, and splits field paths
238     on '/'.
239
240     :param str fieldname: name of the field to import/export
241     :return: split field name
242     :rtype: list of str
243     """
244     fixed_db_id = re.sub(r'([^/])\.id', r'\1/.id', fieldname)
245     fixed_external_id = re.sub(r'([^/]):id', r'\1/id', fixed_db_id)
246     return fixed_external_id.split('/')
247
248 class except_orm(Exception):
249     def __init__(self, name, value):
250         self.name = name
251         self.value = value
252         self.args = (name, value)
253
254 class BrowseRecordError(Exception):
255     pass
256
257 class browse_null(object):
258     """ Readonly python database object browser
259     """
260
261     def __init__(self):
262         self.id = False
263
264     def __getitem__(self, name):
265         return None
266
267     def __getattr__(self, name):
268         return None  # XXX: return self ?
269
270     def __int__(self):
271         return False
272
273     def __str__(self):
274         return ''
275
276     def __nonzero__(self):
277         return False
278
279     def __unicode__(self):
280         return u''
281
282
283 #
284 # TODO: execute an object method on browse_record_list
285 #
286 class browse_record_list(list):
287     """ Collection of browse objects
288
289         Such an instance will be returned when doing a ``browse([ids..])``
290         and will be iterable, yielding browse() objects
291     """
292
293     def __init__(self, lst, context=None):
294         if not context:
295             context = {}
296         super(browse_record_list, self).__init__(lst)
297         self.context = context
298
299
300 class browse_record(object):
301     """ An object that behaves like a row of an object's table.
302         It has attributes after the columns of the corresponding object.
303
304         Examples::
305
306             uobj = pool.get('res.users')
307             user_rec = uobj.browse(cr, uid, 104)
308             name = user_rec.name
309     """
310
311     def __init__(self, cr, uid, id, table, cache, context=None,
312                  list_class=browse_record_list, fields_process=None):
313         """
314         :param table: the browsed object (inherited from orm)
315         :param dict cache: a dictionary of model->field->data to be shared
316                            across browse objects, thus reducing the SQL
317                            read()s. It can speed up things a lot, but also be
318                            disastrous if not discarded after write()/unlink()
319                            operations
320         :param dict context: dictionary with an optional context
321         """
322         if fields_process is None:
323             fields_process = {}
324         if context is None:
325             context = {}
326         self._list_class = list_class
327         self._cr = cr
328         self._uid = uid
329         self._id = id
330         self._table = table # deprecated, use _model!
331         self._model = table
332         self._table_name = self._table._name
333         self.__logger = logging.getLogger('openerp.osv.orm.browse_record.' + self._table_name)
334         self._context = context
335         self._fields_process = fields_process
336
337         cache.setdefault(table._name, {})
338         self._data = cache[table._name]
339
340 #        if not (id and isinstance(id, (int, long,))):
341 #            raise BrowseRecordError(_('Wrong ID for the browse record, got %r, expected an integer.') % (id,))
342 #        if not table.exists(cr, uid, id, context):
343 #            raise BrowseRecordError(_('Object %s does not exists') % (self,))
344
345         if id not in self._data:
346             self._data[id] = {'id': id}
347
348         self._cache = cache
349
350     def __getitem__(self, name):
351         if name == 'id':
352             return self._id
353
354         if name not in self._data[self._id]:
355             # build the list of fields we will fetch
356
357             # fetch the definition of the field which was asked for
358             if name in self._table._columns:
359                 col = self._table._columns[name]
360             elif name in self._table._inherit_fields:
361                 col = self._table._inherit_fields[name][2]
362             elif hasattr(self._table, str(name)):
363                 attr = getattr(self._table, name)
364                 if isinstance(attr, (types.MethodType, types.LambdaType, types.FunctionType)):
365                     def function_proxy(*args, **kwargs):
366                         if 'context' not in kwargs and self._context:
367                             kwargs.update(context=self._context)
368                         return attr(self._cr, self._uid, [self._id], *args, **kwargs)
369                     return function_proxy
370                 else:
371                     return attr
372             else:
373                 error_msg = "Field '%s' does not exist in object '%s'" % (name, self) 
374                 self.__logger.warning(error_msg)
375                 raise KeyError(error_msg)
376
377             # if the field is a classic one or a many2one, we'll fetch all classic and many2one fields
378             if col._prefetch:
379                 # gen the list of "local" (ie not inherited) fields which are classic or many2one
380                 fields_to_fetch = filter(lambda x: x[1]._classic_write, self._table._columns.items())
381                 # gen the list of inherited fields
382                 inherits = map(lambda x: (x[0], x[1][2]), self._table._inherit_fields.items())
383                 # complete the field list with the inherited fields which are classic or many2one
384                 fields_to_fetch += filter(lambda x: x[1]._classic_write, inherits)
385             # otherwise we fetch only that field
386             else:
387                 fields_to_fetch = [(name, col)]
388             ids = filter(lambda id: name not in self._data[id], self._data.keys())
389             # read the results
390             field_names = map(lambda x: x[0], fields_to_fetch)
391             field_values = self._table.read(self._cr, self._uid, ids, field_names, context=self._context, load="_classic_write")
392
393             # TODO: improve this, very slow for reports
394             if self._fields_process:
395                 lang = self._context.get('lang', 'en_US') or 'en_US'
396                 lang_obj_ids = self.pool.get('res.lang').search(self._cr, self._uid, [('code', '=', lang)])
397                 if not lang_obj_ids:
398                     raise Exception(_('Language with code "%s" is not defined in your system !\nDefine it through the Administration menu.') % (lang,))
399                 lang_obj = self.pool.get('res.lang').browse(self._cr, self._uid, lang_obj_ids[0])
400
401                 for field_name, field_column in fields_to_fetch:
402                     if field_column._type in self._fields_process:
403                         for result_line in field_values:
404                             result_line[field_name] = self._fields_process[field_column._type](result_line[field_name])
405                             if result_line[field_name]:
406                                 result_line[field_name].set_value(self._cr, self._uid, result_line[field_name], self, field_column, lang_obj)
407
408             if not field_values:
409                 # Where did those ids come from? Perhaps old entries in ir_model_dat?
410                 _logger.warning("No field_values found for ids %s in %s", ids, self)
411                 raise KeyError('Field %s not found in %s'%(name, self))
412             # create browse records for 'remote' objects
413             for result_line in field_values:
414                 new_data = {}
415                 for field_name, field_column in fields_to_fetch:
416                     if field_column._type == 'many2one':
417                         if result_line[field_name]:
418                             obj = self._table.pool.get(field_column._obj)
419                             if isinstance(result_line[field_name], (list, tuple)):
420                                 value = result_line[field_name][0]
421                             else:
422                                 value = result_line[field_name]
423                             if value:
424                                 # FIXME: this happen when a _inherits object
425                                 #        overwrite a field of it parent. Need
426                                 #        testing to be sure we got the right
427                                 #        object and not the parent one.
428                                 if not isinstance(value, browse_record):
429                                     if obj is None:
430                                         # In some cases the target model is not available yet, so we must ignore it,
431                                         # which is safe in most cases, this value will just be loaded later when needed.
432                                         # This situation can be caused by custom fields that connect objects with m2o without
433                                         # respecting module dependencies, causing relationships to be connected to soon when
434                                         # the target is not loaded yet.
435                                         continue
436                                     new_data[field_name] = browse_record(self._cr,
437                                         self._uid, value, obj, self._cache,
438                                         context=self._context,
439                                         list_class=self._list_class,
440                                         fields_process=self._fields_process)
441                                 else:
442                                     new_data[field_name] = value
443                             else:
444                                 new_data[field_name] = browse_null()
445                         else:
446                             new_data[field_name] = browse_null()
447                     elif field_column._type in ('one2many', 'many2many') and len(result_line[field_name]):
448                         new_data[field_name] = self._list_class([browse_record(self._cr, self._uid, id, self._table.pool.get(field_column._obj), self._cache, context=self._context, list_class=self._list_class, fields_process=self._fields_process) for id in result_line[field_name]], self._context)
449                     elif field_column._type in ('reference'):
450                         if result_line[field_name]:
451                             if isinstance(result_line[field_name], browse_record):
452                                 new_data[field_name] = result_line[field_name]
453                             else:
454                                 ref_obj, ref_id = result_line[field_name].split(',')
455                                 ref_id = long(ref_id)
456                                 if ref_id:
457                                     obj = self._table.pool.get(ref_obj)
458                                     new_data[field_name] = browse_record(self._cr, self._uid, ref_id, obj, self._cache, context=self._context, list_class=self._list_class, fields_process=self._fields_process)
459                                 else:
460                                     new_data[field_name] = browse_null()
461                         else:
462                             new_data[field_name] = browse_null()
463                     else:
464                         new_data[field_name] = result_line[field_name]
465                 self._data[result_line['id']].update(new_data)
466
467         if not name in self._data[self._id]:
468             # How did this happen? Could be a missing model due to custom fields used too soon, see above.
469             self.__logger.error("Fields to fetch: %s, Field values: %s", field_names, field_values)
470             self.__logger.error("Cached: %s, Table: %s", self._data[self._id], self._table)
471             raise KeyError(_('Unknown attribute %s in %s ') % (name, self))
472         return self._data[self._id][name]
473
474     def __getattr__(self, name):
475         try:
476             return self[name]
477         except KeyError, e:
478             raise AttributeError(e)
479
480     def __contains__(self, name):
481         return (name in self._table._columns) or (name in self._table._inherit_fields) or hasattr(self._table, name)
482
483     def __iter__(self):
484         raise NotImplementedError("Iteration is not allowed on %s" % self)
485
486     def __hasattr__(self, name):
487         return name in self
488
489     def __int__(self):
490         return self._id
491
492     def __str__(self):
493         return "browse_record(%s, %d)" % (self._table_name, self._id)
494
495     def __eq__(self, other):
496         if not isinstance(other, browse_record):
497             return False
498         return (self._table_name, self._id) == (other._table_name, other._id)
499
500     def __ne__(self, other):
501         if not isinstance(other, browse_record):
502             return True
503         return (self._table_name, self._id) != (other._table_name, other._id)
504
505     # we need to define __unicode__ even though we've already defined __str__
506     # because we have overridden __getattr__
507     def __unicode__(self):
508         return unicode(str(self))
509
510     def __hash__(self):
511         return hash((self._table_name, self._id))
512
513     __repr__ = __str__
514
515     def refresh(self):
516         """Force refreshing this browse_record's data and all the data of the
517            records that belong to the same cache, by emptying the cache completely,
518            preserving only the record identifiers (for prefetching optimizations).
519         """
520         for model, model_cache in self._cache.iteritems():
521             # only preserve the ids of the records that were in the cache
522             cached_ids = dict([(i, {'id': i}) for i in model_cache.keys()])
523             self._cache[model].clear()
524             self._cache[model].update(cached_ids)
525
526 def pg_varchar(size=0):
527     """ Returns the VARCHAR declaration for the provided size:
528
529     * If no size (or an empty or negative size is provided) return an
530       'infinite' VARCHAR
531     * Otherwise return a VARCHAR(n)
532
533     :type int size: varchar size, optional
534     :rtype: str
535     """
536     if size:
537         if not isinstance(size, int):
538             raise TypeError("VARCHAR parameter should be an int, got %s"
539                             % type(size))
540         if size > 0:
541             return 'VARCHAR(%d)' % size
542     return 'VARCHAR'
543
544 FIELDS_TO_PGTYPES = {
545     fields.boolean: 'bool',
546     fields.integer: 'int4',
547     fields.text: 'text',
548     fields.html: 'text',
549     fields.date: 'date',
550     fields.datetime: 'timestamp',
551     fields.binary: 'bytea',
552     fields.many2one: 'int4',
553     fields.serialized: 'text',
554 }
555
556 def get_pg_type(f, type_override=None):
557     """
558     :param fields._column f: field to get a Postgres type for
559     :param type type_override: use the provided type for dispatching instead of the field's own type
560     :returns: (postgres_identification_type, postgres_type_specification)
561     :rtype: (str, str)
562     """
563     field_type = type_override or type(f)
564
565     if field_type in FIELDS_TO_PGTYPES:
566         pg_type =  (FIELDS_TO_PGTYPES[field_type], FIELDS_TO_PGTYPES[field_type])
567     elif issubclass(field_type, fields.float):
568         if f.digits:
569             pg_type = ('numeric', 'NUMERIC')
570         else:
571             pg_type = ('float8', 'DOUBLE PRECISION')
572     elif issubclass(field_type, (fields.char, fields.reference)):
573         pg_type = ('varchar', pg_varchar(f.size))
574     elif issubclass(field_type, fields.selection):
575         if (isinstance(f.selection, list) and isinstance(f.selection[0][0], int))\
576                 or getattr(f, 'size', None) == -1:
577             pg_type = ('int4', 'INTEGER')
578         else:
579             pg_type = ('varchar', pg_varchar(getattr(f, 'size', None)))
580     elif issubclass(field_type, fields.function):
581         if f._type == 'selection':
582             pg_type = ('varchar', pg_varchar())
583         else:
584             pg_type = get_pg_type(f, getattr(fields, f._type))
585     else:
586         _logger.warning('%s type not supported!', field_type)
587         pg_type = None
588
589     return pg_type
590
591
592 class MetaModel(type):
593     """ Metaclass for the Model.
594
595     This class is used as the metaclass for the Model class to discover
596     the models defined in a module (i.e. without instanciating them).
597     If the automatic discovery is not needed, it is possible to set the
598     model's _register attribute to False.
599
600     """
601
602     module_to_models = {}
603
604     def __init__(self, name, bases, attrs):
605         if not self._register:
606             self._register = True
607             super(MetaModel, self).__init__(name, bases, attrs)
608             return
609
610         # The (OpenERP) module name can be in the `openerp.addons` namespace
611         # or not. For instance module `sale` can be imported as
612         # `openerp.addons.sale` (the good way) or `sale` (for backward
613         # compatibility).
614         module_parts = self.__module__.split('.')
615         if len(module_parts) > 2 and module_parts[0] == 'openerp' and \
616             module_parts[1] == 'addons':
617             module_name = self.__module__.split('.')[2]
618         else:
619             module_name = self.__module__.split('.')[0]
620         if not hasattr(self, '_module'):
621             self._module = module_name
622
623         # Remember which models to instanciate for this module.
624         self.module_to_models.setdefault(self._module, []).append(self)
625
626
627 # Definition of log access columns, automatically added to models if
628 # self._log_access is True
629 LOG_ACCESS_COLUMNS = {
630     'create_uid': 'INTEGER REFERENCES res_users ON DELETE SET NULL',
631     'create_date': 'TIMESTAMP',
632     'write_uid': 'INTEGER REFERENCES res_users ON DELETE SET NULL',
633     'write_date': 'TIMESTAMP'
634 }
635 # special columns automatically created by the ORM
636 MAGIC_COLUMNS =  ['id'] + LOG_ACCESS_COLUMNS.keys()
637
638 class BaseModel(object):
639     """ Base class for OpenERP models.
640
641     OpenERP models are created by inheriting from this class' subclasses:
642
643         * Model: for regular database-persisted models
644         * TransientModel: for temporary data, stored in the database but automatically
645                           vaccuumed every so often
646         * AbstractModel: for abstract super classes meant to be shared by multiple
647                         _inheriting classes (usually Models or TransientModels)
648
649     The system will later instantiate the class once per database (on
650     which the class' module is installed).
651
652     To create a class that should not be instantiated, the _register class attribute
653     may be set to False.
654     """
655     __metaclass__ = MetaModel
656     _register = False # Set to false if the model shouldn't be automatically discovered.
657     _name = None
658     _columns = {}
659     _constraints = []
660     _defaults = {}
661     _rec_name = None
662     _parent_name = 'parent_id'
663     _parent_store = False
664     _parent_order = False
665     _date_name = 'date'
666     _order = 'id'
667     _sequence = None
668     _description = None
669     _needaction = False
670
671     # dict of {field:method}, with method returning the name_get of records
672     # to include in the _read_group, if grouped on this field
673     _group_by_full = {}
674
675     # Transience
676     _transient = False # True in a TransientModel
677     _transient_max_count = None
678     _transient_max_hours = None
679     _transient_check_time = 20
680
681     # structure:
682     #  { 'parent_model': 'm2o_field', ... }
683     _inherits = {}
684
685     # Mapping from inherits'd field name to triple (m, r, f, n) where m is the
686     # model from which it is inherits'd, r is the (local) field towards m, f
687     # is the _column object itself, and n is the original (i.e. top-most)
688     # parent model.
689     # Example:
690     #  { 'field_name': ('parent_model', 'm2o_field_to_reach_parent',
691     #                   field_column_obj, origina_parent_model), ... }
692     _inherit_fields = {}
693
694     # Mapping field name/column_info object
695     # This is similar to _inherit_fields but:
696     # 1. includes self fields,
697     # 2. uses column_info instead of a triple.
698     _all_columns = {}
699
700     _table = None
701     _invalids = set()
702     _log_create = False
703     _sql_constraints = []
704     _protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists']
705
706     CONCURRENCY_CHECK_FIELD = '__last_update'
707
708     def log(self, cr, uid, id, message, secondary=False, context=None):
709         return _logger.warning("log() is deprecated. Please use OpenChatter notification system instead of the res.log mechanism.")
710     
711     def view_init(self, cr, uid, fields_list, context=None):
712         """Override this method to do specific things when a view on the object is opened."""
713         pass
714
715     def _field_create(self, cr, context=None):
716         """ Create entries in ir_model_fields for all the model's fields.
717
718         If necessary, also create an entry in ir_model, and if called from the
719         modules loading scheme (by receiving 'module' in the context), also
720         create entries in ir_model_data (for the model and the fields).
721
722         - create an entry in ir_model (if there is not already one),
723         - create an entry in ir_model_data (if there is not already one, and if
724           'module' is in the context),
725         - update ir_model_fields with the fields found in _columns
726           (TODO there is some redundancy as _columns is updated from
727           ir_model_fields in __init__).
728
729         """
730         if context is None:
731             context = {}
732         cr.execute("SELECT id FROM ir_model WHERE model=%s", (self._name,))
733         if not cr.rowcount:
734             cr.execute('SELECT nextval(%s)', ('ir_model_id_seq',))
735             model_id = cr.fetchone()[0]
736             cr.execute("INSERT INTO ir_model (id,model, name, info,state) VALUES (%s, %s, %s, %s, %s)", (model_id, self._name, self._description, self.__doc__, 'base'))
737         else:
738             model_id = cr.fetchone()[0]
739         if 'module' in context:
740             name_id = 'model_'+self._name.replace('.', '_')
741             cr.execute('select * from ir_model_data where name=%s and module=%s', (name_id, context['module']))
742             if not cr.rowcount:
743                 cr.execute("INSERT INTO ir_model_data (name,date_init,date_update,module,model,res_id) VALUES (%s, (now() at time zone 'UTC'), (now() at time zone 'UTC'), %s, %s, %s)", \
744                     (name_id, context['module'], 'ir.model', model_id)
745                 )
746
747         cr.commit()
748
749         cr.execute("SELECT * FROM ir_model_fields WHERE model=%s", (self._name,))
750         cols = {}
751         for rec in cr.dictfetchall():
752             cols[rec['name']] = rec
753
754         ir_model_fields_obj = self.pool.get('ir.model.fields')
755
756         # sparse field should be created at the end, as it depends on its serialized field already existing
757         model_fields = sorted(self._columns.items(), key=lambda x: 1 if x[1]._type == 'sparse' else 0)
758         for (k, f) in model_fields:
759             vals = {
760                 'model_id': model_id,
761                 'model': self._name,
762                 'name': k,
763                 'field_description': f.string,
764                 'ttype': f._type,
765                 'relation': f._obj or '',
766                 'view_load': (f.view_load and 1) or 0,
767                 'select_level': tools.ustr(f.select or 0),
768                 'readonly': (f.readonly and 1) or 0,
769                 'required': (f.required and 1) or 0,
770                 'selectable': (f.selectable and 1) or 0,
771                 'translate': (f.translate and 1) or 0,
772                 'relation_field': f._fields_id if isinstance(f, fields.one2many) else '',
773                 'serialization_field_id': None,
774             }
775             if getattr(f, 'serialization_field', None):
776                 # resolve link to serialization_field if specified by name
777                 serialization_field_id = ir_model_fields_obj.search(cr, 1, [('model','=',vals['model']), ('name', '=', f.serialization_field)])
778                 if not serialization_field_id:
779                     raise except_orm(_('Error'), _("Serialization field `%s` not found for sparse field `%s`!") % (f.serialization_field, k))
780                 vals['serialization_field_id'] = serialization_field_id[0]
781
782             # When its a custom field,it does not contain f.select
783             if context.get('field_state', 'base') == 'manual':
784                 if context.get('field_name', '') == k:
785                     vals['select_level'] = context.get('select', '0')
786                 #setting value to let the problem NOT occur next time
787                 elif k in cols:
788                     vals['select_level'] = cols[k]['select_level']
789
790             if k not in cols:
791                 cr.execute('select nextval(%s)', ('ir_model_fields_id_seq',))
792                 id = cr.fetchone()[0]
793                 vals['id'] = id
794                 cr.execute("""INSERT INTO ir_model_fields (
795                     id, model_id, model, name, field_description, ttype,
796                     relation,view_load,state,select_level,relation_field, translate, serialization_field_id
797                 ) VALUES (
798                     %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s
799                 )""", (
800                     id, vals['model_id'], vals['model'], vals['name'], vals['field_description'], vals['ttype'],
801                      vals['relation'], bool(vals['view_load']), 'base',
802                     vals['select_level'], vals['relation_field'], bool(vals['translate']), vals['serialization_field_id']
803                 ))
804                 if 'module' in context:
805                     name1 = 'field_' + self._table + '_' + k
806                     cr.execute("select name from ir_model_data where name=%s", (name1,))
807                     if cr.fetchone():
808                         name1 = name1 + "_" + str(id)
809                     cr.execute("INSERT INTO ir_model_data (name,date_init,date_update,module,model,res_id) VALUES (%s, (now() at time zone 'UTC'), (now() at time zone 'UTC'), %s, %s, %s)", \
810                         (name1, context['module'], 'ir.model.fields', id)
811                     )
812             else:
813                 for key, val in vals.items():
814                     if cols[k][key] != vals[key]:
815                         cr.execute('update ir_model_fields set field_description=%s where model=%s and name=%s', (vals['field_description'], vals['model'], vals['name']))
816                         cr.commit()
817                         cr.execute("""UPDATE ir_model_fields SET
818                             model_id=%s, field_description=%s, ttype=%s, relation=%s,
819                             view_load=%s, select_level=%s, readonly=%s ,required=%s, selectable=%s, relation_field=%s, translate=%s, serialization_field_id=%s
820                         WHERE
821                             model=%s AND name=%s""", (
822                                 vals['model_id'], vals['field_description'], vals['ttype'],
823                                 vals['relation'], bool(vals['view_load']),
824                                 vals['select_level'], bool(vals['readonly']), bool(vals['required']), bool(vals['selectable']), vals['relation_field'], bool(vals['translate']), vals['serialization_field_id'], vals['model'], vals['name']
825                             ))
826                         break
827         cr.commit()
828
829     #
830     # Goal: try to apply inheritance at the instanciation level and
831     #       put objects in the pool var
832     #
833     @classmethod
834     def create_instance(cls, pool, cr):
835         """ Instanciate a given model.
836
837         This class method instanciates the class of some model (i.e. a class
838         deriving from osv or osv_memory). The class might be the class passed
839         in argument or, if it inherits from another class, a class constructed
840         by combining the two classes.
841
842         The ``attributes`` argument specifies which parent class attributes
843         have to be combined.
844
845         TODO: the creation of the combined class is repeated at each call of
846         this method. This is probably unnecessary.
847
848         """
849         attributes = ['_columns', '_defaults', '_inherits', '_constraints',
850             '_sql_constraints']
851
852         parent_names = getattr(cls, '_inherit', None)
853         if parent_names:
854             if isinstance(parent_names, (str, unicode)):
855                 name = cls._name or parent_names
856                 parent_names = [parent_names]
857             else:
858                 name = cls._name
859             # for res.parnter.address compatiblity, should be remove in v7
860             if 'res.partner.address' in parent_names:
861                 parent_names.pop(parent_names.index('res.partner.address'))
862                 parent_names.append('res.partner')
863             if not name:
864                 raise TypeError('_name is mandatory in case of multiple inheritance')
865
866             for parent_name in ((type(parent_names)==list) and parent_names or [parent_names]):
867                 parent_model = pool.get(parent_name)
868                 if not parent_model:
869                     raise TypeError('The model "%s" specifies an unexisting parent class "%s"\n'
870                         'You may need to add a dependency on the parent class\' module.' % (name, parent_name))
871                 if not getattr(cls, '_original_module', None) and name == parent_model._name:
872                     cls._original_module = parent_model._original_module
873                 parent_class = parent_model.__class__
874                 nattr = {}
875                 for s in attributes:
876                     new = copy.copy(getattr(parent_model, s, {}))
877                     if s == '_columns':
878                         # Don't _inherit custom fields.
879                         for c in new.keys():
880                             if new[c].manual:
881                                 del new[c]
882                         # Duplicate float fields because they have a .digits
883                         # cache (which must be per-registry, not server-wide).
884                         for c in new.keys():
885                             if new[c]._type == 'float':
886                                 new[c] = copy.copy(new[c])
887                     if hasattr(new, 'update'):
888                         new.update(cls.__dict__.get(s, {}))
889                     elif s=='_constraints':
890                         for c in cls.__dict__.get(s, []):
891                             exist = False
892                             for c2 in range(len(new)):
893                                  #For _constraints, we should check field and methods as well
894                                  if new[c2][2]==c[2] and (new[c2][0] == c[0] \
895                                         or getattr(new[c2][0],'__name__', True) == \
896                                             getattr(c[0],'__name__', False)):
897                                     # If new class defines a constraint with
898                                     # same function name, we let it override
899                                     # the old one.
900                                     
901                                     new[c2] = c
902                                     exist = True
903                                     break
904                             if not exist:
905                                 new.append(c)
906                     else:
907                         new.extend(cls.__dict__.get(s, []))
908                     nattr[s] = new
909                 cls = type(name, (cls, parent_class), dict(nattr, _register=False))
910         if not getattr(cls, '_original_module', None):
911             cls._original_module = cls._module
912         obj = object.__new__(cls)
913         obj.__init__(pool, cr)
914         return obj
915
916     def __new__(cls):
917         """Register this model.
918
919         This doesn't create an instance but simply register the model
920         as being part of the module where it is defined.
921
922         """
923
924
925         # Set the module name (e.g. base, sale, accounting, ...) on the class.
926         module = cls.__module__.split('.')[0]
927         if not hasattr(cls, '_module'):
928             cls._module = module
929
930         # Record this class in the list of models to instantiate for this module,
931         # managed by the metaclass.
932         module_model_list = MetaModel.module_to_models.setdefault(cls._module, [])
933         if cls not in module_model_list:
934             module_model_list.append(cls)
935
936         # Since we don't return an instance here, the __init__
937         # method won't be called.
938         return None
939
940     def __init__(self, pool, cr):
941         """ Initialize a model and make it part of the given registry.
942
943         - copy the stored fields' functions in the osv_pool,
944         - update the _columns with the fields found in ir_model_fields,
945         - ensure there is a many2one for each _inherits'd parent,
946         - update the children's _columns,
947         - give a chance to each field to initialize itself.
948
949         """
950         pool.add(self._name, self)
951         self.pool = pool
952
953         if not self._name and not hasattr(self, '_inherit'):
954             name = type(self).__name__.split('.')[0]
955             msg = "The class %s has to have a _name attribute" % name
956
957             _logger.error(msg)
958             raise except_orm('ValueError', msg)
959
960         if not self._description:
961             self._description = self._name
962         if not self._table:
963             self._table = self._name.replace('.', '_')
964
965         if not hasattr(self, '_log_access'):
966             # If _log_access is not specified, it is the same value as _auto.
967             self._log_access = getattr(self, "_auto", True)
968
969         self._columns = self._columns.copy()
970         for store_field in self._columns:
971             f = self._columns[store_field]
972             if hasattr(f, 'digits_change'):
973                 f.digits_change(cr)
974             def not_this_field(stored_func):
975                 x, y, z, e, f, l = stored_func
976                 return x != self._name or y != store_field
977             self.pool._store_function[self._name] = filter(not_this_field, self.pool._store_function.get(self._name, []))
978             if not isinstance(f, fields.function):
979                 continue
980             if not f.store:
981                 continue
982             sm = f.store
983             if sm is True:
984                 sm = {self._name: (lambda self, cr, uid, ids, c={}: ids, None, 10, None)}
985             for object, aa in sm.items():
986                 if len(aa) == 4:
987                     (fnct, fields2, order, length) = aa
988                 elif len(aa) == 3:
989                     (fnct, fields2, order) = aa
990                     length = None
991                 else:
992                     raise except_orm('Error',
993                         ('Invalid function definition %s in object %s !\nYou must use the definition: store={object:(fnct, fields, priority, time length)}.' % (store_field, self._name)))
994                 self.pool._store_function.setdefault(object, [])
995                 self.pool._store_function[object].append((self._name, store_field, fnct, tuple(fields2) if fields2 else None, order, length))
996                 self.pool._store_function[object].sort(lambda x, y: cmp(x[4], y[4]))
997
998         for (key, _, msg) in self._sql_constraints:
999             self.pool._sql_error[self._table+'_'+key] = msg
1000
1001         # Load manual fields
1002
1003         cr.execute("SELECT id FROM ir_model_fields WHERE name=%s AND model=%s", ('state', 'ir.model.fields'))
1004         if cr.fetchone():
1005             cr.execute('SELECT * FROM ir_model_fields WHERE model=%s AND state=%s', (self._name, 'manual'))
1006             for field in cr.dictfetchall():
1007                 if field['name'] in self._columns:
1008                     continue
1009                 attrs = {
1010                     'string': field['field_description'],
1011                     'required': bool(field['required']),
1012                     'readonly': bool(field['readonly']),
1013                     'domain': eval(field['domain']) if field['domain'] else None,
1014                     'size': field['size'],
1015                     'ondelete': field['on_delete'],
1016                     'translate': (field['translate']),
1017                     'manual': True,
1018                     #'select': int(field['select_level'])
1019                 }
1020
1021                 if field['serialization_field_id']:
1022                     cr.execute('SELECT name FROM ir_model_fields WHERE id=%s', (field['serialization_field_id'],))
1023                     attrs.update({'serialization_field': cr.fetchone()[0], 'type': field['ttype']})
1024                     if field['ttype'] in ['many2one', 'one2many', 'many2many']:
1025                         attrs.update({'relation': field['relation']})
1026                     self._columns[field['name']] = fields.sparse(**attrs)
1027                 elif field['ttype'] == 'selection':
1028                     self._columns[field['name']] = fields.selection(eval(field['selection']), **attrs)
1029                 elif field['ttype'] == 'reference':
1030                     self._columns[field['name']] = fields.reference(selection=eval(field['selection']), **attrs)
1031                 elif field['ttype'] == 'many2one':
1032                     self._columns[field['name']] = fields.many2one(field['relation'], **attrs)
1033                 elif field['ttype'] == 'one2many':
1034                     self._columns[field['name']] = fields.one2many(field['relation'], field['relation_field'], **attrs)
1035                 elif field['ttype'] == 'many2many':
1036                     _rel1 = field['relation'].replace('.', '_')
1037                     _rel2 = field['model'].replace('.', '_')
1038                     _rel_name = 'x_%s_%s_%s_rel' % (_rel1, _rel2, field['name'])
1039                     self._columns[field['name']] = fields.many2many(field['relation'], _rel_name, 'id1', 'id2', **attrs)
1040                 else:
1041                     self._columns[field['name']] = getattr(fields, field['ttype'])(**attrs)
1042         self._inherits_check()
1043         self._inherits_reload()
1044         if not self._sequence:
1045             self._sequence = self._table + '_id_seq'
1046         for k in self._defaults:
1047             assert (k in self._columns) or (k in self._inherit_fields), 'Default function defined in %s but field %s does not exist !' % (self._name, k,)
1048         for f in self._columns:
1049             self._columns[f].restart()
1050
1051         # Transience
1052         if self.is_transient():
1053             self._transient_check_count = 0
1054             self._transient_max_count = config.get('osv_memory_count_limit')
1055             self._transient_max_hours = config.get('osv_memory_age_limit')
1056             assert self._log_access, "TransientModels must have log_access turned on, "\
1057                                      "in order to implement their access rights policy"
1058
1059         # Validate rec_name
1060         if self._rec_name is not None:
1061             assert self._rec_name in self._columns.keys() + ['id'], "Invalid rec_name %s for model %s" % (self._rec_name, self._name)
1062         else:
1063             self._rec_name = 'name'
1064
1065
1066     def __export_row(self, cr, uid, row, fields, context=None):
1067         if context is None:
1068             context = {}
1069
1070         def check_type(field_type):
1071             if field_type == 'float':
1072                 return 0.0
1073             elif field_type == 'integer':
1074                 return 0
1075             elif field_type == 'boolean':
1076                 return 'False'
1077             return ''
1078
1079         def selection_field(in_field):
1080             col_obj = self.pool.get(in_field.keys()[0])
1081             if f[i] in col_obj._columns.keys():
1082                 return  col_obj._columns[f[i]]
1083             elif f[i] in col_obj._inherits.keys():
1084                 selection_field(col_obj._inherits)
1085             else:
1086                 return False
1087
1088         def _get_xml_id(self, cr, uid, r):
1089             model_data = self.pool.get('ir.model.data')
1090             data_ids = model_data.search(cr, uid, [('model', '=', r._table_name), ('res_id', '=', r['id'])])
1091             if len(data_ids):
1092                 d = model_data.read(cr, uid, data_ids, ['name', 'module'])[0]
1093                 if d['module']:
1094                     r = '%s.%s' % (d['module'], d['name'])
1095                 else:
1096                     r = d['name']
1097             else:
1098                 postfix = 0
1099                 while True:
1100                     n = self._table+'_'+str(r['id']) + (postfix and ('_'+str(postfix)) or '' )
1101                     if not model_data.search(cr, uid, [('name', '=', n)]):
1102                         break
1103                     postfix += 1
1104                 model_data.create(cr, uid, {
1105                     'name': n,
1106                     'model': self._name,
1107                     'res_id': r['id'],
1108                     'module': '__export__',
1109                 })
1110                 r = '__export__.'+n
1111             return r
1112
1113         lines = []
1114         data = map(lambda x: '', range(len(fields)))
1115         done = []
1116         for fpos in range(len(fields)):
1117             f = fields[fpos]
1118             if f:
1119                 r = row
1120                 i = 0
1121                 while i < len(f):
1122                     cols = False
1123                     if f[i] == '.id':
1124                         r = r['id']
1125                     elif f[i] == 'id':
1126                         r = _get_xml_id(self, cr, uid, r)
1127                     else:
1128                         r = r[f[i]]
1129                         # To display external name of selection field when its exported
1130                         if f[i] in self._columns.keys():
1131                             cols = self._columns[f[i]]
1132                         elif f[i] in self._inherit_fields.keys():
1133                             cols = selection_field(self._inherits)
1134                         if cols and cols._type == 'selection':
1135                             sel_list = cols.selection
1136                             if r and type(sel_list) == type([]):
1137                                 r = [x[1] for x in sel_list if r==x[0]]
1138                                 r = r and r[0] or False
1139                     if not r:
1140                         if f[i] in self._columns:
1141                             r = check_type(self._columns[f[i]]._type)
1142                         elif f[i] in self._inherit_fields:
1143                             r = check_type(self._inherit_fields[f[i]][2]._type)
1144                         data[fpos] = r or False
1145                         break
1146                     if isinstance(r, (browse_record_list, list)):
1147                         first = True
1148                         fields2 = map(lambda x: (x[:i+1]==f[:i+1] and x[i+1:]) \
1149                                 or [], fields)
1150                         if fields2 in done:
1151                             if [x for x in fields2 if x]:
1152                                 break
1153                         done.append(fields2)
1154                         if cols and cols._type=='many2many' and len(fields[fpos])>(i+1) and (fields[fpos][i+1]=='id'):
1155                             data[fpos] = ','.join([_get_xml_id(self, cr, uid, x) for x in r])
1156                             break
1157
1158                         for row2 in r:
1159                             lines2 = row2._model.__export_row(cr, uid, row2, fields2,
1160                                     context)
1161                             if first:
1162                                 for fpos2 in range(len(fields)):
1163                                     if lines2 and lines2[0][fpos2]:
1164                                         data[fpos2] = lines2[0][fpos2]
1165                                 if not data[fpos]:
1166                                     dt = ''
1167                                     for rr in r:
1168                                         name_relation = self.pool.get(rr._table_name)._rec_name
1169                                         if isinstance(rr[name_relation], browse_record):
1170                                             rr = rr[name_relation]
1171                                         rr_name = self.pool.get(rr._table_name).name_get(cr, uid, [rr.id], context=context)
1172                                         rr_name = rr_name and rr_name[0] and rr_name[0][1] or ''
1173                                         dt += tools.ustr(rr_name or '') + ','
1174                                     data[fpos] = dt[:-1]
1175                                     break
1176                                 lines += lines2[1:]
1177                                 first = False
1178                             else:
1179                                 lines += lines2
1180                         break
1181                     i += 1
1182                 if i == len(f):
1183                     if isinstance(r, browse_record):
1184                         r = self.pool.get(r._table_name).name_get(cr, uid, [r.id], context=context)
1185                         r = r and r[0] and r[0][1] or ''
1186                     data[fpos] = tools.ustr(r or '')
1187         return [data] + lines
1188
1189     def export_data(self, cr, uid, ids, fields_to_export, context=None):
1190         """
1191         Export fields for selected objects
1192
1193         :param cr: database cursor
1194         :param uid: current user id
1195         :param ids: list of ids
1196         :param fields_to_export: list of fields
1197         :param context: context arguments, like lang, time zone
1198         :rtype: dictionary with a *datas* matrix
1199
1200         This method is used when exporting data via client menu
1201
1202         """
1203         if context is None:
1204             context = {}
1205         cols = self._columns.copy()
1206         for f in self._inherit_fields:
1207             cols.update({f: self._inherit_fields[f][2]})
1208         fields_to_export = map(fix_import_export_id_paths, fields_to_export)
1209         datas = []
1210         for row in self.browse(cr, uid, ids, context):
1211             datas += self.__export_row(cr, uid, row, fields_to_export, context)
1212         return {'datas': datas}
1213
1214     def import_data(self, cr, uid, fields, datas, mode='init', current_module='', noupdate=False, context=None, filename=None):
1215         """Import given data in given module
1216
1217         This method is used when importing data via client menu.
1218
1219         Example of fields to import for a sale.order::
1220
1221             .id,                         (=database_id)
1222             partner_id,                  (=name_search)
1223             order_line/.id,              (=database_id)
1224             order_line/name,
1225             order_line/product_id/id,    (=xml id)
1226             order_line/price_unit,
1227             order_line/product_uom_qty,
1228             order_line/product_uom/id    (=xml_id)
1229
1230         This method returns a 4-tuple with the following structure::
1231
1232             (return_code, errored_resource, error_message, unused)
1233
1234         * The first item is a return code, it is ``-1`` in case of
1235           import error, or the last imported row number in case of success
1236         * The second item contains the record data dict that failed to import
1237           in case of error, otherwise it's 0
1238         * The third item contains an error message string in case of error,
1239           otherwise it's 0
1240         * The last item is currently unused, with no specific semantics
1241
1242         :param fields: list of fields to import
1243         :param data: data to import
1244         :param mode: 'init' or 'update' for record creation
1245         :param current_module: module name
1246         :param noupdate: flag for record creation
1247         :param filename: optional file to store partial import state for recovery
1248         :returns: 4-tuple in the form (return_code, errored_resource, error_message, unused)
1249         :rtype: (int, dict or 0, str or 0, str or 0)
1250         """
1251         if not context:
1252             context = {}
1253         fields = map(fix_import_export_id_paths, fields)
1254         ir_model_data_obj = self.pool.get('ir.model.data')
1255
1256         # mode: id (XML id) or .id (database id) or False for name_get
1257         def _get_id(model_name, id, current_module=False, mode='id'):
1258             if mode=='.id':
1259                 id = int(id)
1260                 obj_model = self.pool.get(model_name)
1261                 ids = obj_model.search(cr, uid, [('id', '=', int(id))])
1262                 if not len(ids):
1263                     raise Exception(_("Database ID doesn't exist: %s : %s") %(model_name, id))
1264             elif mode=='id':
1265                 if '.' in id:
1266                     module, xml_id = id.rsplit('.', 1)
1267                 else:
1268                     module, xml_id = current_module, id
1269                 record_id = ir_model_data_obj._get_id(cr, uid, module, xml_id)
1270                 ir_model_data = ir_model_data_obj.read(cr, uid, [record_id], ['res_id'])
1271                 if not ir_model_data:
1272                     raise ValueError('No references to %s.%s' % (module, xml_id))
1273                 id = ir_model_data[0]['res_id']
1274             else:
1275                 obj_model = self.pool.get(model_name)
1276                 ids = obj_model.name_search(cr, uid, id, operator='=', context=context)
1277                 if not ids:
1278                     raise ValueError('No record found for %s' % (id,))
1279                 id = ids[0][0]
1280             return id
1281
1282         # IN:
1283         #   datas: a list of records, each record is defined by a list of values
1284         #   prefix: a list of prefix fields ['line_ids']
1285         #   position: the line to process, skip is False if it's the first line of the current record
1286         # OUT:
1287         #   (res, position, warning, res_id) with
1288         #     res: the record for the next line to process (including it's one2many)
1289         #     position: the new position for the next line
1290         #     res_id: the ID of the record if it's a modification
1291         def process_liness(self, datas, prefix, current_module, model_name, fields_def, position=0, skip=0):
1292             line = datas[position]
1293             row = {}
1294             warning = []
1295             data_res_id = False
1296             xml_id = False
1297             nbrmax = position+1
1298
1299             done = {}
1300             for i, field in enumerate(fields):
1301                 res = False
1302                 if i >= len(line):
1303                     raise Exception(_('Please check that all your lines have %d columns.'
1304                         'Stopped around line %d having %d columns.') % \
1305                             (len(fields), position+2, len(line)))
1306                 if not line[i]:
1307                     continue
1308
1309                 if field[:len(prefix)] <> prefix:
1310                     if line[i] and skip:
1311                         return False
1312                     continue
1313                 field_name = field[len(prefix)]
1314
1315                 #set the mode for m2o, o2m, m2m : xml_id/id/name
1316                 if len(field) == len(prefix)+1:
1317                     mode = False
1318                 else:
1319                     mode = field[len(prefix)+1]
1320
1321                 # TODO: improve this by using csv.csv_reader
1322                 def many_ids(line, relation, current_module, mode):
1323                     res = []
1324                     for db_id in line.split(config.get('csv_internal_sep')):
1325                         res.append(_get_id(relation, db_id, current_module, mode))
1326                     return [(6,0,res)]
1327
1328                 # ID of the record using a XML ID
1329                 if field_name == 'id':
1330                     try:
1331                         data_res_id = _get_id(model_name, line[i], current_module)
1332                     except ValueError:
1333                         pass
1334                     xml_id = line[i]
1335                     continue
1336
1337                 # ID of the record using a database ID
1338                 elif field_name == '.id':
1339                     data_res_id = _get_id(model_name, line[i], current_module, '.id')
1340                     continue
1341
1342                 field_type = fields_def[field_name]['type']
1343                 # recursive call for getting children and returning [(0,0,{})] or [(1,ID,{})]
1344                 if field_type == 'one2many':
1345                     if field_name in done:
1346                         continue
1347                     done[field_name] = True
1348                     relation = fields_def[field_name]['relation']
1349                     relation_obj = self.pool.get(relation)
1350                     newfd = relation_obj.fields_get( cr, uid, context=context )
1351                     pos = position
1352
1353                     res = []
1354
1355                     first = 0
1356                     while pos < len(datas):
1357                         res2 = process_liness(self, datas, prefix + [field_name], current_module, relation_obj._name, newfd, pos, first)
1358                         if not res2:
1359                             break
1360                         (newrow, pos, w2, data_res_id2, xml_id2) = res2
1361                         nbrmax = max(nbrmax, pos)
1362                         warning += w2
1363                         first += 1
1364
1365                         if (not newrow) or not reduce(lambda x, y: x or y, newrow.values(), 0):
1366                             break
1367
1368                         res.append( (data_res_id2 and 1 or 0, data_res_id2 or 0, newrow) )
1369
1370                 elif field_type == 'many2one':
1371                     relation = fields_def[field_name]['relation']
1372                     res = _get_id(relation, line[i], current_module, mode)
1373
1374                 elif field_type == 'many2many':
1375                     relation = fields_def[field_name]['relation']
1376                     res = many_ids(line[i], relation, current_module, mode)
1377
1378                 elif field_type == 'integer':
1379                     res = line[i] and int(line[i]) or 0
1380                 elif field_type == 'boolean':
1381                     res = line[i].lower() not in ('0', 'false', 'off')
1382                 elif field_type == 'float':
1383                     res = line[i] and float(line[i]) or 0.0
1384                 elif field_type == 'selection':
1385                     for key, val in fields_def[field_name]['selection']:
1386                         if tools.ustr(line[i]) in [tools.ustr(key), tools.ustr(val)]:
1387                             res = key
1388                             break
1389                     if line[i] and not res:
1390                         _logger.warning(
1391                             _("key '%s' not found in selection field '%s'"),
1392                             tools.ustr(line[i]), tools.ustr(field_name))
1393                         warning.append(_("Key/value '%s' not found in selection field '%s'") % (
1394                             tools.ustr(line[i]), tools.ustr(field_name)))
1395
1396                 else:
1397                     res = line[i]
1398
1399                 row[field_name] = res or False
1400
1401             return row, nbrmax, warning, data_res_id, xml_id
1402
1403         fields_def = self.fields_get(cr, uid, context=context)
1404
1405         position = 0
1406         if config.get('import_partial') and filename:
1407             with open(config.get('import_partial'), 'rb') as partial_import_file:
1408                 data = pickle.load(partial_import_file)
1409                 position = data.get(filename, 0)
1410
1411         while position<len(datas):
1412             (res, position, warning, res_id, xml_id) = \
1413                     process_liness(self, datas, [], current_module, self._name, fields_def, position=position)
1414             if len(warning):
1415                 cr.rollback()
1416                 return -1, res, 'Line ' + str(position) +' : ' + '!\n'.join(warning), ''
1417
1418             try:
1419                 ir_model_data_obj._update(cr, uid, self._name,
1420                      current_module, res, mode=mode, xml_id=xml_id,
1421                      noupdate=noupdate, res_id=res_id, context=context)
1422             except Exception, e:
1423                 return -1, res, 'Line ' + str(position) + ' : ' + tools.ustr(e), ''
1424
1425             if config.get('import_partial') and filename and (not (position%100)):
1426                 with open(config.get('import_partial'), 'rb') as partial_import:
1427                     data = pickle.load(partial_import)
1428                 data[filename] = position
1429                 with open(config.get('import_partial'), 'wb') as partial_import:
1430                     pickle.dump(data, partial_import)
1431                 if context.get('defer_parent_store_computation'):
1432                     self._parent_store_compute(cr)
1433                 cr.commit()
1434
1435         if context.get('defer_parent_store_computation'):
1436             self._parent_store_compute(cr)
1437         return position, 0, 0, 0
1438
1439     def get_invalid_fields(self, cr, uid):
1440         return list(self._invalids)
1441
1442     def _validate(self, cr, uid, ids, context=None):
1443         context = context or {}
1444         lng = context.get('lang', False) or 'en_US'
1445         trans = self.pool.get('ir.translation')
1446         error_msgs = []
1447         for constraint in self._constraints:
1448             fun, msg, fields = constraint
1449             if not fun(self, cr, uid, ids):
1450                 # Check presence of __call__ directly instead of using
1451                 # callable() because it will be deprecated as of Python 3.0
1452                 if hasattr(msg, '__call__'):
1453                     tmp_msg = msg(self, cr, uid, ids, context=context)
1454                     if isinstance(tmp_msg, tuple):
1455                         tmp_msg, params = tmp_msg
1456                         translated_msg = tmp_msg % params
1457                     else:
1458                         translated_msg = tmp_msg
1459                 else:
1460                     translated_msg = trans._get_source(cr, uid, self._name, 'constraint', lng, msg) or msg
1461                 error_msgs.append(
1462                         _("Error occurred while validating the field(s) %s: %s") % (','.join(fields), translated_msg)
1463                 )
1464                 self._invalids.update(fields)
1465         if error_msgs:
1466             cr.rollback()
1467             raise except_orm('ValidateError', '\n'.join(error_msgs))
1468         else:
1469             self._invalids.clear()
1470
1471     def default_get(self, cr, uid, fields_list, context=None):
1472         """
1473         Returns default values for the fields in fields_list.
1474
1475         :param fields_list: list of fields to get the default values for (example ['field1', 'field2',])
1476         :type fields_list: list
1477         :param context: optional context dictionary - it may contains keys for specifying certain options
1478                         like ``context_lang`` (language) or ``context_tz`` (timezone) to alter the results of the call.
1479                         It may contain keys in the form ``default_XXX`` (where XXX is a field name), to set
1480                         or override a default value for a field.
1481                         A special ``bin_size`` boolean flag may also be passed in the context to request the
1482                         value of all fields.binary columns to be returned as the size of the binary instead of its
1483                         contents. This can also be selectively overriden by passing a field-specific flag
1484                         in the form ``bin_size_XXX: True/False`` where ``XXX`` is the name of the field.
1485                         Note: The ``bin_size_XXX`` form is new in OpenERP v6.0.
1486         :return: dictionary of the default values (set on the object model class, through user preferences, or in the context)
1487         """
1488         # trigger view init hook
1489         self.view_init(cr, uid, fields_list, context)
1490
1491         if not context:
1492             context = {}
1493         defaults = {}
1494
1495         # get the default values for the inherited fields
1496         for t in self._inherits.keys():
1497             defaults.update(self.pool.get(t).default_get(cr, uid, fields_list,
1498                 context))
1499
1500         # get the default values defined in the object
1501         for f in fields_list:
1502             if f in self._defaults:
1503                 if callable(self._defaults[f]):
1504                     defaults[f] = self._defaults[f](self, cr, uid, context)
1505                 else:
1506                     defaults[f] = self._defaults[f]
1507
1508             fld_def = ((f in self._columns) and self._columns[f]) \
1509                     or ((f in self._inherit_fields) and self._inherit_fields[f][2]) \
1510                     or False
1511
1512             if isinstance(fld_def, fields.property):
1513                 property_obj = self.pool.get('ir.property')
1514                 prop_value = property_obj.get(cr, uid, f, self._name, context=context)
1515                 if prop_value:
1516                     if isinstance(prop_value, (browse_record, browse_null)):
1517                         defaults[f] = prop_value.id
1518                     else:
1519                         defaults[f] = prop_value
1520                 else:
1521                     if f not in defaults:
1522                         defaults[f] = False
1523
1524         # get the default values set by the user and override the default
1525         # values defined in the object
1526         ir_values_obj = self.pool.get('ir.values')
1527         res = ir_values_obj.get(cr, uid, 'default', False, [self._name])
1528         for id, field, field_value in res:
1529             if field in fields_list:
1530                 fld_def = (field in self._columns) and self._columns[field] or self._inherit_fields[field][2]
1531                 if fld_def._type == 'many2one':
1532                     obj = self.pool.get(fld_def._obj)
1533                     if not obj.search(cr, uid, [('id', '=', field_value or False)]):
1534                         continue
1535                 if fld_def._type == 'many2many':
1536                     obj = self.pool.get(fld_def._obj)
1537                     field_value2 = []
1538                     for i in range(len(field_value)):
1539                         if not obj.search(cr, uid, [('id', '=',
1540                             field_value[i])]):
1541                             continue
1542                         field_value2.append(field_value[i])
1543                     field_value = field_value2
1544                 if fld_def._type == 'one2many':
1545                     obj = self.pool.get(fld_def._obj)
1546                     field_value2 = []
1547                     for i in range(len(field_value)):
1548                         field_value2.append({})
1549                         for field2 in field_value[i]:
1550                             if field2 in obj._columns.keys() and obj._columns[field2]._type == 'many2one':
1551                                 obj2 = self.pool.get(obj._columns[field2]._obj)
1552                                 if not obj2.search(cr, uid,
1553                                         [('id', '=', field_value[i][field2])]):
1554                                     continue
1555                             elif field2 in obj._inherit_fields.keys() and obj._inherit_fields[field2][2]._type == 'many2one':
1556                                 obj2 = self.pool.get(obj._inherit_fields[field2][2]._obj)
1557                                 if not obj2.search(cr, uid,
1558                                         [('id', '=', field_value[i][field2])]):
1559                                     continue
1560                             # TODO add test for many2many and one2many
1561                             field_value2[i][field2] = field_value[i][field2]
1562                     field_value = field_value2
1563                 defaults[field] = field_value
1564
1565         # get the default values from the context
1566         for key in context or {}:
1567             if key.startswith('default_') and (key[8:] in fields_list):
1568                 defaults[key[8:]] = context[key]
1569         return defaults
1570
1571     def fields_get_keys(self, cr, user, context=None):
1572         res = self._columns.keys()
1573         # TODO I believe this loop can be replace by
1574         # res.extend(self._inherit_fields.key())
1575         for parent in self._inherits:
1576             res.extend(self.pool.get(parent).fields_get_keys(cr, user, context))
1577         return res
1578
1579     def _rec_name_fallback(self, cr, uid, context=None):
1580         rec_name = self._rec_name
1581         if rec_name not in self._columns:
1582             rec_name = self._columns.keys()[0] if len(self._columns.keys()) > 0 else "id"
1583         return rec_name
1584
1585     #
1586     # Overload this method if you need a window title which depends on the context
1587     #
1588     def view_header_get(self, cr, user, view_id=None, view_type='form', context=None):
1589         return False
1590
1591     def user_has_groups(self, cr, uid, groups, context=None):
1592         """Return true if the user is at least member of one of the groups
1593            in groups_str. Typically used to resolve ``groups`` attribute
1594            in view and model definitions. 
1595
1596            :param str groups: comma-separated list of fully-qualified group
1597                               external IDs, e.g.: ``base.group_user,base.group_system``
1598            :return: True if the current user is a member of one of the
1599                     given groups
1600         """
1601         return any([self.pool.get('res.users').has_group(cr, uid, group_ext_id)
1602                         for group_ext_id in groups.split(',')])
1603
1604     def __view_look_dom(self, cr, user, node, view_id, in_tree_view, model_fields, context=None):
1605         """Return the description of the fields in the node.
1606
1607         In a normal call to this method, node is a complete view architecture
1608         but it is actually possible to give some sub-node (this is used so
1609         that the method can call itself recursively).
1610
1611         Originally, the field descriptions are drawn from the node itself.
1612         But there is now some code calling fields_get() in order to merge some
1613         of those information in the architecture.
1614
1615         """
1616         if context is None:
1617             context = {}
1618         result = False
1619         fields = {}
1620         children = True
1621
1622         modifiers = {}
1623
1624         def encode(s):
1625             if isinstance(s, unicode):
1626                 return s.encode('utf8')
1627             return s
1628
1629         def check_group(node):
1630             """Apply group restrictions,  may be set at view level or model level::
1631                * at view level this means the element should be made invisible to
1632                  people who are not members
1633                * at model level (exclusively for fields, obviously), this means
1634                  the field should be completely removed from the view, as it is
1635                  completely unavailable for non-members
1636
1637                :return: True if field should be included in the result of fields_view_get 
1638             """
1639             if node.tag == 'field' and node.get('name') in self._all_columns:
1640                 column = self._all_columns[node.get('name')].column
1641                 if column.groups and not self.user_has_groups(cr, user,
1642                                                               groups=column.groups,
1643                                                               context=context):
1644                     node.getparent().remove(node)
1645                     fields.pop(node.get('name'), None)
1646                     # no point processing view-level ``groups`` anymore, return
1647                     return False
1648             if node.get('groups'):
1649                 can_see = self.user_has_groups(cr, user,
1650                                                groups=node.get('groups'),
1651                                                context=context)
1652                 if not can_see:
1653                     node.set('invisible', '1')
1654                     modifiers['invisible'] = True
1655                     if 'attrs' in node.attrib:
1656                         del(node.attrib['attrs']) #avoid making field visible later
1657                 del(node.attrib['groups'])
1658             return True
1659
1660         if node.tag in ('field', 'node', 'arrow'):
1661             if node.get('object'):
1662                 attrs = {}
1663                 views = {}
1664                 xml = "<form>"
1665                 for f in node:
1666                     if f.tag in ('field'):
1667                         xml += etree.tostring(f, encoding="utf-8")
1668                 xml += "</form>"
1669                 new_xml = etree.fromstring(encode(xml))
1670                 ctx = context.copy()
1671                 ctx['base_model_name'] = self._name
1672                 xarch, xfields = self.pool.get(node.get('object')).__view_look_dom_arch(cr, user, new_xml, view_id, ctx)
1673                 views['form'] = {
1674                     'arch': xarch,
1675                     'fields': xfields
1676                 }
1677                 attrs = {'views': views}
1678                 fields = xfields
1679             if node.get('name'):
1680                 attrs = {}
1681                 try:
1682                     if node.get('name') in self._columns:
1683                         column = self._columns[node.get('name')]
1684                     else:
1685                         column = self._inherit_fields[node.get('name')][2]
1686                 except Exception:
1687                     column = False
1688
1689                 if column:
1690                     relation = self.pool.get(column._obj)
1691
1692                     children = False
1693                     views = {}
1694                     for f in node:
1695                         if f.tag in ('form', 'tree', 'graph', 'kanban'):
1696                             node.remove(f)
1697                             ctx = context.copy()
1698                             ctx['base_model_name'] = self._name
1699                             xarch, xfields = relation.__view_look_dom_arch(cr, user, f, view_id, ctx)
1700                             views[str(f.tag)] = {
1701                                 'arch': xarch,
1702                                 'fields': xfields
1703                             }
1704                     attrs = {'views': views}
1705                     if node.get('widget') and node.get('widget') == 'selection':
1706                         # Prepare the cached selection list for the client. This needs to be
1707                         # done even when the field is invisible to the current user, because
1708                         # other events could need to change its value to any of the selectable ones
1709                         # (such as on_change events, refreshes, etc.)
1710
1711                         # If domain and context are strings, we keep them for client-side, otherwise
1712                         # we evaluate them server-side to consider them when generating the list of
1713                         # possible values
1714                         # TODO: find a way to remove this hack, by allow dynamic domains
1715                         dom = []
1716                         if column._domain and not isinstance(column._domain, basestring):
1717                             dom = column._domain
1718                         dom += eval(node.get('domain', '[]'), {'uid': user, 'time': time})
1719                         search_context = dict(context)
1720                         if column._context and not isinstance(column._context, basestring):
1721                             search_context.update(column._context)
1722                         attrs['selection'] = relation._name_search(cr, user, '', dom, context=search_context, limit=None, name_get_uid=1)
1723                         if (node.get('required') and not int(node.get('required'))) or not column.required:
1724                             attrs['selection'].append((False, ''))
1725                 fields[node.get('name')] = attrs
1726
1727                 field = model_fields.get(node.get('name'))
1728                 if field:
1729                     transfer_field_to_modifiers(field, modifiers)
1730
1731
1732         elif node.tag in ('form', 'tree'):
1733             result = self.view_header_get(cr, user, False, node.tag, context)
1734             if result:
1735                 node.set('string', result)
1736             in_tree_view = node.tag == 'tree'
1737
1738         elif node.tag == 'calendar':
1739             for additional_field in ('date_start', 'date_delay', 'date_stop', 'color'):
1740                 if node.get(additional_field):
1741                     fields[node.get(additional_field)] = {}
1742
1743         if not check_group(node):
1744             # node must be removed, no need to proceed further with its children
1745             return fields
1746
1747         # The view architeture overrides the python model.
1748         # Get the attrs before they are (possibly) deleted by check_group below
1749         transfer_node_to_modifiers(node, modifiers, context, in_tree_view)
1750
1751         # TODO remove attrs couterpart in modifiers when invisible is true ?
1752
1753         # translate view
1754         if 'lang' in context:
1755             if node.text and node.text.strip():
1756                 trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.text.strip())
1757                 if trans:
1758                     node.text = node.text.replace(node.text.strip(), trans)
1759             if node.tail and node.tail.strip():
1760                 trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.tail.strip())
1761                 if trans:
1762                     node.tail =  node.tail.replace(node.tail.strip(), trans)
1763
1764             if node.get('string') and not result:
1765                 trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.get('string'))
1766                 if trans == node.get('string') and ('base_model_name' in context):
1767                     # If translation is same as source, perhaps we'd have more luck with the alternative model name
1768                     # (in case we are in a mixed situation, such as an inherited view where parent_view.model != model
1769                     trans = self.pool.get('ir.translation')._get_source(cr, user, context['base_model_name'], 'view', context['lang'], node.get('string'))
1770                 if trans:
1771                     node.set('string', trans)
1772
1773             for attr_name in ('confirm', 'sum', 'help', 'placeholder'):
1774                 attr_value = node.get(attr_name)
1775                 if attr_value:
1776                     trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], attr_value)
1777                     if trans:
1778                         node.set(attr_name, trans)
1779
1780         for f in node:
1781             if children or (node.tag == 'field' and f.tag in ('filter','separator')):
1782                 fields.update(self.__view_look_dom(cr, user, f, view_id, in_tree_view, model_fields, context))
1783
1784         transfer_modifiers_to_node(modifiers, node)
1785         return fields
1786
1787     def _disable_workflow_buttons(self, cr, user, node):
1788         """ Set the buttons in node to readonly if the user can't activate them. """
1789         if user == 1:
1790             # admin user can always activate workflow buttons
1791             return node
1792
1793         # TODO handle the case of more than one workflow for a model or multiple
1794         # transitions with different groups and same signal
1795         usersobj = self.pool.get('res.users')
1796         buttons = (n for n in node.getiterator('button') if n.get('type') != 'object')
1797         for button in buttons:
1798             user_groups = usersobj.read(cr, user, [user], ['groups_id'])[0]['groups_id']
1799             cr.execute("""SELECT DISTINCT t.group_id
1800                         FROM wkf
1801                   INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
1802                   INNER JOIN wkf_transition t ON (t.act_to = a.id)
1803                        WHERE wkf.osv = %s
1804                          AND t.signal = %s
1805                          AND t.group_id is NOT NULL
1806                    """, (self._name, button.get('name')))
1807             group_ids = [x[0] for x in cr.fetchall() if x[0]]
1808             can_click = not group_ids or bool(set(user_groups).intersection(group_ids))
1809             button.set('readonly', str(int(not can_click)))
1810         return node
1811
1812     def __view_look_dom_arch(self, cr, user, node, view_id, context=None):
1813         """ Return an architecture and a description of all the fields.
1814
1815         The field description combines the result of fields_get() and
1816         __view_look_dom().
1817
1818         :param node: the architecture as as an etree
1819         :return: a tuple (arch, fields) where arch is the given node as a
1820             string and fields is the description of all the fields.
1821
1822         """
1823         fields = {}
1824         if node.tag == 'diagram':
1825             if node.getchildren()[0].tag == 'node':
1826                 node_fields = self.pool.get(node.getchildren()[0].get('object')).fields_get(cr, user, None, context)
1827                 fields.update(node_fields)
1828             if node.getchildren()[1].tag == 'arrow':
1829                 arrow_fields = self.pool.get(node.getchildren()[1].get('object')).fields_get(cr, user, None, context)
1830                 fields.update(arrow_fields)
1831         else:
1832             fields = self.fields_get(cr, user, None, context)
1833         fields_def = self.__view_look_dom(cr, user, node, view_id, False, fields, context=context)
1834         node = self._disable_workflow_buttons(cr, user, node)
1835         arch = etree.tostring(node, encoding="utf-8").replace('\t', '')
1836         for k in fields.keys():
1837             if k not in fields_def:
1838                 del fields[k]
1839         for field in fields_def:
1840             if field == 'id':
1841                 # sometime, the view may contain the (invisible) field 'id' needed for a domain (when 2 objects have cross references)
1842                 fields['id'] = {'readonly': True, 'type': 'integer', 'string': 'ID'}
1843             elif field in fields:
1844                 fields[field].update(fields_def[field])
1845             else:
1846                 cr.execute('select name, model from ir_ui_view where (id=%s or inherit_id=%s) and arch like %s', (view_id, view_id, '%%%s%%' % field))
1847                 res = cr.fetchall()[:]
1848                 model = res[0][1]
1849                 res.insert(0, ("Can't find field '%s' in the following view parts composing the view of object model '%s':" % (field, model), None))
1850                 msg = "\n * ".join([r[0] for r in res])
1851                 msg += "\n\nEither you wrongly customized this view, or some modules bringing those views are not compatible with your current data model"
1852                 _logger.error(msg)
1853                 raise except_orm('View error', msg)
1854         return arch, fields
1855
1856     def _get_default_form_view(self, cr, user, context=None):
1857         """ Generates a default single-line form view using all fields
1858         of the current model except the m2m and o2m ones.
1859
1860         :param cr: database cursor
1861         :param int user: user id
1862         :param dict context: connection context
1863         :returns: a form view as an lxml document
1864         :rtype: etree._Element
1865         """
1866         view = etree.Element('form', string=self._description)
1867         # TODO it seems fields_get can be replaced by _all_columns (no need for translation)
1868         for field, descriptor in self.fields_get(cr, user, context=context).iteritems():
1869             if descriptor['type'] in ('one2many', 'many2many'):
1870                 continue
1871             etree.SubElement(view, 'field', name=field)
1872             if descriptor['type'] == 'text':
1873                 etree.SubElement(view, 'newline')
1874         return view
1875
1876     def _get_default_search_view(self, cr, user, context=None):
1877         """ Generates a single-field search view, based on _rec_name.
1878
1879         :param cr: database cursor
1880         :param int user: user id
1881         :param dict context: connection context
1882         :returns: a tree view as an lxml document
1883         :rtype: etree._Element
1884         """
1885         view = etree.Element('search', string=self._description)
1886         etree.SubElement(view, 'field', name=self._rec_name_fallback(cr, user, context))
1887         return view
1888
1889     def _get_default_tree_view(self, cr, user, context=None):
1890         """ Generates a single-field tree view, based on _rec_name.
1891
1892         :param cr: database cursor
1893         :param int user: user id
1894         :param dict context: connection context
1895         :returns: a tree view as an lxml document
1896         :rtype: etree._Element
1897         """
1898         view = etree.Element('tree', string=self._description)
1899         etree.SubElement(view, 'field', name=self._rec_name_fallback(cr, user, context))
1900         return view
1901
1902     def _get_default_calendar_view(self, cr, user, context=None):
1903         """ Generates a default calendar view by trying to infer
1904         calendar fields from a number of pre-set attribute names
1905         
1906         :param cr: database cursor
1907         :param int user: user id
1908         :param dict context: connection context
1909         :returns: a calendar view
1910         :rtype: etree._Element
1911         """
1912         def set_first_of(seq, in_, to):
1913             """Sets the first value of ``seq`` also found in ``in_`` to
1914             the ``to`` attribute of the view being closed over.
1915
1916             Returns whether it's found a suitable value (and set it on
1917             the attribute) or not
1918             """
1919             for item in seq:
1920                 if item in in_:
1921                     view.set(to, item)
1922                     return True
1923             return False
1924
1925         view = etree.Element('calendar', string=self._description)
1926         etree.SubElement(view, 'field', self._rec_name_fallback(cr, user, context))
1927
1928         if (self._date_name not in self._columns):
1929             date_found = False
1930             for dt in ['date', 'date_start', 'x_date', 'x_date_start']:
1931                 if dt in self._columns:
1932                     self._date_name = dt
1933                     date_found = True
1934                     break
1935
1936             if not date_found:
1937                 raise except_orm(_('Invalid Object Architecture!'), _("Insufficient fields for Calendar View!"))
1938         view.set('date_start', self._date_name)
1939
1940         set_first_of(["user_id", "partner_id", "x_user_id", "x_partner_id"],
1941                      self._columns, 'color')
1942
1943         if not set_first_of(["date_stop", "date_end", "x_date_stop", "x_date_end"],
1944                             self._columns, 'date_stop'):
1945             if not set_first_of(["date_delay", "planned_hours", "x_date_delay", "x_planned_hours"],
1946                                 self._columns, 'date_delay'):
1947                 raise except_orm(
1948                     _('Invalid Object Architecture!'),
1949                     _("Insufficient fields to generate a Calendar View for %s, missing a date_stop or a date_delay" % (self._name)))
1950
1951         return view
1952
1953     #
1954     # if view_id, view_type is not required
1955     #
1956     def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
1957         """
1958         Get the detailed composition of the requested view like fields, model, view architecture
1959
1960         :param cr: database cursor
1961         :param user: current user id
1962         :param view_id: id of the view or None
1963         :param view_type: type of the view to return if view_id is None ('form', tree', ...)
1964         :param context: context arguments, like lang, time zone
1965         :param toolbar: true to include contextual actions
1966         :param submenu: deprecated
1967         :return: dictionary describing the composition of the requested view (including inherited views and extensions)
1968         :raise AttributeError:
1969                             * if the inherited view has unknown position to work with other than 'before', 'after', 'inside', 'replace'
1970                             * if some tag other than 'position' is found in parent view
1971         :raise Invalid ArchitectureError: if there is view type other than form, tree, calendar, search etc defined on the structure
1972
1973         """
1974         if context is None:
1975             context = {}
1976
1977         def encode(s):
1978             if isinstance(s, unicode):
1979                 return s.encode('utf8')
1980             return s
1981
1982         def raise_view_error(error_msg, child_view_id):
1983             view, child_view = self.pool.get('ir.ui.view').browse(cr, user, [view_id, child_view_id], context)
1984             error_msg = error_msg % {'parent_xml_id': view.xml_id} 
1985             raise AttributeError("View definition error for inherited view '%s' on model '%s': %s"
1986                                  %  (child_view.xml_id, self._name, error_msg))
1987
1988         def locate(source, spec):
1989             """ Locate a node in a source (parent) architecture.
1990
1991             Given a complete source (parent) architecture (i.e. the field
1992             `arch` in a view), and a 'spec' node (a node in an inheriting
1993             view that specifies the location in the source view of what
1994             should be changed), return (if it exists) the node in the
1995             source view matching the specification.
1996
1997             :param source: a parent architecture to modify
1998             :param spec: a modifying node in an inheriting view
1999             :return: a node in the source matching the spec
2000
2001             """
2002             if spec.tag == 'xpath':
2003                 nodes = source.xpath(spec.get('expr'))
2004                 return nodes[0] if nodes else None
2005             elif spec.tag == 'field':
2006                 # Only compare the field name: a field can be only once in a given view
2007                 # at a given level (and for multilevel expressions, we should use xpath
2008                 # inheritance spec anyway).
2009                 for node in source.getiterator('field'):
2010                     if node.get('name') == spec.get('name'):
2011                         return node
2012                 return None
2013
2014             for node in source.getiterator(spec.tag):
2015                 if isinstance(node, SKIPPED_ELEMENT_TYPES):
2016                     continue
2017                 if all(node.get(attr) == spec.get(attr) \
2018                         for attr in spec.attrib
2019                             if attr not in ('position','version')):
2020                     # Version spec should match parent's root element's version 
2021                     if spec.get('version') and spec.get('version') != source.get('version'):
2022                         return None
2023                     return node
2024             return None
2025
2026         def apply_inheritance_specs(source, specs_arch, inherit_id=None):
2027             """ Apply an inheriting view.
2028
2029             Apply to a source architecture all the spec nodes (i.e. nodes
2030             describing where and what changes to apply to some parent
2031             architecture) given by an inheriting view.
2032
2033             :param source: a parent architecture to modify
2034             :param specs_arch: a modifying architecture in an inheriting view
2035             :param inherit_id: the database id of the inheriting view
2036             :return: a modified source where the specs are applied
2037
2038             """
2039             specs_tree = etree.fromstring(encode(specs_arch))
2040             # Queue of specification nodes (i.e. nodes describing where and
2041             # changes to apply to some parent architecture).
2042             specs = [specs_tree]
2043
2044             while len(specs):
2045                 spec = specs.pop(0)
2046                 if isinstance(spec, SKIPPED_ELEMENT_TYPES):
2047                     continue
2048                 if spec.tag == 'data':
2049                     specs += [ c for c in specs_tree ]
2050                     continue
2051                 node = locate(source, spec)
2052                 if node is not None:
2053                     pos = spec.get('position', 'inside')
2054                     if pos == 'replace':
2055                         if node.getparent() is None:
2056                             source = copy.deepcopy(spec[0])
2057                         else:
2058                             for child in spec:
2059                                 node.addprevious(child)
2060                             node.getparent().remove(node)
2061                     elif pos == 'attributes':
2062                         for child in spec.getiterator('attribute'):
2063                             attribute = (child.get('name'), child.text and child.text.encode('utf8') or None)
2064                             if attribute[1]:
2065                                 node.set(attribute[0], attribute[1])
2066                             else:
2067                                 del(node.attrib[attribute[0]])
2068                     else:
2069                         sib = node.getnext()
2070                         for child in spec:
2071                             if pos == 'inside':
2072                                 node.append(child)
2073                             elif pos == 'after':
2074                                 if sib is None:
2075                                     node.addnext(child)
2076                                     node = child
2077                                 else:
2078                                     sib.addprevious(child)
2079                             elif pos == 'before':
2080                                 node.addprevious(child)
2081                             else:
2082                                 raise_view_error("Invalid position value: '%s'" % pos, inherit_id)
2083                 else:
2084                     attrs = ''.join([
2085                         ' %s="%s"' % (attr, spec.get(attr))
2086                         for attr in spec.attrib
2087                         if attr != 'position'
2088                     ])
2089                     tag = "<%s%s>" % (spec.tag, attrs)
2090                     if spec.get('version') and spec.get('version') != source.get('version'):
2091                         raise_view_error("Mismatching view API version for element '%s': %r vs %r in parent view '%%(parent_xml_id)s'" % \
2092                                             (tag, spec.get('version'), source.get('version')), inherit_id)
2093                     raise_view_error("Element '%s' not found in parent view '%%(parent_xml_id)s'" % tag, inherit_id)
2094                     
2095             return source
2096
2097         def apply_view_inheritance(cr, user, source, inherit_id):
2098             """ Apply all the (directly and indirectly) inheriting views.
2099
2100             :param source: a parent architecture to modify (with parent
2101                 modifications already applied)
2102             :param inherit_id: the database view_id of the parent view
2103             :return: a modified source where all the modifying architecture
2104                 are applied
2105
2106             """
2107             sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, user, inherit_id, self._name)
2108             for (view_arch, view_id) in sql_inherit:
2109                 source = apply_inheritance_specs(source, view_arch, view_id)
2110                 source = apply_view_inheritance(cr, user, source, view_id)
2111             return source
2112
2113         result = {'type': view_type, 'model': self._name}
2114
2115         sql_res = False
2116         parent_view_model = None
2117         view_ref = context.get(view_type + '_view_ref')
2118         # Search for a root (i.e. without any parent) view.
2119         while True:
2120             if view_ref and not view_id:
2121                 if '.' in view_ref:
2122                     module, view_ref = view_ref.split('.', 1)
2123                     cr.execute("SELECT res_id FROM ir_model_data WHERE model='ir.ui.view' AND module=%s AND name=%s", (module, view_ref))
2124                     view_ref_res = cr.fetchone()
2125                     if view_ref_res:
2126                         view_id = view_ref_res[0]
2127
2128             if view_id:
2129                 cr.execute("""SELECT arch,name,field_parent,id,type,inherit_id,model
2130                               FROM ir_ui_view
2131                               WHERE id=%s""", (view_id,))
2132             else:
2133                 cr.execute("""SELECT arch,name,field_parent,id,type,inherit_id,model
2134                               FROM ir_ui_view
2135                               WHERE model=%s AND type=%s AND inherit_id IS NULL
2136                               ORDER BY priority""", (self._name, view_type))
2137             sql_res = cr.dictfetchone()
2138
2139             if not sql_res:
2140                 break
2141
2142             view_id = sql_res['inherit_id'] or sql_res['id']
2143             parent_view_model = sql_res['model']
2144             if not sql_res['inherit_id']:
2145                 break
2146
2147         # if a view was found
2148         if sql_res:
2149             source = etree.fromstring(encode(sql_res['arch']))
2150             result.update(
2151                 arch=apply_view_inheritance(cr, user, source, sql_res['id']),
2152                 type=sql_res['type'],
2153                 view_id=sql_res['id'],
2154                 name=sql_res['name'],
2155                 field_parent=sql_res['field_parent'] or False)
2156         else:
2157             # otherwise, build some kind of default view
2158             try:
2159                 view = getattr(self, '_get_default_%s_view' % view_type)(
2160                     cr, user, context)
2161             except AttributeError:
2162                 # what happens here, graph case?
2163                 raise except_orm(_('Invalid Architecture!'), _("There is no view of type '%s' defined for the structure!") % view_type)
2164
2165             result.update(
2166                 arch=view,
2167                 name='default',
2168                 field_parent=False,
2169                 view_id=0)
2170
2171         if parent_view_model != self._name:
2172             ctx = context.copy()
2173             ctx['base_model_name'] = parent_view_model
2174         else:
2175             ctx = context
2176         xarch, xfields = self.__view_look_dom_arch(cr, user, result['arch'], view_id, context=ctx)
2177         result['arch'] = xarch
2178         result['fields'] = xfields
2179
2180         if toolbar:
2181             def clean(x):
2182                 x = x[2]
2183                 for key in ('report_sxw_content', 'report_rml_content',
2184                         'report_sxw', 'report_rml',
2185                         'report_sxw_content_data', 'report_rml_content_data'):
2186                     if key in x:
2187                         del x[key]
2188                 return x
2189             ir_values_obj = self.pool.get('ir.values')
2190             resprint = ir_values_obj.get(cr, user, 'action',
2191                     'client_print_multi', [(self._name, False)], False,
2192                     context)
2193             resaction = ir_values_obj.get(cr, user, 'action',
2194                     'client_action_multi', [(self._name, False)], False,
2195                     context)
2196
2197             resrelate = ir_values_obj.get(cr, user, 'action',
2198                     'client_action_relate', [(self._name, False)], False,
2199                     context)
2200             resaction = [clean(action) for action in resaction
2201                          if view_type == 'tree' or not action[2].get('multi')]
2202             resprint = [clean(print_) for print_ in resprint
2203                         if view_type == 'tree' or not print_[2].get('multi')]
2204             resrelate = map(lambda x: x[2], resrelate)
2205
2206             for x in itertools.chain(resprint, resaction, resrelate):
2207                 x['string'] = x['name']
2208
2209             result['toolbar'] = {
2210                 'print': resprint,
2211                 'action': resaction,
2212                 'relate': resrelate
2213             }
2214         return result
2215
2216     _view_look_dom_arch = __view_look_dom_arch
2217
2218     def search_count(self, cr, user, args, context=None):
2219         if not context:
2220             context = {}
2221         res = self.search(cr, user, args, context=context, count=True)
2222         if isinstance(res, list):
2223             return len(res)
2224         return res
2225
2226     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
2227         """
2228         Search for records based on a search domain.
2229
2230         :param cr: database cursor
2231         :param user: current user id
2232         :param args: list of tuples specifying the search domain [('field_name', 'operator', value), ...]. Pass an empty list to match all records.
2233         :param offset: optional number of results to skip in the returned values (default: 0)
2234         :param limit: optional max number of records to return (default: **None**)
2235         :param order: optional columns to sort by (default: self._order=id )
2236         :param context: optional context arguments, like lang, time zone
2237         :type context: dictionary
2238         :param count: optional (default: **False**), if **True**, returns only the number of records matching the criteria, not their ids
2239         :return: id or list of ids of records matching the criteria
2240         :rtype: integer or list of integers
2241         :raise AccessError: * if user tries to bypass access rules for read on the requested object.
2242
2243         **Expressing a search domain (args)**
2244
2245         Each tuple in the search domain needs to have 3 elements, in the form: **('field_name', 'operator', value)**, where:
2246
2247             * **field_name** must be a valid name of field of the object model, possibly following many-to-one relationships using dot-notation, e.g 'street' or 'partner_id.country' are valid values.
2248             * **operator** must be a string with a valid comparison operator from this list: ``=, !=, >, >=, <, <=, like, ilike, in, not in, child_of, parent_left, parent_right``
2249               The semantics of most of these operators are obvious.
2250               The ``child_of`` operator will look for records who are children or grand-children of a given record,
2251               according to the semantics of this model (i.e following the relationship field named by
2252               ``self._parent_name``, by default ``parent_id``.
2253             * **value** must be a valid value to compare with the values of **field_name**, depending on its type.
2254
2255         Domain criteria can be combined using 3 logical operators than can be added between tuples:  '**&**' (logical AND, default), '**|**' (logical OR), '**!**' (logical NOT).
2256         These are **prefix** operators and the arity of the '**&**' and '**|**' operator is 2, while the arity of the '**!**' is just 1.
2257         Be very careful about this when you combine them the first time.
2258
2259         Here is an example of searching for Partners named *ABC* from Belgium and Germany whose language is not english ::
2260
2261             [('name','=','ABC'),'!',('language.code','=','en_US'),'|',('country_id.code','=','be'),('country_id.code','=','de'))
2262
2263         The '&' is omitted as it is the default, and of course we could have used '!=' for the language, but what this domain really represents is::
2264
2265             (name is 'ABC' AND (language is NOT english) AND (country is Belgium OR Germany))
2266
2267         """
2268         return self._search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
2269
2270     def name_get(self, cr, user, ids, context=None):
2271         """Returns the preferred display value (text representation) for the records with the
2272            given ``ids``. By default this will be the value of the ``name`` column, unless
2273            the model implements a custom behavior.
2274            Can sometimes be seen as the inverse function of :meth:`~.name_search`, but it is not
2275            guaranteed to be.
2276
2277            :rtype: list(tuple)
2278            :return: list of pairs ``(id,text_repr)`` for all records with the given ``ids``.
2279         """
2280         if not ids:
2281             return []
2282         if isinstance(ids, (int, long)):
2283             ids = [ids]
2284
2285         if self._rec_name in self._all_columns:
2286             rec_name_column = self._all_columns[self._rec_name].column
2287             return [(r['id'], rec_name_column.as_display_name(cr, user, self, r[self._rec_name], context=context))
2288                         for r in self.read(cr, user, ids, [self._rec_name],
2289                                        load='_classic_write', context=context)]
2290         return [(id, "%s,%s" % (self._name, id)) for id in ids]
2291
2292     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
2293         """Search for records that have a display name matching the given ``name`` pattern if compared
2294            with the given ``operator``, while also matching the optional search domain (``args``).
2295            This is used for example to provide suggestions based on a partial value for a relational
2296            field.
2297            Sometimes be seen as the inverse function of :meth:`~.name_get`, but it is not
2298            guaranteed to be.
2299
2300            This method is equivalent to calling :meth:`~.search` with a search domain based on ``name``
2301            and then :meth:`~.name_get` on the result of the search.
2302
2303            :param list args: optional search domain (see :meth:`~.search` for syntax),
2304                              specifying further restrictions
2305            :param str operator: domain operator for matching the ``name`` pattern, such as ``'like'``
2306                                 or ``'='``.
2307            :param int limit: optional max number of records to return
2308            :rtype: list
2309            :return: list of pairs ``(id,text_repr)`` for all matching records. 
2310         """
2311         return self._name_search(cr, user, name, args, operator, context, limit)
2312
2313     def name_create(self, cr, uid, name, context=None):
2314         """Creates a new record by calling :meth:`~.create` with only one
2315            value provided: the name of the new record (``_rec_name`` field).
2316            The new record will also be initialized with any default values applicable
2317            to this model, or provided through the context. The usual behavior of
2318            :meth:`~.create` applies.
2319            Similarly, this method may raise an exception if the model has multiple
2320            required fields and some do not have default values.
2321
2322            :param name: name of the record to create
2323
2324            :rtype: tuple
2325            :return: the :meth:`~.name_get` pair value for the newly-created record.
2326         """
2327         rec_id = self.create(cr, uid, {self._rec_name: name}, context);
2328         return self.name_get(cr, uid, [rec_id], context)[0]
2329
2330     # private implementation of name_search, allows passing a dedicated user for the name_get part to
2331     # solve some access rights issues
2332     def _name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
2333         if args is None:
2334             args = []
2335         if context is None:
2336             context = {}
2337         args = args[:]
2338         # optimize out the default criterion of ``ilike ''`` that matches everything
2339         if not (name == '' and operator == 'ilike'):
2340             args += [(self._rec_name, operator, name)]
2341         access_rights_uid = name_get_uid or user
2342         ids = self._search(cr, user, args, limit=limit, context=context, access_rights_uid=access_rights_uid)
2343         res = self.name_get(cr, access_rights_uid, ids, context)
2344         return res
2345
2346     def read_string(self, cr, uid, id, langs, fields=None, context=None):
2347         res = {}
2348         res2 = {}
2349         self.pool.get('ir.translation').check_read(cr, uid)
2350         if not fields:
2351             fields = self._columns.keys() + self._inherit_fields.keys()
2352         #FIXME: collect all calls to _get_source into one SQL call.
2353         for lang in langs:
2354             res[lang] = {'code': lang}
2355             for f in fields:
2356                 if f in self._columns:
2357                     res_trans = self.pool.get('ir.translation')._get_source(cr, uid, self._name+','+f, 'field', lang)
2358                     if res_trans:
2359                         res[lang][f] = res_trans
2360                     else:
2361                         res[lang][f] = self._columns[f].string
2362         for table in self._inherits:
2363             cols = intersect(self._inherit_fields.keys(), fields)
2364             res2 = self.pool.get(table).read_string(cr, uid, id, langs, cols, context)
2365         for lang in res2:
2366             if lang in res:
2367                 res[lang]['code'] = lang
2368             for f in res2[lang]:
2369                 res[lang][f] = res2[lang][f]
2370         return res
2371
2372     def write_string(self, cr, uid, id, langs, vals, context=None):
2373         self.pool.get('ir.translation').check_write(cr, uid)
2374         #FIXME: try to only call the translation in one SQL
2375         for lang in langs:
2376             for field in vals:
2377                 if field in self._columns:
2378                     src = self._columns[field].string
2379                     self.pool.get('ir.translation')._set_ids(cr, uid, self._name+','+field, 'field', lang, [0], vals[field], src)
2380         for table in self._inherits:
2381             cols = intersect(self._inherit_fields.keys(), vals)
2382             if cols:
2383                 self.pool.get(table).write_string(cr, uid, id, langs, vals, context)
2384         return True
2385
2386     def _add_missing_default_values(self, cr, uid, values, context=None):
2387         missing_defaults = []
2388         avoid_tables = [] # avoid overriding inherited values when parent is set
2389         for tables, parent_field in self._inherits.items():
2390             if parent_field in values:
2391                 avoid_tables.append(tables)
2392         for field in self._columns.keys():
2393             if not field in values:
2394                 missing_defaults.append(field)
2395         for field in self._inherit_fields.keys():
2396             if (field not in values) and (self._inherit_fields[field][0] not in avoid_tables):
2397                 missing_defaults.append(field)
2398
2399         if len(missing_defaults):
2400             # override defaults with the provided values, never allow the other way around
2401             defaults = self.default_get(cr, uid, missing_defaults, context)
2402             for dv in defaults:
2403                 if ((dv in self._columns and self._columns[dv]._type == 'many2many') \
2404                      or (dv in self._inherit_fields and self._inherit_fields[dv][2]._type == 'many2many')) \
2405                         and defaults[dv] and isinstance(defaults[dv][0], (int, long)):
2406                     defaults[dv] = [(6, 0, defaults[dv])]
2407                 if (dv in self._columns and self._columns[dv]._type == 'one2many' \
2408                     or (dv in self._inherit_fields and self._inherit_fields[dv][2]._type == 'one2many')) \
2409                         and isinstance(defaults[dv], (list, tuple)) and defaults[dv] and isinstance(defaults[dv][0], dict):
2410                     defaults[dv] = [(0, 0, x) for x in defaults[dv]]
2411             defaults.update(values)
2412             values = defaults
2413         return values
2414
2415     def clear_caches(self):
2416         """ Clear the caches
2417
2418         This clears the caches associated to methods decorated with
2419         ``tools.ormcache`` or ``tools.ormcache_multi``.
2420         """
2421         try:
2422             getattr(self, '_ormcache')
2423             self._ormcache = {}
2424         except AttributeError:
2425             pass
2426
2427
2428     def _read_group_fill_results(self, cr, uid, domain, groupby, groupby_list, aggregated_fields,
2429                                  read_group_result, read_group_order=None, context=None):
2430         """Helper method for filling in empty groups for all possible values of
2431            the field being grouped by"""
2432
2433         # self._group_by_full should map groupable fields to a method that returns
2434         # a list of all aggregated values that we want to display for this field,
2435         # in the form of a m2o-like pair (key,label).
2436         # This is useful to implement kanban views for instance, where all columns
2437         # should be displayed even if they don't contain any record.
2438
2439         # Grab the list of all groups that should be displayed, including all present groups 
2440         present_group_ids = [x[groupby][0] for x in read_group_result if x[groupby]]
2441         all_groups = self._group_by_full[groupby](self, cr, uid, present_group_ids, domain,
2442                                                   read_group_order=read_group_order,
2443                                                   access_rights_uid=openerp.SUPERUSER_ID,
2444                                                   context=context)
2445
2446         result_template = dict.fromkeys(aggregated_fields, False)
2447         result_template[groupby + '_count'] = 0
2448         if groupby_list and len(groupby_list) > 1:
2449             result_template['__context'] = {'group_by': groupby_list[1:]}
2450
2451         # Merge the left_side (current results as dicts) with the right_side (all
2452         # possible values as m2o pairs). Both lists are supposed to be using the
2453         # same ordering, and can be merged in one pass.
2454         result = []
2455         known_values = {}
2456         def append_left(left_side):
2457             grouped_value = left_side[groupby] and left_side[groupby][0]
2458             if not grouped_value in known_values:
2459                 result.append(left_side)
2460                 known_values[grouped_value] = left_side
2461             else:
2462                 count_attr = groupby + '_count'
2463                 known_values[grouped_value].update({count_attr: left_side[count_attr]})
2464         def append_right(right_side):
2465             grouped_value = right_side[0]
2466             if not grouped_value in known_values:
2467                 line = dict(result_template)
2468                 line[groupby] = right_side
2469                 line['__domain'] = [(groupby,'=',grouped_value)] + domain
2470                 result.append(line)
2471                 known_values[grouped_value] = line
2472         while read_group_result or all_groups:
2473             left_side = read_group_result[0] if read_group_result else None
2474             right_side = all_groups[0] if all_groups else None
2475             assert left_side is None or left_side[groupby] is False \
2476                  or isinstance(left_side[groupby], (tuple,list)), \
2477                 'M2O-like pair expected, got %r' % left_side[groupby]
2478             assert right_side is None or isinstance(right_side, (tuple,list)), \
2479                 'M2O-like pair expected, got %r' % right_side
2480             if left_side is None:
2481                 append_right(all_groups.pop(0))
2482             elif right_side is None:
2483                 append_left(read_group_result.pop(0))
2484             elif left_side[groupby] == right_side:
2485                 append_left(read_group_result.pop(0))
2486                 all_groups.pop(0) # discard right_side
2487             elif not left_side[groupby] or not left_side[groupby][0]:
2488                 # left side == "Undefined" entry, not present on right_side
2489                 append_left(read_group_result.pop(0))
2490             else:
2491                 append_right(all_groups.pop(0))
2492         return result
2493
2494     def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
2495         """
2496         Get the list of records in list view grouped by the given ``groupby`` fields
2497
2498         :param cr: database cursor
2499         :param uid: current user id
2500         :param domain: list specifying search criteria [['field_name', 'operator', 'value'], ...]
2501         :param list fields: list of fields present in the list view specified on the object
2502         :param list groupby: fields by which the records will be grouped
2503         :param int offset: optional number of records to skip
2504         :param int limit: optional max number of records to return
2505         :param dict context: context arguments, like lang, time zone
2506         :param list orderby: optional ``order by`` specification, for
2507                              overriding the natural sort ordering of the
2508                              groups, see also :py:meth:`~osv.osv.osv.search`
2509                              (supported only for many2one fields currently)
2510         :return: list of dictionaries(one dictionary for each record) containing:
2511
2512                     * the values of fields grouped by the fields in ``groupby`` argument
2513                     * __domain: list of tuples specifying the search criteria
2514                     * __context: dictionary with argument like ``groupby``
2515         :rtype: [{'field_name_1': value, ...]
2516         :raise AccessError: * if user has no read rights on the requested object
2517                             * if user tries to bypass access rules for read on the requested object
2518
2519         """
2520         context = context or {}
2521         self.check_read(cr, uid)
2522         if not fields:
2523             fields = self._columns.keys()
2524
2525         query = self._where_calc(cr, uid, domain, context=context)
2526         self._apply_ir_rules(cr, uid, query, 'read', context=context)
2527
2528         # Take care of adding join(s) if groupby is an '_inherits'ed field
2529         groupby_list = groupby
2530         qualified_groupby_field = groupby
2531         if groupby:
2532             if isinstance(groupby, list):
2533                 groupby = groupby[0]
2534             qualified_groupby_field = self._inherits_join_calc(groupby, query)
2535
2536         if groupby:
2537             assert not groupby or groupby in fields, "Fields in 'groupby' must appear in the list of fields to read (perhaps it's missing in the list view?)"
2538             groupby_def = self._columns.get(groupby) or (self._inherit_fields.get(groupby) and self._inherit_fields.get(groupby)[2])
2539             assert groupby_def and groupby_def._classic_write, "Fields in 'groupby' must be regular database-persisted fields (no function or related fields), or function fields with store=True"
2540
2541         # TODO it seems fields_get can be replaced by _all_columns (no need for translation)
2542         fget = self.fields_get(cr, uid, fields)
2543         flist = ''
2544         group_count = group_by = groupby
2545         if groupby:
2546             if fget.get(groupby):
2547                 groupby_type = fget[groupby]['type']
2548                 if groupby_type in ('date', 'datetime'):
2549                     qualified_groupby_field = "to_char(%s,'yyyy-mm')" % qualified_groupby_field
2550                     flist = "%s as %s " % (qualified_groupby_field, groupby)
2551                 elif groupby_type == 'boolean':
2552                     qualified_groupby_field = "coalesce(%s,false)" % qualified_groupby_field
2553                     flist = "%s as %s " % (qualified_groupby_field, groupby)
2554                 else:
2555                     flist = qualified_groupby_field
2556             else:
2557                 # Don't allow arbitrary values, as this would be a SQL injection vector!
2558                 raise except_orm(_('Invalid group_by'),
2559                                  _('Invalid group_by specification: "%s".\nA group_by specification must be a list of valid fields.')%(groupby,))
2560
2561         aggregated_fields = [
2562             f for f in fields
2563             if f not in ('id', 'sequence')
2564             if fget[f]['type'] in ('integer', 'float')
2565             if (f in self._columns and getattr(self._columns[f], '_classic_write'))]
2566         for f in aggregated_fields:
2567             group_operator = fget[f].get('group_operator', 'sum')
2568             if flist:
2569                 flist += ', '
2570             qualified_field = '"%s"."%s"' % (self._table, f)
2571             flist += "%s(%s) AS %s" % (group_operator, qualified_field, f)
2572
2573         gb = groupby and (' GROUP BY ' + qualified_groupby_field) or ''
2574
2575         from_clause, where_clause, where_clause_params = query.get_sql()
2576         where_clause = where_clause and ' WHERE ' + where_clause
2577         limit_str = limit and ' limit %d' % limit or ''
2578         offset_str = offset and ' offset %d' % offset or ''
2579         if len(groupby_list) < 2 and context.get('group_by_no_leaf'):
2580             group_count = '_'
2581         cr.execute('SELECT min(%s.id) AS id, count(%s.id) AS %s_count' % (self._table, self._table, group_count) + (flist and ',') + flist + ' FROM ' + from_clause + where_clause + gb + limit_str + offset_str, where_clause_params)
2582         alldata = {}
2583         groupby = group_by
2584         for r in cr.dictfetchall():
2585             for fld, val in r.items():
2586                 if val == None: r[fld] = False
2587             alldata[r['id']] = r
2588             del r['id']
2589
2590         order = orderby or groupby
2591         data_ids = self.search(cr, uid, [('id', 'in', alldata.keys())], order=order, context=context)
2592         # the IDS of records that have groupby field value = False or '' should be sorted too
2593         data_ids += filter(lambda x:x not in data_ids, alldata.keys())
2594         data = self.read(cr, uid, data_ids, groupby and [groupby] or ['id'], context=context)
2595         # restore order of the search as read() uses the default _order (this is only for groups, so the size of data_read shoud be small):
2596         data.sort(lambda x,y: cmp(data_ids.index(x['id']), data_ids.index(y['id'])))
2597
2598         for d in data:
2599             if groupby:
2600                 d['__domain'] = [(groupby, '=', alldata[d['id']][groupby] or False)] + domain
2601                 if not isinstance(groupby_list, (str, unicode)):
2602                     if groupby or not context.get('group_by_no_leaf', False):
2603                         d['__context'] = {'group_by': groupby_list[1:]}
2604             if groupby and groupby in fget:
2605                 if d[groupby] and fget[groupby]['type'] in ('date', 'datetime'):
2606                     dt = datetime.datetime.strptime(alldata[d['id']][groupby][:7], '%Y-%m')
2607                     days = calendar.monthrange(dt.year, dt.month)[1]
2608
2609                     d[groupby] = datetime.datetime.strptime(d[groupby][:10], '%Y-%m-%d').strftime('%B %Y')
2610                     d['__domain'] = [(groupby, '>=', alldata[d['id']][groupby] and datetime.datetime.strptime(alldata[d['id']][groupby][:7] + '-01', '%Y-%m-%d').strftime('%Y-%m-%d') or False),\
2611                                      (groupby, '<=', alldata[d['id']][groupby] and datetime.datetime.strptime(alldata[d['id']][groupby][:7] + '-' + str(days), '%Y-%m-%d').strftime('%Y-%m-%d') or False)] + domain
2612                 del alldata[d['id']][groupby]
2613             d.update(alldata[d['id']])
2614             del d['id']
2615
2616         if groupby and groupby in self._group_by_full:
2617             data = self._read_group_fill_results(cr, uid, domain, groupby, groupby_list,
2618                                                  aggregated_fields, data, read_group_order=order,
2619                                                  context=context)
2620
2621         return data
2622
2623     def _inherits_join_add(self, current_table, parent_model_name, query):
2624         """
2625         Add missing table SELECT and JOIN clause to ``query`` for reaching the parent table (no duplicates)
2626         :param current_table: current model object
2627         :param parent_model_name: name of the parent model for which the clauses should be added
2628         :param query: query object on which the JOIN should be added
2629         """
2630         inherits_field = current_table._inherits[parent_model_name]
2631         parent_model = self.pool.get(parent_model_name)
2632         parent_table_name = parent_model._table
2633         quoted_parent_table_name = '"%s"' % parent_table_name
2634         if quoted_parent_table_name not in query.tables:
2635             query.tables.append(quoted_parent_table_name)
2636             query.where_clause.append('(%s.%s = %s.id)' % (current_table._table, inherits_field, parent_table_name))
2637
2638
2639
2640     def _inherits_join_calc(self, field, query):
2641         """
2642         Adds missing table select and join clause(s) to ``query`` for reaching
2643         the field coming from an '_inherits' parent table (no duplicates).
2644
2645         :param field: name of inherited field to reach
2646         :param query: query object on which the JOIN should be added
2647         :return: qualified name of field, to be used in SELECT clause
2648         """
2649         current_table = self
2650         while field in current_table._inherit_fields and not field in current_table._columns:
2651             parent_model_name = current_table._inherit_fields[field][0]
2652             parent_table = self.pool.get(parent_model_name)
2653             self._inherits_join_add(current_table, parent_model_name, query)
2654             current_table = parent_table
2655         return '"%s".%s' % (current_table._table, field)
2656
2657     def _parent_store_compute(self, cr):
2658         if not self._parent_store:
2659             return
2660         _logger.info('Computing parent left and right for table %s...', self._table)
2661         def browse_rec(root, pos=0):
2662 # TODO: set order
2663             where = self._parent_name+'='+str(root)
2664             if not root:
2665                 where = self._parent_name+' IS NULL'
2666             if self._parent_order:
2667                 where += ' order by '+self._parent_order
2668             cr.execute('SELECT id FROM '+self._table+' WHERE '+where)
2669             pos2 = pos + 1
2670             for id in cr.fetchall():
2671                 pos2 = browse_rec(id[0], pos2)
2672             cr.execute('update '+self._table+' set parent_left=%s, parent_right=%s where id=%s', (pos, pos2, root))
2673             return pos2 + 1
2674         query = 'SELECT id FROM '+self._table+' WHERE '+self._parent_name+' IS NULL'
2675         if self._parent_order:
2676             query += ' order by ' + self._parent_order
2677         pos = 0
2678         cr.execute(query)
2679         for (root,) in cr.fetchall():
2680             pos = browse_rec(root, pos)
2681         return True
2682
2683     def _update_store(self, cr, f, k):
2684         _logger.info("storing computed values of fields.function '%s'", k)
2685         ss = self._columns[k]._symbol_set
2686         update_query = 'UPDATE "%s" SET "%s"=%s WHERE id=%%s' % (self._table, k, ss[0])
2687         cr.execute('select id from '+self._table)
2688         ids_lst = map(lambda x: x[0], cr.fetchall())
2689         while ids_lst:
2690             iids = ids_lst[:40]
2691             ids_lst = ids_lst[40:]
2692             res = f.get(cr, self, iids, k, SUPERUSER_ID, {})
2693             for key, val in res.items():
2694                 if f._multi:
2695                     val = val[k]
2696                 # if val is a many2one, just write the ID
2697                 if type(val) == tuple:
2698                     val = val[0]
2699                 if (val<>False) or (type(val)<>bool):
2700                     cr.execute(update_query, (ss[1](val), key))
2701
2702     def _check_selection_field_value(self, cr, uid, field, value, context=None):
2703         """Raise except_orm if value is not among the valid values for the selection field"""
2704         if self._columns[field]._type == 'reference':
2705             val_model, val_id_str = value.split(',', 1)
2706             val_id = False
2707             try:
2708                 val_id = long(val_id_str)
2709             except ValueError:
2710                 pass
2711             if not val_id:
2712                 raise except_orm(_('ValidateError'),
2713                                  _('Invalid value for reference field "%s.%s" (last part must be a non-zero integer): "%s"') % (self._table, field, value))
2714             val = val_model
2715         else:
2716             val = value
2717         if isinstance(self._columns[field].selection, (tuple, list)):
2718             if val in dict(self._columns[field].selection):
2719                 return
2720         elif val in dict(self._columns[field].selection(self, cr, uid, context=context)):
2721             return
2722         raise except_orm(_('ValidateError'),
2723             _('The value "%s" for the field "%s.%s" is not in the selection') % (value, self._table, field))
2724
2725     def _check_removed_columns(self, cr, log=False):
2726         # iterate on the database columns to drop the NOT NULL constraints
2727         # of fields which were required but have been removed (or will be added by another module)
2728         columns = [c for c in self._columns if not (isinstance(self._columns[c], fields.function) and not self._columns[c].store)]
2729         columns += MAGIC_COLUMNS
2730         cr.execute("SELECT a.attname, a.attnotnull"
2731                    "  FROM pg_class c, pg_attribute a"
2732                    " WHERE c.relname=%s"
2733                    "   AND c.oid=a.attrelid"
2734                    "   AND a.attisdropped=%s"
2735                    "   AND pg_catalog.format_type(a.atttypid, a.atttypmod) NOT IN ('cid', 'tid', 'oid', 'xid')"
2736                    "   AND a.attname NOT IN %s", (self._table, False, tuple(columns))),
2737
2738         for column in cr.dictfetchall():
2739             if log:
2740                 _logger.debug("column %s is in the table %s but not in the corresponding object %s",
2741                               column['attname'], self._table, self._name)
2742             if column['attnotnull']:
2743                 cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" DROP NOT NULL' % (self._table, column['attname']))
2744                 _schema.debug("Table '%s': column '%s': dropped NOT NULL constraint",
2745                               self._table, column['attname'])
2746
2747     def _save_constraint(self, cr, constraint_name, type):
2748         """
2749         Record the creation of a constraint for this model, to make it possible
2750         to delete it later when the module is uninstalled. Type can be either
2751         'f' or 'u' depending on the constraing being a foreign key or not.
2752         """
2753         assert type in ('f', 'u')
2754         cr.execute("""
2755             SELECT 1 FROM ir_model_constraint, ir_module_module
2756             WHERE ir_model_constraint.module=ir_module_module.id
2757                 AND ir_model_constraint.name=%s
2758                 AND ir_module_module.name=%s
2759             """, (constraint_name, self._module))
2760         if not cr.rowcount:
2761             cr.execute("""
2762                 INSERT INTO ir_model_constraint
2763                     (name, date_init, date_update, module, model, type)
2764                 VALUES (%s, now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC',
2765                     (SELECT id FROM ir_module_module WHERE name=%s),
2766                     (SELECT id FROM ir_model WHERE model=%s), %s)""",
2767                     (constraint_name, self._module, self._name, type))
2768
2769     def _save_relation_table(self, cr, relation_table):
2770         """
2771         Record the creation of a many2many for this model, to make it possible
2772         to delete it later when the module is uninstalled.
2773         """
2774         cr.execute("""
2775             SELECT 1 FROM ir_model_relation, ir_module_module
2776             WHERE ir_model_relation.module=ir_module_module.id
2777                 AND ir_model_relation.name=%s
2778                 AND ir_module_module.name=%s
2779             """, (relation_table, self._module))
2780         if not cr.rowcount:
2781             cr.execute("""INSERT INTO ir_model_relation (name, date_init, date_update, module, model)
2782                                  VALUES (%s, now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC',
2783                     (SELECT id FROM ir_module_module WHERE name=%s),
2784                     (SELECT id FROM ir_model WHERE model=%s))""",
2785                        (relation_table, self._module, self._name))
2786
2787     # checked version: for direct m2o starting from `self`
2788     def _m2o_add_foreign_key_checked(self, source_field, dest_model, ondelete):
2789         assert self.is_transient() or not dest_model.is_transient(), \
2790             'Many2One relationships from non-transient Model to TransientModel are forbidden'
2791         if self.is_transient() and not dest_model.is_transient():
2792             # TransientModel relationships to regular Models are annoying
2793             # usually because they could block deletion due to the FKs.
2794             # So unless stated otherwise we default them to ondelete=cascade.
2795             ondelete = ondelete or 'cascade'
2796         self._foreign_keys.append((self._table, source_field, dest_model._table, ondelete or 'set null'))
2797         _schema.debug("Table '%s': added foreign key '%s' with definition=REFERENCES \"%s\" ON DELETE %s",
2798             self._table, source_field, dest_model._table, ondelete)
2799
2800     # unchecked version: for custom cases, such as m2m relationships
2801     def _m2o_add_foreign_key_unchecked(self, source_table, source_field, dest_model, ondelete):
2802         self._foreign_keys.append((source_table, source_field, dest_model._table, ondelete or 'set null'))
2803         _schema.debug("Table '%s': added foreign key '%s' with definition=REFERENCES \"%s\" ON DELETE %s",
2804             source_table, source_field, dest_model._table, ondelete)
2805
2806     def _drop_constraint(self, cr, source_table, constraint_name):
2807         cr.execute("ALTER TABLE %s DROP CONSTRAINT %s" % (source_table,constraint_name))
2808
2809     def _m2o_fix_foreign_key(self, cr, source_table, source_field, dest_model, ondelete):
2810         # Find FK constraint(s) currently established for the m2o field,
2811         # and see whether they are stale or not 
2812         cr.execute("""SELECT confdeltype as ondelete_rule, conname as constraint_name,
2813                              cl2.relname as foreign_table
2814                       FROM pg_constraint as con, pg_class as cl1, pg_class as cl2,
2815                            pg_attribute as att1, pg_attribute as att2
2816                       WHERE con.conrelid = cl1.oid 
2817                         AND cl1.relname = %s 
2818                         AND con.confrelid = cl2.oid 
2819                         AND array_lower(con.conkey, 1) = 1 
2820                         AND con.conkey[1] = att1.attnum 
2821                         AND att1.attrelid = cl1.oid 
2822                         AND att1.attname = %s 
2823                         AND array_lower(con.confkey, 1) = 1 
2824                         AND con.confkey[1] = att2.attnum 
2825                         AND att2.attrelid = cl2.oid 
2826                         AND att2.attname = %s 
2827                         AND con.contype = 'f'""", (source_table, source_field, 'id'))
2828         constraints = cr.dictfetchall()
2829         if constraints:
2830             if len(constraints) == 1:
2831                 # Is it the right constraint?
2832                 cons, = constraints 
2833                 if cons['ondelete_rule'] != POSTGRES_CONFDELTYPES.get((ondelete or 'set null').upper(), 'a')\
2834                     or cons['foreign_table'] != dest_model._table:
2835                     _schema.debug("Table '%s': dropping obsolete FK constraint: '%s'",
2836                                   source_table, cons['constraint_name'])
2837                     self._drop_constraint(cr, source_table, cons['constraint_name'])
2838                     self._m2o_add_foreign_key_checked(source_field, dest_model, ondelete)
2839                 # else it's all good, nothing to do!
2840             else:
2841                 # Multiple FKs found for the same field, drop them all, and re-create
2842                 for cons in constraints:
2843                     _schema.debug("Table '%s': dropping duplicate FK constraints: '%s'",
2844                                   source_table, cons['constraint_name'])
2845                     self._drop_constraint(cr, source_table, cons['constraint_name'])
2846                 self._m2o_add_foreign_key_checked(source_field, dest_model, ondelete)
2847
2848
2849
2850     def _auto_init(self, cr, context=None):
2851         """
2852
2853         Call _field_create and, unless _auto is False:
2854
2855         - create the corresponding table in database for the model,
2856         - possibly add the parent columns in database,
2857         - possibly add the columns 'create_uid', 'create_date', 'write_uid',
2858           'write_date' in database if _log_access is True (the default),
2859         - report on database columns no more existing in _columns,
2860         - remove no more existing not null constraints,
2861         - alter existing database columns to match _columns,
2862         - create database tables to match _columns,
2863         - add database indices to match _columns,
2864         - save in self._foreign_keys a list a foreign keys to create (see
2865           _auto_end).
2866
2867         """
2868         self._foreign_keys = []
2869         raise_on_invalid_object_name(self._name)
2870         if context is None:
2871             context = {}
2872         store_compute = False
2873         todo_end = []
2874         update_custom_fields = context.get('update_custom_fields', False)
2875         self._field_create(cr, context=context)
2876         create = not self._table_exist(cr)
2877         if getattr(self, '_auto', True):
2878
2879             if create:
2880                 self._create_table(cr)
2881
2882             cr.commit()
2883             if self._parent_store:
2884                 if not self._parent_columns_exist(cr):
2885                     self._create_parent_columns(cr)
2886                     store_compute = True
2887
2888             # Create the create_uid, create_date, write_uid, write_date, columns if desired.
2889             if self._log_access:
2890                 self._add_log_columns(cr)
2891
2892             self._check_removed_columns(cr, log=False)
2893
2894             # iterate on the "object columns"
2895             column_data = self._select_column_data(cr)
2896
2897             for k, f in self._columns.iteritems():
2898                 if k in MAGIC_COLUMNS:
2899                     continue
2900                 # Don't update custom (also called manual) fields
2901                 if f.manual and not update_custom_fields:
2902                     continue
2903
2904                 if isinstance(f, fields.one2many):
2905                     self._o2m_raise_on_missing_reference(cr, f)
2906
2907                 elif isinstance(f, fields.many2many):
2908                     self._m2m_raise_or_create_relation(cr, f)
2909
2910                 else:
2911                     res = column_data.get(k)
2912
2913                     # The field is not found as-is in database, try if it
2914                     # exists with an old name.
2915                     if not res and hasattr(f, 'oldname'):
2916                         res = column_data.get(f.oldname)
2917                         if res:
2918                             cr.execute('ALTER TABLE "%s" RENAME "%s" TO "%s"' % (self._table, f.oldname, k))
2919                             res['attname'] = k
2920                             column_data[k] = res
2921                             _schema.debug("Table '%s': renamed column '%s' to '%s'",
2922                                 self._table, f.oldname, k)
2923
2924                     # The field already exists in database. Possibly
2925                     # change its type, rename it, drop it or change its
2926                     # constraints.
2927                     if res:
2928                         f_pg_type = res['typname']
2929                         f_pg_size = res['size']
2930                         f_pg_notnull = res['attnotnull']
2931                         if isinstance(f, fields.function) and not f.store and\
2932                                 not getattr(f, 'nodrop', False):
2933                             _logger.info('column %s (%s) in table %s removed: converted to a function !\n',
2934                                          k, f.string, self._table)
2935                             cr.execute('ALTER TABLE "%s" DROP COLUMN "%s" CASCADE' % (self._table, k))
2936                             cr.commit()
2937                             _schema.debug("Table '%s': dropped column '%s' with cascade",
2938                                 self._table, k)
2939                             f_obj_type = None
2940                         else:
2941                             f_obj_type = get_pg_type(f) and get_pg_type(f)[0]
2942
2943                         if f_obj_type:
2944                             ok = False
2945                             casts = [
2946                                 ('text', 'char', pg_varchar(f.size), '::%s' % pg_varchar(f.size)),
2947                                 ('varchar', 'text', 'TEXT', ''),
2948                                 ('int4', 'float', get_pg_type(f)[1], '::'+get_pg_type(f)[1]),
2949                                 ('date', 'datetime', 'TIMESTAMP', '::TIMESTAMP'),
2950                                 ('timestamp', 'date', 'date', '::date'),
2951                                 ('numeric', 'float', get_pg_type(f)[1], '::'+get_pg_type(f)[1]),
2952                                 ('float8', 'float', get_pg_type(f)[1], '::'+get_pg_type(f)[1]),
2953                             ]
2954                             if f_pg_type == 'varchar' and f._type == 'char' and f_pg_size < f.size:
2955                                 cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO temp_change_size' % (self._table, k))
2956                                 cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, pg_varchar(f.size)))
2957                                 cr.execute('UPDATE "%s" SET "%s"=temp_change_size::%s' % (self._table, k, pg_varchar(f.size)))
2958                                 cr.execute('ALTER TABLE "%s" DROP COLUMN temp_change_size CASCADE' % (self._table,))
2959                                 cr.commit()
2960                                 _schema.debug("Table '%s': column '%s' (type varchar) changed size from %s to %s",
2961                                     self._table, k, f_pg_size, f.size)
2962                             for c in casts:
2963                                 if (f_pg_type==c[0]) and (f._type==c[1]):
2964                                     if f_pg_type != f_obj_type:
2965                                         ok = True
2966                                         cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO temp_change_size' % (self._table, k))
2967                                         cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, c[2]))
2968                                         cr.execute(('UPDATE "%s" SET "%s"=temp_change_size'+c[3]) % (self._table, k))
2969                                         cr.execute('ALTER TABLE "%s" DROP COLUMN temp_change_size CASCADE' % (self._table,))
2970                                         cr.commit()
2971                                         _schema.debug("Table '%s': column '%s' changed type from %s to %s",
2972                                             self._table, k, c[0], c[1])
2973                                     break
2974
2975                             if f_pg_type != f_obj_type:
2976                                 if not ok:
2977                                     i = 0
2978                                     while True:
2979                                         newname = k + '_moved' + str(i)
2980                                         cr.execute("SELECT count(1) FROM pg_class c,pg_attribute a " \
2981                                             "WHERE c.relname=%s " \
2982                                             "AND a.attname=%s " \
2983                                             "AND c.oid=a.attrelid ", (self._table, newname))
2984                                         if not cr.fetchone()[0]:
2985                                             break
2986                                         i += 1
2987                                     if f_pg_notnull:
2988                                         cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" DROP NOT NULL' % (self._table, k))
2989                                     cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % (self._table, k, newname))
2990                                     cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, get_pg_type(f)[1]))
2991                                     cr.execute("COMMENT ON COLUMN %s.\"%s\" IS %%s" % (self._table, k), (f.string,))
2992                                     _schema.debug("Table '%s': column '%s' has changed type (DB=%s, def=%s), data moved to column %s !",
2993                                         self._table, k, f_pg_type, f._type, newname)
2994
2995                             # if the field is required and hasn't got a NOT NULL constraint
2996                             if f.required and f_pg_notnull == 0:
2997                                 # set the field to the default value if any
2998                                 if k in self._defaults:
2999                                     if callable(self._defaults[k]):
3000                                         default = self._defaults[k](self, cr, SUPERUSER_ID, context)
3001                                     else:
3002                                         default = self._defaults[k]
3003
3004                                     if (default is not None):
3005                                         ss = self._columns[k]._symbol_set
3006                                         query = 'UPDATE "%s" SET "%s"=%s WHERE "%s" is NULL' % (self._table, k, ss[0], k)
3007                                         cr.execute(query, (ss[1](default),))
3008                                 # add the NOT NULL constraint
3009                                 cr.commit()
3010                                 try:
3011                                     cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, k), log_exceptions=False)
3012                                     cr.commit()
3013                                     _schema.debug("Table '%s': column '%s': added NOT NULL constraint",
3014                                         self._table, k)
3015                                 except Exception:
3016                                     msg = "Table '%s': unable to set a NOT NULL constraint on column '%s' !\n"\
3017                                         "If you want to have it, you should update the records and execute manually:\n"\
3018                                         "ALTER TABLE %s ALTER COLUMN %s SET NOT NULL"
3019                                     _schema.warning(msg, self._table, k, self._table, k)
3020                                 cr.commit()
3021                             elif not f.required and f_pg_notnull == 1:
3022                                 cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" DROP NOT NULL' % (self._table, k))
3023                                 cr.commit()
3024                                 _schema.debug("Table '%s': column '%s': dropped NOT NULL constraint",
3025                                     self._table, k)
3026                             # Verify index
3027                             indexname = '%s_%s_index' % (self._table, k)
3028                             cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = %s and tablename = %s", (indexname, self._table))
3029                             res2 = cr.dictfetchall()
3030                             if not res2 and f.select:
3031                                 cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (self._table, k, self._table, k))
3032                                 cr.commit()
3033                                 if f._type == 'text':
3034                                     # FIXME: for fields.text columns we should try creating GIN indexes instead (seems most suitable for an ERP context)
3035                                     msg = "Table '%s': Adding (b-tree) index for text column '%s'."\
3036                                         "This is probably useless (does not work for fulltext search) and prevents INSERTs of long texts"\
3037                                         " because there is a length limit for indexable btree values!\n"\
3038                                         "Use a search view instead if you simply want to make the field searchable."
3039                                     _schema.warning(msg, self._table, k, f._type)
3040                             if res2 and not f.select:
3041                                 cr.execute('DROP INDEX "%s_%s_index"' % (self._table, k))
3042                                 cr.commit()
3043                                 msg = "Table '%s': dropping index for column '%s' of type '%s' as it is not required anymore"
3044                                 _schema.debug(msg, self._table, k, f._type)
3045
3046                             if isinstance(f, fields.many2one):
3047                                 dest_model = self.pool.get(f._obj)
3048                                 if dest_model._table != 'ir_actions':
3049                                     self._m2o_fix_foreign_key(cr, self._table, k, dest_model, f.ondelete)
3050
3051                     # The field doesn't exist in database. Create it if necessary.
3052                     else:
3053                         if not isinstance(f, fields.function) or f.store:
3054                             # add the missing field
3055                             cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, get_pg_type(f)[1]))
3056                             cr.execute("COMMENT ON COLUMN %s.\"%s\" IS %%s" % (self._table, k), (f.string,))
3057                             _schema.debug("Table '%s': added column '%s' with definition=%s",
3058                                 self._table, k, get_pg_type(f)[1])
3059
3060                             # initialize it
3061                             if not create and k in self._defaults:
3062                                 if callable(self._defaults[k]):
3063                                     default = self._defaults[k](self, cr, SUPERUSER_ID, context)
3064                                 else:
3065                                     default = self._defaults[k]
3066
3067                                 ss = self._columns[k]._symbol_set
3068                                 query = 'UPDATE "%s" SET "%s"=%s' % (self._table, k, ss[0])
3069                                 cr.execute(query, (ss[1](default),))
3070                                 cr.commit()
3071                                 _logger.debug("Table '%s': setting default value of new column %s", self._table, k)
3072
3073                             # remember the functions to call for the stored fields
3074                             if isinstance(f, fields.function):
3075                                 order = 10
3076                                 if f.store is not True: # i.e. if f.store is a dict
3077                                     order = f.store[f.store.keys()[0]][2]
3078                                 todo_end.append((order, self._update_store, (f, k)))
3079
3080                             # and add constraints if needed
3081                             if isinstance(f, fields.many2one):
3082                                 if not self.pool.get(f._obj):
3083                                     raise except_orm('Programming Error', ('There is no reference available for %s') % (f._obj,))
3084                                 dest_model = self.pool.get(f._obj)
3085                                 ref = dest_model._table
3086                                 # ir_actions is inherited so foreign key doesn't work on it
3087                                 if ref != 'ir_actions':
3088                                     self._m2o_add_foreign_key_checked(k, dest_model, f.ondelete)
3089                             if f.select:
3090                                 cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (self._table, k, self._table, k))
3091                             if f.required:
3092                                 try:
3093                                     cr.commit()
3094                                     cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, k), log_exceptions=False)
3095                                     _schema.debug("Table '%s': column '%s': added a NOT NULL constraint",
3096                                         self._table, k)
3097                                 except Exception:
3098                                     msg = "WARNING: unable to set column %s of table %s not null !\n"\
3099                                         "Try to re-run: openerp-server --update=module\n"\
3100                                         "If it doesn't work, update records and execute manually:\n"\
3101                                         "ALTER TABLE %s ALTER COLUMN %s SET NOT NULL"
3102                                     _logger.warning(msg, k, self._table, self._table, k)
3103                             cr.commit()
3104
3105         else:
3106             cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (self._table,))
3107             create = not bool(cr.fetchone())
3108
3109         cr.commit()     # start a new transaction
3110
3111         self._add_sql_constraints(cr)
3112
3113         if create:
3114             self._execute_sql(cr)
3115
3116         if store_compute:
3117             self._parent_store_compute(cr)
3118             cr.commit()
3119
3120         return todo_end
3121
3122     def _auto_end(self, cr, context=None):
3123         """ Create the foreign keys recorded by _auto_init. """
3124         for t, k, r, d in self._foreign_keys:
3125             cr.execute('ALTER TABLE "%s" ADD FOREIGN KEY ("%s") REFERENCES "%s" ON DELETE %s' % (t, k, r, d))
3126             self._save_constraint(cr, "%s_%s_fkey" % (t, k), 'f')
3127         cr.commit()
3128         del self._foreign_keys
3129
3130
3131     def _table_exist(self, cr):
3132         cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (self._table,))
3133         return cr.rowcount
3134
3135
3136     def _create_table(self, cr):
3137         cr.execute('CREATE TABLE "%s" (id SERIAL NOT NULL, PRIMARY KEY(id)) WITHOUT OIDS' % (self._table,))
3138         cr.execute(("COMMENT ON TABLE \"%s\" IS %%s" % self._table), (self._description,))
3139         _schema.debug("Table '%s': created", self._table)
3140
3141
3142     def _parent_columns_exist(self, cr):
3143         cr.execute("""SELECT c.relname
3144             FROM pg_class c, pg_attribute a
3145             WHERE c.relname=%s AND a.attname=%s AND c.oid=a.attrelid
3146             """, (self._table, 'parent_left'))
3147         return cr.rowcount
3148
3149
3150     def _create_parent_columns(self, cr):
3151         cr.execute('ALTER TABLE "%s" ADD COLUMN "parent_left" INTEGER' % (self._table,))
3152         cr.execute('ALTER TABLE "%s" ADD COLUMN "parent_right" INTEGER' % (self._table,))
3153         if 'parent_left' not in self._columns:
3154             _logger.error('create a column parent_left on object %s: fields.integer(\'Left Parent\', select=1)',
3155                           self._table)
3156             _schema.debug("Table '%s': added column '%s' with definition=%s",
3157                 self._table, 'parent_left', 'INTEGER')
3158         elif not self._columns['parent_left'].select:
3159             _logger.error('parent_left column on object %s must be indexed! Add select=1 to the field definition)',
3160                           self._table)
3161         if 'parent_right' not in self._columns:
3162             _logger.error('create a column parent_right on object %s: fields.integer(\'Right Parent\', select=1)',
3163                           self._table)
3164             _schema.debug("Table '%s': added column '%s' with definition=%s",
3165                 self._table, 'parent_right', 'INTEGER')
3166         elif not self._columns['parent_right'].select:
3167             _logger.error('parent_right column on object %s must be indexed! Add select=1 to the field definition)',
3168                           self._table)
3169         if self._columns[self._parent_name].ondelete != 'cascade':
3170             _logger.error("The column %s on object %s must be set as ondelete='cascade'",
3171                           self._parent_name, self._name)
3172
3173         cr.commit()
3174
3175
3176     def _add_log_columns(self, cr):
3177         for field, field_def in LOG_ACCESS_COLUMNS.iteritems():
3178             cr.execute("""
3179                 SELECT c.relname
3180                   FROM pg_class c, pg_attribute a
3181                  WHERE c.relname=%s AND a.attname=%s AND c.oid=a.attrelid
3182                 """, (self._table, field))
3183             if not cr.rowcount:
3184                 cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, field, field_def))
3185                 cr.commit()
3186                 _schema.debug("Table '%s': added column '%s' with definition=%s",
3187                     self._table, field, field_def)
3188
3189
3190     def _select_column_data(self, cr):
3191         # attlen is the number of bytes necessary to represent the type when
3192         # the type has a fixed size. If the type has a varying size attlen is
3193         # -1 and atttypmod is the size limit + 4, or -1 if there is no limit.
3194         # Thus the query can return a negative size for a unlimited varchar.
3195         cr.execute("SELECT c.relname,a.attname,a.attlen,a.atttypmod,a.attnotnull,a.atthasdef,t.typname,CASE WHEN a.attlen=-1 THEN a.atttypmod-4 ELSE a.attlen END as size " \
3196            "FROM pg_class c,pg_attribute a,pg_type t " \
3197            "WHERE c.relname=%s " \
3198            "AND c.oid=a.attrelid " \
3199            "AND a.atttypid=t.oid", (self._table,))
3200         return dict(map(lambda x: (x['attname'], x),cr.dictfetchall()))
3201
3202
3203     def _o2m_raise_on_missing_reference(self, cr, f):
3204         # TODO this check should be a method on fields.one2many.
3205         
3206         other = self.pool.get(f._obj)
3207         if other:
3208             # TODO the condition could use fields_get_keys().
3209             if f._fields_id not in other._columns.keys():
3210                 if f._fields_id not in other._inherit_fields.keys():
3211                     raise except_orm('Programming Error', ("There is no reference field '%s' found for '%s'") % (f._fields_id, f._obj,))
3212
3213     def _m2m_raise_or_create_relation(self, cr, f):
3214         m2m_tbl, col1, col2 = f._sql_names(self)
3215         self._save_relation_table(cr, m2m_tbl)
3216         cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (m2m_tbl,))
3217         if not cr.dictfetchall():
3218             if not self.pool.get(f._obj):
3219                 raise except_orm('Programming Error', ('Many2Many destination model does not exist: `%s`') % (f._obj,))
3220             dest_model = self.pool.get(f._obj)
3221             ref = dest_model._table
3222             cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s")) WITH OIDS' % (m2m_tbl, col1, col2, col1, col2))
3223             # create foreign key references with ondelete=cascade, unless the targets are SQL views
3224             cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (ref,))
3225             if not cr.fetchall():
3226                 self._m2o_add_foreign_key_unchecked(m2m_tbl, col2, dest_model, 'cascade')
3227             cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (self._table,))
3228             if not cr.fetchall():
3229                 self._m2o_add_foreign_key_unchecked(m2m_tbl, col1, self, 'cascade')
3230
3231             cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (m2m_tbl, col1, m2m_tbl, col1))
3232             cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (m2m_tbl, col2, m2m_tbl, col2))
3233             cr.execute("COMMENT ON TABLE \"%s\" IS 'RELATION BETWEEN %s AND %s'" % (m2m_tbl, self._table, ref))
3234             cr.commit()
3235             _schema.debug("Create table '%s': m2m relation between '%s' and '%s'", m2m_tbl, self._table, ref)
3236
3237
3238     def _add_sql_constraints(self, cr):
3239         """
3240
3241         Modify this model's database table constraints so they match the one in
3242         _sql_constraints.
3243
3244         """
3245         def unify_cons_text(txt):
3246             return txt.lower().replace(', ',',').replace(' (','(')
3247
3248         for (key, con, _) in self._sql_constraints:
3249             conname = '%s_%s' % (self._table, key)
3250
3251             self._save_constraint(cr, conname, 'u')
3252             cr.execute("SELECT conname, pg_catalog.pg_get_constraintdef(oid, true) as condef FROM pg_constraint where conname=%s", (conname,))
3253             existing_constraints = cr.dictfetchall()
3254             sql_actions = {
3255                 'drop': {
3256                     'execute': False,
3257                     'query': 'ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (self._table, conname, ),
3258                     'msg_ok': "Table '%s': dropped constraint '%s'. Reason: its definition changed from '%%s' to '%s'" % (
3259                         self._table, conname, con),
3260                     'msg_err': "Table '%s': unable to drop \'%s\' constraint !" % (self._table, con),
3261                     'order': 1,
3262                 },
3263                 'add': {
3264                     'execute': False,
3265                     'query': 'ALTER TABLE "%s" ADD CONSTRAINT "%s" %s' % (self._table, conname, con,),
3266                     'msg_ok': "Table '%s': added constraint '%s' with definition=%s" % (self._table, conname, con),
3267                     'msg_err': "Table '%s': unable to add \'%s\' constraint !\n If you want to have it, you should update the records and execute manually:\n%%s" % (
3268                         self._table, con),
3269                     'order': 2,
3270                 },
3271             }
3272
3273             if not existing_constraints:
3274                 # constraint does not exists:
3275                 sql_actions['add']['execute'] = True
3276                 sql_actions['add']['msg_err'] = sql_actions['add']['msg_err'] % (sql_actions['add']['query'], )
3277             elif unify_cons_text(con) not in [unify_cons_text(item['condef']) for item in existing_constraints]:
3278                 # constraint exists but its definition has changed:
3279                 sql_actions['drop']['execute'] = True
3280                 sql_actions['drop']['msg_ok'] = sql_actions['drop']['msg_ok'] % (existing_constraints[0]['condef'].lower(), )
3281                 sql_actions['add']['execute'] = True
3282                 sql_actions['add']['msg_err'] = sql_actions['add']['msg_err'] % (sql_actions['add']['query'], )
3283
3284             # we need to add the constraint:
3285             sql_actions = [item for item in sql_actions.values()]
3286             sql_actions.sort(key=lambda x: x['order'])
3287             for sql_action in [action for action in sql_actions if action['execute']]:
3288                 try:
3289                     cr.execute(sql_action['query'])
3290                     cr.commit()
3291                     _schema.debug(sql_action['msg_ok'])
3292                 except:
3293                     _schema.warning(sql_action['msg_err'])
3294                     cr.rollback()
3295
3296
3297     def _execute_sql(self, cr):
3298         """ Execute the SQL code from the _sql attribute (if any)."""
3299         if hasattr(self, "_sql"):
3300             for line in self._sql.split(';'):
3301                 line2 = line.replace('\n', '').strip()
3302                 if line2:
3303                     cr.execute(line2)
3304                     cr.commit()
3305
3306     #
3307     # Update objects that uses this one to update their _inherits fields
3308     #
3309
3310     def _inherits_reload_src(self):
3311         """ Recompute the _inherit_fields mapping on each _inherits'd child model."""
3312         for obj in self.pool.models.values():
3313             if self._name in obj._inherits:
3314                 obj._inherits_reload()
3315
3316
3317     def _inherits_reload(self):
3318         """ Recompute the _inherit_fields mapping.
3319
3320         This will also call itself on each inherits'd child model.
3321
3322         """
3323         res = {}
3324         for table in self._inherits:
3325             other = self.pool.get(table)
3326             for col in other._columns.keys():
3327                 res[col] = (table, self._inherits[table], other._columns[col], table)
3328             for col in other._inherit_fields.keys():
3329                 res[col] = (table, self._inherits[table], other._inherit_fields[col][2], other._inherit_fields[col][3])
3330         self._inherit_fields = res
3331         self._all_columns = self._get_column_infos()
3332         self._inherits_reload_src()
3333
3334
3335     def _get_column_infos(self):
3336         """Returns a dict mapping all fields names (direct fields and
3337            inherited field via _inherits) to a ``column_info`` struct
3338            giving detailed columns """
3339         result = {}
3340         for k, (parent, m2o, col, original_parent) in self._inherit_fields.iteritems():
3341             result[k] = fields.column_info(k, col, parent, m2o, original_parent)
3342         for k, col in self._columns.iteritems():
3343             result[k] = fields.column_info(k, col)
3344         return result
3345
3346
3347     def _inherits_check(self):
3348         for table, field_name in self._inherits.items():
3349             if field_name not in self._columns:
3350                 _logger.info('Missing many2one field definition for _inherits reference "%s" in "%s", using default one.', field_name, self._name)
3351                 self._columns[field_name] = fields.many2one(table, string="Automatically created field to link to parent %s" % table,
3352                                                              required=True, ondelete="cascade")
3353             elif not self._columns[field_name].required or self._columns[field_name].ondelete.lower() != "cascade":
3354                 _logger.warning('Field definition for _inherits reference "%s" in "%s" must be marked as "required" with ondelete="cascade", forcing it.', field_name, self._name)
3355                 self._columns[field_name].required = True
3356                 self._columns[field_name].ondelete = "cascade"
3357
3358     #def __getattr__(self, name):
3359     #    """
3360     #    Proxies attribute accesses to the `inherits` parent so we can call methods defined on the inherited parent
3361     #    (though inherits doesn't use Python inheritance).
3362     #    Handles translating between local ids and remote ids.
3363     #    Known issue: doesn't work correctly when using python's own super(), don't involve inherit-based inheritance
3364     #                 when you have inherits.
3365     #    """
3366     #    for model, field in self._inherits.iteritems():
3367     #        proxy = self.pool.get(model)
3368     #        if hasattr(proxy, name):
3369     #            attribute = getattr(proxy, name)
3370     #            if not hasattr(attribute, '__call__'):
3371     #                return attribute
3372     #            break
3373     #    else:
3374     #        return super(orm, self).__getattr__(name)
3375
3376     #    def _proxy(cr, uid, ids, *args, **kwargs):
3377     #        objects = self.browse(cr, uid, ids, kwargs.get('context', None))
3378     #        lst = [obj[field].id for obj in objects if obj[field]]
3379     #        return getattr(proxy, name)(cr, uid, lst, *args, **kwargs)
3380
3381     #    return _proxy
3382
3383
3384     def fields_get(self, cr, user, allfields=None, context=None, write_access=True):
3385         """ Return the definition of each field.
3386
3387         The returned value is a dictionary (indiced by field name) of
3388         dictionaries. The _inherits'd fields are included. The string, help,
3389         and selection (if present) attributes are translated.
3390
3391         :param cr: database cursor
3392         :param user: current user id
3393         :param fields: list of fields
3394         :param context: context arguments, like lang, time zone
3395         :return: dictionary of field dictionaries, each one describing a field of the business object
3396         :raise AccessError: * if user has no create/write rights on the requested object
3397
3398         """
3399         if context is None:
3400             context = {}
3401
3402         write_access = self.check_write(cr, user, False) or \
3403             self.check_create(cr, user, False)
3404
3405         res = {}
3406
3407         translation_obj = self.pool.get('ir.translation')
3408         for parent in self._inherits:
3409             res.update(self.pool.get(parent).fields_get(cr, user, allfields, context))
3410
3411         for f, field in self._columns.iteritems():
3412             if (allfields and f not in allfields) or \
3413                 (field.groups and not self.user_has_groups(cr, user, groups=field.groups, context=context)):
3414                 continue
3415
3416             res[f] = fields.field_to_dict(self, cr, user, field, context=context)
3417
3418             if not write_access:
3419                 res[f]['readonly'] = True
3420                 res[f]['states'] = {}
3421
3422             if 'string' in res[f]:
3423                 res_trans = translation_obj._get_source(cr, user, self._name + ',' + f, 'field', context.get('lang', False) or 'en_US')
3424                 if res_trans:
3425                     res[f]['string'] = res_trans
3426             if 'help' in res[f]:
3427                 help_trans = translation_obj._get_source(cr, user, self._name + ',' + f, 'help', context.get('lang', False) or 'en_US')
3428                 if help_trans:
3429                     res[f]['help'] = help_trans
3430             if 'selection' in res[f]:
3431                 if isinstance(field.selection, (tuple, list)):
3432                     sel = field.selection
3433                     sel2 = []
3434                     for key, val in sel:
3435                         val2 = None
3436                         if val:
3437                             val2 = translation_obj._get_source(cr, user, self._name + ',' + f, 'selection', context.get('lang', False) or 'en_US', val)
3438                         sel2.append((key, val2 or val))
3439                     res[f]['selection'] = sel2
3440
3441         return res
3442
3443     def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
3444         """ Read records with given ids with the given fields
3445
3446         :param cr: database cursor
3447         :param user: current user id
3448         :param ids: id or list of the ids of the records to read
3449         :param fields: optional list of field names to return (default: all fields would be returned)
3450         :type fields: list (example ['field_name_1', ...])
3451         :param context: optional context dictionary - it may contains keys for specifying certain options
3452                         like ``context_lang``, ``context_tz`` to alter the results of the call.
3453                         A special ``bin_size`` boolean flag may also be passed in the context to request the
3454                         value of all fields.binary columns to be returned as the size of the binary instead of its
3455                         contents. This can also be selectively overriden by passing a field-specific flag
3456                         in the form ``bin_size_XXX: True/False`` where ``XXX`` is the name of the field.
3457                         Note: The ``bin_size_XXX`` form is new in OpenERP v6.0.
3458         :return: list of dictionaries((dictionary per record asked)) with requested field values
3459         :rtype: [{‘name_of_the_field’: value, ...}, ...]
3460         :raise AccessError: * if user has no read rights on the requested object
3461                             * if user tries to bypass access rules for read on the requested object
3462
3463         """
3464
3465         if not context:
3466             context = {}
3467         self.check_read(cr, user)
3468         if not fields:
3469             fields = list(set(self._columns.keys() + self._inherit_fields.keys()))
3470         if isinstance(ids, (int, long)):
3471             select = [ids]
3472         else:
3473             select = ids
3474         select = map(lambda x: isinstance(x, dict) and x['id'] or x, select)
3475         result = self._read_flat(cr, user, select, fields, context, load)
3476
3477         for r in result:
3478             for key, v in r.items():
3479                 if v is None:
3480                     r[key] = False
3481
3482         if isinstance(ids, (int, long, dict)):
3483             return result and result[0] or False
3484         return result
3485
3486     def _read_flat(self, cr, user, ids, fields_to_read, context=None, load='_classic_read'):
3487         if not context:
3488             context = {}
3489         if not ids:
3490             return []
3491         if fields_to_read == None:
3492             fields_to_read = self._columns.keys()
3493
3494         # Construct a clause for the security rules.
3495         # 'tables' hold the list of tables necessary for the SELECT including the ir.rule clauses,
3496         # or will at least contain self._table.
3497         rule_clause, rule_params, tables = self.pool.get('ir.rule').domain_get(cr, user, self._name, 'read', context=context)
3498
3499         # all inherited fields + all non inherited fields for which the attribute whose name is in load is True
3500         fields_pre = [f for f in fields_to_read if
3501                            f == self.CONCURRENCY_CHECK_FIELD
3502                         or (f in self._columns and getattr(self._columns[f], '_classic_write'))
3503                      ] + self._inherits.values()
3504
3505         res = []
3506         if len(fields_pre):
3507             def convert_field(f):
3508                 f_qual = '%s."%s"' % (self._table, f) # need fully-qualified references in case len(tables) > 1
3509                 if f in ('create_date', 'write_date'):
3510                     return "date_trunc('second', %s) as %s" % (f_qual, f)
3511                 if f == self.CONCURRENCY_CHECK_FIELD:
3512                     if self._log_access:
3513                         return "COALESCE(%s.write_date, %s.create_date, (now() at time zone 'UTC'))::timestamp AS %s" % (self._table, self._table, f,)
3514                     return "(now() at time zone 'UTC')::timestamp AS %s" % (f,)
3515                 if isinstance(self._columns[f], fields.binary) and context.get('bin_size', False):
3516                     return 'length(%s) as "%s"' % (f_qual, f)
3517                 return f_qual
3518
3519             fields_pre2 = map(convert_field, fields_pre)
3520             order_by = self._parent_order or self._order
3521             select_fields = ','.join(fields_pre2 + [self._table + '.id'])
3522             query = 'SELECT %s FROM %s WHERE %s.id IN %%s' % (select_fields, ','.join(tables), self._table)
3523             if rule_clause:
3524                 query += " AND " + (' OR '.join(rule_clause))
3525             query += " ORDER BY " + order_by
3526             for sub_ids in cr.split_for_in_conditions(ids):
3527                 if rule_clause:
3528                     cr.execute(query, [tuple(sub_ids)] + rule_params)
3529                     self._check_record_rules_result_count(cr, user, sub_ids, 'read', context=context)
3530                 else:
3531                     cr.execute(query, (tuple(sub_ids),))
3532                 res.extend(cr.dictfetchall())
3533         else:
3534             res = map(lambda x: {'id': x}, ids)
3535
3536         for f in fields_pre:
3537             if f == self.CONCURRENCY_CHECK_FIELD:
3538                 continue
3539             if self._columns[f].translate:
3540                 ids = [x['id'] for x in res]
3541                 #TODO: optimize out of this loop
3542                 res_trans = self.pool.get('ir.translation')._get_ids(cr, user, self._name+','+f, 'model', context.get('lang', False) or 'en_US', ids)
3543                 for r in res:
3544                     r[f] = res_trans.get(r['id'], False) or r[f]
3545
3546         for table in self._inherits:
3547             col = self._inherits[table]
3548             cols = [x for x in intersect(self._inherit_fields.keys(), fields_to_read) if x not in self._columns.keys()]
3549             if not cols:
3550                 continue
3551             res2 = self.pool.get(table).read(cr, user, [x[col] for x in res], cols, context, load)
3552
3553             res3 = {}
3554             for r in res2:
3555                 res3[r['id']] = r
3556                 del r['id']
3557
3558             for record in res:
3559                 if not record[col]: # if the record is deleted from _inherits table?
3560                     continue
3561                 record.update(res3[record[col]])
3562                 if col not in fields_to_read:
3563                     del record[col]
3564
3565         # all fields which need to be post-processed by a simple function (symbol_get)
3566         fields_post = filter(lambda x: x in self._columns and self._columns[x]._symbol_get, fields_to_read)
3567         if fields_post:
3568             for r in res:
3569                 for f in fields_post:
3570                     r[f] = self._columns[f]._symbol_get(r[f])
3571         ids = [x['id'] for x in res]
3572
3573         # all non inherited fields for which the attribute whose name is in load is False
3574         fields_post = filter(lambda x: x in self._columns and not getattr(self._columns[x], load), fields_to_read)
3575
3576         # Compute POST fields
3577         todo = {}
3578         for f in fields_post:
3579             todo.setdefault(self._columns[f]._multi, [])
3580             todo[self._columns[f]._multi].append(f)
3581         for key, val in todo.items():
3582             if key:
3583                 res2 = self._columns[val[0]].get(cr, self, ids, val, user, context=context, values=res)
3584                 assert res2 is not None, \
3585                     'The function field "%s" on the "%s" model returned None\n' \
3586                     '(a dictionary was expected).' % (val[0], self._name)
3587                 for pos in val:
3588                     for record in res:
3589                         if isinstance(res2[record['id']], str): res2[record['id']] = eval(res2[record['id']]) #TOCHECK : why got string instend of dict in python2.6
3590                         multi_fields = res2.get(record['id'],{})
3591                         if multi_fields:
3592                             record[pos] = multi_fields.get(pos,[])
3593             else:
3594                 for f in val:
3595                     res2 = self._columns[f].get(cr, self, ids, f, user, context=context, values=res)
3596                     for record in res:
3597                         if res2:
3598                             record[f] = res2[record['id']]
3599                         else:
3600                             record[f] = []
3601
3602         # Warn about deprecated fields now that fields_pre and fields_post are computed
3603         # Explicitly use list() because we may receive tuples
3604         for f in list(fields_pre) + list(fields_post):
3605             field_column = self._all_columns.get(f) and self._all_columns.get(f).column
3606             if field_column and field_column.deprecated:
3607                 _logger.warning('Field %s.%s is deprecated: %s', self._name, f, field_column.deprecated)
3608
3609         readonly = None
3610         for vals in res:
3611             for field in vals.copy():
3612                 fobj = None
3613                 if field in self._columns:
3614                     fobj = self._columns[field]
3615
3616                 if not fobj:
3617                     continue
3618                 groups = fobj.read
3619                 if groups:
3620                     edit = False
3621                     for group in groups:
3622                         module = group.split(".")[0]
3623                         grp = group.split(".")[1]
3624                         cr.execute("select count(*) from res_groups_users_rel where gid IN (select res_id from ir_model_data where name=%s and module=%s and model=%s) and uid=%s",  \
3625                                    (grp, module, 'res.groups', user))
3626                         readonly = cr.fetchall()
3627                         if readonly[0][0] >= 1:
3628                             edit = True
3629                             break
3630                         elif readonly[0][0] == 0:
3631                             edit = False
3632                         else:
3633                             edit = False
3634
3635                     if not edit:
3636                         if type(vals[field]) == type([]):
3637                             vals[field] = []
3638                         elif type(vals[field]) == type(0.0):
3639                             vals[field] = 0
3640                         elif type(vals[field]) == type(''):
3641                             vals[field] = '=No Permission='
3642                         else:
3643                             vals[field] = False
3644         return res
3645
3646     # TODO check READ access
3647     def perm_read(self, cr, user, ids, context=None, details=True):
3648         """
3649         Returns some metadata about the given records.
3650
3651         :param details: if True, \*_uid fields are replaced with the name of the user
3652         :return: list of ownership dictionaries for each requested record
3653         :rtype: list of dictionaries with the following keys:
3654
3655                     * id: object id
3656                     * create_uid: user who created the record
3657                     * create_date: date when the record was created
3658                     * write_uid: last user who changed the record
3659                     * write_date: date of the last change to the record
3660                     * xmlid: XML ID to use to refer to this record (if there is one), in format ``module.name``
3661         """
3662         if not context:
3663             context = {}
3664         if not ids:
3665             return []
3666         fields = ''
3667         uniq = isinstance(ids, (int, long))
3668         if uniq:
3669             ids = [ids]
3670         fields = ['id']
3671         if self._log_access:
3672             fields += ['create_uid', 'create_date', 'write_uid', 'write_date']
3673         quoted_table = '"%s"' % self._table
3674         fields_str = ",".join('%s.%s'%(quoted_table, field) for field in fields)
3675         query = '''SELECT %s, __imd.module, __imd.name
3676                    FROM %s LEFT JOIN ir_model_data __imd
3677                        ON (__imd.model = %%s and __imd.res_id = %s.id)
3678                    WHERE %s.id IN %%s''' % (fields_str, quoted_table, quoted_table, quoted_table)
3679         cr.execute(query, (self._name, tuple(ids)))
3680         res = cr.dictfetchall()
3681         for r in res:
3682             for key in r:
3683                 r[key] = r[key] or False
3684                 if details and key in ('write_uid', 'create_uid') and r[key]:
3685                     try:
3686                         r[key] = self.pool.get('res.users').name_get(cr, user, [r[key]])[0]
3687                     except Exception:
3688                         pass # Leave the numeric uid there
3689             r['xmlid'] = ("%(module)s.%(name)s" % r) if r['name'] else False
3690             del r['name'], r['module']
3691         if uniq:
3692             return res[ids[0]]
3693         return res
3694
3695     def _check_concurrency(self, cr, ids, context):
3696         if not context:
3697             return
3698         if not (context.get(self.CONCURRENCY_CHECK_FIELD) and self._log_access):
3699             return
3700         check_clause = "(id = %s AND %s < COALESCE(write_date, create_date, (now() at time zone 'UTC'))::timestamp)"
3701         for sub_ids in cr.split_for_in_conditions(ids):
3702             ids_to_check = []
3703             for id in sub_ids:
3704                 id_ref = "%s,%s" % (self._name, id)
3705                 update_date = context[self.CONCURRENCY_CHECK_FIELD].pop(id_ref, None)
3706                 if update_date:
3707                     ids_to_check.extend([id, update_date])
3708             if not ids_to_check:
3709                 continue
3710             cr.execute("SELECT id FROM %s WHERE %s" % (self._table, " OR ".join([check_clause]*(len(ids_to_check)/2))), tuple(ids_to_check))
3711             res = cr.fetchone()
3712             if res:
3713                 # mention the first one only to keep the error message readable
3714                 raise except_orm('ConcurrencyException', _('A document was modified since you last viewed it (%s:%d)') % (self._description, res[0]))
3715
3716     def _check_record_rules_result_count(self, cr, uid, ids, operation, context=None):
3717         """Verify that number of returned rows after applying record rules matches
3718            the length of `ids`, and raise an appropriate exception if it does not.
3719         """
3720         if cr.rowcount != len(ids):
3721             # Attempt to distinguish record rule restriction vs deleted records, 
3722             # to provide a more specific error message
3723             cr.execute('SELECT id FROM ' + self._table + ' WHERE id IN %s', (tuple(ids),))
3724             if cr.rowcount != len(ids):
3725                 if operation == 'unlink':
3726                     # no need to warn about deleting an already deleted record!
3727                     return
3728                 _logger.warning('Failed operation on deleted record(s): %s, uid: %s, model: %s', operation, uid, self._name)
3729                 raise except_orm(_('Missing document(s)'),
3730                                  _('One of the documents you are trying to access has been deleted, please try again after refreshing.'))
3731             _logger.warning('Access Denied by record rules for operation: %s, uid: %s, model: %s', operation, uid, self._name)
3732             raise except_orm(_('Access Denied'),
3733                              _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
3734                                 (self._description, operation))
3735
3736     def check_access_rights(self, cr, uid, operation, raise_exception=True): # no context on purpose.
3737         """Verifies that the operation given by ``operation`` is allowed for the user
3738            according to the access rights."""
3739         return self.pool.get('ir.model.access').check(cr, uid, self._name, operation, raise_exception)
3740
3741     def check_create(self, cr, uid, raise_exception=True):
3742         return self.check_access_rights(cr, uid, 'create', raise_exception)
3743
3744     def check_read(self, cr, uid, raise_exception=True):
3745         return self.check_access_rights(cr, uid, 'read', raise_exception)
3746
3747     def check_unlink(self, cr, uid, raise_exception=True):
3748         return self.check_access_rights(cr, uid, 'unlink', raise_exception)
3749
3750     def check_write(self, cr, uid, raise_exception=True):
3751         return self.check_access_rights(cr, uid, 'write', raise_exception)
3752
3753     def check_access_rule(self, cr, uid, ids, operation, context=None):
3754         """Verifies that the operation given by ``operation`` is allowed for the user
3755            according to ir.rules.
3756
3757            :param operation: one of ``write``, ``unlink``
3758            :raise except_orm: * if current ir.rules do not permit this operation.
3759            :return: None if the operation is allowed
3760         """
3761         if uid == SUPERUSER_ID:
3762             return
3763
3764         if self.is_transient():
3765             # Only one single implicit access rule for transient models: owner only!
3766             # This is ok to hardcode because we assert that TransientModels always
3767             # have log_access enabled so that the create_uid column is always there.
3768             # And even with _inherits, these fields are always present in the local
3769             # table too, so no need for JOINs.
3770             cr.execute("""SELECT distinct create_uid
3771                           FROM %s
3772                           WHERE id IN %%s""" % self._table, (tuple(ids),))
3773             uids = [x[0] for x in cr.fetchall()]
3774             if len(uids) != 1 or uids[0] != uid:
3775                 raise except_orm(_('Access Denied'),
3776                                  _('For this kind of document, you may only access records you created yourself.\n\n(Document type: %s)') % (self._description,))
3777         else:
3778             where_clause, where_params, tables = self.pool.get('ir.rule').domain_get(cr, uid, self._name, operation, context=context)
3779             if where_clause:
3780                 where_clause = ' and ' + ' and '.join(where_clause)
3781                 for sub_ids in cr.split_for_in_conditions(ids):
3782                     cr.execute('SELECT ' + self._table + '.id FROM ' + ','.join(tables) +
3783                                ' WHERE ' + self._table + '.id IN %s' + where_clause,
3784                                [sub_ids] + where_params)
3785                     self._check_record_rules_result_count(cr, uid, sub_ids, operation, context=context)
3786
3787     def _workflow_trigger(self, cr, uid, ids, trigger, context=None):
3788         """Call given workflow trigger as a result of a CRUD operation""" 
3789         wf_service = netsvc.LocalService("workflow")
3790         for res_id in ids:
3791             getattr(wf_service, trigger)(uid, self._name, res_id, cr)
3792
3793     def unlink(self, cr, uid, ids, context=None):
3794         """
3795         Delete records with given ids
3796
3797         :param cr: database cursor
3798         :param uid: current user id
3799         :param ids: id or list of ids
3800         :param context: (optional) context arguments, like lang, time zone
3801         :return: True
3802         :raise AccessError: * if user has no unlink rights on the requested object
3803                             * if user tries to bypass access rules for unlink on the requested object
3804         :raise UserError: if the record is default property for other records
3805
3806         """
3807         if not ids:
3808             return True
3809         if isinstance(ids, (int, long)):
3810             ids = [ids]
3811
3812         result_store = self._store_get_values(cr, uid, ids, None, context)
3813
3814         self._check_concurrency(cr, ids, context)
3815
3816         self.check_unlink(cr, uid)
3817
3818         ir_property = self.pool.get('ir.property')
3819         
3820         # Check if the records are used as default properties.
3821         domain = [('res_id', '=', False),
3822                   ('value_reference', 'in', ['%s,%s' % (self._name, i) for i in ids]),
3823                  ]
3824         if ir_property.search(cr, uid, domain, context=context):
3825             raise except_orm(_('Error'), _('Unable to delete this document because it is used as a default property'))
3826
3827         # Delete the records' properties.
3828         property_ids = ir_property.search(cr, uid, [('res_id', 'in', ['%s,%s' % (self._name, i) for i in ids])], context=context)
3829         ir_property.unlink(cr, uid, property_ids, context=context)
3830
3831         self._workflow_trigger(cr, uid, ids, 'trg_delete', context=context)
3832
3833         self.check_access_rule(cr, uid, ids, 'unlink', context=context)
3834         pool_model_data = self.pool.get('ir.model.data')
3835         ir_values_obj = self.pool.get('ir.values')
3836         for sub_ids in cr.split_for_in_conditions(ids):
3837             cr.execute('delete from ' + self._table + ' ' \
3838                        'where id IN %s', (sub_ids,))
3839
3840             # Removing the ir_model_data reference if the record being deleted is a record created by xml/csv file,
3841             # as these are not connected with real database foreign keys, and would be dangling references.
3842             # Note: following steps performed as admin to avoid access rights restrictions, and with no context
3843             #       to avoid possible side-effects during admin calls.
3844             # Step 1. Calling unlink of ir_model_data only for the affected IDS
3845             reference_ids = pool_model_data.search(cr, SUPERUSER_ID, [('res_id','in',list(sub_ids)),('model','=',self._name)])
3846             # Step 2. Marching towards the real deletion of referenced records
3847             if reference_ids:
3848                 pool_model_data.unlink(cr, SUPERUSER_ID, reference_ids)
3849
3850             # For the same reason, removing the record relevant to ir_values
3851             ir_value_ids = ir_values_obj.search(cr, uid,
3852                     ['|',('value','in',['%s,%s' % (self._name, sid) for sid in sub_ids]),'&',('res_id','in',list(sub_ids)),('model','=',self._name)],
3853                     context=context)
3854             if ir_value_ids:
3855                 ir_values_obj.unlink(cr, uid, ir_value_ids, context=context)
3856
3857         for order, object, store_ids, fields in result_store:
3858             if object != self._name:
3859                 obj = self.pool.get(object)
3860                 cr.execute('select id from '+obj._table+' where id IN %s', (tuple(store_ids),))
3861                 rids = map(lambda x: x[0], cr.fetchall())
3862                 if rids:
3863                     obj._store_set_values(cr, uid, rids, fields, context)
3864
3865         return True
3866
3867     #
3868     # TODO: Validate
3869     #
3870     def write(self, cr, user, ids, vals, context=None):
3871         """
3872         Update records with given ids with the given field values
3873
3874         :param cr: database cursor
3875         :param user: current user id
3876         :type user: integer
3877         :param ids: object id or list of object ids to update according to **vals**
3878         :param vals: field values to update, e.g {'field_name': new_field_value, ...}
3879         :type vals: dictionary
3880         :param context: (optional) context arguments, e.g. {'lang': 'en_us', 'tz': 'UTC', ...}
3881         :type context: dictionary
3882         :return: True
3883         :raise AccessError: * if user has no write rights on the requested object
3884                             * if user tries to bypass access rules for write on the requested object
3885         :raise ValidateError: if user tries to enter invalid value for a field that is not in selection
3886         :raise UserError: if a loop would be created in a hierarchy of objects a result of the operation (such as setting an object as its own parent)
3887
3888         **Note**: The type of field values to pass in ``vals`` for relationship fields is specific:
3889
3890             + For a many2many field, a list of tuples is expected.
3891               Here is the list of tuple that are accepted, with the corresponding semantics ::
3892
3893                  (0, 0,  { values })    link to a new record that needs to be created with the given values dictionary
3894                  (1, ID, { values })    update the linked record with id = ID (write *values* on it)
3895                  (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)
3896                  (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)
3897                  (4, ID)                link to existing record with id = ID (adds a relationship)
3898                  (5)                    unlink all (like using (3,ID) for all linked records)
3899                  (6, 0, [IDs])          replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
3900
3901                  Example:
3902                     [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
3903
3904             + For a one2many field, a lits of tuples is expected.
3905               Here is the list of tuple that are accepted, with the corresponding semantics ::
3906
3907                  (0, 0,  { values })    link to a new record that needs to be created with the given values dictionary
3908                  (1, ID, { values })    update the linked record with id = ID (write *values* on it)
3909                  (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)
3910
3911                  Example:
3912                     [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
3913
3914             + For a many2one field, simply use the ID of target record, which must already exist, or ``False`` to remove the link.
3915             + For a reference field, use a string with the model name, a comma, and the target object id (example: ``'product.product, 5'``)
3916
3917         """
3918         readonly = None
3919         for field in vals.copy():
3920             fobj = None
3921             if field in self._columns:
3922                 fobj = self._columns[field]
3923             elif field in self._inherit_fields:
3924                 fobj = self._inherit_fields[field][2]
3925             if not fobj:
3926                 continue
3927             groups = fobj.write
3928
3929             if groups:
3930                 edit = False
3931                 for group in groups:
3932                     module = group.split(".")[0]
3933                     grp = group.split(".")[1]
3934                     cr.execute("select count(*) from res_groups_users_rel where gid IN (select res_id from ir_model_data where name=%s and module=%s and model=%s) and uid=%s", \
3935                                (grp, module, 'res.groups', user))
3936                     readonly = cr.fetchall()
3937                     if readonly[0][0] >= 1:
3938                         edit = True
3939                         break
3940
3941                 if not edit:
3942                     vals.pop(field)
3943
3944         if not context:
3945             context = {}
3946         if not ids:
3947             return True
3948         if isinstance(ids, (int, long)):
3949             ids = [ids]
3950
3951         self._check_concurrency(cr, ids, context)
3952         self.check_write(cr, user)
3953
3954         result = self._store_get_values(cr, user, ids, vals.keys(), context) or []
3955
3956         # No direct update of parent_left/right
3957         vals.pop('parent_left', None)
3958         vals.pop('parent_right', None)
3959
3960         parents_changed = []
3961         parent_order = self._parent_order or self._order
3962         if self._parent_store and (self._parent_name in vals):
3963             # The parent_left/right computation may take up to
3964             # 5 seconds. No need to recompute the values if the
3965             # parent is the same.
3966             # Note: to respect parent_order, nodes must be processed in
3967             # order, so ``parents_changed`` must be ordered properly.
3968             parent_val = vals[self._parent_name]
3969             if parent_val:
3970                 query = "SELECT id FROM %s WHERE id IN %%s AND (%s != %%s OR %s IS NULL) ORDER BY %s" % \
3971                                 (self._table, self._parent_name, self._parent_name, parent_order)
3972                 cr.execute(query, (tuple(ids), parent_val))
3973             else:
3974                 query = "SELECT id FROM %s WHERE id IN %%s AND (%s IS NOT NULL) ORDER BY %s" % \
3975                                 (self._table, self._parent_name, parent_order)
3976                 cr.execute(query, (tuple(ids),))
3977             parents_changed = map(operator.itemgetter(0), cr.fetchall())
3978
3979         upd0 = []
3980         upd1 = []
3981         upd_todo = []
3982         updend = []
3983         direct = []
3984         totranslate = context.get('lang', False) and (context['lang'] != 'en_US')
3985         for field in vals:
3986             field_column = self._all_columns.get(field) and self._all_columns.get(field).column
3987             if field_column and field_column.deprecated:
3988                 _logger.warning('Field %s.%s is deprecated: %s', self._name, field, field_column.deprecated)
3989             if field in self._columns:
3990                 if self._columns[field]._classic_write and not (hasattr(self._columns[field], '_fnct_inv')):
3991                     if (not totranslate) or not self._columns[field].translate:
3992                         upd0.append('"'+field+'"='+self._columns[field]._symbol_set[0])
3993                         upd1.append(self._columns[field]._symbol_set[1](vals[field]))
3994                     direct.append(field)
3995                 else:
3996                     upd_todo.append(field)
3997             else:
3998                 updend.append(field)
3999             if field in self._columns \
4000                     and hasattr(self._columns[field], 'selection') \
4001                     and vals[field]:
4002                 self._check_selection_field_value(cr, user, field, vals[field], context=context)
4003
4004         if self._log_access:
4005             upd0.append('write_uid=%s')
4006             upd0.append("write_date=(now() at time zone 'UTC')")
4007             upd1.append(user)
4008
4009         if len(upd0):
4010             self.check_access_rule(cr, user, ids, 'write', context=context)
4011             for sub_ids in cr.split_for_in_conditions(ids):
4012                 cr.execute('update ' + self._table + ' set ' + ','.join(upd0) + ' ' \
4013                            'where id IN %s', upd1 + [sub_ids])
4014                 if cr.rowcount != len(sub_ids):
4015                     raise except_orm(_('AccessError'),
4016                                      _('One of the records you are trying to modify has already been deleted (Document type: %s).') % self._description)
4017
4018             if totranslate:
4019                 # TODO: optimize
4020                 for f in direct:
4021                     if self._columns[f].translate:
4022                         src_trans = self.pool.get(self._name).read(cr, user, ids, [f])[0][f]
4023                         if not src_trans:
4024                             src_trans = vals[f]
4025                             # Inserting value to DB
4026                             self.write(cr, user, ids, {f: vals[f]})
4027                         self.pool.get('ir.translation')._set_ids(cr, user, self._name+','+f, 'model', context['lang'], ids, vals[f], src_trans)
4028
4029
4030         # call the 'set' method of fields which are not classic_write
4031         upd_todo.sort(lambda x, y: self._columns[x].priority-self._columns[y].priority)
4032
4033         # default element in context must be removed when call a one2many or many2many
4034         rel_context = context.copy()
4035         for c in context.items():
4036             if c[0].startswith('default_'):
4037                 del rel_context[c[0]]
4038
4039         for field in upd_todo:
4040             for id in ids:
4041                 result += self._columns[field].set(cr, self, id, field, vals[field], user, context=rel_context) or []
4042
4043         unknown_fields = updend[:]
4044         for table in self._inherits:
4045             col = self._inherits[table]
4046             nids = []
4047             for sub_ids in cr.split_for_in_conditions(ids):
4048                 cr.execute('select distinct "'+col+'" from "'+self._table+'" ' \
4049                            'where id IN %s', (sub_ids,))
4050                 nids.extend([x[0] for x in cr.fetchall()])
4051
4052             v = {}
4053             for val in updend:
4054                 if self._inherit_fields[val][0] == table:
4055                     v[val] = vals[val]
4056                     unknown_fields.remove(val)
4057             if v:
4058                 self.pool.get(table).write(cr, user, nids, v, context)
4059
4060         if unknown_fields:
4061             _logger.warning(
4062                 'No such field(s) in model %s: %s.',
4063                 self._name, ', '.join(unknown_fields))
4064         self._validate(cr, user, ids, context)
4065
4066         # TODO: use _order to set dest at the right position and not first node of parent
4067         # We can't defer parent_store computation because the stored function
4068         # fields that are computer may refer (directly or indirectly) to
4069         # parent_left/right (via a child_of domain)
4070         if parents_changed:
4071             if self.pool._init:
4072                 self.pool._init_parent[self._name] = True
4073             else:
4074                 order = self._parent_order or self._order
4075                 parent_val = vals[self._parent_name]
4076                 if parent_val:
4077                     clause, params = '%s=%%s' % (self._parent_name,), (parent_val,)
4078                 else:
4079                     clause, params = '%s IS NULL' % (self._parent_name,), ()
4080
4081                 for id in parents_changed:
4082                     cr.execute('SELECT parent_left, parent_right FROM %s WHERE id=%%s' % (self._table,), (id,))
4083                     pleft, pright = cr.fetchone()
4084                     distance = pright - pleft + 1
4085
4086                     # Positions of current siblings, to locate proper insertion point;
4087                     # this can _not_ be fetched outside the loop, as it needs to be refreshed
4088                     # after each update, in case several nodes are sequentially inserted one
4089                     # next to the other (i.e computed incrementally)
4090                     cr.execute('SELECT parent_right, id FROM %s WHERE %s ORDER BY %s' % (self._table, clause, parent_order), params)
4091                     parents = cr.fetchall()
4092
4093                     # Find Position of the element
4094                     position = None
4095                     for (parent_pright, parent_id) in parents:
4096                         if parent_id == id:
4097                             break
4098                         position = parent_pright + 1
4099
4100                     # It's the first node of the parent
4101                     if not position:
4102                         if not parent_val:
4103                             position = 1
4104                         else:
4105                             cr.execute('select parent_left from '+self._table+' where id=%s', (parent_val,))
4106                             position = cr.fetchone()[0] + 1
4107
4108                     if pleft < position <= pright:
4109                         raise except_orm(_('UserError'), _('Recursivity Detected.'))
4110
4111                     if pleft < position:
4112                         cr.execute('update '+self._table+' set parent_left=parent_left+%s where parent_left>=%s', (distance, position))
4113                         cr.execute('update '+self._table+' set parent_right=parent_right+%s where parent_right>=%s', (distance, position))
4114                         cr.execute('update '+self._table+' set parent_left=parent_left+%s, parent_right=parent_right+%s where parent_left>=%s and parent_left<%s', (position-pleft, position-pleft, pleft, pright))
4115                     else:
4116                         cr.execute('update '+self._table+' set parent_left=parent_left+%s where parent_left>=%s', (distance, position))
4117                         cr.execute('update '+self._table+' set parent_right=parent_right+%s where parent_right>=%s', (distance, position))
4118                         cr.execute('update '+self._table+' set parent_left=parent_left-%s, parent_right=parent_right-%s where parent_left>=%s and parent_left<%s', (pleft-position+distance, pleft-position+distance, pleft+distance, pright+distance))
4119
4120         result += self._store_get_values(cr, user, ids, vals.keys(), context)
4121         result.sort()
4122
4123         done = {}
4124         for order, object, ids_to_update, fields_to_recompute in result:
4125             key = (object, tuple(fields_to_recompute))
4126             done.setdefault(key, {})
4127             # avoid to do several times the same computation
4128             todo = []
4129             for id in ids_to_update:
4130                 if id not in done[key]:
4131                     done[key][id] = True
4132                     todo.append(id)
4133             self.pool.get(object)._store_set_values(cr, user, todo, fields_to_recompute, context)
4134
4135         self._workflow_trigger(cr, user, ids, 'trg_write', context=context)
4136         return True
4137
4138     #
4139     # TODO: Should set perm to user.xxx
4140     #
4141     def create(self, cr, user, vals, context=None):
4142         """
4143         Create a new record for the model.
4144
4145         The values for the new record are initialized using the ``vals``
4146         argument, and if necessary the result of ``default_get()``.
4147
4148         :param cr: database cursor
4149         :param user: current user id
4150         :type user: integer
4151         :param vals: field values for new record, e.g {'field_name': field_value, ...}
4152         :type vals: dictionary
4153         :param context: optional context arguments, e.g. {'lang': 'en_us', 'tz': 'UTC', ...}
4154         :type context: dictionary
4155         :return: id of new record created
4156         :raise AccessError: * if user has no create rights on the requested object
4157                             * if user tries to bypass access rules for create on the requested object
4158         :raise ValidateError: if user tries to enter invalid value for a field that is not in selection
4159         :raise UserError: if a loop would be created in a hierarchy of objects a result of the operation (such as setting an object as its own parent)
4160
4161         **Note**: The type of field values to pass in ``vals`` for relationship fields is specific.
4162         Please see the description of the :py:meth:`~osv.osv.osv.write` method for details about the possible values and how
4163         to specify them.
4164
4165         """
4166         if not context:
4167             context = {}
4168
4169         if self.is_transient():
4170             self._transient_vacuum(cr, user)
4171
4172         self.check_create(cr, user)
4173
4174         if self._log_access:
4175             for f in LOG_ACCESS_COLUMNS:
4176                 if vals.pop(f, None) is not None:
4177                     _logger.warning(
4178                         'Field `%s` is not allowed when creating the model `%s`.',
4179                         f, self._name)
4180         vals = self._add_missing_default_values(cr, user, vals, context)
4181
4182         tocreate = {}
4183         for v in self._inherits:
4184             if self._inherits[v] not in vals:
4185                 tocreate[v] = {}
4186             else:
4187                 tocreate[v] = {'id': vals[self._inherits[v]]}
4188         (upd0, upd1, upd2) = ('', '', [])
4189         upd_todo = []
4190         unknown_fields = []
4191         for v in vals.keys():
4192             if v in self._inherit_fields and v not in self._columns:
4193                 (table, col, col_detail, original_parent) = self._inherit_fields[v]
4194                 tocreate[table][v] = vals[v]
4195                 del vals[v]
4196             else:
4197                 if (v not in self._inherit_fields) and (v not in self._columns):
4198                     del vals[v]
4199                     unknown_fields.append(v)
4200         if unknown_fields:
4201             _logger.warning(
4202                 'No such field(s) in model %s: %s.',
4203                 self._name, ', '.join(unknown_fields))
4204
4205         # Try-except added to filter the creation of those records whose filds are readonly.
4206         # Example : any dashboard which has all the fields readonly.(due to Views(database views))
4207         try:
4208             cr.execute("SELECT nextval('"+self._sequence+"')")
4209         except:
4210             raise except_orm(_('UserError'),
4211                 _('You cannot perform this operation. New Record Creation is not allowed for this object as this object is for reporting purpose.'))
4212
4213         id_new = cr.fetchone()[0]
4214         for table in tocreate:
4215             if self._inherits[table] in vals:
4216                 del vals[self._inherits[table]]
4217
4218             record_id = tocreate[table].pop('id', None)
4219
4220             if record_id is None or not record_id:
4221                 record_id = self.pool.get(table).create(cr, user, tocreate[table], context=context)
4222             else:
4223                 self.pool.get(table).write(cr, user, [record_id], tocreate[table], context=context)
4224
4225             upd0 += ',' + self._inherits[table]
4226             upd1 += ',%s'
4227             upd2.append(record_id)
4228
4229         #Start : Set bool fields to be False if they are not touched(to make search more powerful)
4230         bool_fields = [x for x in self._columns.keys() if self._columns[x]._type=='boolean']
4231
4232         for bool_field in bool_fields:
4233             if bool_field not in vals:
4234                 vals[bool_field] = False
4235         #End
4236         for field in vals.copy():
4237             fobj = None
4238             if field in self._columns:
4239                 fobj = self._columns[field]
4240             else:
4241                 fobj = self._inherit_fields[field][2]
4242             if not fobj:
4243                 continue
4244             groups = fobj.write
4245             if groups:
4246                 edit = False
4247                 for group in groups:
4248                     module = group.split(".")[0]
4249                     grp = group.split(".")[1]
4250                     cr.execute("select count(*) from res_groups_users_rel where gid IN (select res_id from ir_model_data where name='%s' and module='%s' and model='%s') and uid=%s" % \
4251                                (grp, module, 'res.groups', user))
4252                     readonly = cr.fetchall()
4253                     if readonly[0][0] >= 1:
4254                         edit = True
4255                         break
4256                     elif readonly[0][0] == 0:
4257                         edit = False
4258                     else:
4259                         edit = False
4260
4261                 if not edit:
4262                     vals.pop(field)
4263         for field in vals:
4264             if self._columns[field]._classic_write:
4265                 upd0 = upd0 + ',"' + field + '"'
4266                 upd1 = upd1 + ',' + self._columns[field]._symbol_set[0]
4267                 upd2.append(self._columns[field]._symbol_set[1](vals[field]))
4268             else:
4269                 if not isinstance(self._columns[field], fields.related):
4270                     upd_todo.append(field)
4271             if field in self._columns \
4272                     and hasattr(self._columns[field], 'selection') \
4273                     and vals[field]:
4274                 self._check_selection_field_value(cr, user, field, vals[field], context=context)
4275         if self._log_access:
4276             upd0 += ',create_uid,create_date'
4277             upd1 += ",%s,(now() at time zone 'UTC')"
4278             upd2.append(user)
4279         cr.execute('insert into "'+self._table+'" (id'+upd0+") values ("+str(id_new)+upd1+')', tuple(upd2))
4280         self.check_access_rule(cr, user, [id_new], 'create', context=context)
4281         upd_todo.sort(lambda x, y: self._columns[x].priority-self._columns[y].priority)
4282
4283         if self._parent_store and not context.get('defer_parent_store_computation'):
4284             if self.pool._init:
4285                 self.pool._init_parent[self._name] = True
4286             else:
4287                 parent = vals.get(self._parent_name, False)
4288                 if parent:
4289                     cr.execute('select parent_right from '+self._table+' where '+self._parent_name+'=%s order by '+(self._parent_order or self._order), (parent,))
4290                     pleft_old = None
4291                     result_p = cr.fetchall()
4292                     for (pleft,) in result_p:
4293                         if not pleft:
4294                             break
4295                         pleft_old = pleft
4296                     if not pleft_old:
4297                         cr.execute('select parent_left from '+self._table+' where id=%s', (parent,))
4298                         pleft_old = cr.fetchone()[0]
4299                     pleft = pleft_old
4300                 else:
4301                     cr.execute('select max(parent_right) from '+self._table)
4302                     pleft = cr.fetchone()[0] or 0
4303                 cr.execute('update '+self._table+' set parent_left=parent_left+2 where parent_left>%s', (pleft,))
4304                 cr.execute('update '+self._table+' set parent_right=parent_right+2 where parent_right>%s', (pleft,))
4305                 cr.execute('update '+self._table+' set parent_left=%s,parent_right=%s where id=%s', (pleft+1, pleft+2, id_new))
4306
4307         # default element in context must be remove when call a one2many or many2many
4308         rel_context = context.copy()
4309         for c in context.items():
4310             if c[0].startswith('default_'):
4311                 del rel_context[c[0]]
4312
4313         result = []
4314         for field in upd_todo:
4315             result += self._columns[field].set(cr, self, id_new, field, vals[field], user, rel_context) or []
4316         self._validate(cr, user, [id_new], context)
4317
4318         if not context.get('no_store_function', False):
4319             result += self._store_get_values(cr, user, [id_new], vals.keys(), context)
4320             result.sort()
4321             done = []
4322             for order, object, ids, fields2 in result:
4323                 if not (object, ids, fields2) in done:
4324                     self.pool.get(object)._store_set_values(cr, user, ids, fields2, context)
4325                     done.append((object, ids, fields2))
4326
4327         if self._log_create and not (context and context.get('no_store_function', False)):
4328             message = self._description + \
4329                 " '" + \
4330                 self.name_get(cr, user, [id_new], context=context)[0][1] + \
4331                 "' " + _("created.")
4332             self.log(cr, user, id_new, message, True, context=context)
4333         self._workflow_trigger(cr, user, [id_new], 'trg_create', context=context)
4334         return id_new
4335
4336     def browse(self, cr, uid, select, context=None, list_class=None, fields_process=None):
4337         """Fetch records as objects allowing to use dot notation to browse fields and relations
4338
4339         :param cr: database cursor
4340         :param uid: current user id
4341         :param select: id or list of ids.
4342         :param context: context arguments, like lang, time zone
4343         :rtype: object or list of objects requested
4344
4345         """
4346         self._list_class = list_class or browse_record_list
4347         cache = {}
4348         # need to accepts ints and longs because ids coming from a method
4349         # launched by button in the interface have a type long...
4350         if isinstance(select, (int, long)):
4351             return browse_record(cr, uid, select, self, cache, context=context, list_class=self._list_class, fields_process=fields_process)
4352         elif isinstance(select, list):
4353             return self._list_class([browse_record(cr, uid, id, self, cache, context=context, list_class=self._list_class, fields_process=fields_process) for id in select], context=context)
4354         else:
4355             return browse_null()
4356
4357     def _store_get_values(self, cr, uid, ids, fields, context):
4358         """Returns an ordered list of fields.functions to call due to
4359            an update operation on ``fields`` of records with ``ids``,
4360            obtained by calling the 'store' functions of these fields,
4361            as setup by their 'store' attribute.
4362
4363            :return: [(priority, model_name, [record_ids,], [function_fields,])]
4364         """
4365         if fields is None: fields = []
4366         stored_functions = self.pool._store_function.get(self._name, [])
4367
4368         # use indexed names for the details of the stored_functions:
4369         model_name_, func_field_to_compute_, id_mapping_fnct_, trigger_fields_, priority_ = range(5)
4370
4371         # only keep functions that should be triggered for the ``fields``
4372         # being written to.
4373         to_compute = [f for f in stored_functions \
4374                 if ((not f[trigger_fields_]) or set(fields).intersection(f[trigger_fields_]))]
4375
4376         mapping = {}
4377         for function in to_compute:
4378             # use admin user for accessing objects having rules defined on store fields
4379             target_ids = [id for id in function[id_mapping_fnct_](self, cr, SUPERUSER_ID, ids, context) if id]
4380
4381             # the compound key must consider the priority and model name
4382             key = (function[priority_], function[model_name_])
4383             for target_id in target_ids:
4384                 mapping.setdefault(key, {}).setdefault(target_id,set()).add(tuple(function))
4385
4386         # Here mapping looks like:
4387         # { (10, 'model_a') : { target_id1: [ (function_1_tuple, function_2_tuple) ], ... }
4388         #   (20, 'model_a') : { target_id2: [ (function_3_tuple, function_4_tuple) ], ... }
4389         #   (99, 'model_a') : { target_id1: [ (function_5_tuple, function_6_tuple) ], ... }
4390         # }
4391
4392         # Now we need to generate the batch function calls list
4393         # call_map =
4394         #   { (10, 'model_a') : [(10, 'model_a', [record_ids,], [function_fields,])] }
4395         call_map = {}
4396         for ((priority,model), id_map) in mapping.iteritems():
4397             functions_ids_maps = {}
4398             # function_ids_maps =
4399             #   { (function_1_tuple, function_2_tuple) : [target_id1, target_id2, ..] }
4400             for id, functions in id_map.iteritems():
4401                 functions_ids_maps.setdefault(tuple(functions), []).append(id)
4402             for functions, ids in functions_ids_maps.iteritems():
4403                 call_map.setdefault((priority,model),[]).append((priority, model, ids,
4404                                                                  [f[func_field_to_compute_] for f in functions]))
4405         ordered_keys = call_map.keys()
4406         ordered_keys.sort()
4407         result = []
4408         if ordered_keys:
4409             result = reduce(operator.add, (call_map[k] for k in ordered_keys))
4410         return result
4411
4412     def _store_set_values(self, cr, uid, ids, fields, context):
4413         """Calls the fields.function's "implementation function" for all ``fields``, on records with ``ids`` (taking care of
4414            respecting ``multi`` attributes), and stores the resulting values in the database directly."""
4415         if not ids:
4416             return True
4417         field_flag = False
4418         field_dict = {}
4419         if self._log_access:
4420             cr.execute('select id,write_date from '+self._table+' where id IN %s', (tuple(ids),))
4421             res = cr.fetchall()
4422             for r in res:
4423                 if r[1]:
4424                     field_dict.setdefault(r[0], [])
4425                     res_date = time.strptime((r[1])[:19], '%Y-%m-%d %H:%M:%S')
4426                     write_date = datetime.datetime.fromtimestamp(time.mktime(res_date))
4427                     for i in self.pool._store_function.get(self._name, []):
4428                         if i[5]:
4429                             up_write_date = write_date + datetime.timedelta(hours=i[5])
4430                             if datetime.datetime.now() < up_write_date:
4431                                 if i[1] in fields:
4432                                     field_dict[r[0]].append(i[1])
4433                                     if not field_flag:
4434                                         field_flag = True
4435         todo = {}
4436         keys = []
4437         for f in fields:
4438             if self._columns[f]._multi not in keys:
4439                 keys.append(self._columns[f]._multi)
4440             todo.setdefault(self._columns[f]._multi, [])
4441             todo[self._columns[f]._multi].append(f)
4442         for key in keys:
4443             val = todo[key]
4444             if key:
4445                 # use admin user for accessing objects having rules defined on store fields
4446                 result = self._columns[val[0]].get(cr, self, ids, val, SUPERUSER_ID, context=context)
4447                 for id, value in result.items():
4448                     if field_flag:
4449                         for f in value.keys():
4450                             if f in field_dict[id]:
4451                                 value.pop(f)
4452                     upd0 = []
4453                     upd1 = []
4454                     for v in value:
4455                         if v not in val:
4456                             continue
4457                         if self._columns[v]._type == 'many2one':
4458                             try:
4459                                 value[v] = value[v][0]
4460                             except:
4461                                 pass
4462                         upd0.append('"'+v+'"='+self._columns[v]._symbol_set[0])
4463                         upd1.append(self._columns[v]._symbol_set[1](value[v]))
4464                     upd1.append(id)
4465                     if upd0 and upd1:
4466                         cr.execute('update "' + self._table + '" set ' + \
4467                             ','.join(upd0) + ' where id = %s', upd1)
4468
4469             else:
4470                 for f in val:
4471                     # use admin user for accessing objects having rules defined on store fields
4472                     result = self._columns[f].get(cr, self, ids, f, SUPERUSER_ID, context=context)
4473                     for r in result.keys():
4474                         if field_flag:
4475                             if r in field_dict.keys():
4476                                 if f in field_dict[r]:
4477                                     result.pop(r)
4478                     for id, value in result.items():
4479                         if self._columns[f]._type == 'many2one':
4480                             try:
4481                                 value = value[0]
4482                             except:
4483                                 pass
4484                         cr.execute('update "' + self._table + '" set ' + \
4485                             '"'+f+'"='+self._columns[f]._symbol_set[0] + ' where id = %s', (self._columns[f]._symbol_set[1](value), id))
4486         return True
4487
4488     #
4489     # TODO: Validate
4490     #
4491     def perm_write(self, cr, user, ids, fields, context=None):
4492         raise NotImplementedError(_('This method does not exist anymore'))
4493
4494     # TODO: ameliorer avec NULL
4495     def _where_calc(self, cr, user, domain, active_test=True, context=None):
4496         """Computes the WHERE clause needed to implement an OpenERP domain.
4497         :param domain: the domain to compute
4498         :type domain: list
4499         :param active_test: whether the default filtering of records with ``active``
4500                             field set to ``False`` should be applied.
4501         :return: the query expressing the given domain as provided in domain
4502         :rtype: osv.query.Query
4503         """
4504         if not context:
4505             context = {}
4506         domain = domain[:]
4507         # if the object has a field named 'active', filter out all inactive
4508         # records unless they were explicitely asked for
4509         if 'active' in self._all_columns and (active_test and context.get('active_test', True)):
4510             if domain:
4511                 # the item[0] trick below works for domain items and '&'/'|'/'!'
4512                 # operators too
4513                 if not any(item[0] == 'active' for item in domain):
4514                     domain.insert(0, ('active', '=', 1))
4515             else:
4516                 domain = [('active', '=', 1)]
4517
4518         if domain:
4519             e = expression.expression(cr, user, domain, self, context)
4520             tables = e.get_tables()
4521             where_clause, where_params = e.to_sql()
4522             where_clause = where_clause and [where_clause] or []
4523         else:
4524             where_clause, where_params, tables = [], [], ['"%s"' % self._table]
4525
4526         return Query(tables, where_clause, where_params)
4527
4528     def _check_qorder(self, word):
4529         if not regex_order.match(word):
4530             raise except_orm(_('AccessError'), _('Invalid "order" specified. A valid "order" specification is a comma-separated list of valid field names (optionally followed by asc/desc for the direction)'))
4531         return True
4532
4533     def _apply_ir_rules(self, cr, uid, query, mode='read', context=None):
4534         """Add what's missing in ``query`` to implement all appropriate ir.rules
4535           (using the ``model_name``'s rules or the current model's rules if ``model_name`` is None)
4536
4537            :param query: the current query object
4538         """
4539         def apply_rule(added_clause, added_params, added_tables, parent_model=None, child_object=None):
4540             if added_clause:
4541                 if parent_model and child_object:
4542                     # as inherited rules are being applied, we need to add the missing JOIN
4543                     # to reach the parent table (if it was not JOINed yet in the query)
4544                     child_object._inherits_join_add(child_object, parent_model, query)
4545                 query.where_clause += added_clause
4546                 query.where_clause_params += added_params
4547                 for table in added_tables:
4548                     if table not in query.tables:
4549                         query.tables.append(table)
4550                 return True
4551             return False
4552
4553         # apply main rules on the object
4554         rule_obj = self.pool.get('ir.rule')
4555         apply_rule(*rule_obj.domain_get(cr, uid, self._name, mode, context=context))
4556
4557         # apply ir.rules from the parents (through _inherits)
4558         for inherited_model in self._inherits:
4559             kwargs = dict(parent_model=inherited_model, child_object=self) #workaround for python2.5
4560             apply_rule(*rule_obj.domain_get(cr, uid, inherited_model, mode, context=context), **kwargs)
4561
4562     def _generate_m2o_order_by(self, order_field, query):
4563         """
4564         Add possibly missing JOIN to ``query`` and generate the ORDER BY clause for m2o fields,
4565         either native m2o fields or function/related fields that are stored, including
4566         intermediate JOINs for inheritance if required.
4567
4568         :return: the qualified field name to use in an ORDER BY clause to sort by ``order_field``
4569         """
4570         if order_field not in self._columns and order_field in self._inherit_fields:
4571             # also add missing joins for reaching the table containing the m2o field
4572             qualified_field = self._inherits_join_calc(order_field, query)
4573             order_field_column = self._inherit_fields[order_field][2]
4574         else:
4575             qualified_field = '"%s"."%s"' % (self._table, order_field)
4576             order_field_column = self._columns[order_field]
4577
4578         assert order_field_column._type == 'many2one', 'Invalid field passed to _generate_m2o_order_by()'
4579         if not order_field_column._classic_write and not getattr(order_field_column, 'store', False):
4580             _logger.debug("Many2one function/related fields must be stored " \
4581                 "to be used as ordering fields! Ignoring sorting for %s.%s",
4582                 self._name, order_field)
4583             return
4584
4585         # figure out the applicable order_by for the m2o
4586         dest_model = self.pool.get(order_field_column._obj)
4587         m2o_order = dest_model._order
4588         if not regex_order.match(m2o_order):
4589             # _order is complex, can't use it here, so we default to _rec_name
4590             m2o_order = dest_model._rec_name
4591         else:
4592             # extract the field names, to be able to qualify them and add desc/asc
4593             m2o_order_list = []
4594             for order_part in m2o_order.split(","):
4595                 m2o_order_list.append(order_part.strip().split(" ",1)[0].strip())
4596             m2o_order = m2o_order_list
4597
4598         # Join the dest m2o table if it's not joined yet. We use [LEFT] OUTER join here
4599         # as we don't want to exclude results that have NULL values for the m2o
4600         src_table, src_field = qualified_field.replace('"','').split('.', 1)
4601         query.join((src_table, dest_model._table, src_field, 'id'), outer=True)
4602         qualify = lambda field: '"%s"."%s"' % (dest_model._table, field)
4603         return map(qualify, m2o_order) if isinstance(m2o_order, list) else qualify(m2o_order)
4604
4605
4606     def _generate_order_by(self, order_spec, query):
4607         """
4608         Attempt to consruct an appropriate ORDER BY clause based on order_spec, which must be
4609         a comma-separated list of valid field names, optionally followed by an ASC or DESC direction.
4610
4611         :raise" except_orm in case order_spec is malformed
4612         """
4613         order_by_clause = self._order
4614         if order_spec:
4615             order_by_elements = []
4616             self._check_qorder(order_spec)
4617             for order_part in order_spec.split(','):
4618                 order_split = order_part.strip().split(' ')
4619                 order_field = order_split[0].strip()
4620                 order_direction = order_split[1].strip() if len(order_split) == 2 else ''
4621                 inner_clause = None
4622                 if order_field == 'id':
4623                     order_by_clause = '"%s"."%s"' % (self._table, order_field)
4624                 elif order_field in self._columns:
4625                     order_column = self._columns[order_field]
4626                     if order_column._classic_read:
4627                         inner_clause = '"%s"."%s"' % (self._table, order_field)
4628                     elif order_column._type == 'many2one':
4629                         inner_clause = self._generate_m2o_order_by(order_field, query)
4630                     else:
4631                         continue # ignore non-readable or "non-joinable" fields
4632                 elif order_field in self._inherit_fields:
4633                     parent_obj = self.pool.get(self._inherit_fields[order_field][3])
4634                     order_column = parent_obj._columns[order_field]
4635                     if order_column._classic_read:
4636                         inner_clause = self._inherits_join_calc(order_field, query)
4637                     elif order_column._type == 'many2one':
4638                         inner_clause = self._generate_m2o_order_by(order_field, query)
4639                     else:
4640                         continue # ignore non-readable or "non-joinable" fields
4641                 if inner_clause:
4642                     if isinstance(inner_clause, list):
4643                         for clause in inner_clause:
4644                             order_by_elements.append("%s %s" % (clause, order_direction))
4645                     else:
4646                         order_by_elements.append("%s %s" % (inner_clause, order_direction))
4647             if order_by_elements:
4648                 order_by_clause = ",".join(order_by_elements)
4649
4650         return order_by_clause and (' ORDER BY %s ' % order_by_clause) or ''
4651
4652     def _search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None):
4653         """
4654         Private implementation of search() method, allowing specifying the uid to use for the access right check.
4655         This is useful for example when filling in the selection list for a drop-down and avoiding access rights errors,
4656         by specifying ``access_rights_uid=1`` to bypass access rights check, but not ir.rules!
4657         This is ok at the security level because this method is private and not callable through XML-RPC.
4658
4659         :param access_rights_uid: optional user ID to use when checking access rights
4660                                   (not for ir.rules, this is only for ir.model.access)
4661         """
4662         if context is None:
4663             context = {}
4664         self.check_read(cr, access_rights_uid or user)
4665
4666         # For transient models, restrict acces to the current user, except for the super-user
4667         if self.is_transient() and self._log_access and user != SUPERUSER_ID:
4668             args = expression.AND(([('create_uid', '=', user)], args or []))
4669
4670         query = self._where_calc(cr, user, args, context=context)
4671         self._apply_ir_rules(cr, user, query, 'read', context=context)
4672         order_by = self._generate_order_by(order, query)
4673         from_clause, where_clause, where_clause_params = query.get_sql()
4674
4675         limit_str = limit and ' limit %d' % limit or ''
4676         offset_str = offset and ' offset %d' % offset or ''
4677         where_str = where_clause and (" WHERE %s" % where_clause) or ''
4678
4679         if count:
4680             cr.execute('SELECT count("%s".id) FROM ' % self._table + from_clause + where_str + limit_str + offset_str, where_clause_params)
4681             res = cr.fetchall()
4682             return res[0][0]
4683         cr.execute('SELECT "%s".id FROM ' % self._table + from_clause + where_str + order_by + limit_str + offset_str, where_clause_params)
4684         res = cr.fetchall()
4685         return [x[0] for x in res]
4686
4687     # returns the different values ever entered for one field
4688     # this is used, for example, in the client when the user hits enter on
4689     # a char field
4690     def distinct_field_get(self, cr, uid, field, value, args=None, offset=0, limit=None):
4691         if not args:
4692             args = []
4693         if field in self._inherit_fields:
4694             return self.pool.get(self._inherit_fields[field][0]).distinct_field_get(cr, uid, field, value, args, offset, limit)
4695         else:
4696             return self._columns[field].search(cr, self, args, field, value, offset, limit, uid)
4697
4698     def copy_data(self, cr, uid, id, default=None, context=None):
4699         """
4700         Copy given record's data with all its fields values
4701
4702         :param cr: database cursor
4703         :param user: current user id
4704         :param id: id of the record to copy
4705         :param default: field values to override in the original values of the copied record
4706         :type default: dictionary
4707         :param context: context arguments, like lang, time zone
4708         :type context: dictionary
4709         :return: dictionary containing all the field values
4710         """
4711
4712         if context is None:
4713             context = {}
4714
4715         # avoid recursion through already copied records in case of circular relationship
4716         seen_map = context.setdefault('__copy_data_seen',{})
4717         if id in seen_map.setdefault(self._name,[]):
4718             return
4719         seen_map[self._name].append(id)
4720
4721         if default is None:
4722             default = {}
4723         if 'state' not in default:
4724             if 'state' in self._defaults:
4725                 if callable(self._defaults['state']):
4726                     default['state'] = self._defaults['state'](self, cr, uid, context)
4727                 else:
4728                     default['state'] = self._defaults['state']
4729
4730         context_wo_lang = context.copy()
4731         if 'lang' in context:
4732             del context_wo_lang['lang']
4733         data = self.read(cr, uid, [id,], context=context_wo_lang)
4734         if data:
4735             data = data[0]
4736         else:
4737             raise IndexError( _("Record #%d of %s not found, cannot copy!") %( id, self._name))
4738
4739         # TODO it seems fields_get can be replaced by _all_columns (no need for translation)
4740         fields = self.fields_get(cr, uid, context=context)
4741         for f in fields:
4742             ftype = fields[f]['type']
4743
4744             if self._log_access and f in LOG_ACCESS_COLUMNS:
4745                 del data[f]
4746
4747             if f in default:
4748                 data[f] = default[f]
4749             elif 'function' in fields[f]:
4750                 del data[f]
4751             elif ftype == 'many2one':
4752                 try:
4753                     data[f] = data[f] and data[f][0]
4754                 except:
4755                     pass
4756             elif ftype == 'one2many':
4757                 res = []
4758                 rel = self.pool.get(fields[f]['relation'])
4759                 if data[f]:
4760                     # duplicate following the order of the ids
4761                     # because we'll rely on it later for copying
4762                     # translations in copy_translation()!
4763                     data[f].sort()
4764                     for rel_id in data[f]:
4765                         # the lines are first duplicated using the wrong (old)
4766                         # parent but then are reassigned to the correct one thanks
4767                         # to the (0, 0, ...)
4768                         d = rel.copy_data(cr, uid, rel_id, context=context)
4769                         if d:
4770                             res.append((0, 0, d))
4771                 data[f] = res
4772             elif ftype == 'many2many':
4773                 data[f] = [(6, 0, data[f])]
4774
4775         del data['id']
4776
4777         # make sure we don't break the current parent_store structure and
4778         # force a clean recompute!
4779         for parent_column in ['parent_left', 'parent_right']:
4780             data.pop(parent_column, None)
4781         # Remove _inherits field's from data recursively, missing parents will
4782         # be created by create() (so that copy() copy everything).
4783         def remove_ids(inherits_dict):
4784             for parent_table in inherits_dict:
4785                 del data[inherits_dict[parent_table]]
4786                 remove_ids(self.pool.get(parent_table)._inherits)
4787         remove_ids(self._inherits)
4788         return data
4789
4790     def copy_translations(self, cr, uid, old_id, new_id, context=None):
4791         if context is None:
4792             context = {}
4793
4794         # avoid recursion through already copied records in case of circular relationship
4795         seen_map = context.setdefault('__copy_translations_seen',{})
4796         if old_id in seen_map.setdefault(self._name,[]):
4797             return
4798         seen_map[self._name].append(old_id)
4799
4800         trans_obj = self.pool.get('ir.translation')
4801         # TODO it seems fields_get can be replaced by _all_columns (no need for translation)
4802         fields = self.fields_get(cr, uid, context=context)
4803
4804         translation_records = []
4805         for field_name, field_def in fields.items():
4806             # we must recursively copy the translations for o2o and o2m
4807             if field_def['type'] == 'one2many':
4808                 target_obj = self.pool.get(field_def['relation'])
4809                 old_record, new_record = self.read(cr, uid, [old_id, new_id], [field_name], context=context)
4810                 # here we rely on the order of the ids to match the translations
4811                 # as foreseen in copy_data()
4812                 old_children = sorted(old_record[field_name])
4813                 new_children = sorted(new_record[field_name])
4814                 for (old_child, new_child) in zip(old_children, new_children):
4815                     target_obj.copy_translations(cr, uid, old_child, new_child, context=context)
4816             # and for translatable fields we keep them for copy
4817             elif field_def.get('translate'):
4818                 trans_name = ''
4819                 if field_name in self._columns:
4820                     trans_name = self._name + "," + field_name
4821                 elif field_name in self._inherit_fields:
4822                     trans_name = self._inherit_fields[field_name][0] + "," + field_name
4823                 if trans_name:
4824                     trans_ids = trans_obj.search(cr, uid, [
4825                             ('name', '=', trans_name),
4826                             ('res_id', '=', old_id)
4827                     ])
4828                     translation_records.extend(trans_obj.read(cr, uid, trans_ids, context=context))
4829
4830         for record in translation_records:
4831             del record['id']
4832             record['res_id'] = new_id
4833             trans_obj.create(cr, uid, record, context=context)
4834
4835
4836     def copy(self, cr, uid, id, default=None, context=None):
4837         """
4838         Duplicate record with given id updating it with default values
4839
4840         :param cr: database cursor
4841         :param uid: current user id
4842         :param id: id of the record to copy
4843         :param default: dictionary of field values to override in the original values of the copied record, e.g: ``{'field_name': overriden_value, ...}``
4844         :type default: dictionary
4845         :param context: context arguments, like lang, time zone
4846         :type context: dictionary
4847         :return: id of the newly created record
4848
4849         """
4850         if context is None:
4851             context = {}
4852         context = context.copy()
4853         data = self.copy_data(cr, uid, id, default, context)
4854         new_id = self.create(cr, uid, data, context)
4855         self.copy_translations(cr, uid, id, new_id, context)
4856         return new_id
4857
4858     def exists(self, cr, uid, ids, context=None):
4859         """Checks whether the given id or ids exist in this model,
4860            and return the list of ids that do. This is simple to use for
4861            a truth test on a browse_record::
4862
4863                if record.exists():
4864                    pass
4865
4866            :param ids: id or list of ids to check for existence
4867            :type ids: int or [int]
4868            :return: the list of ids that currently exist, out of
4869                     the given `ids`
4870         """
4871         if type(ids) in (int, long):
4872             ids = [ids]
4873         query = 'SELECT id FROM "%s"' % (self._table)
4874         cr.execute(query + "WHERE ID IN %s", (tuple(ids),))
4875         return [x[0] for x in cr.fetchall()]
4876
4877     def check_recursion(self, cr, uid, ids, context=None, parent=None):
4878         _logger.warning("You are using deprecated %s.check_recursion(). Please use the '_check_recursion()' instead!" % \
4879                         self._name)
4880         assert parent is None or parent in self._columns or parent in self._inherit_fields,\
4881                     "The 'parent' parameter passed to check_recursion() must be None or a valid field name"
4882         return self._check_recursion(cr, uid, ids, context, parent)
4883
4884     def _check_recursion(self, cr, uid, ids, context=None, parent=None):
4885         """
4886         Verifies that there is no loop in a hierarchical structure of records,
4887         by following the parent relationship using the **parent** field until a loop
4888         is detected or until a top-level record is found.
4889
4890         :param cr: database cursor
4891         :param uid: current user id
4892         :param ids: list of ids of records to check
4893         :param parent: optional parent field name (default: ``self._parent_name = parent_id``)
4894         :return: **True** if the operation can proceed safely, or **False** if an infinite loop is detected.
4895         """
4896
4897         if not parent:
4898             parent = self._parent_name
4899         ids_parent = ids[:]
4900         query = 'SELECT distinct "%s" FROM "%s" WHERE id IN %%s' % (parent, self._table)
4901         while ids_parent:
4902             ids_parent2 = []
4903             for i in range(0, len(ids), cr.IN_MAX):
4904                 sub_ids_parent = ids_parent[i:i+cr.IN_MAX]
4905                 cr.execute(query, (tuple(sub_ids_parent),))
4906                 ids_parent2.extend(filter(None, map(lambda x: x[0], cr.fetchall())))
4907             ids_parent = ids_parent2
4908             for i in ids_parent:
4909                 if i in ids:
4910                     return False
4911         return True
4912
4913     def _get_external_ids(self, cr, uid, ids, *args, **kwargs):
4914         """Retrieve the External ID(s) of any database record.
4915
4916         **Synopsis**: ``_get_xml_ids(cr, uid, ids) -> { 'id': ['module.xml_id'] }``
4917
4918         :return: map of ids to the list of their fully qualified External IDs
4919                  in the form ``module.key``, or an empty list when there's no External
4920                  ID for a record, e.g.::
4921
4922                      { 'id': ['module.ext_id', 'module.ext_id_bis'],
4923                        'id2': [] }
4924         """
4925         ir_model_data = self.pool.get('ir.model.data')
4926         data_ids = ir_model_data.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)])
4927         data_results = ir_model_data.read(cr, uid, data_ids, ['module', 'name', 'res_id'])
4928         result = {}
4929         for id in ids:
4930             # can't use dict.fromkeys() as the list would be shared!
4931             result[id] = []
4932         for record in data_results:
4933             result[record['res_id']].append('%(module)s.%(name)s' % record)
4934         return result
4935
4936     def get_external_id(self, cr, uid, ids, *args, **kwargs):
4937         """Retrieve the External ID of any database record, if there
4938         is one. This method works as a possible implementation
4939         for a function field, to be able to add it to any
4940         model object easily, referencing it as ``Model.get_external_id``.
4941
4942         When multiple External IDs exist for a record, only one
4943         of them is returned (randomly).
4944
4945         :return: map of ids to their fully qualified XML ID,
4946                  defaulting to an empty string when there's none
4947                  (to be usable as a function field), 
4948                  e.g.::
4949
4950                      { 'id': 'module.ext_id',
4951                        'id2': '' }
4952         """
4953         results = self._get_xml_ids(cr, uid, ids)
4954         for k, v in results.iteritems():
4955             if results[k]:
4956                 results[k] = v[0]
4957             else:
4958                 results[k] = ''
4959         return results
4960
4961     # backwards compatibility
4962     get_xml_id = get_external_id
4963     _get_xml_ids = _get_external_ids
4964
4965     # Transience
4966     def is_transient(self):
4967         """ Return whether the model is transient.
4968
4969         See TransientModel.
4970
4971         """
4972         return self._transient
4973
4974     def _transient_clean_rows_older_than(self, cr, seconds):
4975         assert self._transient, "Model %s is not transient, it cannot be vacuumed!" % self._name
4976         cr.execute("SELECT id FROM " + self._table + " WHERE"
4977             " COALESCE(write_date, create_date, (now() at time zone 'UTC'))::timestamp <"
4978             " ((now() at time zone 'UTC') - interval %s)", ("%s seconds" % seconds,))
4979         ids = [x[0] for x in cr.fetchall()]
4980         self.unlink(cr, SUPERUSER_ID, ids)
4981
4982     def _transient_clean_old_rows(self, cr, count):
4983         assert self._transient, "Model %s is not transient, it cannot be vacuumed!" % self._name
4984         cr.execute(
4985             "SELECT id, COALESCE(write_date, create_date, (now() at time zone 'UTC'))::timestamp"
4986             " AS t FROM " + self._table +
4987             " ORDER BY t LIMIT %s", (count,))
4988         ids = [x[0] for x in cr.fetchall()]
4989         self.unlink(cr, SUPERUSER_ID, ids)
4990
4991     def _transient_vacuum(self, cr, uid, force=False):
4992         """Clean the transient records.
4993
4994         This unlinks old records from the transient model tables whenever the
4995         "_transient_max_count" or "_max_age" conditions (if any) are reached.
4996         Actual cleaning will happen only once every "_transient_check_time" calls.
4997         This means this method can be called frequently called (e.g. whenever
4998         a new record is created).
4999         """
5000         assert self._transient, "Model %s is not transient, it cannot be vacuumed!" % self._name
5001         self._transient_check_count += 1
5002         if (not force) and (self._transient_check_count % self._transient_check_time):
5003             self._transient_check_count = 0
5004             return True
5005
5006         # Age-based expiration
5007         if self._transient_max_hours:
5008             self._transient_clean_rows_older_than(cr, self._transient_max_hours * 60 * 60)
5009
5010         # Count-based expiration
5011         if self._transient_max_count:
5012             self._transient_clean_old_rows(cr, self._transient_max_count)
5013
5014         return True
5015
5016     def resolve_o2m_commands_to_record_dicts(self, cr, uid, field_name, o2m_commands, fields=None, context=None):
5017         """ Serializes o2m commands into record dictionaries (as if
5018         all the o2m records came from the database via a read()), and
5019         returns an iterable over these dictionaries.
5020
5021         Because o2m commands might be creation commands, not all
5022         record ids will contain an ``id`` field. Commands matching an
5023         existing record (``UPDATE`` and ``LINK_TO``) will have an id.
5024
5025         .. note:: ``CREATE``, ``UPDATE`` and ``LINK_TO`` stand for the
5026                   o2m command codes ``0``, ``1`` and ``4``
5027                   respectively
5028
5029         :param field_name: name of the o2m field matching the commands
5030         :type field_name: str
5031         :param o2m_commands: one2many commands to execute on ``field_name``
5032         :type o2m_commands: list((int|False, int|False, dict|False))
5033         :param fields: list of fields to read from the database, when applicable
5034         :type fields: list(str)
5035         :raises AssertionError: if a command is not ``CREATE``, ``UPDATE`` or ``LINK_TO``
5036         :returns: o2m records in a shape similar to that returned by
5037                   ``read()`` (except records may be missing the ``id``
5038                   field if they don't exist in db)
5039         :rtype: ``list(dict)``
5040         """
5041         o2m_model = self._all_columns[field_name].column._obj
5042
5043         # convert single ids and pairs to tripled commands
5044         commands = []
5045         for o2m_command in o2m_commands:
5046             if not isinstance(o2m_command, (list, tuple)):
5047                 command = 4
5048                 commands.append((command, o2m_command, False))
5049             elif len(o2m_command) == 1:
5050                 (command,) = o2m_command
5051                 commands.append((command, False, False))
5052             elif len(o2m_command) == 2:
5053                 command, id = o2m_command
5054                 commands.append((command, id, False))
5055             else:
5056                 command = o2m_command[0]
5057                 commands.append(o2m_command)
5058             assert command in (0, 1, 4), \
5059                 "Only CREATE, UPDATE and LINK_TO commands are supported in resolver"
5060
5061         # extract records to read, by id, in a mapping dict
5062         ids_to_read = [id for (command, id, _) in commands if command in (1, 4)]
5063         records_by_id = dict(
5064             (record['id'], record)
5065             for record in self.pool.get(o2m_model).read(
5066                 cr, uid, ids_to_read, fields=fields, context=context))
5067
5068         record_dicts = []
5069         # merge record from db with record provided by command
5070         for command, id, record in commands:
5071             item = {}
5072             if command in (1, 4): item.update(records_by_id[id])
5073             if command in (0, 1): item.update(record)
5074             record_dicts.append(item)
5075         return record_dicts
5076
5077 # keep this import here, at top it will cause dependency cycle errors
5078 import expression
5079
5080 class Model(BaseModel):
5081     """Main super-class for regular database-persisted OpenERP models.
5082
5083     OpenERP models are created by inheriting from this class::
5084
5085         class user(Model):
5086             ...
5087
5088     The system will later instantiate the class once per database (on
5089     which the class' module is installed).
5090     """
5091     _register = False # not visible in ORM registry, meant to be python-inherited only
5092     _transient = False # True in a TransientModel
5093
5094 class TransientModel(BaseModel):
5095     """Model super-class for transient records, meant to be temporarily
5096        persisted, and regularly vaccuum-cleaned.
5097
5098        A TransientModel has a simplified access rights management,
5099        all users can create new records, and may only access the
5100        records they created. The super-user has unrestricted access
5101        to all TransientModel records.
5102     """
5103     _register = False # not visible in ORM registry, meant to be python-inherited only
5104     _transient = True
5105
5106 class AbstractModel(BaseModel):
5107     """Abstract Model super-class for creating an abstract class meant to be
5108        inherited by regular models (Models or TransientModels) but not meant to
5109        be usable on its own, or persisted.
5110
5111        Technical note: we don't want to make AbstractModel the super-class of
5112        Model or BaseModel because it would not make sense to put the main
5113        definition of persistence methods such as create() in it, and still we
5114        should be able to override them within an AbstractModel.
5115        """
5116     _auto = False # don't create any database backend for AbstractModels
5117     _register = False # not visible in ORM registry, meant to be python-inherited only
5118
5119
5120 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: