[TYPO] Set the right category for the Point Of Sale
[odoo/odoo.git] / addons / audittrail / audittrail.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from osv import fields, osv
23 from osv.osv import object_proxy
24 from tools.translate import _
25 import pooler
26 import time
27 import tools
28
29 class audittrail_rule(osv.osv):
30     """
31     For Auddittrail Rule
32     """
33     _name = 'audittrail.rule'
34     _description = "Audittrail Rule"
35     _columns = {
36         "name": fields.char("Rule Name", size=32, required=True),
37         "object_id": fields.many2one('ir.model', 'Object', required=True, help="Select object for which you want to generate log."),
38         "user_id": fields.many2many('res.users', 'audittail_rules_users',
39                                             'user_id', 'rule_id', 'Users', help="if  User is not added then it will applicable for all users"),
40         "log_read": fields.boolean("Log Reads", help="Select this if you want to keep track of read/open on any record of the object of this rule"),
41         "log_write": fields.boolean("Log Writes", help="Select this if you want to keep track of modification on any record of the object of this rule"),
42         "log_unlink": fields.boolean("Log Deletes", help="Select this if you want to keep track of deletion on any record of the object of this rule"),
43         "log_create": fields.boolean("Log Creates",help="Select this if you want to keep track of creation on any record of the object of this rule"),
44         "log_action": fields.boolean("Log Action",help="Select this if you want to keep track of actions on the object of this rule"),
45         "log_workflow": fields.boolean("Log Workflow",help="Select this if you want to keep track of workflow on any record of the object of this rule"),
46         "state": fields.selection((("draft", "Draft"), ("subscribed", "Subscribed")), "Status", required=True),
47         "action_id": fields.many2one('ir.actions.act_window', "Action ID"),
48     }
49     _defaults = {
50         'state': 'draft',
51         'log_create': 1,
52         'log_unlink': 1,
53         'log_write': 1,
54     }
55     _sql_constraints = [
56         ('model_uniq', 'unique (object_id)', """There is already a rule defined on this object\n You cannot define another: please edit the existing one.""")
57     ]
58     __functions = {}
59
60     def subscribe(self, cr, uid, ids, *args):
61         """
62         Subscribe Rule for auditing changes on object and apply shortcut for logs on that object.
63         @param cr: the current row, from the database cursor,
64         @param uid: the current user’s ID for security checks,
65         @param ids: List of Auddittrail Rule’s IDs.
66         @return: True
67         """
68         obj_action = self.pool.get('ir.actions.act_window')
69         obj_model = self.pool.get('ir.model.data')
70         #start Loop
71         for thisrule in self.browse(cr, uid, ids):
72             obj = self.pool.get(thisrule.object_id.model)
73             if not obj:
74                 raise osv.except_osv(
75                         _('WARNING: audittrail is not part of the pool'),
76                         _('Change audittrail depends -- Setting rule as DRAFT'))
77                 self.write(cr, uid, [thisrule.id], {"state": "draft"})
78             val = {
79                  "name": 'View Log',
80                  "res_model": 'audittrail.log',
81                  "src_model": thisrule.object_id.model,
82                  "domain": "[('object_id','=', " + str(thisrule.object_id.id) + "), ('res_id', '=', active_id)]"
83
84             }
85             action_id = obj_action.create(cr, uid, val)
86             self.write(cr, uid, [thisrule.id], {"state": "subscribed", "action_id": action_id})
87             keyword = 'client_action_relate'
88             value = 'ir.actions.act_window,' + str(action_id)
89             res = obj_model.ir_set(cr, uid, 'action', keyword, 'View_log_' + thisrule.object_id.model, [thisrule.object_id.model], value, replace=True, isobject=True, xml_id=False)
90             #End Loop
91         return True
92
93     def unsubscribe(self, cr, uid, ids, *args):
94         """
95         Unsubscribe Auditing Rule on object
96         @param cr: the current row, from the database cursor,
97         @param uid: the current user’s ID for security checks,
98         @param ids: List of Auddittrail Rule’s IDs.
99         @return: True
100         """
101         obj_action = self.pool.get('ir.actions.act_window')
102         ir_values_obj = self.pool.get('ir.values')
103         value=''
104         #start Loop
105         for thisrule in self.browse(cr, uid, ids):
106             if thisrule.id in self.__functions:
107                 for function in self.__functions[thisrule.id]:
108                     setattr(function[0], function[1], function[2])
109             w_id = obj_action.search(cr, uid, [('name', '=', 'View Log'), ('res_model', '=', 'audittrail.log'), ('src_model', '=', thisrule.object_id.model)])
110             if w_id:
111                 obj_action.unlink(cr, uid, w_id)
112                 value = "ir.actions.act_window" + ',' + str(w_id[0])
113             val_id = ir_values_obj.search(cr, uid, [('model', '=', thisrule.object_id.model), ('value', '=', value)])
114             if val_id:
115                 ir_values_obj = pooler.get_pool(cr.dbname).get('ir.values')
116                 res = ir_values_obj.unlink(cr, uid, [val_id[0]])
117             self.write(cr, uid, [thisrule.id], {"state": "draft"})
118         #End Loop
119         return True
120
121 class audittrail_log(osv.osv):
122     """
123     For Audittrail Log
124     """
125     _name = 'audittrail.log'
126     _description = "Audittrail Log"
127
128     def _name_get_resname(self, cr, uid, ids, *args):
129         data = {}
130         for resname in self.browse(cr, uid, ids,[]):
131             model_object = resname.object_id
132             res_id = resname.res_id
133             if model_object and res_id:
134                 model_pool = self.pool.get(model_object.model)
135                 res = model_pool.read(cr, uid, res_id, ['name'])
136                 data[resname.id] = res['name']
137             else:
138                  data[resname.id] = False
139         return data
140
141     _columns = {
142         "name": fields.char("Resource Name",size=64),
143         "object_id": fields.many2one('ir.model', 'Object'),
144         "user_id": fields.many2one('res.users', 'User'),
145         "method": fields.char("Method", size=64),
146         "timestamp": fields.datetime("Date"),
147         "res_id": fields.integer('Resource Id'),
148         "line_ids": fields.one2many('audittrail.log.line', 'log_id', 'Log lines'),
149     }
150
151     _defaults = {
152         "timestamp": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S")
153     }
154     _order = "timestamp desc"
155
156 class audittrail_log_line(osv.osv):
157     """
158     Audittrail Log Line.
159     """
160     _name = 'audittrail.log.line'
161     _description = "Log Line"
162     _columns = {
163           'field_id': fields.many2one('ir.model.fields', 'Fields', required=True),
164           'log_id': fields.many2one('audittrail.log', 'Log'),
165           'log': fields.integer("Log ID"),
166           'old_value': fields.text("Old Value"),
167           'new_value': fields.text("New Value"),
168           'old_value_text': fields.text('Old value Text'),
169           'new_value_text': fields.text('New value Text'),
170           'field_description': fields.char('Field Description', size=64),
171         }
172
173 class audittrail_objects_proxy(object_proxy):
174     """ Uses Object proxy for auditing changes on object of subscribed Rules"""
175
176     def get_value_text(self, cr, uid, pool, resource_pool, method, field, value):
177         """
178         Gets textual values for the fields.
179             If the field is a many2one, it returns the name.
180             If it's a one2many or a many2many, it returns a list of name.
181             In other cases, it just returns the value.
182         :param cr: the current row, from the database cursor,
183         :param uid: the current user’s ID for security checks,
184         :param pool: current db's pooler object.
185         :param resource_pool: pooler object of the model which values are being changed.
186         :param field: for which the text value is to be returned.
187         :param value: value of the field.
188         :param recursive: True or False, True will repeat the process recursively
189         :return: string value or a list of values(for O2M/M2M)
190         """
191
192         field_obj = (resource_pool._all_columns.get(field)).column
193         if field_obj._type in ('one2many','many2many'):
194             data = pool.get(field_obj._obj).name_get(cr, uid, value)
195             #return the modifications on x2many fields as a list of names
196             res = map(lambda x:x[1], data)
197         elif field_obj._type == 'many2one':
198             #return the modifications on a many2one field as its value returned by name_get()
199             res = value and value[1] or value
200         else:
201             res = value
202         return res
203
204     def create_log_line(self, cr, uid, log_id, model, lines=[]):
205         """
206         Creates lines for changed fields with its old and new values
207
208         @param cr: the current row, from the database cursor,
209         @param uid: the current user’s ID for security checks,
210         @param model: Object which values are being changed
211         @param lines: List of values for line is to be created
212         """
213         pool = pooler.get_pool(cr.dbname)
214         obj_pool = pool.get(model.model)
215         model_pool = pool.get('ir.model')
216         field_pool = pool.get('ir.model.fields')
217         log_line_pool = pool.get('audittrail.log.line')
218         for line in lines:
219             field_obj = obj_pool._all_columns.get(line['name'])
220             assert field_obj, _("'%s' field does not exist in '%s' model" %(line['name'], model.model))
221             field_obj = field_obj.column
222             old_value = line.get('old_value', '')
223             new_value = line.get('new_value', '')
224             search_models = [model.id]
225             if obj_pool._inherits:
226                 search_models += model_pool.search(cr, uid, [('model', 'in', obj_pool._inherits.keys())])
227             field_id = field_pool.search(cr, uid, [('name', '=', line['name']), ('model_id', 'in', search_models)])
228             if field_obj._type == 'many2one':
229                 old_value = old_value and old_value[0] or old_value
230                 new_value = new_value and new_value[0] or new_value
231             vals = {
232                     "log_id": log_id,
233                     "field_id": field_id and field_id[0] or False,
234                     "old_value": old_value,
235                     "new_value": new_value,
236                     "old_value_text": line.get('old_value_text', ''),
237                     "new_value_text": line.get('new_value_text', ''),
238                     "field_description": field_obj.string
239                     }
240             line_id = log_line_pool.create(cr, uid, vals)
241         return True
242
243     def log_fct(self, cr, uid_orig, model, method, fct_src, *args, **kw):
244         """
245         Logging function: This function is performing the logging operation
246         @param model: Object whose values are being changed
247         @param method: method to log: create, read, write, unlink, action or workflow action
248         @param fct_src: execute method of Object proxy
249
250         @return: Returns result as per method of Object proxy
251         """
252         pool = pooler.get_pool(cr.dbname)
253         resource_pool = pool.get(model)
254         model_pool = pool.get('ir.model')
255         model_ids = model_pool.search(cr, 1, [('model', '=', model)])
256         model_id = model_ids and model_ids[0] or False
257         assert model_id, _("'%s' Model does not exist..." %(model))
258         model = model_pool.browse(cr, 1, model_id)
259
260         # fields to log. currently only used by log on read()
261         field_list = []
262         old_values = new_values = {}
263
264         if method == 'create':
265             res = fct_src(cr, uid_orig, model.model, method, *args, **kw)
266             if res:
267                 res_ids = [res]
268                 new_values = self.get_data(cr, uid_orig, pool, res_ids, model, method)
269         elif method == 'read':
270             res = fct_src(cr, uid_orig, model.model, method, *args, **kw)
271             # build the res_ids and the old_values dict. Here we don't use get_data() to
272             # avoid performing an additional read()
273             res_ids = []
274             for record in res:
275                 res_ids.append(record['id'])
276                 old_values[(model.id, record['id'])] = {'value': record, 'text': record}
277             # log only the fields read
278             field_list = args[1]
279         elif method == 'unlink':
280             res_ids = args[0]
281             old_values = self.get_data(cr, uid_orig, pool, res_ids, model, method)
282             res = fct_src(cr, uid_orig, model.model, method, *args, **kw)
283         else: # method is write, action or workflow action
284             res_ids = []
285             if args:
286                 res_ids = args[0]
287                 if isinstance(res_ids, (long, int)):
288                     res_ids = [res_ids]
289             if res_ids:
290                 # store the old values into a dictionary
291                 old_values = self.get_data(cr, uid_orig, pool, res_ids, model, method)
292             # process the original function, workflow trigger...
293             res = fct_src(cr, uid_orig, model.model, method, *args, **kw)
294             if method == 'copy':
295                 res_ids = [res]
296             if res_ids:
297                 # check the new values and store them into a dictionary
298                 new_values = self.get_data(cr, uid_orig, pool, res_ids, model, method)
299         # compare the old and new values and create audittrail log if needed
300         self.process_data(cr, uid_orig, pool, res_ids, model, method, old_values, new_values, field_list)
301         return res
302
303     def get_data(self, cr, uid, pool, res_ids, model, method):
304         """
305         This function simply read all the fields of the given res_ids, and also recurisvely on
306         all records of a x2m fields read that need to be logged. Then it returns the result in
307         convenient structure that will be used as comparison basis.
308
309             :param cr: the current row, from the database cursor,
310             :param uid: the current user’s ID. This parameter is currently not used as every
311                 operation to get data is made as super admin. Though, it could be usefull later.
312             :param pool: current db's pooler object.
313             :param res_ids: Id's of resource to be logged/compared.
314             :param model: Object whose values are being changed
315             :param method: method to log: create, read, unlink, write, actions, workflow actions
316             :return: dict mapping a tuple (model_id, resource_id) with its value and textual value
317                 { (model_id, resource_id): { 'value': ...
318                                              'textual_value': ...
319                                            },
320                 }
321         """
322         data = {}
323         resource_pool = pool.get(model.model)
324         # read all the fields of the given resources in super admin mode
325         for resource in resource_pool.read(cr, 1, res_ids):
326             values = {}
327             values_text = {}
328             resource_id = resource['id']
329             # loop on each field on the res_ids we just have read
330             for field in resource:
331                 if field in ('__last_update', 'id'):
332                     continue
333                 values[field] = resource[field]
334                 # get the textual value of that field for this record
335                 values_text[field] = self.get_value_text(cr, 1, pool, resource_pool, method, field, resource[field])
336
337                 field_obj = resource_pool._all_columns.get(field).column
338                 if field_obj._type in ('one2many','many2many'):
339                     # check if an audittrail rule apply in super admin mode
340                     if self.check_rules(cr, 1, field_obj._obj, method):
341                         # check if the model associated to a *2m field exists, in super admin mode
342                         x2m_model_ids = pool.get('ir.model').search(cr, 1, [('model', '=', field_obj._obj)])
343                         x2m_model_id = x2m_model_ids and x2m_model_ids[0] or False
344                         assert x2m_model_id, _("'%s' Model does not exist..." %(field_obj._obj))
345                         x2m_model = pool.get('ir.model').browse(cr, 1, x2m_model_id)
346                         #recursive call on x2m fields that need to be checked too
347                         data.update(self.get_data(cr, 1, pool, resource[field], x2m_model, method))
348             data[(model.id, resource_id)] = {'text':values_text, 'value': values}
349         return data
350
351     def prepare_audittrail_log_line(self, cr, uid, pool, model, resource_id, method, old_values, new_values, field_list=[]):
352         """
353         This function compares the old data (i.e before the method was executed) and the new data 
354         (after the method was executed) and returns a structure with all the needed information to
355         log those differences.
356
357         :param cr: the current row, from the database cursor,
358         :param uid: the current user’s ID. This parameter is currently not used as every
359             operation to get data is made as super admin. Though, it could be usefull later.
360         :param pool: current db's pooler object.
361         :param model: model object which values are being changed
362         :param resource_id: ID of record to which values are being changed
363         :param method: method to log: create, read, unlink, write, actions, workflow actions
364         :param old_values: dict of values read before execution of the method
365         :param new_values: dict of values read after execution of the method
366         :param field_list: optional argument containing the list of fields to log. Currently only
367             used when performing a read, it could be usefull later on if we want to log the write
368             on specific fields only.
369
370         :return: dictionary with
371             * keys: tuples build as ID of model object to log and ID of resource to log
372             * values: list of all the changes in field values for this couple (model, resource)
373               return {
374                 (model.id, resource_id): []
375               }
376
377         The reason why the structure returned is build as above is because when modifying an existing 
378         record, we may have to log a change done in a x2many field of that object
379         """
380         key = (model.id, resource_id)
381         lines = {
382             key: []
383         }
384         # loop on all the fields
385         for field_name, field_definition in pool.get(model.model)._all_columns.items():
386             #if the field_list param is given, skip all the fields not in that list
387             if field_list and field_name not in field_list:
388                 continue
389             field_obj = field_definition.column
390             if field_obj._type in ('one2many','many2many'):
391                 # checking if an audittrail rule apply in super admin mode
392                 if self.check_rules(cr, 1, field_obj._obj, method):
393                     # checking if the model associated to a *2m field exists, in super admin mode
394                     x2m_model_ids = pool.get('ir.model').search(cr, 1, [('model', '=', field_obj._obj)])
395                     x2m_model_id = x2m_model_ids and x2m_model_ids[0] or False
396                     assert x2m_model_id, _("'%s' Model does not exist..." %(field_obj._obj))
397                     x2m_model = pool.get('ir.model').browse(cr, 1, x2m_model_id)
398                     # the resource_ids that need to be checked are the sum of both old and previous values (because we
399                     # need to log also creation or deletion in those lists).
400                     x2m_old_values_ids = old_values.get(key, {'value': {}})['value'].get(field_name, [])
401                     x2m_new_values_ids = new_values.get(key, {'value': {}})['value'].get(field_name, [])
402                     # We use list(set(...)) to remove duplicates.
403                     res_ids = list(set(x2m_old_values_ids + x2m_new_values_ids))
404                     for res_id in res_ids:
405                         lines.update(self.prepare_audittrail_log_line(cr, 1, pool, x2m_model, res_id, method, old_values, new_values, field_list))
406             # if the value value is different than the old value: record the change
407             if key not in old_values or key not in new_values or old_values[key]['value'][field_name] != new_values[key]['value'][field_name]:
408                 data = {
409                       'name': field_name,
410                       'new_value': key in new_values and new_values[key]['value'].get(field_name),
411                       'old_value': key in old_values and old_values[key]['value'].get(field_name),
412                       'new_value_text': key in new_values and new_values[key]['text'].get(field_name),
413                       'old_value_text': key in old_values and old_values[key]['text'].get(field_name)
414                 }
415                 lines[key].append(data)
416         return lines
417
418     def process_data(self, cr, uid, pool, res_ids, model, method, old_values={}, new_values={}, field_list=[]):
419         """
420         This function processes and iterates recursively to log the difference between the old
421         data (i.e before the method was executed) and the new data and creates audittrail log
422         accordingly.
423
424         :param cr: the current row, from the database cursor,
425         :param uid: the current user’s ID,
426         :param pool: current db's pooler object.
427         :param res_ids: Id's of resource to be logged/compared.
428         :param model: model object which values are being changed
429         :param method: method to log: create, read, unlink, write, actions, workflow actions
430         :param old_values: dict of values read before execution of the method
431         :param new_values: dict of values read after execution of the method
432         :param field_list: optional argument containing the list of fields to log. Currently only
433             used when performing a read, it could be usefull later on if we want to log the write
434             on specific fields only.
435         :return: True
436         """
437         # loop on all the given ids
438         for res_id in res_ids:
439             # compare old and new values and get audittrail log lines accordingly
440             lines = self.prepare_audittrail_log_line(cr, uid, pool, model, res_id, method, old_values, new_values, field_list)
441
442             # if at least one modification has been found
443             for model_id, resource_id in lines:
444                 vals = {
445                     'method': method,
446                     'object_id': model_id,
447                     'user_id': uid,
448                     'res_id': resource_id,
449                 }
450                 if (model_id, resource_id) not in old_values and method not in ('copy', 'read'):
451                     # the resource was not existing so we are forcing the method to 'create'
452                     # (because it could also come with the value 'write' if we are creating
453                     #  new record through a one2many field)
454                     vals.update({'method': 'create'})
455                 if (model_id, resource_id) not in new_values and method not in ('copy', 'read'):
456                     # the resource is not existing anymore so we are forcing the method to 'unlink'
457                     # (because it could also come with the value 'write' if we are deleting the
458                     #  record through a one2many field)
459                     vals.update({'method': 'unlink'})
460                 # create the audittrail log in super admin mode, only if a change has been detected
461                 if lines[(model_id, resource_id)]:
462                     log_id = pool.get('audittrail.log').create(cr, 1, vals)
463                     model = pool.get('ir.model').browse(cr, uid, model_id)
464                     self.create_log_line(cr, 1, log_id, model, lines[(model_id, resource_id)])
465         return True
466
467     def check_rules(self, cr, uid, model, method):
468         """
469         Checks if auditrails is installed for that db and then if one rule match
470         @param cr: the current row, from the database cursor,
471         @param uid: the current user’s ID,
472         @param model: value of _name of the object which values are being changed
473         @param method: method to log: create, read, unlink,write,actions,workflow actions
474         @return: True or False
475         """
476         pool = pooler.get_pool(cr.dbname)
477         if 'audittrail.rule' in pool.models:
478             model_ids = pool.get('ir.model').search(cr, 1, [('model', '=', model)])
479             model_id = model_ids and model_ids[0] or False
480             if model_id:
481                 rule_ids = pool.get('audittrail.rule').search(cr, 1, [('object_id', '=', model_id), ('state', '=', 'subscribed')])
482                 for rule in pool.get('audittrail.rule').read(cr, 1, rule_ids, ['user_id','log_read','log_write','log_create','log_unlink','log_action','log_workflow']):
483                     if len(rule['user_id']) == 0 or uid in rule['user_id']:
484                         if rule.get('log_'+method,0):
485                             return True
486                         elif method not in ('default_get','read','fields_view_get','fields_get','search','search_count','name_search','name_get','get','request_get', 'get_sc', 'unlink', 'write', 'create'):
487                             if rule['log_action']:
488                                 return True
489
490     def execute_cr(self, cr, uid, model, method, *args, **kw):
491         fct_src = super(audittrail_objects_proxy, self).execute_cr
492         if self.check_rules(cr,uid,model,method):
493             return self.log_fct(cr, uid, model, method, fct_src, *args, **kw)
494         return fct_src(cr, uid, model, method, *args, **kw)
495
496     def exec_workflow_cr(self, cr, uid, model, method, *args, **kw):
497         fct_src = super(audittrail_objects_proxy, self).exec_workflow_cr
498         if self.check_rules(cr,uid,model,'workflow'):
499             return self.log_fct(cr, uid, model, method, fct_src, *args, **kw)
500         return fct_src(cr, uid, model, method, *args, **kw)
501
502 audittrail_objects_proxy()
503
504 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
505