diff -r f168aad30599 trytond/osv/fields.py
--- a/trytond/osv/fields.py	Fri Dec 05 18:30:54 2008 +0100
+++ b/trytond/osv/fields.py	Sun Dec 07 18:20:48 2008 +0100
@@ -287,33 +287,57 @@
     _symbol_f = lambda x: x and int(x) or None
     _symbol_set = (_symbol_c, _symbol_f)
 
-    def __init__(self, obj, string='unknown', left=None, right=None, **args):
+    def __init__(self, obj, string='unknown', left=None, right=None,
+            datetime_field=None, **args):
         Column.__init__(self, string=string, **args)
         self._obj = obj
         self.left = left
         self.right = right
+        self.datetime_field = datetime_field
 
     def get(self, cursor, obj, ids, name, user=None, offset=0, context=None,
             values=None):
         if values is None:
             values = {}
         res = {}
+        ctx_date = {}
         for i in values:
             res[i['id']] = i[name]
+            if self.datetime_field:
+                ctx_date[i['id']] = i.get(self.datetime_field)
+
+        if self.datetime_field:
+            missing_ids = [x for x, y in ctx_date.iteritems() if not y]
+            if missing_ids:
+                for obj_id, value in obj.read(cursor, user, missing_ids,
+                        [self.datetime_field], context=context):
+                    ctx_date[obj_id] = value
+
         for i in ids:
             res.setdefault(i, '')
+
         obj = obj.pool.get(self._obj)
-        obj_names = {}
-        for obj_id, name in obj.name_get(cursor, user,
-                [ x for x in res.values() if x],
-                context=context):
-            obj_names[obj_id] = name
+        if self.datetime_field:
+            for i in res.keys():
+                if res[i]:
+                    ctx = context.copy()
+                    ctx['_datetime'] = ctx_date[i]
+                    _, name = obj.name_get(cursor, user, res[i], context=ctx)[0]
+                    res[i] = (res[i], name)
+                else:
+                    res[i] = False
+        else:
+            obj_names = {}
+            for obj_id, name in obj.name_get(cursor, user,
+                    [x for x in res.values() if x],
+                    context=context):
+                obj_names[obj_id] = name
 
-        for i in res.keys():
-            if res[i] and res[i] in obj_names:
-                res[i] = (res[i], obj_names[res[i]])
-            else:
-                res[i] = False
+            for i in res.keys():
+                if res[i] and res[i] in obj_names:
+                    res[i] = (res[i], obj_names[res[i]])
+                else:
+                    res[i] = False
         return res
 
     def set(self, cursor, obj_src, obj_id, field, values, user=None,
@@ -395,7 +419,6 @@
     def set(self, cursor, obj, obj_id, field, values, user=None, context=None):
         if not values:
             return
-        _table = obj.pool.get(self._obj)._table
         obj = obj.pool.get(self._obj)
         for act in values:
             if act[0] == 'create':
diff -r f168aad30599 trytond/osv/orm.py
--- a/trytond/osv/orm.py	Fri Dec 05 18:30:54 2008 +0100
+++ b/trytond/osv/orm.py	Sun Dec 07 18:20:48 2008 +0100
@@ -10,7 +10,7 @@
 import datetime
 from lxml import etree
 import copy
-from trytond.sql_db import table_handler
+from trytond.sql_db import TableHandler
 from decimal import Decimal
 import logging
 
@@ -95,7 +95,7 @@
         self._id = object_id
         self._table = table
         self._table_name = self._table._name
-        self._context = context
+        self._context = context or {}
         self._language_cache = {}
 
         cache.setdefault(table._name, {})
@@ -164,9 +164,11 @@
                         if ids2 is False:
                             data[i] = BrowseRecordNull()
                         else:
+                            ctx = self._context.copy()
+                            if j.datetime_field:
+                                ctx['_datetime'] = data[j.datetime_field]
                             data[i] = BrowseRecord(self._cursor, self._user,
-                                    ids2, obj, self._cache,
-                                    context=self._context)
+                                    ids2, obj, self._cache, context=ctx)
                     elif j._type in ('one2many', 'many2many') and len(data[i]):
                         data[i] = BrowseRecordList([BrowseRecord(self._cursor,
                             self._user, x, obj,
@@ -306,6 +308,7 @@
     pool = None
     __columns = None
     __defaults = None
+    _history_table = None
 
     def _reset_columns(self):
         self.__columns = None
@@ -490,7 +493,10 @@
             # No db table for this object.
             return
 
-        table = table_handler(cursor, self._table, self._name, module_name)
+        table = TableHandler(cursor, self._table, self._name, module_name)
+        if self._history_table:
+            history_table = TableHandler(cursor, self._history_table, None,
+                    module_name, history=True)
         logs = (
             ('create_date', 'timestamp', 'TIMESTAMP',
                 fields.DateTime._symbol_set, lambda *a: datetime.datetime.now()),
@@ -506,6 +512,21 @@
         for log in logs:
             table.add_raw_column(log[0], (log[1], log[2]), log[3],
                     default_fun=log[4], migrate=False)
+        if self._history_table:
+            history_logs = (
+                    ('create_date', 'timestamp', 'TIMESTAMP',
+                        fields.DateTime._symbol_set),
+                    ('write_date', 'timestamp', 'TIMESTAMP',
+                        fields.DateTime._symbol_set),
+                    ('create_uid', 'int4', 'INTEGER',
+                        fields.Integer._symbol_set),
+                    ('write_uid', 'int4', 'INTEGER',
+                        fields.Integer._symbol_set),
+                    )
+            for log in history_logs:
+                history_table.add_raw_column(log[0], (log[1], log[2]), log[3],
+                        migrate=False)
+            history_table.index_action('id', action='add')
         for field_name, field in self._columns.iteritems():
             default_fun = None
             if field_name in (
@@ -536,6 +557,9 @@
 
                 table.add_raw_column(field_name, field.sql_type(),
                         field._symbol_set, default_fun, field.size)
+                if self._history_table:
+                    history_table.add_raw_column(field_name, field.sql_type(),
+                            field._symbol_set)
 
                 if isinstance(field, (fields.Integer, fields.Float)):
                     table.db_default(field_name, 0)
@@ -558,6 +582,7 @@
                     field_name, action=required and 'add' or 'remove')
 
             elif isinstance(field, fields.Many2Many):
+                #TODO handle history
                 if field._obj in ('res.user', 'res.group'):
                     ref = field._obj.replace('.','_')
                 else:
@@ -584,6 +609,18 @@
 
         for ident, constraint, msg in self._sql_constraints:
             table.add_constraint(ident, constraint)
+
+        if self._history_table:
+            cursor.execute('SELECT id FROM "' + self._table + '"')
+            if cursor.rowcount:
+                cursor.execute('SELECT id FROM "' + self._history_table + '"')
+                if not cursor.rowcount:
+                    columns = ['"' + str(x) + '"' for x in self._columns
+                            if self._columns[x]._classic_write]
+                    cursor.execute('INSERT INTO "' + self._history_table + '" ' \
+                            '(' + ','.join(columns) + ') ' \
+                            'SELECT ' + ','.join(columns) + \
+                            ' FROM "' + self._table + '"')
 
     def __init__(self):
         self._rpc_allowed = [
@@ -1063,9 +1100,22 @@
         res = []
         table_query = ''
         table_args = []
+        order_clause = ','.join([self._table + '.' + x[0] + ' ' + x[1]
+            for x in self._order])
+        history_clause = ''
+        history_limit = ''
+        history_args = []
         if self.table_query(context):
             table_query, table_args = self.table_query(context)
             table_query = '(' + table_query + ') AS '
+        in_max = cursor.IN_MAX
+        if self._history_table and context.get('_datetime'):
+            in_max = 1
+            table_query = self._history_table + ' AS '
+            history_clause = ' AND (COALESCE(write_date, create_date) <= %s)'
+            order_clause = ' COALESCE(write_date, create_date) DESC'
+            history_limit = ' LIMIT 1'
+            history_args = [context['_datetime']]
         if len(fields_pre) :
             fields_pre2 = [(x in ('create_date', 'write_date')) \
                     and ('date_trunc(\'second\', ' + x + ') as ' + x) \
@@ -1073,39 +1123,42 @@
                     if x != '_timestamp']
             if '_timestamp' in fields_pre:
                 if not self.table_query(context):
-                    fields_pre2 += ['(CASE WHEN write_date IS NOT NULL ' \
-                            'THEN write_date ELSE create_date END) ' \
+                    fields_pre2 += ['COALESCE(write_date, create_date) ' \
                             'AS _timestamp']
                 else:
                     fields_pre2 += ['now()::timestamp AS _timestamp']
 
             if len(ids) > cursor.IN_MAX:
                 cursor.execute('SELECT id ' \
+                        'COALESCE(write_date, create_date) ' \
                         'FROM ' + table_query + '"' + self._table + '" ' \
+                        'WHERE true ' + history_clause + ' ' \
                         'ORDER BY ' + \
                         ','.join([self._table + '.' + x[0] + ' ' + x[1]
-                            for x in self._order]), table_args)
-
+                            for x in self._order]), table_args + history_args)
                 i = 0
                 ids_sorted = {}
                 for row in cursor.fetchall():
-                    ids_sorted[row[0]] = i
+                    if row[0] in ids_sorted:
+                        if row[1] <= ids_sorted[row[0]][1]:
+                            continue
+                    ids_sorted[row[0]] = (i, row[1])
                     i += 1
                 ids = ids[:]
-                ids.sort(lambda x, y: ids_sorted[x] - ids_sorted[y])
+                ids.sort(lambda x, y: ids_sorted[x][0] - ids_sorted[y][0])
 
             for i in range(0, len(ids), cursor.IN_MAX):
                 sub_ids = ids[i:i + cursor.IN_MAX]
                 if domain1:
-                    cursor.execute(('SELECT ' + \
+                    cursor.execute('SELECT ' + \
                             ','.join(fields_pre2 + ['id']) + \
                             ' FROM ' + table_query + '\"' + self._table +'\" ' \
                             'WHERE id IN ' \
-                                '(' + ','.join(['%s' for x in sub_ids]) + ')'\
-                            ' AND (' + domain1 + ') ORDER BY ' + \
-                            ','.join([self._table + '.' + x[0] + ' ' + x[1] \
-                            for x in self._order])),
-                            table_args + sub_ids + domain2)
+                                '(' + ','.join(['%s' for x in sub_ids]) + ')' + \
+                            history_clause + \
+                            ' AND (' + domain1 + ') ORDER BY ' + order_clause + \
+                            history_limit,
+                            table_args + sub_ids + history_args + domain2)
                     if not cursor.rowcount == len({}.fromkeys(sub_ids)):
                         raise Exception('AccessError',
                                 'You try to bypass an access rule ' \
@@ -1116,10 +1169,11 @@
                             ','.join(fields_pre2 + ['id']) + \
                             ' FROM ' + table_query + '\"' + self._table + '\" ' \
                             'WHERE id IN ' \
-                                '(' + ','.join(['%s' for x in sub_ids]) + ')'\
-                            ' ORDER BY ' + \
-                            ','.join([self._table + '.' + x[0] + ' ' + x[1] \
-                            for x in self._order]), table_args + sub_ids)
+                                '(' + ','.join(['%s' for x in sub_ids]) + ')' + \
+                            history_clause + \
+                            ' ORDER BY ' + order_clause + \
+                            history_limit,
+                            table_args + sub_ids + history_args)
                 res.extend(cursor.dictfetchall())
         else:
             res = [{'id': x} for x in ids]
@@ -1506,6 +1560,11 @@
             else:
                 cursor.execute('DELETE FROM "'+self._table+'" ' \
                         'WHERE id IN (' + str_d + ')', sub_ids)
+        if self._history_table:
+            for obj_id in ids:
+                cursor.execute('INSERT INTO "' + self._history_table + '" ' \
+                        '(id, write_uid, write_date) VALUES (%s, %s, now())',
+                        (obj_id, user))
 
         for k in tree_ids.keys():
             field = self._columns[k]
@@ -1612,8 +1671,8 @@
                 if self._columns[field]._classic_write:
                     if (not self._columns[field].translate) \
                             or context.get('language', 'en_US') == 'en_US':
-                        upd0.append('"' + field + '"=' + \
-                                self._columns[field]._symbol_set[0])
+                        upd0.append(('"' + field + '"',
+                                self._columns[field]._symbol_set[0]))
                         upd1.append(self._columns[field]._symbol_set[1](
                             vals[field]))
                     direct.append(field)
@@ -1648,50 +1707,51 @@
                                   error_description='xml_record_desc',
                                   context=context)
 
-        upd0.append('write_uid = %s')
-        upd0.append('write_date = now()')
+        upd0.append(('write_uid', '%s'))
+        upd0.append(('write_date', 'now()'))
         upd1.append(user)
 
-        if len(upd0):
-            domain1, domain2 = self.pool.get('ir.rule').domain_get(cursor,
-                    user, self._name, context=context)
+        domain1, domain2 = self.pool.get('ir.rule').domain_get(cursor,
+                user, self._name)
+        if domain1:
+            domain1 = ' AND (' + domain1 + ') '
+        for i in range(0, len(ids), cursor.IN_MAX):
+            sub_ids = ids[i:i + cursor.IN_MAX]
+            ids_str = ','.join(['%s' for x in sub_ids])
             if domain1:
-                domain1 = ' AND (' + domain1 + ') '
-            for i in range(0, len(ids), cursor.IN_MAX):
-                sub_ids = ids[i:i + cursor.IN_MAX]
-                ids_str = ','.join(['%s' for x in sub_ids])
-                if domain1:
-                    cursor.execute('SELECT id FROM "' + self._table + '" ' \
-                            'WHERE id IN (' + ids_str + ') ' + domain1,
-                            sub_ids + domain2)
-                    if not cursor.rowcount == len({}.fromkeys(sub_ids)):
-                        raise Exception('AccessError',
-                                'You try to bypass an access rule ' \
-                                        '(Document type: %s).' % \
-                                        self._description)
-                else:
-                    cursor.execute('SELECT id FROM "' + self._table + '" ' \
-                            'WHERE id IN (' + ids_str + ')', sub_ids)
-                    if not cursor.rowcount == len({}.fromkeys(sub_ids)):
-                        raise Exception('AccessError',
-                                'You try to bypass an access rule ' \
-                                        '(Document type: %s).' % \
-                                        self._description)
-                if domain1:
-                    cursor.execute('UPDATE "' + self._table + '" ' \
-                            'SET ' + ','.join(upd0) + ' ' \
-                            'WHERE id IN (' + ids_str + ') ' + domain1,
-                            upd1 + sub_ids + domain2)
-                else:
-                    cursor.execute('UPDATE "' + self._table + '" ' \
-                            'SET ' + ','.join(upd0) + ' ' \
-                            'WHERE id IN (' + ids_str + ') ', upd1 + sub_ids)
+                cursor.execute('SELECT id FROM "' + self._table + '" ' \
+                        'WHERE id IN (' + ids_str + ') ' + domain1,
+                        sub_ids + domain2)
+                if not cursor.rowcount == len({}.fromkeys(sub_ids)):
+                    raise Exception('AccessError',
+                            'You try to bypass an access rule ' \
+                                    '(Document type: %s).' % \
+                                    self._description)
+            else:
+                cursor.execute('SELECT id FROM "' + self._table + '" ' \
+                        'WHERE id IN (' + ids_str + ')', sub_ids)
+                if not cursor.rowcount == len({}.fromkeys(sub_ids)):
+                    raise Exception('AccessError',
+                            'You try to bypass an access rule ' \
+                                    '(Document type: %s).' % \
+                                    self._description)
+            if domain1:
+                cursor.execute('UPDATE "' + self._table + '" ' \
+                        'SET ' + \
+                        ','.join([x[0] + ' = ' + x[1] for x in upd0]) + ' ' \
+                        'WHERE id IN (' + ids_str + ') ' + domain1,
+                        upd1 + sub_ids + domain2)
+            else:
+                cursor.execute('UPDATE "' + self._table + '" ' \
+                        'SET ' + \
+                        ','.join([x[0] + ' = ' + x[1] for x in upd0]) + ' ' \
+                        'WHERE id IN (' + ids_str + ') ', upd1 + sub_ids)
 
-            for field in direct:
-                if self._columns[field].translate:
-                    self.pool.get('ir.translation')._set_ids(cursor, user,
-                            self._name + ',' + field, 'model',
-                            context.get('language','en_US'), ids, vals[field])
+        for field in direct:
+            if self._columns[field].translate:
+                self.pool.get('ir.translation')._set_ids(cursor, user,
+                        self._name + ',' + field, 'model',
+                        context.get('language','en_US'), ids, vals[field])
 
         # call the 'set' method of fields which are not classic_write
         upd_todo.sort(lambda x, y: self._columns[x].priority - \
@@ -1700,6 +1760,16 @@
             for select_id in ids:
                 self._columns[field].set(cursor, self, select_id, field,
                         vals[field], user, context=context)
+
+        if self._history_table:
+            for obj_id in ids:
+                columns = ['"' + str(x) + '"' for x in self._columns
+                        if self._columns[x]._classic_write]
+                cursor.execute('INSERT INTO "' + self._history_table + '" ' \
+                        '(' + ','.join(columns) + ') '\
+                        'SELECT ' + ','.join(columns) + \
+                        ' FROM "' + self._table + '" ' \
+                        'WHERE id = %s', (obj_id,))
 
         for table in self._inherits:
             col = self._inherits[table]
@@ -1867,11 +1937,19 @@
         cursor.execute('INSERT INTO "' + self._table + '" ' \
                 '(id' + upd0 + ') ' \
                 'VALUES (' + str(id_new) + upd1 + ')', tuple(upd2))
+
         upd_todo.sort(lambda x, y: self._columns[x].priority - \
                 self._columns[y].priority)
         for field in upd_todo:
             self._columns[field].set(cursor, self, id_new, field, vals[field],
                     user=user, context=context)
+
+        if self._history_table:
+            cursor.execute('INSERT INTO "' + self._history_table + '" ' \
+                    '(id' + upd0 + ') ' \
+                    'SELECT id' + upd0 + ' ' \
+                    'FROM "' + self._table + '" ' \
+                    'WHERE id = %s', (id_new,))
 
         self._validate(cursor, user, [id_new], context=context)
 
@@ -2346,12 +2424,20 @@
         if self.table_query(context):
             table_query, table_args = self.table_query(context)
             table_query = '(' + table_query + ') AS '
+        if self._history_table and context.get('_datetime'):
+            table_query = self._history_table + ' AS '
 
         tables = [table_query + '"' + self._table + '"']
         tables_args = table_args
 
         qu1, qu2 = self.__where_calc_oper(cursor, user, args, tables,
                 tables_args, context=context)
+        if self._history_table and context.get('_datetime'):
+            if qu1:
+                qu1 += ' AND'
+            qu1 +=  ' (COALESCE(' + self._table + '.write_date, ' + \
+                    self._table + '.create_date) <= %s)'
+            qu2 += [context['_datetime']]
         return qu1, qu2, tables, tables_args
 
 
@@ -2919,6 +3005,9 @@
             the SQL query string and the arguments.
         :return: a list of ids
         '''
+        if context is None:
+            context = {}
+
         (qu1, qu2, tables, tables_args) = self._where_calc(cursor, user, args,
                 context=context)
 
@@ -2963,14 +3052,32 @@
             res = cursor.fetchall()
             return res[0][0]
         # execute the "main" query to fetch the ids we were searching for
-        query_str = 'SELECT %s.id FROM ' % self._table + \
+        select_field = self._table + '.id'
+        if self._history_table and context.get('_datetime') \
+                and not query_string:
+            select_field += ',  COALESCE(' + self._table + '.write_date, ' + \
+                    self._table + '.create_date)'
+        query_str = 'SELECT '  + select_field + ' FROM ' + \
                 ' '.join(tables) + ' WHERE ' + (qu1 or 'True') + \
                 ' ORDER BY ' + order_by + limit_str + offset_str
         if query_string:
             return (query_str, tables_args + qu2)
         cursor.execute(query_str, tables_args + qu2)
-        res = cursor.fetchall()
-        return [x[0] for x in res]
+        if self._history_table and context.get('_datetime'):
+            res = []
+            ids_date = {}
+            for row in cursor.fetchall():
+                if row[0] in ids_date:
+                    if row[1] <= ids_date[row[0]]:
+                        continue
+                if row[0] in res:
+                    res.remove(row[0])
+                res.append(row[0])
+                ids_date[row[0]] = row[1]
+            return res
+        else:
+            res = cursor.fetchall()
+            return [x[0] for x in res]
 
     def name_get(self, cursor, user, ids, context=None):
         '''
diff -r f168aad30599 trytond/sql_db.py
--- a/trytond/sql_db.py	Fri Dec 05 18:30:54 2008 +0100
+++ b/trytond/sql_db.py	Sun Dec 07 18:20:48 2008 +0100
@@ -218,9 +218,10 @@
     return bool(cursor.rowcount)
 
 
-class table_handler:
+class TableHandler:
 
-    def __init__(self, cursor, table_name, object_name=None, module_name=None):
+    def __init__(self, cursor, table_name, object_name=None, module_name=None,
+            history=False):
         self.table_name = table_name
         self.table = {}
         self.constraint = []
@@ -233,9 +234,13 @@
 
         # Create new table if necessary
         if not table_exist(self.cursor, self.table_name):
-            self.cursor.execute('CREATE TABLE "%s" ' \
-                             "(id SERIAL NOT NULL, " \
-                             "PRIMARY KEY(id))"% self.table_name)
+            if not history:
+                self.cursor.execute('CREATE TABLE "%s" ' \
+                                "(id SERIAL NOT NULL, " \
+                                "PRIMARY KEY(id))"% self.table_name)
+            else:
+                self.cursor.execute('CREATE TABLE "%s" ' \
+                        "(id INTEGER)" % self.table_name)
         self.update_definitions()
 
     def update_definitions(self):
@@ -394,9 +399,9 @@
             on_delete_from, on_delete_to):
         if not table_exist(self.cursor, other_table):
             raise Exception("table %s not found"%other_table)
-        rtable = table_handler(
+        rtable = TableHandler(
             self.cursor, relation_table, object_name=None,
-            module_name= self.module_name)
+            module_name=self.module_name)
         from osv.fields import Integer
         rtable.add_raw_column(rtable_from, ('int4', 'int4'), Integer._symbol_set)
         rtable.add_raw_column(rtable_to, ('int4', 'int4'), Integer._symbol_set)
