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