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