acb2bd092fceba5fe2a0f6e3c206cf9f0efb5070
[odoo/odoo.git] / bin / osv / orm.py
1 # -*- coding: latin1 -*-
2 ##############################################################################
3 #
4 # Copyright (c) 2004-2006 TINY SPRL. (http://tiny.be) All Rights Reserved.
5 #
6 # $Id: orm.py 1008 2005-07-25 14:03:55Z pinky $
7 #
8 # WARNING: This program as such is intended to be used by professional
9 # programmers who take the whole responsability of assessing all potential
10 # consequences resulting from its eventual inadequacies and bugs
11 # End users who are looking for a ready-to-use solution with commercial
12 # garantees and support are strongly adviced to contract a Free Software
13 # Service Company
14 #
15 # This program is Free Software; you can redistribute it and/or
16 # modify it under the terms of the GNU General Public License
17 # as published by the Free Software Foundation; either version 2
18 # of the License, or (at your option) any later version.
19 #
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with this program; if not, write to the Free Software
27 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
28 #
29 ##############################################################################
30
31 #
32 # Object relationnal mapping to postgresql module
33 #    . Hierarchical structure
34 #    . Constraints consistency, validations
35 #    . Object meta Data depends on its status
36 #    . Optimised processing by complex query (multiple actions at once)
37 #    . Default fields value
38 #    . Permissions optimisation
39 #    . Multi-company features
40 #    . Persistant object: DB postgresql
41 #    . Datas conversions
42 #    . Multi-level caching system
43 #    . 2 different inheritancies
44 #    . Fields:
45 #         - classicals (varchar, integer, boolean, ...)
46 #         - relations (one2many, many2one, many2many)
47 #         - functions
48 #
49 #
50
51 from xml import dom
52 from xml.parsers import expat
53 import string
54 import pooler
55 import psycopg
56 import netsvc
57 import re
58
59 import fields
60 import ir
61 import tools
62
63 prof = 0
64
65 def intersect(la, lb):
66         return filter(lambda x: x in lb, la)
67
68 class except_orm(Exception):
69         def __init__(self, name, value):
70                 self.name = name
71                 self.value = value
72                 self.args='no error args'
73
74 #class find_fields(object):
75 #       def _start_el(self,name, attrs):
76 #               if name == 'field' and attrs.get('name', False):
77 #                       self.datas[str(attrs['name'])] = attrs.get('preload','')
78 #       def __init__(self):
79 #               self.datas = {}
80 #       def parse(self, datas):
81 #               p = expat.ParserCreate()
82 #               p.StartElementHandler = self._start_el
83 #               p.Parse(datas, 1)
84 #               return self.datas
85
86 #
87 # TODO: trigger pour chaque action
88 #
89 # Readonly python database object browser
90 class browse_null(object):
91         def __init__(self):
92                 self.id=False
93         def __getitem__(self, name):
94                 return False
95         def __int__(self):
96                 return False
97         def __str__(self):
98                 return ''
99         def __nonzero__(self):
100                 return False
101
102 #
103 # TODO: execute an object method on browse_record_list
104 #
105 class browse_record_list(list):
106         def __init__(self, lst, context={}):
107                 super(browse_record_list, self).__init__(lst)
108                 self.context = context
109
110 #
111 # table : the object (inherited from orm)
112 # context : a dictionnary with an optionnal context
113 #    default to : browse_record_list
114 #
115 class browse_record(object):
116         def __init__(self, cr, uid, id, table, cache, context={}, list_class = None):
117                 assert id, 'Wrong ID for the browse record, got '+str(id)+ ', expected an integer.'
118                 self._list_class = list_class or browse_record_list
119                 self._cr = cr
120                 self._uid = uid
121                 self._id = id
122                 self._table = table
123                 self._table_name = self._table._name
124                 self._context = context
125
126                 cache.setdefault(table._name, {})
127                 self._data = cache[table._name]
128                 if not id in self._data:
129                         self._data[id] = {'id':id}
130                 self._cache = cache
131
132         def __getitem__(self, name):
133                 if name == 'id':
134                         return self._id
135                 if not self._data[self._id].has_key(name):
136                         # build the list of fields we will fetch
137
138                         # fetch the definition of the field which was asked for
139                         if name in self._table._columns:
140                                 col = self._table._columns[name]
141                         elif name in self._table._inherit_fields:
142                                 col = self._table._inherit_fields[name][2]
143                         elif hasattr(self._table, name):
144                                 return getattr(self._table, name)
145                         else:
146                                 print "Programming error: field '%s' does not exist in object '%s' !" % (name, self._table._name)
147                                 return False
148                         
149                         # if the field is a classic one or a many2one, we'll fetch all classic and many2one fields
150                         if col._classic_write:
151                                 # gen the list of "local" (ie not inherited) fields which are classic or many2one
152                                 ffields = filter(lambda x: x[1]._classic_write, self._table._columns.items())
153                                 # gen the list of inherited fields
154                                 inherits = map(lambda x: (x[0], x[1][2]), self._table._inherit_fields.items())
155                                 # complete the field list with the inherited fields which are classic or many2one
156                                 ffields += filter(lambda x: x[1]._classic_write, inherits)
157                         # otherwise we fetch only that field
158                         else:
159                                 ffields = [(name,col)]
160                                 
161                         if isinstance(col, fields.function):
162                                 ids = [self._id]
163                         else:
164                                 # filter out all ids which were already fetched (and are already in _data)
165                                 ids = filter(lambda id: not self._data[id].has_key(name), self._data.keys())
166                         
167                         # read the data
168                         datas = self._table.read(self._cr, self._uid, ids, map(lambda x: x[0], ffields), context=self._context, load="_classic_write")
169                         
170                         # create browse records for 'remote' objects
171                         for data in datas:
172                                 for n,f in ffields:
173                                         if isinstance(f, fields.many2one) or isinstance(f, fields.one2one):
174                                                 if data[n]:
175                                                         obj = self._table.pool.get(f._obj)
176                                                         compids=False
177                                                         if 'company_id' in obj._columns and not self._uid == 1:
178                                                                 compids = tools.get_user_companies(self._cr, self._uid)
179                                                                 if compids:
180                                                                         self._cr.execute('SELECT id FROM '+obj._table+' where id = %d AND  (company_id in ('+','.join(map(str,compids))+') or company_id is null)', (data[n],))
181                                                                         if not self._cr.fetchall():
182                                                                                 raise except_orm('BrowseError', 'Object %s (id:%d) is linked to the object %s (id:%d) which is not in your company' %(self._table._description, self._id, obj._description, data[n]))
183                                                         data[n] = browse_record(self._cr, self._uid, data[n], obj, self._cache, context=self._context, list_class=self._list_class)
184                                                 else:
185                                                         data[n] = browse_null()
186                                         elif isinstance(f, (fields.one2many, fields.many2many)) and len(data[n]):
187                                                 data[n] = self._list_class([browse_record(self._cr,self._uid,id,self._table.pool.get(f._obj),self._cache, context=self._context, list_class=self._list_class) for id in data[n]], self._context)
188                                 self._data[data['id']].update(data)
189                 return self._data[self._id][name]
190
191         def __getattr__(self, name):
192 #               raise an AttributeError exception.
193                 return self[name]
194
195         def __int__(self):
196                 return self._id
197
198         def __str__(self):
199                 return "browse_record(%s, %d)" % (self._table_name, self._id)
200
201         def __eq__(self, other):
202                 return (self._table_name, self._id) == (other._table_name, other._id)
203
204         def __ne__(self, other):
205                 return (self._table_name, self._id) != (other._table_name, other._id)
206
207         # we need to define __unicode__ even though we've already defined __str__ 
208         # because we have overridden __getattr__
209         def __unicode__(self):
210                 return unicode(str(self))
211
212         def __hash__(self):
213                 return hash((self._table_name, self._id))
214
215         __repr__ = __str__
216
217
218 # returns a tuple (type returned by postgres when the column was created, type expression to create the column)
219 def get_pg_type(f):
220         type_dict = {fields.boolean:'bool', fields.integer:'int4', fields.text:'text', fields.date:'date', fields.time:'time', fields.datetime:'timestamp', fields.binary:'bytea', fields.many2one:'int4'}
221
222         if type_dict.has_key(type(f)):
223                 f_type = (type_dict[type(f)], type_dict[type(f)])
224         elif isinstance(f, fields.float):
225                 if f.digits:
226                         f_type = ('numeric', 'NUMERIC(%d,%d)' % (f.digits[0],f.digits[1]))
227                 else:
228                         f_type = ('float8', 'DOUBLE PRECISION')
229         elif isinstance(f, (fields.char, fields.reference)):
230                 f_type = ('varchar', 'VARCHAR(%d)' % (f.size,))
231         elif isinstance(f, fields.selection):
232                 if isinstance(f.selection, list) and isinstance(f.selection[0][0], (str, unicode)):
233                         f_size = reduce(lambda x,y: max(x,len(y[0])), f.selection, 16)
234                 elif isinstance(f.selection, list) and isinstance(f.selection[0][0], int):
235                         f_size = -1
236                 else:
237                         f_size = (hasattr(f,'size') and f.size) or 16
238                         
239                 if f_size == -1:
240                         f_type = ('int4', 'INTEGER')
241                 else:
242                         f_type = ('varchar', 'VARCHAR(%d)' % f_size)
243         elif isinstance(f, fields.function) and type_dict.has_key(type(locals().get('fields.'+(f._type)))):
244                 t=type(locals().get('fields.'+(f._type)))
245                 f_type = (type_dict[t], type_dict[t])
246         elif isinstance(f, fields.function) and f._type == 'float':
247                 f_type = ('float8', 'DOUBLE PRECISION')
248         else:
249                 logger = netsvc.Logger()
250                 logger.notifyChannel("init", netsvc.LOG_WARNING, '%s type not supported!' % (f_type))
251                 f_type = None
252         return f_type
253
254                 
255         
256 class orm(object):
257         _columns = {}
258         _sql_constraints = []
259         _constraints = []
260         _defaults = {}
261         _log_access = True
262         _table = None
263         _name = None
264         _rec_name = 'name'
265         _order = 'id'
266         _inherits = {}
267         _sequence = None
268         _description = None
269         _protected = ['read','write','create','default_get','perm_read','perm_write','unlink','fields_get','fields_view_get','search','name_get','distinct_field_get','name_search','copy','import_data']
270         def _field_create(self, cr):
271                 cr.execute("SELECT id FROM ir_model WHERE model='%s'" % self._name)
272                 if not cr.rowcount:
273                         # reference model in order to have a description of its fonctionnality in custom_report
274                         cr.execute("INSERT INTO ir_model (model, name, info) VALUES (%s, %s, %s)", (self._name, self._description, self.__doc__)) 
275                 cr.commit()
276
277                 for k in self._columns:
278                         f = self._columns[k]
279                         cr.execute("select id, relate from ir_model_fields where model=%s and name=%s", (self._name,k))
280                         if not cr.rowcount:
281                                 cr.execute("select id from ir_model where model='%s'" % self._name)
282                                 model_id = cr.fetchone()[0]
283                                 cr.execute("INSERT INTO ir_model_fields (model_id, model, name, field_description, ttype, relate,relation,group_name,view_load) VALUES (%d,%s,%s,%s,%s,%s,%s,%s,%s)", (model_id, self._name, k, f.string.replace("'", " "), f._type, (f.relate and 'True') or 'False', f._obj or 'NULL', f.group_name or '', (f.view_load and 'True') or 'False'))
284                         else:
285                                 id, relate = cr.fetchone()
286                                 if relate != f.relate:
287                                         cr.execute("UPDATE ir_model_fields SET relate=%s WHERE id=%d", ((f.relate and 'True') or 'False', id))
288                 cr.commit()
289
290         def _auto_init(self, cr):
291                 logger = netsvc.Logger()
292                 create = False
293                 self._field_create(cr)
294                 if not hasattr(self, "_auto") or self._auto:
295                         cr.execute("SELECT relname FROM pg_class WHERE relkind in ('r','v') AND relname='%s'" % self._table)
296                         if not cr.rowcount:
297                                 cr.execute("CREATE TABLE %s(id SERIAL NOT NULL, perm_id INTEGER, PRIMARY KEY(id)) WITH OIDS" % self._table)
298                                 create = True
299                         cr.commit()
300                         if self._log_access:
301                                 logs = {
302                                         'create_uid': 'INTEGER REFERENCES res_users ON DELETE SET NULL',
303                                         'create_date': 'TIMESTAMP',
304                                         'write_uid': 'INTEGER REFERENCES res_users ON DELETE SET NULL',
305                                         'write_date': 'TIMESTAMP'
306                                 }
307                                 for k in logs:
308                                         cr.execute(
309                                                 """
310                                                 SELECT c.relname 
311                                                 FROM pg_class c, pg_attribute a
312                                                 WHERE c.relname='%s' AND a.attname='%s' AND c.oid=a.attrelid
313                                                 """ % (self._table, k))
314                                         if not cr.rowcount:
315                                                 cr.execute("ALTER TABLE %s ADD COLUMN %s %s" % 
316                                                         (self._table, k, logs[k]))
317                                                 cr.commit()
318
319                         # iterate on the database columns to drop the NOT NULL constraints
320                         # of fields which were required but have been removed
321                         cr.execute(
322                                 "SELECT a.attname, a.attnotnull "\
323                                 "FROM pg_class c, pg_attribute a "\
324                                 "WHERE c.oid=a.attrelid AND c.relname='%s'" % self._table)
325                         db_columns = cr.dictfetchall()
326                         for column in db_columns:
327                                 if column['attname'] not in ('id', 'oid', 'tableoid', 'ctid', 'xmin', 'xmax', 'cmin', 'cmax'):
328                                         if column['attnotnull'] and column['attname'] not in self._columns:
329                                                 cr.execute("ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL" % (self._table, column['attname']))
330
331                         # iterate on the "object columns"
332                         for k in self._columns:
333                                 if k in ('id','perm_id','write_uid','write_date','create_uid','create_date'):
334                                         continue
335                                         #raise 'Can not define a column %s. Reserved keyword !' % (k,)
336                                 f = self._columns[k]
337
338                                 if isinstance(f, fields.one2many):
339                                         cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='%s'"%f._obj)
340                                         if cr.fetchone():
341                                                 q = """SELECT count(*) as c FROM pg_class c,pg_attribute a WHERE c.relname='%s' AND a.attname='%s' AND c.oid=a.attrelid"""%(f._obj,f._fields_id)
342                                                 cr.execute(q)
343                                                 res = cr.fetchone()[0]
344                                                 if not res:
345                                                         cr.execute("ALTER TABLE %s ADD FOREIGN KEY (%s) REFERENCES %s ON DELETE SET NULL" % (self._obj, f._fields_id, f._table))
346                                 elif isinstance(f, fields.many2many):
347                                         cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='%s'"%f._rel)
348                                         if not cr.dictfetchall():
349                                                 #FIXME: Remove this try/except
350                                                 try:
351                                                         ref = self.pool.get(f._obj)._table
352                                                 except AttributeError:
353                                                         ref = f._obj.replace('.','_')
354                                                 cr.execute("CREATE TABLE %s (%s INTEGER NOT NULL REFERENCES %s ON DELETE CASCADE, %s INTEGER NOT NULL REFERENCES %s ON DELETE CASCADE) WITH OIDS"%(f._rel,f._id1,self._table,f._id2,ref))
355                                                 cr.execute("CREATE INDEX %s_%s_index ON %s (%s)" % (f._rel,f._id1,f._rel,f._id1))
356                                                 cr.execute("CREATE INDEX %s_%s_index ON %s (%s)" % (f._rel,f._id2,f._rel,f._id2))
357                                                 cr.commit()
358                                 else:
359                                         q = """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 FROM pg_class c,pg_attribute a,pg_type t WHERE c.relname='%s' AND a.attname='%s' AND c.oid=a.attrelid AND a.atttypid=t.oid""" % (self._table, k.lower())
360                                         cr.execute(q)
361                                         res = cr.dictfetchall() 
362                                         if not res:
363                                                 if not isinstance(f,fields.function) or f.store:
364
365                                                         # add the missing field
366                                                         cr.execute("ALTER TABLE %s ADD COLUMN %s %s" % (self._table, k, get_pg_type(f)[1]))
367                                                         
368                                                         # initialize it
369                                                         if not create and k in self._defaults:
370                                                                 default = self._defaults[k](self, cr, 1, {})
371                                                                 if not default:
372                                                                         cr.execute("UPDATE %s SET %s=NULL" % (self._table, k))
373                                                                 else:
374                                                                         cr.execute("UPDATE %s SET %s='%s'" % (self._table, k, default))
375
376                                                         # and add constraints if needed
377                                                         if isinstance(f, fields.many2one):
378                                                                 #FIXME: Remove this try/except
379                                                                 try:
380                                                                         ref = self.pool.get(f._obj)._table
381                                                                 except AttributeError:
382                                                                         ref = f._obj.replace('.','_')
383                                                                 cr.execute("ALTER TABLE %s ADD FOREIGN KEY (%s) REFERENCES %s ON DELETE %s" % (self._table, k, ref, f.ondelete))
384                                                         if f.select:
385                                                                 cr.execute("CREATE INDEX %s_%s_index ON %s (%s)" % (self._table, k, self._table, k))
386                                                         if f.required:
387                                                                 cr.commit()
388                                                                 try:
389                                                                         cr.execute("ALTER TABLE %s ALTER COLUMN %s SET NOT NULL" % (self._table, k))
390                                                                 except:
391                                                                         logger.notifyChannel('init', netsvc.LOG_WARNING, 'WARNING: unable to set column %s of table %s not null !\nTry to re-run: tinyerp-server.py --update=module\nIf it doesn\'t work, update records and execute manually:\nALTER TABLE %s ALTER COLUMN %s SET NOT NULL' % (k, self._table, self._table, k))
392                                                         cr.commit()
393                                         elif len(res)==1:
394                                                 f_pg_def = res[0]
395                                                 f_pg_type = f_pg_def['typname']
396                                                 f_pg_size = f_pg_def['size']
397                                                 f_pg_notnull = f_pg_def['attnotnull']
398                                                 if isinstance(f, fields.function) and not f.store:
399                                                         logger.notifyChannel('init', netsvc.LOG_WARNING, 'column %s (%s) in table %s was converted to a function !\nYou should remove this column from your database.' % (k, f.string, self._table))
400                                                         f_obj_type = None
401                                                 else:
402                                                         f_obj_type = get_pg_type(f) and get_pg_type(f)[0]
403                                                 
404                                                 if f_obj_type:
405                                                         if f_pg_type != f_obj_type:
406                                                                 logger.notifyChannel('init', netsvc.LOG_WARNING, "column '%s' in table '%s' has changed type (DB = %s, def = %s) !" % (k, self._table, f_pg_type, f._type))
407                                                         if f_pg_type == 'varchar' and f._type == 'char' and f_pg_size != f.size:
408                                                                 # columns with the name 'type' cannot be changed for an unknown reason?!
409                                                                 if k != 'type':
410                                                                         if f_pg_size > f.size:
411                                                                                 logger.notifyChannel('init', netsvc.LOG_WARNING, "column '%s' in table '%s' has changed size (DB = %d, def = %d), strings will be truncated !" % (k, self._table, f_pg_size, f.size))
412 #TODO: check si y a des donnees qui vont poser probleme (select char_length(...))
413 #TODO: issue a log message even if f_pg_size < f.size
414                                                                         cr.execute("ALTER TABLE %s RENAME COLUMN %s TO temp_change_size" % (self._table,k))
415                                                                         cr.execute("ALTER TABLE %s ADD COLUMN %s VARCHAR(%d)" % (self._table,k,f.size))
416                                                                         cr.execute("UPDATE %s SET %s=temp_change_size::VARCHAR(%d)" % (self._table,k,f.size))
417                                                                         cr.execute("ALTER TABLE %s DROP COLUMN temp_change_size" % (self._table,))
418                                                                         cr.commit()
419                                                         # if the field is required and hasn't got a NOT NULL constraint
420                                                         if f.required and f_pg_notnull == 0:
421                                                                 # set the field to the default value if any
422                                                                 if self._defaults.has_key(k):
423                                                                         default = self._defaults[k](self, cr, 1, {})
424                                                                         if not (default is False):
425                                                                                 cr.execute("UPDATE %s SET %s='%s' WHERE %s is NULL" % (self._table, k, default, k))
426                                                                                 cr.commit()
427                                                                 # add the NOT NULL constraint
428                                                                 try:
429                                                                         cr.execute("ALTER TABLE %s ALTER COLUMN %s SET NOT NULL" % (self._table, k))
430                                                                         cr.commit()
431                                                                 except:
432                                                                         logger.notifyChannel('init', netsvc.LOG_WARNING, 'unable to set a NOT NULL constraint on column %s of the %s table !\nIf you want to have it, you should update the records and execute manually:\nALTER TABLE %s ALTER COLUMN %s SET NOT NULL' % (k, self._table, self._table, k))
433                                                                 cr.commit()
434                                                         elif not f.required and f_pg_notnull == 1:
435                                                                 cr.execute("ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL" % (self._table,k))
436                                                                 cr.commit()
437                                                         cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = '%s_%s_index' and tablename = '%s'" % (self._table, k, self._table))
438                                                         res = cr.dictfetchall()
439                                                         if not res and f.select:
440                                                                 cr.execute("CREATE INDEX %s_%s_index ON %s (%s)" % (self._table, k, self._table, k))
441                                                                 cr.commit()
442                                                         if res and not f.select:
443                                                                 cr.execute("DROP INDEX %s_%s_index" % (self._table, k))
444                                                                 cr.commit()
445                                         else:
446                                                 print "ERROR"
447                 else:
448                         cr.execute("SELECT relname FROM pg_class WHERE relkind in ('r','v') AND relname='%s'" % self._table)
449                         create = not bool(cr.fetchone())
450
451                 for (key,con,_) in self._sql_constraints:
452                         cr.execute("SELECT conname FROM pg_constraint where conname='%s_%s'" % (self._table, key))
453                         if not cr.dictfetchall():
454                                 try:
455                                         cr.execute('alter table %s add constraint %s_%s %s' % (self._table,self._table,key, con,))
456                                         cr.commit()
457                                 except:
458                                         logger.notifyChannel('init', netsvc.LOG_WARNING, 'unable to add \'%s\' constraint on table %s !\n If you want to have it, you should update the records and execute manually:\nALTER table %s ADD CONSTRAINT %s_%s %s' % (con, self._table, self._table,self._table,key, con,))
459
460                 if create:
461                         if hasattr(self,"_sql"):
462                                 for line in self._sql.split(';'):
463                                         line2 = line.replace('\n','').strip()
464                                         if line2:
465                                                 cr.execute(line2)
466                                                 cr.commit()
467
468         def __init__(self):
469                 if not self._table:
470                         self._table=self._name.replace('.','_')
471                 if not self._description:
472                         self._description = self._name
473                 for (key,_,msg) in self._sql_constraints:
474                         self.pool._sql_error[self._table+'_'+key] = msg
475                 
476 #               if self.__class__.__name__ != 'fake_class':
477                 self._inherits_reload()
478                 if not self._sequence:
479                         self._sequence = self._table+'_id_seq'
480                 for k in self._defaults:
481                         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,)
482
483         #
484         # Update objects that uses this one to update their _inherits fields
485         #
486         def _inherits_reload_src(self):
487                 for obj in self.pool.obj_pool.values():
488                         if self._name in obj._inherits:
489                                 obj._inherits_reload()
490
491         def _inherits_reload(self):
492                 res = {}
493                 for table in self._inherits:
494                         res.update(self.pool.get(table)._inherit_fields)
495                         for col in self.pool.get(table)._columns.keys():
496                                 res[col]=(table, self._inherits[table], self.pool.get(table)._columns[col])
497                         for col in self.pool.get(table)._inherit_fields.keys():
498                                 res[col]=(table, self._inherits[table], self.pool.get(table)._inherit_fields[col][2])
499                 self._inherit_fields=res
500                 self._inherits_reload_src()
501
502         def browse(self, cr, uid, select, context={}, list_class=None):
503                 self._list_class = list_class or browse_record_list
504                 cache = {}
505                 # need to accepts ints and longs because ids coming from a method 
506                 # launched by button in the interface have a type long...
507                 if isinstance(select, (int, long)):
508                         return browse_record(cr,uid,select,self,cache, context=context, list_class=self._list_class)
509                 elif isinstance(select,list):
510                         return self._list_class([browse_record(cr,uid,id,self,cache, context=context, list_class=self._list_class) for id in select], context)
511                 else:
512                         return []
513
514         # TODO: implement this
515         def __export_row(self, cr, uid, row, fields, prefix, context={}):
516                 lines = []
517                 data = map(lambda x: '', range(len(fields)))
518                 done = []
519                 for fpos in range(len(fields)):
520                         f = fields[fpos]
521                         if f and f[:len(prefix)] == prefix:
522                                 r = row
523                                 i = 0
524                                 while i<len(f):
525                                         r = r[f[i]]
526                                         if not r:
527                                                 break
528                                         if isinstance(r, (browse_record_list, list)):
529                                                 first = True
530                                                 fields2 = map(lambda x: (x[:i+1]==f[:i+1] and x[i+1:]) or [], fields)
531                                                 if fields2 in done:
532                                                         break
533                                                 done.append(fields2)
534                                                 for row2 in r:
535                                                         lines2 = self.__export_row(cr, uid, row2, fields2, f[i+2:], context)
536                                                         if first:
537                                                                 for fpos2 in range(len(fields)):
538                                                                         if lines2 and lines2[0][fpos2]:
539                                                                                 data[fpos2] = lines2[0][fpos2]
540                                                                 lines+= lines2[1:]
541                                                                 first = False
542                                                         else:
543                                                                 lines+= lines2
544                                                 break
545                                         i+=1
546                                 if i==len(f):
547                                         data[fpos] = str(r or '')
548                 return [data] + lines
549
550         def export_data(self, cr, uid, ids, fields, context={}):
551                 fields = map(lambda x: x.split('/'), fields)
552                 datas = []
553                 for row in self.browse(cr, uid, ids, context):
554                         datas += self.__export_row(cr, uid, row, fields, [], context)
555                 return datas
556
557         def import_data(self, cr, uid, fields, datas, context={}):
558                 fields = map(lambda x: x.split('/'), fields)
559
560                 def process_liness(self, datas, prefix, fields_def, position=0):
561                         line = datas[position]
562                         row = {}
563                         translate = {}
564                         todo = []
565                         warning = ''
566                         #
567                         # Import normal fields
568                         #
569                         for i in range(len(fields)):
570                                 if i>=len(line):
571                                         raise 'Please check that all your lines have %d cols.' % (len(fields),)
572                                 field = fields[i]
573                                 if (len(field)==len(prefix)+1) and field[len(prefix)].endswith(':id'):
574                                         res_id = False
575                                         if line[i]:
576                                                 if fields_def[field[len(prefix)][:-3]]['type']=='many2many':
577                                                         res_id = []
578                                                         for word in line[i].split(','):
579                                                                 module, xml_id = word.rsplit('.', 1)
580                                                                 ir_model_data_obj = self.pool.get('ir.model.data')
581                                                                 id=ir_model_data_obj._get_id(cr, uid, module, xml_id)
582                                                                 res_id2=ir_model_data_obj.read(cr, uid, [id], ['res_id'])[0]['res_id']
583                                                                 if res_id2:
584                                                                         res_id.append(res_id2)
585                                                         if len(res_id):
586                                                                 res_id=[(6,0,res_id)]
587                                                 else:
588                                                         module, xml_id = line[i].rsplit('.', 1)
589                                                         ir_model_data_obj = self.pool.get('ir.model.data')
590                                                         id=ir_model_data_obj._get_id(cr, uid, module, xml_id)
591                                                         res_id=ir_model_data_obj.read(cr, uid, [id], ['res_id'])[0]['res_id']
592                                         row[field[0][:-3]] = res_id or False
593                                         continue
594                                 if (len(field)==len(prefix)+1) and len(field[len(prefix)].split(':lang=')) == 2:
595                                         f, lang = field[len(prefix)].split(':lang=')
596                                         translate.setdefault(lang, {})[f]=line[i] or False
597                                         continue
598                                 if (len(field)==len(prefix)+1) and (prefix==field[0:len(prefix)]):
599                                         if fields_def[field[len(prefix)]]['type']=='integer':
600                                                 res =line[i] and int(line[i])
601                                         elif fields_def[field[len(prefix)]]['type']=='float':
602                                                 res =line[i] and float(line[i])
603                                         elif fields_def[field[len(prefix)]]['type']=='selection':
604                                                 res = False
605                                                 for key,val in fields_def[field[len(prefix)]]['selection']:
606                                                         if key==line[i]:             #val==line[i] if from the client !
607                                                                 res = key
608                                                 if line[i] and not res:
609                                                         logger = netsvc.Logger()
610                                                         logger.notifyChannel("import", netsvc.LOG_WARNING, "key '%s' not found in selection field '%s'" %(line[i], field[len(prefix)]))
611                                         elif fields_def[field[len(prefix)]]['type']=='many2one':
612                                                 res = False
613                                                 if line[i]:
614                                                         relation = fields_def[field[len(prefix)]]['relation']
615                                                         res2 = self.pool.get(relation).name_search(cr, uid, line[i], [],operator='=')
616                                                         res = (res2 and res2[0][0]) or False
617                                                         if not res and line[i]:
618                                                                 warning += ('Relation not found: '+line[i]+' on '+relation + ' !\n')
619                                                         if not res:
620                                                                 print 'Relation not found: '+line[i]+' on '+relation + ' !\n'
621                                         elif fields_def[field[len(prefix)]]['type']=='many2many':
622                                                 res = []
623                                                 if line[i]:
624                                                         relation = fields_def[field[len(prefix)]]['relation']
625                                                         for word in line[i].split(','):
626                                                                 res2 = self.pool.get(relation).name_search(cr, uid, word, [],operator='=')
627                                                                 res3 = (res2 and res2[0][0]) or False
628                                                                 if res3:
629                                                                         res.append(res3)
630                                                         if len(res):
631                                                                 res= [(6,0,res)]
632                                         else:
633                                                 res = line[i] or False
634                                         row[field[len(prefix)]] = res
635                                 elif (prefix==field[0:len(prefix)]):
636                                         if field[0] not in todo:
637                                                 todo.append(field[len(prefix)])
638
639
640                         #
641                         # Import one2many fields
642                         #
643                         nbrmax = 0
644                         for field in todo:
645                                 newfd = self.pool.get(fields_def[field]['relation']).fields_get(cr, uid, context=context)
646                                 (newrow,max2,w2, translate2) = process_liness(self, datas, prefix+[field], newfd, position)
647                                 nbrmax = max(nbrmax, max2)
648                                 warning = warning+w2
649                                 reduce(lambda x,y: x and y, newrow)
650                                 row[field] = (reduce(lambda x,y: x or y, newrow.values()) and [(0,0,newrow)]) or []
651                                 i = max2
652                                 while (position+i)<len(datas):
653                                         ok = True
654                                         for j in range(len(fields)):
655                                                 field2 = fields[j]
656                                                 if (len(field2)<=(len(prefix)+1)) and datas[position+i][j]:
657                                                         ok = False
658                                         if not ok:
659                                                 break
660
661                                         (newrow,max2,w2, translate2) = process_liness(self, datas, prefix+[field], newfd, position+i)
662                                         warning = warning+w2
663                                         if reduce(lambda x,y: x or y, newrow.values()):
664                                                 row[field].append((0,0,newrow))
665                                         i+=max2
666                                         nbrmax = max(nbrmax, i)
667
668                         if len(prefix)==0:
669                                 for i in range(max(nbrmax,1)):
670                                         #if datas:
671                                         datas.pop(0)
672                         result = [row, nbrmax+1, warning, translate]
673                         return result
674
675                 fields_def = self.fields_get(cr, uid, context=context)
676                 done = 0
677                 while len(datas):
678                         (res,other,warning,translate) = process_liness(self, datas, [], fields_def)
679                         try:
680                                 id=self.create(cr, uid, res, context)
681                                 for lang in translate:
682                                         context2=context.copy()
683                                         context2['lang']=lang
684                                         self.write(cr, uid, [id], translate[lang], context2)
685                         except Exception, e:
686                                 print e
687                                 cr.rollback()
688                                 return (-1, res, e[0], warning)
689                         done += 1
690                 #
691                 # TODO: Send a request with the result and multi-thread !
692                 #
693                 return (done, 0, 0, 0)
694
695         def read(self, cr, user, ids, fields=None, context={}, load='_classic_read'):
696                 self.pool.get('ir.model.access').check(cr, user, self._name, 'read')
697                 if not fields:
698                         fields = self._columns.keys() + self._inherit_fields.keys()
699                 result =  self._read_flat(cr, user, ids, fields, context, load)
700                 for r in result:
701                         for key,v in r.items():
702                                 if v == None:
703                                         r[key]=False
704                 return result
705
706         def _read_flat(self, cr, user, ids, fields, context={}, load='_classic_read'):
707                 if not ids:
708                         return []
709
710                 if fields==None:
711                         fields = self._columns.keys()
712
713                 # if the object has a field named 'company_id', filter out all
714                 # records which do not concern the current company (the company
715                 # of the current user) or its "childs"
716                 company_clause='true'
717                 compids=False
718                 if 'company_id' in self._columns and not user == 1:
719                         compids = tools.get_user_companies(cr, user)
720                         if compids:
721                                 company_clause = '(company_id in ('+','.join(map(str,compids))+') or company_id is null)'
722
723                 # all inherited fields + all non inherited fields for which the attribute whose name is in load is True
724                 fields_pre = filter(lambda x: x in self._columns and getattr(self._columns[x],'_classic_write'), fields) + self._inherits.values()
725
726                 if len(fields_pre) or compids:
727                         cr.execute('select %s from %s where id = any(array[%s]) and %s order by %s' % (','.join(fields_pre + ['id']), self._table, ','.join([str(x) for x in ids]), company_clause, self._order))
728                         uniq_id = []
729                         [uniq_id.append(i) for i in ids if not uniq_id.count(i)]
730                         if not cr.rowcount == len(uniq_id) and compids:
731                                 raise except_orm('ReadError', 'You try to read objects (%s) that is not in your company' % self._description)
732                         res = cr.dictfetchall()
733                 else:
734                         res = map(lambda x: {'id':x}, ids)
735
736                 if context.get('lang', False) and context['lang'] != 'en_EN':
737                         for f in fields_pre:
738                                 if self._columns[f].translate:
739                                         ids = map(lambda x: x['id'], res)
740                                         res_trans = self.pool.get('ir.translation')._get_ids(cr, user, self._name+','+f, 'model', context['lang'], ids)
741                                         for r in res:
742                                                 r[f] = res_trans.get(r['id'], r[f])
743
744                 for table in self._inherits:
745                         col = self._inherits[table]
746                         cols = intersect(self._inherit_fields.keys(), fields)
747                         res2 = self.pool.get(table).read(cr, user, [x[col] for x in res], cols, context, load)
748
749                         res3 = {}
750                         for r in res2:
751                                 res3[r['id']] = r
752                                 del r['id']
753
754                         for record in res:
755                                 record.update(res3[record[col]])
756                                 if col not in fields:
757                                         del record[col]
758
759                 # all fields which need to be post-processed by a simple function (symbol_get)
760                 fields_post = filter(lambda x: x in self._columns and self._columns[x]._symbol_get, fields)
761                 if fields_post:
762                         # maybe it would be faster to iterate on the fields then on res, so that we wouldn't need
763                         # to get the _symbol_get in each occurence
764                         for r in res:
765                                 for f in fields_post:
766                                         r[f] = self.columns[f]._symbol_get(r[f])
767                 ids = map(lambda x: x['id'], res)
768
769                 # all non inherited fields for which the attribute whose name is in load is False
770                 fields_post = filter(lambda x: x in self._columns and not getattr(self._columns[x], load), fields)
771                 for f in fields_post:
772                         # get the value of that field for all records/ids
773                         res2 = self._columns[f].get(cr, self, ids, f, user, context=context, values=res)
774                         for record in res:
775                                 record[f] = res2[record['id']]
776
777                 return res
778
779         def _validate(self, cr, uid, ids):
780                 field_error = []
781                 field_err_str = []
782                 for field in self._constraints:
783                         if not field[0](self, cr, uid, ids):
784                                 if len(field)>1:
785                                         field_error+=field[2]
786                                 field_err_str.append(field[1])
787                 if len(field_err_str):
788                         cr.rollback()
789                         raise except_orm('ValidateError', ('\n'.join(field_err_str), ','.join(field_error)))
790
791         def default_get(self, cr, uid, fields, context={}):
792                 value = {}
793                 # get the default values for the inherited fields
794                 for t in self._inherits.keys():
795                         value.update(self.pool.get(t).default_get(cr, uid, fields, context))
796
797                 # get the default values defined in the object
798                 for f in fields:
799                         if f in self._defaults:
800                                 value[f] = self._defaults[f](self, cr, uid, context)
801
802                 # get the default values set by the user and override the default
803                 # values defined in the object
804                 res = ir.ir_get(cr, uid, 'default', False, [self._name])
805                 for id, field, field_value in res:
806                         if field in fields:
807                                 value[field] = field_value 
808                 return value
809
810         def perm_read(self, cr, user, ids, context={}):
811                 fields = ', p.level, p.uid, p.gid'
812                 if self._log_access:
813                         fields +=', u.create_uid, u.create_date, u.write_uid, u.write_date'
814                 ids_str = string.join(map(lambda x:str(x), ids),',')
815                 cr.execute('select u.id'+fields+' from perm p right join '+self._table+' u on u.perm_id=p.id where u.id=any(array['+ids_str+'])')
816                 res = cr.dictfetchall()
817 #               for record in res:
818 #                       for f in ('ox','ux','gx','uid','gid'):
819 #                               if record[f]==None:
820 #                                       record[f]=False
821                 for r in res:
822                         for key in r:
823                                 r[key] = r[key] or False
824                                 if key in ('write_uid','create_uid','uid'):
825                                         if r[key]:
826                                                 r[key] = self.pool.get('res.users').name_get(cr, user, [r[key]])[0]
827                 return res
828
829         def unlink(self, cr, uid, ids, context={}):
830                 self.pool.get('ir.model.access').check(cr, uid, self._name, 'unlink')
831                 if not len(ids):
832                         return True
833                 wf_service = netsvc.LocalService("workflow")
834                 for id in ids:
835                         wf_service.trg_delete(uid, self._name, id, cr)
836                 str_d = string.join(('%d',)*len(ids),',')
837
838                 cr.execute('select * from '+self._table+' where id in ('+str_d+')', ids)
839                 res = cr.dictfetchall()
840                 #for key in self._inherits:
841                 #       ids2 = [x[self._inherits[key]] for x in res]
842                 #       self.pool.get(key).unlink(cr, uid, ids2)
843                 cr.execute('delete from inherit where (obj_type=%s and obj_id in ('+str_d+')) or (inst_type=%s and inst_id in ('+str_d+'))', (self._name,)+tuple(ids)+(self._name,)+tuple(ids))
844                 cr.execute('delete from '+self._table+' where id in ('+str_d+')', ids)
845                 return True
846
847         #
848         # TODO: Validate
849         #
850         def write(self, cr, user, ids, vals, context={}):
851                 self.pool.get('ir.model.access').check(cr, user, self._name, 'write')
852                 #for v in self._inherits.values():
853                 #       assert v not in vals, (v, vals)
854                 if not ids:
855                         return
856                 ids_str = string.join(map(lambda x:str(x), ids),',')
857                 upd0=[]
858                 upd1=[]
859                 upd_todo=[]
860                 updend=[]
861                 direct = []
862                 totranslate = context.get('lang', False) and (context['lang'] != 'en_EN')
863                 for field in vals:
864                         if field in self._columns:
865                                 if self._columns[field]._classic_write:
866                                         if (not totranslate) or not self._columns[field].translate:
867                                                 upd0.append(field+'='+self._columns[field]._symbol_set[0])
868                                                 upd1.append(self._columns[field]._symbol_set[1](vals[field]))
869                                         direct.append(field)
870                                 else:
871                                         upd_todo.append(field)
872                         else:
873                                 updend.append(field)
874                 if self._log_access:
875                         upd0.append('write_uid=%d')
876                         upd0.append('write_date=current_timestamp(0)')
877                         upd1.append(user)
878
879                 if len(upd0):
880                         cr.execute('update '+self._table+' set '+string.join(upd0,',')+' where id in ('+ids_str+')', upd1)
881
882                         if totranslate:
883                                 for f in direct:
884                                         if self._columns[f].translate:
885                                                 self.pool.get('ir.translation')._set_ids(cr, user, self._name+','+f,'model',  context['lang'], ids,vals[f])
886
887                 # call the 'set' method of fields which are not classic_write
888                 upd_todo.sort(lambda x,y: self._columns[x].priority-self._columns[y].priority)
889                 for field in upd_todo:
890                         for id in ids:
891                                 self._columns[field].set(cr, self, id, field, vals[field], user)
892
893                 for table in self._inherits:
894                         col = self._inherits[table]
895                         cr.execute('select distinct '+col+' from '+self._table+' where id in ('+ids_str+')', upd1)
896                         nids = [x[0] for x in cr.fetchall()]
897
898                         v = {}
899                         for val in updend:
900                                 if self._inherit_fields[val][0]==table:
901                                         v[val]=vals[val]
902                         self.pool.get(table).write(cr, user, nids, v, context)
903
904                 self._validate(cr, user, ids)
905
906                 wf_service = netsvc.LocalService("workflow")
907                 for id in ids:
908                         wf_service.trg_write(user, self._name, id, cr)
909                 self._update_function_stored(cr, user, ids, context=context)
910                 return True
911
912         #
913         # TODO: Should set perm to user.xxx
914         #
915         def create(self, cr, user, vals, context={}):
916                 """ create(cr, user, vals, context) -> int
917                 cr = database cursor
918                 user = user id
919                 vals = dictionary of the form {'field_name':field_value, ...}
920                 """
921                 self.pool.get('ir.model.access').check(cr, user, self._name, 'create')
922
923                 default = []
924
925                 avoid_table = []
926                 for (t,c) in self._inherits.items():
927                         if c in vals:
928                                 avoid_table.append(t)
929                 for f in self._columns.keys(): # + self._inherit_fields.keys():
930                         if not f in vals:
931                                 default.append(f)
932                 for f in self._inherit_fields.keys():
933                         if (not f in vals) and (not self._inherit_fields[f][0] in avoid_table):
934                                 default.append(f)
935
936                 if len(default):
937                         vals.update(self.default_get(cr, user, default, context))
938
939                 tocreate = {}
940                 for v in self._inherits:
941                         if self._inherits[v] not in vals:
942                                 tocreate[v] = {}
943
944                 #cr.execute('select perm_id from res_users where id=%d', (user,))
945                 perm = False #cr.fetchone()[0]
946                 (upd0, upd1, upd2) = ('', '', [])
947                 upd_todo = []
948
949                 for v in vals.keys():
950                         if v in self._inherit_fields:
951                                 (table,col,col_detail) = self._inherit_fields[v]
952                                 tocreate[table][v] = vals[v]
953                                 del vals[v]
954
955                 cr.execute("select nextval('"+self._sequence+"')")
956                 id_new = cr.fetchone()[0]
957                 for table in tocreate:
958                         id = self.pool.get(table).create(cr, user, tocreate[table])
959                         upd0 += ','+self._inherits[table]
960                         upd1 += ',%d'
961                         upd2.append(id)
962                         cr.execute('insert into inherit (obj_type,obj_id,inst_type,inst_id) values (%s,%d,%s,%d)', (table,id,self._name,id_new))
963
964                 for field in vals:
965                         if self._columns[field]._classic_write:
966                                 upd0=upd0+','+field
967                                 upd1=upd1+','+self._columns[field]._symbol_set[0]
968                                 upd2.append(self._columns[field]._symbol_set[1](vals[field]))
969                         else:
970                                 upd_todo.append(field)
971                 if self._log_access:
972                         upd0 += ',create_uid,create_date'
973                         upd1 += ',%d,current_timestamp(0)'
974                         upd2.append(user)
975                 cr.execute('insert into '+self._table+' (id,perm_id'+upd0+") values ("+str(id_new)+',NULL'+upd1+')', tuple(upd2))
976                 upd_todo.sort(lambda x,y: self._columns[x].priority-self._columns[y].priority)
977                 for field in upd_todo:
978                         self._columns[field].set(cr, self, id_new, field, vals[field], user, context)
979
980                 self._validate(cr, user, [id_new])
981
982                 wf_service = netsvc.LocalService("workflow")
983                 wf_service.trg_create(user, self._name, id_new, cr)
984                 self._update_function_stored(cr, user, [id_new], context=context)
985                 return id_new
986
987         def _update_function_stored(self, cr, user, ids, context={}):
988                 f=filter(lambda a: isinstance(self._columns[a], fields.function) and self._columns[a].store, self._columns)
989                 if f:
990                         result=self.read(cr, user, ids, fields=f, context=context)
991                         for res in result:
992                                 upd0=[]
993                                 upd1=[]
994                                 for field in res:
995                                         if field not in f:
996                                                 continue
997                                         upd0.append(field+'='+self._columns[field]._symbol_set[0])
998                                         upd1.append(self._columns[field]._symbol_set[1](res[field]))
999                                 upd1.append(res['id'])
1000                                 cr.execute('update '+self._table+' set '+string.join(upd0,',')+ ' where id = %d', upd1)
1001                 return True
1002
1003         #
1004         # TODO: Validate
1005         #
1006         def perm_write(self, cr, user, ids, fields, context={}):
1007                 query = []
1008                 vals = []
1009                 keys = fields.keys()
1010                 for x in keys:
1011                         query.append(x+'=%d')
1012                         vals.append(fields[x])
1013                 cr.execute('select id from perm where ' + ' and '.join(query) + ' limit 1', vals)
1014                 res = cr.fetchone()
1015                 if res:
1016                         id = res[0]
1017                 else:
1018                         cr.execute("select nextval('perm_id_seq')")
1019                         id = cr.fetchone()[0]
1020                         cr.execute('insert into perm (id,' + ','.join(keys) + ') values (' + str(id) + ',' + ('%d,'*(len(keys)-1)+'%d')+')', vals)
1021                 ids_str = ','.join(map(str, ids))
1022                 cr.execute('update '+self._table+' set perm_id=%d where id in ('+ids_str+')', (id,))
1023                 return True
1024
1025         # returns the definition of each field in the object
1026         # the optional fields parameter can limit the result to some fields
1027         def fields_get(self, cr, user, fields=None, context={}):
1028                 res = {}
1029                 for parent in self._inherits:
1030                         res.update(self.pool.get(parent).fields_get(cr, user, fields, context))
1031                 for f in self._columns.keys():
1032                         res[f] = {'type': self._columns[f]._type}
1033                         for arg in ('string','readonly','states','size','required','change_default','translate', 'help', 'select'):
1034                                 if getattr(self._columns[f], arg):
1035                                         res[f][arg] = getattr(self._columns[f], arg)
1036                         for arg in ('digits', 'invisible'):
1037                                 if hasattr(self._columns[f], arg) and getattr(self._columns[f], arg):
1038                                         res[f][arg] = getattr(self._columns[f], arg)
1039
1040                         # translate the field label
1041                         if context.get('lang', False) and context['lang'] != 'en_EN':
1042                                 res_trans = self.pool.get('ir.translation')._get_source(cr, user, self._name+','+f, 'field', context['lang'])
1043                                 if res_trans:
1044                                         res[f]['string'] = res_trans
1045
1046                         if hasattr(self._columns[f], 'selection'):
1047                                 if isinstance(self._columns[f].selection, (tuple, list)):
1048                                         sel = self._columns[f].selection
1049                                         
1050                                         # translate each selection option
1051                                         if context.get('lang', False) and context['lang'] != 'en_EN':
1052                                                 sel2 = []
1053                                                 for (key,val) in sel:
1054                                                         val2 = self.pool.get('ir.translation')._get_source(cr, user, self._name+','+f, 'selection', context['lang'], val)
1055                                                         sel2.append((key, val2 or val))
1056                                                 sel = sel2
1057                                         res[f]['selection'] = sel
1058                                 else:
1059                                         # call the 'dynamic selection' function
1060                                         res[f]['selection'] = self._columns[f].selection(self, cr, user, context)
1061                         if res[f]['type'] in ('one2many', 'many2many', 'many2one', 'one2one'):
1062                                 res[f]['relation'] = self._columns[f]._obj
1063                                 res[f]['domain'] = self._columns[f]._domain
1064                                 res[f]['context'] = self._columns[f]._context
1065
1066                 if fields:
1067                         # filter out fields which aren't in the fields list
1068                         for r in res.keys():
1069                                 if r not in fields:
1070                                         del res[r]
1071                 return res
1072
1073         #
1074         # Overload this method if you need a window title which depends on the context
1075         #
1076         def view_header_get(self, cr, user, view_id=None, view_type='form', context={}):
1077                 return False
1078
1079         def __view_look_dom(self, cr, user, node, context={}):
1080                 result = False
1081                 fields = {}
1082                 childs = True
1083                 if node.nodeType==node.ELEMENT_NODE and node.localName=='field':
1084                         if node.hasAttribute('name'):
1085                                 attrs = {}
1086                                 try:
1087                                         if node.getAttribute('name') in self._columns:
1088                                                 relation = self._columns[node.getAttribute('name')]._obj
1089                                         else:
1090                                                 relation = self._inherit_fields[node.getAttribute('name')][2]._obj
1091                                 except:
1092                                         relation = False
1093                                 if relation:
1094                                         childs = False
1095                                         views = {}
1096                                         for f in node.childNodes:
1097                                                 if f.nodeType==f.ELEMENT_NODE and f.localName in ('form','tree'):
1098                                                         node.removeChild(f)
1099                                                         xarch,xfields = self.pool.get(relation).__view_look_dom_arch(cr, user, f, context)
1100                                                         views[str(f.localName)] = {
1101                                                                 'arch': xarch,
1102                                                                 'fields': xfields
1103                                                         }
1104                                         attrs = {'views': views}
1105                                 fields[node.getAttribute('name')] = attrs
1106
1107                 elif node.nodeType==node.ELEMENT_NODE and node.localName in ('form','tree'):
1108                         result = self.view_header_get(cr, user, False, node.localName, context)
1109                         if result:
1110                                 node.setAttribute('string', result)
1111
1112                 if node.nodeType == node.ELEMENT_NODE:
1113                         # translate view
1114                         if ('lang' in context) and node.hasAttribute('string') and node.getAttribute('string') and not result:
1115                                 trans = tools.translate(cr, user, self._name, 'view', context['lang'], node.getAttribute('string').encode('utf8'))
1116                                 if trans:
1117                                         node.setAttribute('string', trans.decode('utf8'))
1118                         #
1119                         # Add view for properties !
1120                         #
1121                         if node.localName=='properties':
1122                                 parent = node.parentNode
1123
1124                                 doc = node.ownerDocument
1125
1126                                 models = map(lambda x: "'"+x+"'", [self._name] + self._inherits.keys())
1127                                 cr.execute('select id,name,group_name from ir_model_fields where model in ('+','.join(models)+') and view_load order by group_name, id')
1128                                 oldgroup = None
1129                                 res = cr.fetchall()
1130                                 for id, fname, gname in res:
1131                                         if oldgroup != gname:
1132                                                 child = doc.createElement('separator')
1133                                                 child.setAttribute('string', gname)
1134                                                 child.setAttribute('colspan', "4")
1135                                                 oldgroup = gname
1136                                                 parent.insertBefore(child, node)
1137
1138                                         child = doc.createElement('field')
1139                                         child.setAttribute('name', fname)
1140                                         parent.insertBefore(child, node)
1141                                 parent.removeChild(node)
1142
1143                 if childs:
1144                         for f in node.childNodes:
1145                                 fields.update(self.__view_look_dom(cr, user, f,context))
1146                 return fields
1147
1148         def __view_look_dom_arch(self, cr, user, node, context={}):
1149                 fields_def = self.__view_look_dom(cr, user, node, context=context)
1150                 arch = node.toxml()
1151                 fields = self.fields_get(cr, user, fields_def.keys(), context)
1152                 for field in fields_def:
1153                         fields[field].update(fields_def[field])
1154                 return arch,fields
1155
1156
1157         #
1158         # if view_id, view_type is not required
1159         #
1160         def fields_view_get(self, cr, user, view_id=None, view_type='form', context={}, toolbar=False):
1161                 def _inherit_apply(src, inherit):
1162                         def _find(node, node2):
1163                                 if node.nodeType==node.ELEMENT_NODE and node.localName==node2.localName:
1164                                         res = True
1165                                         for attr in node2.attributes.keys():
1166                                                 if attr=='position':
1167                                                         continue
1168                                                 if node.hasAttribute(attr):
1169                                                         if node.getAttribute(attr)==node2.getAttribute(attr):
1170                                                                 continue
1171                                                 res = False
1172                                         if res:
1173                                                 return node
1174                                 for child in node.childNodes:
1175                                         res = _find(child, node2)
1176                                         if res: return res
1177                                 return None
1178
1179                         doc_src = dom.minidom.parseString(src)
1180                         doc_dest = dom.minidom.parseString(inherit)
1181                         for node2 in doc_dest.childNodes:
1182                                 if not node2.nodeType==node2.ELEMENT_NODE:
1183                                         continue
1184                                 node = _find(doc_src, node2)
1185                                 if node:
1186                                         pos = 'inside'
1187                                         if node2.hasAttribute('position'):
1188                                                 pos = node2.getAttribute('position')
1189                                         if pos=='replace':
1190                                                 parent = node.parentNode
1191                                                 for child in node2.childNodes:
1192                                                         if child.nodeType==child.ELEMENT_NODE:
1193                                                                 parent.insertBefore(child, node)
1194                                                 parent.removeChild(node)
1195                                         else:
1196                                                 for child in node2.childNodes:
1197                                                         if child.nodeType==child.ELEMENT_NODE:
1198                                                                 if pos=='inside':
1199                                                                         node.appendChild(child)
1200                                                                 elif pos=='after':
1201                                                                         sib = node.nextSibling
1202                                                                         if sib:
1203                                                                                 node.parentNode.insertBefore(child, sib)
1204                                                                         else:
1205                                                                                 node.parentNode.appendChild(child)
1206                                                                 elif pos=='before':
1207                                                                         node.parentNode.insertBefore(child, node)
1208                                                                 else:
1209                                                                         raise AttributeError, 'Unknown position in inherited view %s !' % pos
1210                                 else:
1211                                         attrs = ''.join([
1212                                                 ' %s="%s"' % (attr, node2.getAttribute(attr)) 
1213                                                 for attr in node2.attributes.keys() 
1214                                                 if attr != 'position'
1215                                         ])
1216                                         tag = "<%s%s>" % (node2.localName, attrs)
1217                                         raise AttributeError, "Couldn't find tag '%s' in parent view !" % tag
1218                         return doc_src.toxml()
1219
1220                 result = {'type':view_type, 'model':self._name}
1221
1222                 ok = True
1223                 model = True
1224                 while ok:
1225                         if view_id:
1226                                 where = (model and (" and model='%s'" % (self._name,))) or ''
1227                                 cr.execute('select arch,name,field_parent,id,type,inherit_id from ir_ui_view where id=%d'+where, (view_id,))
1228                         else:
1229                                 cr.execute('select arch,name,field_parent,id,type,inherit_id from ir_ui_view where model=%s and type=%s order by priority', (self._name,view_type))
1230                         sql_res = cr.fetchone()
1231                         if not sql_res:
1232                                 break
1233                         ok = sql_res[5]
1234                         view_id = ok or sql_res[3]
1235                         model = False
1236
1237                 # if a view was found
1238                 if sql_res:
1239                         result['type'] = sql_res[4]
1240                         result['view_id'] = sql_res[3]
1241                         result['arch'] = sql_res[0]
1242
1243                         def _inherit_apply_rec(result, inherit_id):
1244                                 # get all views which inherit from (ie modify) this view
1245                                 cr.execute('select arch,id from ir_ui_view where inherit_id=%d and model=%s order by priority', (inherit_id, self._name))
1246                                 sql_inherit = cr.fetchall()
1247                                 for (inherit,id) in sql_inherit:
1248                                         result = _inherit_apply(result, inherit)
1249                                         result = _inherit_apply_rec(result,id)
1250                                 return result
1251
1252                         result['arch'] = _inherit_apply_rec(result['arch'], sql_res[3])
1253
1254                         result['name'] = sql_res[1]
1255                         result['field_parent'] = sql_res[2] or False
1256                 else:
1257                         # otherwise, build some kind of default view
1258                         if view_type == 'form':
1259                                 res = self.fields_get(cr, user, context=context)
1260                                 xml = '''<?xml version="1.0"?>\n<form string="%s">\n''' % (self._description,)
1261                                 for x in res:
1262                                         if res[x]['type'] not in ('one2many', 'many2many'):
1263                                                 xml += '\t<field name="%s"/>\n' % (x,)
1264                                                 if res[x]['type'] == 'text':
1265                                                         xml += "<newline/>"
1266                                 xml += "</form>"
1267                         elif view_type == 'tree':
1268                                 xml = '''<?xml version="1.0"?>\n<tree string="%s">\n\t<field name="%s"/>\n</tree>''' % (self._description,self._rec_name)
1269                         else:
1270                                 xml = ''
1271                         result['arch'] = xml
1272                         result['name'] = 'default'
1273                         result['field_parent'] = False
1274                         result['view_id'] = 0
1275
1276                 doc = dom.minidom.parseString(result['arch'])
1277                 xarch, xfields = self.__view_look_dom_arch(cr, user, doc, context=context)
1278                 result['arch'] = xarch
1279                 result['fields'] = xfields
1280                 if toolbar:
1281                         resprint = self.pool.get('ir.values').get(cr, user, 'action', 'client_print_multi', [(self._name, False)], False, context)
1282                         resaction = self.pool.get('ir.values').get(cr, user, 'action', 'client_action_multi', [(self._name, False)], False, context)
1283                         resprint = map(lambda x:x[2], resprint)
1284                         resaction = map(lambda x:x[2], resaction)
1285                         resaction = filter(lambda x: not x.get('multi',False), resaction)
1286                         for x in resprint+resaction:
1287                                 x['string'] = x['name']
1288                         ids = self.pool.get('ir.model.fields').search(cr, user, [('relation','=',self._name),('relate','=',1)])
1289                         resrelate = self.pool.get('ir.model.fields').read(cr, user, ids, ['name','model_id'], context)
1290                         models = self.pool.get('ir.model').read(cr, user, map(lambda x: x['model_id'][0], resrelate), ['name'], context)
1291                         dmodels = {}
1292                         for m in models:
1293                                 dmodels[m['id']] = m['name']
1294                         for x in resrelate:
1295                                 x['string'] = dmodels[x['model_id'][0]]
1296                         
1297                         result['toolbar'] = {
1298                                 'print': resprint,
1299                                 'action': resaction,
1300                                 'relate': resrelate
1301                         }
1302
1303                 return result
1304
1305         # TODO: ameliorer avec NULL
1306         def _where_calc(self, args):
1307                 qu1, qu2 = [], []
1308                 for x in args:
1309                         table=self
1310                         if len(x) > 3:
1311                                 table=x[3]
1312                         if x[1] != 'in':
1313 #FIXME: this replace all (..., '=', False) values with 'is null' and this is 
1314 # not what we want for real boolean fields. The problem is, we can't change it
1315 # easily because we use False everywhere instead of None
1316 # NOTE FAB: we can't use None because it is not accepted by XML-RPC, that's why
1317 # boolean (0-1), None -> False
1318 # Ged> boolean fields are not always = 0 or 1
1319                                 if (x[2] is False) and (x[1]=='='):
1320                                         qu1.append(x[0]+' is null')
1321                                 elif (x[2] is False) and (x[1]=='<>' or x[1]=='!='):
1322                                         qu1.append(x[0]+' is not null')
1323                                 else:
1324                                         if x[0]=='id':
1325                                                 if x[1]=='join':
1326                                                         qu1.append('(%s.%s = %s)' % (table._table, x[0], x[2]))
1327                                                 else:
1328                                                         qu1.append('(%s.%s %s %%s)' % (table._table, x[0], x[1]))
1329                                                         qu2.append(x[2])
1330                                         else:
1331                                                 if x[1] in ('like', 'ilike'):
1332                                                         if isinstance(x[2], str):
1333                                                                 str_utf8 = x[2]
1334                                                         elif isinstance(x[2], unicode):
1335                                                                 str_utf8 = x[2].encode('utf-8')
1336                                                         else:
1337                                                                 str_utf8 = str(x[2])
1338                                                         qu2.append('%%%s%%' % str_utf8)
1339                                                 else:
1340                                                         qu2.append(table._columns[x[0]]._symbol_set[1](x[2]))
1341                                                 if x[1]=='=like':
1342                                                         x1 = 'like'
1343                                                 else:
1344                                                         x1 = x[1]
1345                                                 qu1.append('(%s.%s %s %s)' % (table._table, x[0], x1, table._columns[x[0]]._symbol_set[0]))
1346                         elif x[1]=='in':
1347                                 if len(x[2])>0:
1348                                         todel = []
1349                                         for xitem in range(len(x[2])):
1350                                                 if x[2][xitem]==False and isinstance(x[2][xitem],bool):
1351                                                         todel.append(xitem)
1352                                         for xitem in todel[::-1]:
1353                                                 del x[2][xitem]
1354                                         if x[0]=='id':
1355                                                 qu1.append('(%s.id=any(array[%s]))' % (table._table, ','.join(['%d'] * len(x[2])),))
1356                                         else:
1357                                                 qu1.append('(%s.%s in (%s))' % (table._table, x[0], ','.join([table._columns[x[0]]._symbol_set[0]]*len(x[2]))))
1358                                         if todel:
1359                                                 qu1[-1] = '('+qu1[-1]+' or '+x[0]+' is null)'
1360                                         qu2+=x[2]
1361                                 else:
1362                                         qu1.append(' (1=0)')
1363                 return (qu1,qu2)
1364
1365         def search(self, cr, user, args, offset=0, limit=None, order=None, context={}, translate=False):
1366                 # if the object has a field named 'active', filter out all inactive
1367                 # records unless they were explicitely asked for
1368                 if 'active' in self._columns:
1369                         i = 0
1370                         active_found = False
1371                         while i<len(args):
1372                                 if args[i][0]=='active':
1373                                         if not args[i][2]:
1374                                                 active_found = True
1375                                 i += 1
1376                         if not active_found:
1377                                 args.append(('active', '=', 1))
1378
1379
1380
1381                 # if the object has a field named 'company_id', filter out all
1382                 # records which do not concern the current company (the company
1383                 # of the current user) or its "childs"
1384                 if 'company_id' in self._columns and not user == 1:
1385                         compids = tools.get_user_companies(cr, user)
1386                         if compids:
1387                                 compids.append(False)
1388                                 args.append(('company_id','in',compids))
1389
1390                 i = 0
1391                 tables=[self._table]
1392                 joins=[]
1393                 while i<len(args):
1394                         table=self
1395                         if args[i][0] in self._inherit_fields:
1396                                 table=self.pool.get(self._inherit_fields[args[i][0]][0])
1397                                 if (table._table not in tables):
1398                                         tables.append(table._table)
1399                                         joins.append(('id', 'join', '%s.%s' % (self._table, self._inherits[table._name]), table))
1400                         field = table._columns.get(args[i][0],False)
1401                         if not field:
1402                                 i+=1
1403                                 continue
1404                         if field._type=='one2many':
1405                                 field_obj = self.pool.get(field._obj)
1406
1407                                 # get the ids of the records of the "distant" resource
1408                                 ids2 = [x[0] for x in field_obj.name_search(cr, user, args[i][2], [], args[i][1])]
1409                                 if not ids2:
1410                                         args[i] = ('id','=','0')
1411                                 else:
1412                                         cr.execute('select '+field._fields_id+' from '+field_obj._table+' where id = any(array['+','.join(map(str,ids2))+'])')
1413                                         ids3 = [x[0] for x in cr.fetchall()]
1414
1415                                         args[i] = ('id', 'in', ids3)
1416                                 i+=1
1417
1418                         elif field._type=='many2many':
1419                                 if args[i][1]=='child_of':
1420                                         if isinstance(args[i][2], basestring):
1421                                                 ids2 = [x[0] for x in self.pool.get(field._obj).name_search(cr, user, args[i][2], [], 'like')]
1422                                         else:
1423                                                 ids2 = args[i][2]
1424                                         def _rec_get(ids):
1425                                                 if not len(ids): return []
1426                                                 cr.execute('select '+field._id1+' from '+field._rel+' where '+field._id2+' in ('+','.join(map(str,ids))+')')
1427                                                 ids = [x[0] for x in cr.fetchall()]
1428                                                 return ids + _rec_get(ids)
1429                                         args[i] = ('id','in',ids2+_rec_get(ids2))
1430                                 else:
1431                                         if isinstance(args[i][2], basestring):
1432                                                 res_ids = [x[0] for x in self.pool.get(field._obj).name_search(cr, user, args[i][2], [], args[i][1])]
1433                                         else:
1434                                                 res_ids = args[i][2]
1435                                         if not len(res_ids):
1436                                                 return []
1437                                         cr.execute('select '+field._id1+' from '+field._rel+' where '+field._id2+' in ('+','.join(map(str, res_ids))+')')
1438                                         args[i] = ('id', 'in', map(lambda x: x[0], cr.fetchall()))
1439                                 i+=1
1440
1441                         elif field._type=='many2one':
1442                                 if args[i][1]=='child_of':
1443                                         if isinstance(args[i][2], basestring):
1444                                                 ids2 = [x[0] for x in self.pool.get(field._obj).name_search(cr, user, args[i][2], [], 'like')]
1445                                         else:
1446                                                 ids2 = args[i][2]
1447                                         def _rec_get(ids, table):
1448                                                 if not len(ids):
1449                                                         return []
1450                                                 if 'active' in table._columns:
1451                                                         ids2 = table.search(cr, user, [(args[i][0],'in',ids),('active','=',0)])
1452                                                         ids2 += table.search(cr, user, [(args[i][0],'in',ids),('active','=',1)])
1453                                                 else:
1454                                                         ids2 = table.search(cr, user, [(args[i][0], 'in', ids)])
1455                                                 return ids + _rec_get(ids2, table)
1456                                         args[i] = ('id','in',ids2+_rec_get(ids2, table), table)
1457                                 else:
1458                                         if isinstance(args[i][2], basestring):
1459                                                 res_ids = self.pool.get(field._obj).name_search(cr, user, args[i][2], [], args[i][1])
1460                                                 args[i] = (args[i][0],'in',map(lambda x: x[0], res_ids), table)
1461                                         else:
1462                                                 args[i] += (table,)
1463                                 i+=1
1464                         elif field._properties:
1465                                 arg = [args.pop(i)]
1466                                 j = i
1467                                 while j<len(args):
1468                                         if args[j][0]==arg[0][0]:
1469                                                 arg.append(args.pop(j))
1470                                         else:
1471                                                 j+=1
1472                                 if field._fnct_search:
1473                                         args.extend(field.search(cr, user, self, arg[0][0], arg))
1474                         else:
1475                                 if field.translate and translate and context.get('lang', False) and context['lang'] != 'en_EN':
1476                                         if args[i][1] in ('like', 'ilike'):
1477                                                 args[i][2] = '%%%s%%' % args[i][2]
1478                                         cr.execute('select res_id from ir_translation where name = %s and lang = %s and type = %s and value '+args[i][1]+' %s', (table._name+','+args[i][0], context['lang'], 'model', args[i][2]))
1479                                         ids = map(lambda x: x[0], cr.fetchall())
1480                                         cr.execute('select id from '+table._table+' where '+args[i][0]+' '+args[i][1]+' %s', (args[i][2],))
1481                                         ids += map(lambda x: x[0], cr.fetchall())
1482                                         args[i] = ('id', 'in', ids, table)
1483                                 else:
1484                                         args[i] += (table,)
1485                                 i+=1
1486                 args.extend(joins)
1487
1488                 # compute the where, order by, limit and offset clauses
1489                 (qu1,qu2) = self._where_calc(args)
1490                 if len(qu1):
1491                         qu1 = ' where '+string.join(qu1,' and ')
1492                 else:
1493                         qu1 = ''
1494                 order_by = order or self._order
1495
1496                 limit_str = limit and ' limit %d' % limit or ''
1497                 offset_str = offset and ' offset %d' % offset or ''
1498                 
1499                 # execute the "main" query to fetch the ids we were searching for
1500                 cr.execute('select %s.id from ' % self._table + ','.join(tables) +qu1+' order by '+order_by+limit_str+offset_str, qu2)
1501                 res = cr.fetchall()
1502                 return [x[0] for x in res]
1503
1504         # returns the different values ever entered for one field
1505         # this is used, for example, in the client when the user hits enter on 
1506         # a char field
1507         def distinct_field_get(self, cr, uid, field, value, args=[], offset=0, limit=None):
1508                 if field in self._inherit_fields:
1509                         return self.pool.get(self._inherit_fields[field][0]).distinct_field_get(cr, uid,field,value,args,offset,limit)
1510                 else:
1511                         return self._columns[field].search(cr, self, args, field, value, offset, limit, uid)
1512
1513         def name_get(self, cr, user, ids, context={}):
1514                 if not len(ids):
1515                         return []
1516                 return [(r['id'], r[self._rec_name]) for r in self.read(cr, user, ids, [self._rec_name], context, load='_classic_write')]
1517
1518         def name_search(self, cr, user, name='', args=[], operator='ilike', context={}, limit=80):
1519                 if name:
1520                         args += [(self._rec_name,operator,name)]
1521                 ids = self.search(cr, user, args, limit=limit)
1522                 res = self.name_get(cr, user, ids, context)
1523                 return res
1524
1525         def copy(self, cr, uid, id, default=None, context={}):
1526                 if not default:
1527                         default = {}
1528                 if 'state' not in default:
1529                         if 'state' in self._defaults:
1530                                 default['state'] = self._defaults['state'](self, cr, uid, context)
1531                 data = self.read(cr, uid, [id], context=context)[0]
1532                 fields = self.fields_get(cr, uid)
1533                 for f in fields:
1534                         ftype = fields[f]['type']
1535
1536                         if self._log_access and f in ('create_date', 'create_uid', 'write_date', 'write_uid'):
1537                                 del data[f]
1538
1539                         if f in default:
1540                                 data[f] = default[f]
1541                         elif ftype == 'function':
1542                                 del data[f]
1543                         elif ftype == 'many2one':
1544                                 try:
1545                                         data[f] = data[f] and data[f][0]
1546                                 except:
1547                                         pass
1548                         elif ftype in ('one2many', 'one2one'):
1549                                 res = []
1550                                 rel = self.pool.get(fields[f]['relation'])
1551                                 for rel_id in data[f]:
1552                                         # the lines are first duplicated using the wrong (old) 
1553                                         # parent but then are reassigned to the correct one thanks
1554                                         # to the (4, ...)
1555                                         res.append((4, rel.copy(cr, uid, rel_id, context=context)))
1556                                 data[f] = res
1557                         elif ftype == 'many2many':
1558                                 data[f] = [(6, 0, data[f])]
1559                 del data['id']
1560                 for v in self._inherits:
1561                         del data[self._inherits[v]]
1562                 return self.create(cr, uid, data)
1563
1564         def read_string(self, cr, uid, id, langs, fields=None, context={}):
1565                 res = {}
1566                 res2 = {}
1567                 self.pool.get('ir.model.access').check(cr, uid, 'ir.translation', 'read')
1568                 if not fields:
1569                         fields = self._columns.keys() + self._inherit_fields.keys()
1570                 for lang in langs:
1571                         res[lang] = {'code': lang}
1572                         for f in fields:
1573                                 if f in self._columns:
1574                                         res_trans = self.pool.get('ir.translation')._get_source(cr, uid, self._name+','+f, 'field', lang)
1575                                         if res_trans:
1576                                                 res[lang][f]=res_trans
1577                                         else:
1578                                                 res[lang][f]=self._columns[f].string
1579                 for table in self._inherits:
1580                         cols = intersect(self._inherit_fields.keys(), fields)
1581                         res2 = self.pool.get(table).read_string(cr, uid, id, langs, cols, context)
1582                 for lang in res2:
1583                         if lang in res:
1584                                 res[lang]={'code': lang}
1585                         for f in res2[lang]:
1586                                 res[lang][f]=res2[lang][f]
1587                 return res
1588
1589         def write_string(self, cr, uid, id, langs, vals, context={}):
1590                 self.pool.get('ir.model.access').check(cr, uid, 'ir.translation', 'write')
1591                 for lang in langs:
1592                         for field in vals:
1593                                 if field in self._columns:
1594                                         self.pool.get('ir.translation')._set_ids(cr, uid, self._name+','+field, 'field', lang, [0], vals[field])
1595                 for table in self._inherits:
1596                         cols = intersect(self._inherit_fields.keys(), vals)
1597                         if cols:
1598                                 self.pool.get(table).write_string(cr, uid, id, langs, vals, context)
1599                 return True
1600
1601 # vim:noexpandtab:ts=4