# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import base64 import json from lxml import etree from odoo import http, _ from odoo.http import request from odoo.addons.web_studio.controllers import main from odoo.exceptions import ValidationError, UserError class WebStudioReportController(main.WebStudioController): @http.route('/web_studio/create_new_report', type='json', auth='user') def create_new_report(self, model_name, layout): if layout == 'web.basic_layout': arch_document = etree.fromstring("""
""") else: arch_document = etree.fromstring("""
""" % {'layout': layout}) view_document = request.env['ir.ui.view'].create({ 'name': 'studio_report_document', 'type': 'qweb', 'arch': etree.tostring(arch_document, encoding='utf-8', pretty_print=True), }) new_view_document_xml_id = view_document.get_external_id()[view_document.id] view_document.name = '%s_document' % new_view_document_xml_id view_document.key = '%s_document' % new_view_document_xml_id if layout == 'web.basic_layout': arch = etree.fromstring("""

""" % {'layout': layout, 'document': new_view_document_xml_id}) else: arch = etree.fromstring(""" """ % {'document': new_view_document_xml_id}) view = request.env['ir.ui.view'].create({ 'name': 'studio_main_report', 'type': 'qweb', 'arch': etree.tostring(arch, encoding='utf-8', pretty_print=True), }) # FIXME: When website is installed, we need to set key as xmlid to search on a valid domain # See '_view_obj' in 'website/model/ir.ui.view' view.name = new_view_document_xml_id view.key = new_view_document_xml_id model = request.env['ir.model']._get(model_name) report = request.env['ir.actions.report'].create({ 'name': _('%s Report', model.name), 'model': model.model, 'report_type': 'qweb-pdf', 'report_name': view.name, }) # make it available in the print menu report.create_action() return { 'id': report.id, } @http.route('/web_studio/print_report', type='json', auth='user') def print_report(self, report_name, record_id): report = request.env['ir.actions.report']._get_report_from_name(report_name) return report.report_action(record_id) @http.route('/web_studio/edit_report', type='json', auth='user') def edit_report(self, report_id, values): report = request.env['ir.actions.report'].browse(report_id) if report: if 'attachment_use' in values: if values['attachment_use']: values['attachment'] = "'%s'" % report.name else: # disable saving as attachment altogether values['attachment'] = False if 'groups_id' in values: values['groups_id'] = [(6, 0, values['groups_id'])] if 'display_in_print' in values: if values['display_in_print']: report.create_action() else: report.unlink_action() values.pop('display_in_print') report.write(values) return report.read() @http.route('/web_studio/read_paperformat', type='json', auth='user') def read_paperformat(self, report_id): report = request.env['ir.actions.report'].browse(report_id) return report.get_paperformat().read() @http.route('/web_studio/get_widgets_available_options', type='json', auth='user') def get_widgets_available_options(self): fields = dict() records = request.env['ir.model'].search([('model', 'like', 'ir.qweb.field.%')]) for record in records: fields[record.model[14:]] = request.env[record.model].get_available_options() return fields @http.route('/web_studio/get_report_views', type='json', auth='user') def get_report_views(self, report_name, record_id=None): if record_id is None: raise UserError(_("To edit this document please create a record first")) loaded = set() views = {} def get_report_view(key): view = request.env['ir.ui.view'].search([ ('key', '=', key), ('type', '=', 'qweb'), ('mode', '=', 'primary'), ], limit=1) if not view: raise UserError(_("No view found for the given report!")) return view def process_template_groups(element): """ `get_template` only returns the groups names but we also need need their id and display name in Studio to edit them (many2many tags widget). These data are thus added on the node. This processing is quite similar to what has been done on views. """ for node in element.iter(): if node.get('groups'): request.env['ir.ui.view'].set_studio_groups(node) def load_arch(view_name): if view_name in loaded: return loaded.add(view_name) view = get_report_view(view_name) studio_view = self._get_studio_view(view) element = request.env['ir.qweb'].with_context(full_branding=True)._get_template(view.id)[0] process_template_groups(element) views[view.id] = { 'arch': etree.tostring(element), 'key': view.key, 'studio_arch': studio_view.arch_db or "", 'studio_view_id': studio_view.id, 'view_id': view.id, } for node in element.getroottree().findall("//*[@t-call]"): tcall = node.get("t-call") if '{' in tcall: # this t-call value is dynamic (e.g. t-call="{{company.tmp}}) # so its corresponding view cannot be read # this template won't be returned to the Editor so it won't # be customizable continue load_arch(tcall) return view.id load_arch(report_name) main_view_id = get_report_view(report_name).id report_html = self._test_report(report_name, record_id) return { 'report_html': report_html and report_html[0], 'main_view_id': main_view_id, 'views': views, } @http.route('/web_studio/edit_report_view', type='json', auth='user') def edit_report_view(self, report_name, report_views, record_id, operations=None): # a report can be composed of multiple views (with t-call) ; we might # thus need to apply operations on multiple views # create groups of operations by view groups = {} ops = [] for op in operations: ops += op.get('inheritance', [op]) for op in ops: if str(op['view_id']) not in groups: groups[str(op['view_id'])] = [] groups[str(op['view_id'])].append(op) parser = etree.XMLParser(remove_blank_text=True) for group_view_id in groups: view = request.env['ir.ui.view'].browse(int(group_view_id)) if view.key in request.env['ir.ui.view'].TEMPLATE_VIEWS_BLACKLIST: raise ValidationError(_("You cannot modify this view, it is part of the generic layout")) arch = etree.fromstring(report_views[group_view_id]['studio_arch'], parser=parser) for op in groups[group_view_id]: if not op.get('type'): # apply changes content = etree.fromstring(op['content'], etree.HTMLParser()) for node in content[0]: etree.SubElement(arch, 'xpath', { 'expr': op['xpath'], 'position': op['position'], }).append(node) else: # call the right operation handler op['position'] = op['type'] op['target'] = { 'xpath_info': [{ 'tag': g.split('[')[0], 'indice': g.split('[')[1][:-1] if '[' in g else 1 } for g in op['xpath'].split('/')[1:]] } getattr(self, '_operation_%s' % (op['type']))(arch, op) # Save or create changes into studio view, identifiable by xmlid # Example for view id 42 of model crm.lead: web-studio_crm.lead-42 new_arch = etree.tostring(arch, encoding='unicode', pretty_print=True) self._set_studio_view(view, new_arch) # Normalize the view # studio_view = self._get_studio_view(view) # try: # normalized_view = studio_view.normalize() # self._set_studio_view(view, normalized_view) # except ValidationError: # Element '<...>' cannot be located in parent view # # If the studio view is not applicable after normalization, let's # # just ignore the normalization step, it's better to have a studio # # view that is not optimized than to prevent the user from making # # the change he would like to make. # self._set_studio_view(view, new_arch) # in case of undo, there could be no operation anymore for a view so # the view thus need to be reset intact_view_ids = report_views.keys() - groups.keys() for view_id in intact_view_ids: intact_view = request.env['ir.ui.view'].browse(int(view_id)) studio_view = self._get_studio_view(intact_view) if studio_view: studio_view.arch_db = report_views[view_id]['studio_arch'] result = self.get_report_views(report_name, record_id) return result @http.route('/web_studio/edit_report_view_arch', type='json', auth='user') def edit_report_view_arch(self, report_name, record_id, view_id, view_arch): view = request.env['ir.ui.view'].browse(view_id) view.write({'arch': view_arch}) # TODO: we might need to keep studio_arch as it was before the changes result = self.get_report_views(report_name, record_id) return result @http.route('/web_studio/edit_report/test_load_assets', type='json', auth='user') def edit_report_test_load_css(self): Qweb = request.env['ir.qweb'] Attachment = request.env['ir.attachment'] html = Qweb._render('web.report_layout', values={ 'studio': True, }) root = etree.fromstring(html).getroottree() links = [link.get('href') for link in root.findall("//link")] if 'assets' in request.session.debug: domain = [] for link in links: if domain: domain = ['|'] + domain domain.append(('name', '=', link.replace('/web/assets/debug/', ''))) attachments = Attachment.search(domain) else: link_ids = [int(link.replace('/web/assets/', '').split('-', 1)[0]) for link in links] attachments = Attachment.browse(link_ids) css = {a.name: base64.b64decode(a.datas) for a in attachments} return { "css": css } def _test_report(self, report_name, record_id): # render the report to catch a rendering error try: return request.env['ir.actions.report']._render_qweb_html(report_name, [record_id], { 'full_branding': True, 'studio': True, }) except Exception as err: # the report could not be rendered which probably means the last # operation was incorrect return [{ "error": err, "message": str(err), }]