Files
jikimo_sf/web_studio/controllers/report.py
2023-04-14 17:42:23 +08:00

317 lines
13 KiB
Python

# -*- 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("""
<t t-name="studio_report_document">
<div class="page"/>
</t>
""")
else:
arch_document = etree.fromstring("""
<t t-name="studio_report_document">
<t t-call="%(layout)s">
<div class="page"/>
</t>
</t>
""" % {'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("""
<t t-name="studio_main_report">
<t t-foreach="docs" t-as="doc">
<t t-call="%(layout)s">
<t t-call="%(document)s_document"/>
<p style="page-break-after: always;"/>
</t>
</t>
</t>
""" % {'layout': layout, 'document': new_view_document_xml_id})
else:
arch = etree.fromstring("""
<t t-name="studio_main_report">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="%(document)s_document"/>
</t>
</t>
</t>
""" % {'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 "<data/>",
'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),
}]