Merge remote-tracking branch 'upstream/7.0' into 7.0-travis
[odoo/odoo.git] / openerp / tools / test_reports.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2010-2012 OpenERP s.a. (<http://openerp.com>).
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 """ Helper functions for reports testing.
23
24     Please /do not/ import this file by default, but only explicitly call it
25     through the code of yaml tests.
26 """
27
28 import openerp.netsvc as netsvc
29 import openerp.tools as tools
30 import logging
31 import openerp.pooler as pooler
32 from openerp.tools.safe_eval import safe_eval
33 from subprocess import Popen, PIPE
34 import os
35 import tempfile
36
37 _logger = logging.getLogger(__name__)
38
39 def try_report(cr, uid, rname, ids, data=None, context=None, our_module=None):
40     """ Try to render a report <rname> with contents of ids
41     
42         This function should also check for common pitfalls of reports.
43     """
44     if data is None:
45         data = {}
46     if context is None:
47         context = {}
48     if rname.startswith('report.'):
49         rname_s = rname[7:]
50     else:
51         rname_s = rname
52     _logger.log(netsvc.logging.TEST, "  - Trying %s.create(%r)", rname, ids)
53     res = netsvc.LocalService(rname).create(cr, uid, ids, data, context)
54     if not isinstance(res, tuple):
55         raise RuntimeError("Result of %s.create() should be a (data,format) tuple, now it is a %s" % \
56                                 (rname, type(res)))
57     (res_data, res_format) = res
58
59     if not res_data:
60         raise ValueError("Report %s produced an empty result!" % rname)
61
62     if tools.config['test_report_directory']:
63         file(os.path.join(tools.config['test_report_directory'], rname+ '.'+res_format), 'wb+').write(res_data)
64
65     _logger.debug("Have a %s report for %s, will examine it", res_format, rname)
66     if res_format == 'pdf':
67         if res_data[:5] != '%PDF-':
68             raise ValueError("Report %s produced a non-pdf header, %r" % (rname, res_data[:10]))
69
70         res_text = False
71         try:
72             fd, rfname = tempfile.mkstemp(suffix=res_format)
73             os.write(fd, res_data)
74             os.close(fd)
75
76             proc = Popen(['pdftotext', '-enc', 'UTF-8', '-nopgbrk', rfname, '-'], shell=False, stdout=PIPE)
77             stdout, stderr = proc.communicate()
78             res_text = tools.ustr(stdout)
79             os.unlink(rfname)
80         except Exception:
81             _logger.debug("Unable to parse PDF report: install pdftotext to perform automated tests.")
82
83         if res_text is not False:
84             for line in res_text.split('\n'):
85                 if ('[[' in line) or ('[ [' in line):
86                     _logger.error("Report %s may have bad expression near: \"%s\".", rname, line[80:])
87             # TODO more checks, what else can be a sign of a faulty report?
88     elif res_format == 'foobar':
89         # TODO
90         pass
91     else:
92         _logger.warning("Report %s produced a \"%s\" chunk, cannot examine it", rname, res_format)
93         return False
94
95     _logger.log(netsvc.logging.TEST, "  + Report %s produced correctly.", rname)
96     return True
97
98 def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
99                 wiz_data=None, wiz_buttons=None,
100                 context=None, our_module=None):
101     """Take an ir.action.act_window and follow it until a report is produced
102
103         :param action_id: the integer id of an action, or a reference to xml id
104                 of the act_window (can search [our_module.]+xml_id
105         :param active_model, active_ids: call the action as if it had been launched
106                 from that model+ids (tree/form view action)
107         :param wiz_data: a dictionary of values to use in the wizard, if needed.
108                 They will override (or complete) the default values of the
109                 wizard form.
110         :param wiz_buttons: a list of button names, or button icon strings, which
111                 should be preferred to press during the wizard.
112                 Eg. 'OK' or 'gtk-print'
113         :param our_module: the name of the calling module (string), like 'account'
114     """
115
116     if not our_module and isinstance(action_id, basestring):
117         if '.' in action_id:
118             our_module = action_id.split('.', 1)[0]
119
120     if context is None:
121         context = {}
122     else:
123         context = context.copy() # keep it local
124     # TODO context fill-up
125
126     pool = pooler.get_pool(cr.dbname)
127
128     def log_test(msg, *args):
129         _logger.log(netsvc.logging.TEST, "  - " + msg, *args)
130
131     datas = {}
132     if active_model:
133         datas['model'] = active_model
134     if active_ids:
135         datas['ids'] = active_ids
136
137     if not wiz_buttons:
138         wiz_buttons = []
139
140     if isinstance(action_id, basestring):
141         if '.' in action_id:
142             act_module, act_xmlid = action_id.split('.', 1)
143         else:
144             if not our_module:
145                 raise ValueError('You cannot only specify action_id "%s" without a module name' % action_id)
146             act_module = our_module
147             act_xmlid = action_id
148         act_model, act_id = pool.get('ir.model.data').get_object_reference(cr, uid, act_module, act_xmlid)
149     else:
150         assert isinstance(action_id, (long, int))
151         act_model = 'ir.action.act_window'     # assume that
152         act_id = action_id
153         act_xmlid = '<%s>' % act_id
154
155     def _exec_action(action, datas, context):
156         # taken from client/modules/action/main.py:84 _exec_action()
157         if isinstance(action, bool) or 'type' not in action:
158             return
159         # Updating the context : Adding the context of action in order to use it on Views called from buttons
160         if datas.get('id',False):
161             context.update( {'active_id': datas.get('id',False), 'active_ids': datas.get('ids',[]), 'active_model': datas.get('model',False)})
162         context.update(safe_eval(action.get('context','{}'), context.copy()))
163         if action['type'] in ['ir.actions.act_window', 'ir.actions.submenu']:
164             for key in ('res_id', 'res_model', 'view_type', 'view_mode',
165                     'limit', 'auto_refresh', 'search_view', 'auto_search', 'search_view_id'):
166                 datas[key] = action.get(key, datas.get(key, None))
167
168             view_id = False
169             if action.get('views', []):
170                 if isinstance(action['views'],list):
171                     view_id = action['views'][0][0]
172                     datas['view_mode']= action['views'][0][1]
173                 else:
174                     if action.get('view_id', False):
175                         view_id = action['view_id'][0]
176             elif action.get('view_id', False):
177                 view_id = action['view_id'][0]
178
179             assert datas['res_model'], "Cannot use the view without a model"
180             # Here, we have a view that we need to emulate
181             log_test("will emulate a %s view: %s#%s",
182                         action['view_type'], datas['res_model'], view_id or '?')
183
184             view_res = pool.get(datas['res_model']).fields_view_get(cr, uid, view_id, action['view_type'], context)
185             assert view_res and view_res.get('arch'), "Did not return any arch for the view"
186             view_data = {}
187             if view_res.get('fields',{}).keys():
188                 view_data = pool.get(datas['res_model']).default_get(cr, uid, view_res['fields'].keys(), context)
189             if datas.get('form'):
190                 view_data.update(datas.get('form'))
191             if wiz_data:
192                 view_data.update(wiz_data)
193             _logger.debug("View data is: %r", view_data)
194
195             for fk, field in view_res.get('fields',{}).items():
196                 # Default fields returns list of int, while at create()
197                 # we need to send a [(6,0,[int,..])]
198                 if field['type'] in ('one2many', 'many2many') \
199                         and view_data.get(fk, False) \
200                         and isinstance(view_data[fk], list) \
201                         and not isinstance(view_data[fk][0], tuple) :
202                     view_data[fk] = [(6, 0, view_data[fk])]
203
204             action_name = action.get('name')
205             try:
206                 from xml.dom import minidom
207                 cancel_found = False
208                 buttons = []
209                 dom_doc = minidom.parseString(view_res['arch'])
210                 if not action_name:
211                     action_name = dom_doc.documentElement.getAttribute('name')
212
213                 for button in dom_doc.getElementsByTagName('button'):
214                     button_weight = 0
215                     if button.getAttribute('special') == 'cancel':
216                         cancel_found = True
217                         continue
218                     if button.getAttribute('icon') == 'gtk-cancel':
219                         cancel_found = True
220                         continue
221                     if button.getAttribute('default_focus') == '1':
222                         button_weight += 20
223                     if button.getAttribute('string') in wiz_buttons:
224                         button_weight += 30
225                     elif button.getAttribute('icon') in wiz_buttons:
226                         button_weight += 10
227                     string = button.getAttribute('string') or '?%s' % len(buttons)
228
229                     buttons.append( { 'name': button.getAttribute('name'),
230                                 'string': string,
231                                 'type': button.getAttribute('type'),
232                                 'weight': button_weight,
233                                 })
234             except Exception, e:
235                 _logger.warning("Cannot resolve the view arch and locate the buttons!", exc_info=True)
236                 raise AssertionError(e.args[0])
237
238             if not datas['res_id']:
239                 # it is probably an orm_memory object, we need to create
240                 # an instance
241                 datas['res_id'] = pool.get(datas['res_model']).create(cr, uid, view_data, context)
242
243             if not buttons:
244                 raise AssertionError("view form doesn't have any buttons to press!")
245
246             buttons.sort(key=lambda b: b['weight'])
247             _logger.debug('Buttons are: %s', ', '.join([ '%s: %d' % (b['string'], b['weight']) for b in buttons]))
248
249             res = None
250             while buttons and not res:
251                 b = buttons.pop()
252                 log_test("in the \"%s\" form, I will press the \"%s\" button.", action_name, b['string'])
253                 if not b['type']:
254                     log_test("the \"%s\" button has no type, cannot use it", b['string'])
255                     continue
256                 if b['type'] == 'object':
257                     #there we are! press the button!
258                     fn =  getattr(pool.get(datas['res_model']), b['name'])
259                     if not fn:
260                         _logger.error("The %s model doesn't have a %s attribute!", datas['res_model'], b['name'])
261                         continue
262                     res = fn(cr, uid, [datas['res_id'],], context)
263                     break
264                 else:
265                     _logger.warning("in the \"%s\" form, the \"%s\" button has unknown type %s",
266                         action_name, b['string'], b['type'])
267             return res
268
269         elif action['type']=='ir.actions.report.xml':
270             if 'window' in datas:
271                 del datas['window']
272             if not datas:
273                 datas = action.get('datas',{})
274             datas = datas.copy()
275             ids = datas.get('ids')
276             if 'ids' in datas:
277                 del datas['ids']
278             res = try_report(cr, uid, 'report.'+action['report_name'], ids, datas, context, our_module=our_module)
279             return res
280         else:
281             raise Exception("Cannot handle action of type %s" % act_model)
282
283     log_test("will be using %s action %s #%d", act_model, act_xmlid, act_id)
284     action = pool.get(act_model).read(cr, uid, act_id, context=context)
285     assert action, "Could not read action %s[%s]" %(act_model, act_id)
286     loop = 0
287     while action:
288         loop += 1
289         # This part tries to emulate the loop of the Gtk client
290         if loop > 100:
291             _logger.error("Passed %d loops, giving up", loop)
292             raise Exception("Too many loops at action")
293         log_test("it is an %s action at loop #%d", action.get('type', 'unknown'), loop)
294         result = _exec_action(action, datas, context)
295         if not isinstance(result, dict):
296             break
297         datas = result.get('datas', {})
298         if datas:
299             del result['datas']
300         action = result
301
302     return True
303
304 #eof
305
306 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: