summaryrefslogtreecommitdiff
path: root/addons/web/static
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static')
-rw-r--r--addons/web/static/src/css/reset.min.css106
-rw-r--r--addons/web/static/src/js/report/client_action.js200
-rw-r--r--addons/web/static/src/js/report/qwebactionmanager.js140
-rw-r--r--addons/web/static/src/js/report/report.editor.js109
-rw-r--r--addons/web/static/src/js/report/subst.js14
-rw-r--r--addons/web/static/src/js/report/utils.js26
-rw-r--r--addons/web/static/src/less/layout_background.less68
-rw-r--r--addons/web/static/src/less/layout_boxed.less72
-rw-r--r--addons/web/static/src/less/layout_clean.less84
-rw-r--r--addons/web/static/src/less/report.editor.less26
-rw-r--r--addons/web/static/src/less/report.less35
-rw-r--r--addons/web/static/src/less/report_backend.less14
-rw-r--r--addons/web/static/src/xml/report.xml21
13 files changed, 915 insertions, 0 deletions
diff --git a/addons/web/static/src/css/reset.min.css b/addons/web/static/src/css/reset.min.css
new file mode 100644
index 00000000000..ee21ad74e7d
--- /dev/null
+++ b/addons/web/static/src/css/reset.min.css
@@ -0,0 +1,106 @@
+/*
+
+HTML5 CSS Reset
+Based on Eric Meyer's CSS Reset
+and html5doctor.com HTML5 Reset
+
+Copyright (c) 2011 736 Computing Services Limited
+Released under the MIT license. http://opensource.736cs.com/licenses/mit
+
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, font, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, i, center, dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, audio, canvas, details, figcaption,
+figure, footer, header, hgroup, mark, menu, meter, nav,
+output, progress, section, summary, time, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ outline: 0;
+ font-size: 100%;
+ vertical-align: baseline;
+ background: transparent;
+}
+
+body {
+ line-height: 1;
+}
+
+article, aside, dialog, figure, footer, header,
+hgroup, nav, section, blockquote {
+ display: block;
+}
+
+nav ul {
+ list-style: none;
+}
+
+ol {
+ list-style: decimal;
+}
+
+ul {
+ list-style: disc;
+}
+
+ul ul {
+ list-style: circle;
+}
+
+blockquote, q {
+ quotes: none;
+}
+
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+
+ins {
+ text-decoration: underline;
+}
+
+del {
+ text-decoration: line-through;
+}
+
+mark {
+ background: none;
+}
+
+abbr[title], dfn[title] {
+ border-bottom:1px dotted #000;
+ cursor:help;
+}
+
+/* tables still need 'cellspacing="0"' in the markup */
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+hr {
+ display: block;
+ height: 1px;
+ border: 0;
+ border-top: 1px solid #ccc;
+ margin: 1em 0;
+ padding: 0;
+}
+
+input[type="submit"], input[type="button"], button {
+ padding: 0 !important;
+ margin: 0 !important;
+}
+
+input, select, a img {
+ vertical-align: middle;
+}
diff --git a/addons/web/static/src/js/report/client_action.js b/addons/web/static/src/js/report/client_action.js
new file mode 100644
index 00000000000..60265c0a49d
--- /dev/null
+++ b/addons/web/static/src/js/report/client_action.js
@@ -0,0 +1,200 @@
+odoo.define('report.client_action', function (require) {
+'use strict';
+
+var core = require('web.core');
+var ControlPanelMixin = require('web.ControlPanelMixin');
+var session = require('web.session');
+var Widget = require('web.Widget');
+var utils = require('report.utils');
+
+var QWeb = core.qweb;
+
+
+var AUTHORIZED_MESSAGES = [
+ 'report.editor:save_ok',
+ 'report.editor:discard_ok',
+ 'report:do_action',
+];
+
+var ReportAction = Widget.extend(ControlPanelMixin, {
+
+ template: 'report.client_action',
+
+ init: function (parent, action, options) {
+ this._super.apply(this, arguments);
+
+ options = options || {};
+
+ this.action_manager = parent;
+ this.title = options.display_name || options.name;
+
+ this.edit_mode_available = false;
+ this.in_edit_mode = false;
+ this.report_url = options.report_url;
+
+ // Extra info that will be useful to build a qweb-pdf action.
+ this.report_name = options.report_name;
+ this.report_file = options.report_file;
+ this.data = options.data || {};
+ this.context = options.context || {};
+ },
+
+ start: function () {
+ var self = this;
+ this.set('title', this.title);
+ this.iframe = this.$('iframe')[0];
+ return $.when(this._super.apply(this, arguments), session.is_bound).then(function () {
+ var web_base_url = session['web.base.url'];
+ var trusted_host = utils.get_host_from_url(web_base_url);
+ var trusted_protocol = utils.get_protocol_from_url(web_base_url);
+ self.trusted_origin = utils.build_origin(trusted_protocol, trusted_host);
+
+ self.$buttons = $(QWeb.render('report.client_action.ControlButtons', {}));
+ self.$buttons.on('click', '.o_report_edit', self.on_click_edit);
+ self.$buttons.on('click', '.o_report_print', self.on_click_print);
+ self.$buttons.on('click', '.o_report_save', self.on_click_save);
+ self.$buttons.on('click', '.o_report_discard', self.on_click_discard);
+
+ self._update_control_panel();
+
+ // Load the report in the iframe. Note that we use a relative URL.
+ self.iframe.src = self.report_url;
+
+ // Once the iframe is loaded, check if we can edit the report.
+ self.iframe.onload = function () {
+ self._on_iframe_loaded();
+ };
+ });
+ },
+
+ do_show: function () {
+ this._update_control_panel();
+ return this._super.apply(this, arguments);
+ },
+
+ on_attach_callback: function () {
+ // Register now the postMessage event handler. We only want to listen to ~trusted
+ // messages and we can only filter them by their origin, so we chose to ignore the
+ // messages that do not come from `web.base.url`.
+ $(window).on('message', this, this.on_message_received);
+ },
+
+ on_detach_callback: function () {
+ $(window).off('message', this.on_message_received);
+ },
+
+ _on_iframe_loaded: function () {
+ var editable = $(this.iframe).contents().find('html').data('editable');
+ if (editable === 1) {
+ this.edit_mode_available = true;
+ this._update_control_panel();
+ }
+ },
+
+ _update_control_panel: function () {
+ this.update_control_panel({
+ breadcrumbs: this.action_manager.get_breadcrumbs(),
+ cp_content: {
+ $buttons: this.$buttons,
+ },
+ });
+ this._update_control_panel_buttons();
+ },
+
+ /**
+ * Helper allowing to toggle groups of buttons in the control panel
+ * according to the `this.in_edit_mode` flag.
+ */
+ _update_control_panel_buttons: function () {
+ this.$buttons.filter('div.o_report_edit_mode').toggle(this.in_edit_mode);
+ this.$buttons.filter('div.o_report_no_edit_mode').toggle(! this.in_edit_mode);
+ this.$buttons.filter('div.o_edit_mode_available').toggle(core.debug && this.edit_mode_available && ! this.in_edit_mode);
+ },
+
+ /**
+ * Event handler of the message post. We only handle them if they're from
+ * `web.base.url` host and protocol and if they're part of `AUTHORIZED_MESSAGES`.
+ */
+ on_message_received: function (ev) {
+ // Check the origin of the received message.
+ var message_origin = utils.build_origin(ev.originalEvent.source.location.protocol, ev.originalEvent.source.location.host);
+ if (message_origin === this.trusted_origin) {
+
+ // Check the syntax of the received message.
+ var message = ev.originalEvent.data;
+ if (_.isObject(message)) {
+ message = message.message;
+ }
+ if (! _.isString(message) || (_.isString(message) && ! _.contains(AUTHORIZED_MESSAGES, message))) {
+ return;
+ }
+
+ switch(message) {
+ case 'report.editor:save_ok':
+ // Reload the iframe in order to disable the editor.
+ this.iframe.src = this.report_url;
+ this.in_edit_mode = false;
+ this._update_control_panel_buttons();
+ break;
+ case 'report.editor:discard_ok':
+ // Reload the iframe in order to disable the editor.
+ this.iframe.src = this.report_url;
+ this.in_edit_mode = false;
+ this._update_control_panel_buttons();
+ break;
+ case 'report:do_action':
+ return this.do_action(ev.originalEvent.data.action);
+ default:
+ }
+ }
+ },
+
+ /**
+ * Helper allowing to send a message to the `this.el` iframe's window and
+ * seting the `targetOrigin` as `this.trusted_origin` (which is the
+ * `web.base.url` ir.config_parameter key) - in other word, only when using
+ * this method we only send the message to a trusted domain.
+ */
+ _post_message: function (message) {
+ this.iframe.contentWindow.postMessage(message, this.trusted_origin);
+ },
+
+ on_click_edit: function () {
+ // We reload the iframe with a special query string to enable the editor.
+ if (this.report_url.indexOf('?') === -1) {
+ this.iframe.src = this.report_url + '?enable_editor=1';
+ } else {
+ this.iframe.src = this.report_url + '&enable_editor=1';
+ }
+ this.in_edit_mode = true;
+ this._update_control_panel_buttons();
+ },
+
+ on_click_discard: function () {
+ this._post_message('report.editor:ask_discard');
+ },
+
+ on_click_save: function () {
+ this._post_message('report.editor:ask_save');
+ },
+
+ on_click_print: function () {
+ var action = {
+ 'type': 'ir.actions.report',
+ 'report_type': 'qweb-pdf',
+ 'report_name': this.report_name,
+ 'report_file': this.report_file,
+ 'data': this.data,
+ 'context': this.context,
+ 'display_name': this.title,
+ };
+ return this.do_action(action);
+ },
+
+});
+
+core.action_registry.add('report.client_action', ReportAction);
+
+return ReportAction;
+
+});
diff --git a/addons/web/static/src/js/report/qwebactionmanager.js b/addons/web/static/src/js/report/qwebactionmanager.js
new file mode 100644
index 00000000000..3c8fee859e3
--- /dev/null
+++ b/addons/web/static/src/js/report/qwebactionmanager.js
@@ -0,0 +1,140 @@
+odoo.define('report.report', function (require) {
+'use strict';
+
+var ActionManager = require('web.ActionManager');
+var core = require('web.core');
+var crash_manager = require('web.crash_manager');
+var framework = require('web.framework');
+
+var _t = core._t;
+var _lt = core._lt;
+
+
+var wkhtmltopdf_state;
+
+// Messages that will be shown to the user (if needed).
+var WKHTMLTOPDF_MESSAGES = {
+ 'install': _lt('Unable to find Wkhtmltopdf on this \nsystem. The report will be shown in html.<br><br><a href="http://wkhtmltopdf.org/" target="_blank">\nwkhtmltopdf.org</a>'),
+ 'workers': _lt('You need to start OpenERP with at least two \nworkers to print a pdf version of the reports.'),
+ 'upgrade': _lt('You should upgrade your version of\nWkhtmltopdf to at least 0.12.0 in order to get a correct display of headers and footers as well as\nsupport for table-breaking between pages.<br><br><a href="http://wkhtmltopdf.org/" \ntarget="_blank">wkhtmltopdf.org</a>'),
+ 'broken': _lt('Your installation of Wkhtmltopdf seems to be broken. The report will be shown in html.<br><br><a href="http://wkhtmltopdf.org/" target="_blank">wkhtmltopdf.org</a>')
+};
+
+var trigger_download = function (session, response, c, action, options) {
+ session.get_file({
+ url: '/report/download',
+ data: {data: JSON.stringify(response)},
+ complete: framework.unblockUI,
+ error: c.rpc_error.bind(c),
+ success: function () {
+ if (action && options && !action.dialog) {
+ options.on_close();
+ }
+ },
+ });
+};
+
+/**
+ * This helper will generate an object containing the report's url (as value)
+ * for every qweb-type we support (as key). It's convenient because we may want
+ * to use another report's type at some point (for example, when `qweb-pdf` is
+ * not available).
+ */
+var make_report_url = function (action) {
+ var report_urls = {
+ 'qweb-html': '/report/html/' + action.report_name,
+ 'qweb-pdf': '/report/pdf/' + action.report_name,
+ };
+ // We may have to build a query string with `action.data`. It's the place
+ // were report's using a wizard to customize the output traditionally put
+ // their options.
+ if (_.isUndefined(action.data) || _.isNull(action.data) || (_.isObject(action.data) && _.isEmpty(action.data))) {
+ if (action.context.active_ids) {
+ var active_ids_path = '/' + action.context.active_ids.join(',');
+ // Update the report's type - report's url mapping.
+ report_urls = _.mapObject(report_urls, function (value, key) {
+ return value += active_ids_path;
+ });
+ }
+ } else {
+ var serialized_options_path = '?options=' + encodeURIComponent(JSON.stringify(action.data));
+ serialized_options_path += '&context=' + encodeURIComponent(JSON.stringify(action.context));
+ // Update the report's type - report's url mapping.
+ report_urls = _.mapObject(report_urls, function (value, key) {
+ return value += serialized_options_path;
+ });
+ }
+ return report_urls;
+};
+
+ActionManager.include({
+ ir_actions_report: function (action, options) {
+ var self = this;
+ action = _.clone(action);
+
+ var report_urls = make_report_url(action);
+
+ if (action.report_type === 'qweb-html') {
+ var client_action_options = _.extend({}, options, {
+ report_url: report_urls['qweb-html'],
+ report_name: action.report_name,
+ report_file: action.report_file,
+ data: action.data,
+ context: action.context,
+ name: action.name,
+ display_name: action.display_name,
+ });
+ return this.do_action('report.client_action', client_action_options);
+ } else if (action.report_type === 'qweb-pdf') {
+ framework.blockUI();
+ // Before doing anything, we check the state of wkhtmltopdf on the server.
+ (wkhtmltopdf_state = wkhtmltopdf_state || this._rpc({route: '/report/check_wkhtmltopdf'})).then(function (state) {
+ // Display a notification to the user according to wkhtmltopdf's state.
+ if (WKHTMLTOPDF_MESSAGES[state]) {
+ self.do_notify(_t('Report'), WKHTMLTOPDF_MESSAGES[state], true);
+ }
+
+ if (state === 'upgrade' || state === 'ok') {
+ // Trigger the download of the PDF report.
+ var response;
+ var c = crash_manager;
+
+ var treated_actions = [];
+ var current_action = action;
+ do {
+ report_urls = make_report_url(current_action);
+ response = [
+ report_urls['qweb-pdf'],
+ action.report_type, //The 'root' report is considered the maine one, so we use its type for all the others.
+ ];
+ trigger_download(self.getSession(), response, c, current_action, options);
+
+ treated_actions.push(current_action);
+ current_action = current_action.next_report_to_generate;
+ } while (current_action && !_.contains(treated_actions, current_action));
+ //Second part of the condition for security reasons (avoids infinite loop possibilty).
+
+ return;
+
+ } else {
+ // Open the report in the client action if generating the PDF is not possible.
+ var client_action_options = _.extend({}, options, {
+ report_url: report_urls['qweb-html'],
+ report_name: action.report_name,
+ report_file: action.report_file,
+ data: action.data,
+ context: action.context,
+ name: action.name,
+ display_name: action.display_name,
+ });
+ framework.unblockUI();
+ return self.do_action('report.client_action', client_action_options);
+ }
+ });
+ } else {
+ return self._super(action, options);
+ }
+ }
+});
+
+});
diff --git a/addons/web/static/src/js/report/report.editor.js b/addons/web/static/src/js/report/report.editor.js
new file mode 100644
index 00000000000..57942f9b79c
--- /dev/null
+++ b/addons/web/static/src/js/report/report.editor.js
@@ -0,0 +1,109 @@
+odoo.define('report.editor', function (require) {
+'use strict';
+
+var ajax = require('web.ajax');
+var core = require('web.core');
+var utils = require('report.utils');
+var editor = require('web_editor.editor');
+var options = require('web_editor.snippets.options');
+
+var web_base_url = $('html').attr('web-base-url');
+var trusted_host = utils.get_host_from_url(web_base_url);
+var trusted_protocol = utils.get_protocol_from_url(web_base_url);
+var trusted_origin = utils.build_origin(trusted_protocol, trusted_host);
+
+ajax.loadXML('/web/static/src/xml/base_common.xml', core.qweb);
+
+// Patch the editor's behavior when it is launched inside an iframe.
+if (window.self !== window.top) {
+ $(document.body).addClass('o_in_iframe'); // in order to apply css rules
+
+ // As `reload` is called after `save` and `cancel`, we nullify this function
+ // if the editor in order to be able to chain some deferred to `save` and
+ // `cancel`.
+ editor.reload = function () {
+ return $.when();
+ };
+
+ // And now we chain some deferred to `save` and `cancel` in order to inform
+ // the report's client action that the actions are done.
+ editor.Class.include({
+ save: function () {
+ return this._super.apply(this, arguments).then(function () {
+ window.parent.postMessage('report.editor:save_ok', trusted_origin);
+ });
+ },
+ cancel: function () {
+ return this._super.apply(this, arguments).then(function () {
+ window.parent.postMessage('report.editor:discard_ok', trusted_origin);
+ });
+ },
+ });
+
+ // postMessage logic.
+ var AUTHORIZED_MESSAGES = [
+ 'report.editor:ask_discard',
+ 'report.editor:ask_save',
+ ];
+
+ window.addEventListener('message', function (ev) {
+ // Check the origin of the received message.
+ var message_origin = utils.build_origin(ev.source.location.protocol, ev.source.location.host);
+ if (message_origin === trusted_origin) {
+ // Check the syntax of the received message.
+ var message = ev.data;
+ if (! _.isString(message) || (_.isString(message) && ! _.contains(AUTHORIZED_MESSAGES, message))) {
+ return;
+ }
+
+ switch(message) {
+ case 'report.editor:ask_save':
+ editor.editor_bar.save();
+ break;
+ case 'report.editor:ask_discard':
+ editor.editor_bar.cancel();
+ break;
+ default:
+ }
+ }
+ }, false);
+
+ // Allow to send commands to the webclient when the editor is disabled.
+ if (window.location.search.indexOf('enable_editor') === -1) {
+ // `do_action` command
+ $('[res-id][res-model][view-type]')
+ .wrap('<a/>')
+ .attr('href', '#')
+ .on('click', function (ev) {
+ ev.preventDefault();
+ var action = {
+ 'type': 'ir.actions.act_window',
+ 'view_type': $(this).attr('view-type'),
+ 'view_mode': $(this).attr('view-mode') || $(this).attr('view-type'),
+ 'res_id': Number($(this).attr('res-id')),
+ 'res_model': $(this).attr('res-model'),
+ 'views': [[$(this).attr('view-id') || false, $(this).attr('view-type')]],
+ };
+ window.parent.postMessage({
+ 'message': 'report:do_action',
+ 'action': action,
+ }, trusted_origin);
+ });
+ }
+}
+
+options.registry.many2one.include({
+ select_record: function (li) {
+ var self = this;
+ this._super(li);
+ if (this.$target.data('oe-field') === "partner_id") {
+ var $img = $('.header .row img:first');
+ var css = window.getComputedStyle($img[0]);
+ $img.css("max-height", css.height+'px');
+ $img.attr("src", "/web/image/res.partner/"+self.ID+"/image");
+ setTimeout(function () { $img.removeClass('o_dirty'); },0);
+ }
+ }
+});
+
+});
diff --git a/addons/web/static/src/js/report/subst.js b/addons/web/static/src/js/report/subst.js
new file mode 100644
index 00000000000..076a5ff208a
--- /dev/null
+++ b/addons/web/static/src/js/report/subst.js
@@ -0,0 +1,14 @@
+function subst() {
+ var vars = {};
+ var x = document.location.search.substring(1).split('&');
+ for (var i in x) {
+ var z = x[i].split('=', 2);
+ vars[z[0]] = unescape(z[1]);
+ }
+ var x=['frompage', 'topage', 'page', 'webpage', 'section', 'subsection', 'subsubsection'];
+ for (var i in x) {
+ var y = document.getElementsByClassName(x[i]);
+ for (var j=0; j<y.length; ++j)
+ y[j].textContent = vars[x[i]];
+ }
+}
diff --git a/addons/web/static/src/js/report/utils.js b/addons/web/static/src/js/report/utils.js
new file mode 100644
index 00000000000..64776efb4db
--- /dev/null
+++ b/addons/web/static/src/js/report/utils.js
@@ -0,0 +1,26 @@
+odoo.define('report.utils', function (require) {
+'use strict';
+
+function get_protocol_from_url (url) {
+ var a = document.createElement('a');
+ a.href = url;
+ return a.protocol;
+}
+
+function get_host_from_url (url) {
+ var a = document.createElement('a');
+ a.href = url;
+ return a.host;
+}
+
+function build_origin (protocol, host) {
+ return protocol + '//' + host;
+}
+
+return {
+ 'get_protocol_from_url': get_protocol_from_url,
+ 'get_host_from_url': get_host_from_url,
+ 'build_origin': build_origin,
+};
+
+});
diff --git a/addons/web/static/src/less/layout_background.less b/addons/web/static/src/less/layout_background.less
new file mode 100644
index 00000000000..f00fec6b33e
--- /dev/null
+++ b/addons/web/static/src/less/layout_background.less
@@ -0,0 +1,68 @@
+.o_background_footer, .o_background_header, .o_report_layout_background {
+ font-family: 'Lato';
+ color: @gray;
+}
+.o_background_header {
+ border-bottom: 1px solid @gray-lighter;
+ .company_address {
+ min-width: 300px;
+ }
+ img {
+ max-height: 96px;
+ max-width: 300px;
+ margin-right: 16px;
+ }
+ h3 {
+ color: @odoo-brand-primary;
+ font-weight: 700;
+ max-width: 300px;
+ }
+}
+.o_background_footer {
+ ul {
+ border-top: 1px solid @odoo-brand-primary;
+ border-bottom: 1px solid @odoo-brand-primary;
+ padding: 4px 0;
+ margin: 0 0 4px 0;
+ li {
+ color: @odoo-brand-primary;
+ }
+ }
+}
+.o_report_layout_background {
+ background-image: url(/report/static/src/img/bg_background_template.jpg);
+ background-size: cover;
+ background-position: bottom center;
+ background-repeat: no-repeat;
+ min-height: 920px;
+ thead tr th, h2 {
+ color: @odoo-brand-primary;
+ }
+ tbody {
+ color: @gray;
+ tr:nth-child(odd) {
+ background-color: rgba(184, 171, 180, 0.2);
+ }
+ tr td {
+ border-top: none;
+ }
+ }
+ /*Total table*/
+ .row > .col-xs-4 > table {
+ tr:nth-child(odd) {
+ background-color: transparent !important;
+ }
+ tr:first-child td {
+ /*we could get rid of this rule if .border-black td would be terminated in report.less*/
+ border-top: none !important;
+ }
+ tr:last-child td {
+ color: @odoo-brand-primary;
+ }
+ }
+ #total {
+ table tr:last-child td {
+ border-bottom: 1px solid black;
+ }
+ }
+}
diff --git a/addons/web/static/src/less/layout_boxed.less b/addons/web/static/src/less/layout_boxed.less
new file mode 100644
index 00000000000..a81e09f347b
--- /dev/null
+++ b/addons/web/static/src/less/layout_boxed.less
@@ -0,0 +1,72 @@
+.o_boxed_footer, .o_boxed_header, .o_report_layout_boxed {
+ font-family: 'Lato';
+ color: @gray;
+ font-size: 12px;
+}
+.o_boxed_header {
+ border-bottom: 1px solid @gray-lighter;
+ img {
+ max-height: 100px;
+ }
+ h4 {
+ color: @o-we-color-text-normal;
+ font-weight: 700;
+ text-transform: uppercase;
+ }
+}
+.o_boxed_footer {
+ border-top: 3px solid @gray;
+ ul {
+ margin: 4px 0;
+ }
+}
+.o_report_layout_boxed {
+ // This is not a general rule since the id is not used in all the reports,
+ // we should make a general structure for report body to use it everywhere
+ #informations {
+ border: 1px solid @gray;
+ padding-top: 10px;
+ margin: 0;
+ }
+ > h2 {
+ text-transform: uppercase;
+ }
+ table {
+ border: 1px solid @gray;
+ thead {
+ tr th {
+ border-bottom: 3px solid @gray !important;
+ text-transform: uppercase;
+ }
+ }
+ tbody {
+ color: @gray;
+ tr {
+ td {
+ border-top: none;
+ border-right: 1px solid @gray;
+ }
+ td:last-child {
+ background-color: @gray-lighter;
+ }
+ }
+ }
+ }
+ /*Total table*/
+ .row > .col-xs-4 > table {
+ td {
+ border-right: none;
+ }
+ tr:last-child td, tr:first-child td {
+ /*we could get rid of this rule if .border-black td would be terminated in report.less*/
+ border-top: none !important;
+ }
+ tr:first-child td {
+ background-color: transparent;
+ }
+ tr:last-child td {
+ background-color: @gray;
+ color: #fff;
+ }
+ }
+}
diff --git a/addons/web/static/src/less/layout_clean.less b/addons/web/static/src/less/layout_clean.less
new file mode 100644
index 00000000000..85e9c2d8491
--- /dev/null
+++ b/addons/web/static/src/less/layout_clean.less
@@ -0,0 +1,84 @@
+.o_clean_footer, .o_clean_header, .o_report_layout_clean {
+ font-family: 'Lato';
+ color: #000;
+}
+.o_clean_header {
+ img {
+ max-height: 90px;
+ max-width: 300px;
+ }
+}
+.o_clean_footer {
+ margin: 0 3px;
+ border-top: 3px solid @odoo-brand-primary;
+ h4 {
+ color: @odoo-brand-primary;
+ font-weight: bolder;
+ }
+ .pagenumber {
+ width: 35px;
+ height: 35px;
+ border: 3px solid @odoo-brand-primary;
+ background-color: @odoo-brand-optional;
+ color: white;
+ padding-top: 6px;
+ text-align: center;
+ }
+}
+.o_report_layout_clean {
+ h1, h2, h3 {
+ color: @odoo-brand-optional;
+ font-weight: bolder;
+ }
+ strong {
+ color: @odoo-brand-primary;
+ }
+ table {
+ thead {
+ color: @odoo-brand-primary;
+ tr th {
+ text-align: left !important;
+ border-top: 3px solid @odoo-brand-primary !important;
+ text-transform: uppercase;
+ }
+ tr th:last-child {
+ text-align: right !important;
+ }
+ tr th:first-child {
+ width: 48%;
+ }
+ }
+ tbody {
+ color: #000;
+ tr:first-child td {
+ border-top: none;
+ }
+ tr:last-child td {
+ border-bottom: 3px solid @odoo-brand-primary;
+ }
+ tr {
+ td {
+ padding: 15px 5px;
+ text-align: left !important;
+ }
+ td:last-child {
+ text-align: right !important;
+ }
+ }
+ }
+ }
+ /*Total table*/
+ .row > .col-xs-4 {
+ width: 43%;
+ margin-top: -20px;
+ > table {
+ tr:last-child td {
+ border-top: 1px solid @gray-lighter !important;
+ }
+ tr:first-child td {
+ /*we could get rid of this rule if .border-black td would be terminated in report.less*/
+ border-top: none !important;
+ }
+ }
+ }
+}
diff --git a/addons/web/static/src/less/report.editor.less b/addons/web/static/src/less/report.editor.less
new file mode 100644
index 00000000000..206ddb87037
--- /dev/null
+++ b/addons/web/static/src/less/report.editor.less
@@ -0,0 +1,26 @@
+.o_editable {
+ &:not(:empty), &[data-oe-type] {
+ &[date-oe-model="ir.ui.view"] {
+ &, th, td {
+ &:hover {
+ -moz-box-shadow: #DDCC33 0 0 5px 2px inset;
+ -webkit-box-shadow: #DDCC33 0 0 5px 2px inset;
+ box-shadow: #DDCC33 0 0 5px 2px inset;
+ }
+ }
+ }
+ }
+}
+
+body.o_in_iframe {
+ margin-top: 40px!important;
+ div.o_css_editor {
+ > form {
+ display: none;
+ }
+ }
+
+ .header, .footer {
+ display: none;
+ }
+}
diff --git a/addons/web/static/src/less/report.less b/addons/web/static/src/less/report.less
new file mode 100644
index 00000000000..f34a2393fbe
--- /dev/null
+++ b/addons/web/static/src/less/report.less
@@ -0,0 +1,35 @@
+html, body {
+ height: 100%;
+}
+
+body {
+ color: #000 !important;
+ word-wrap: break-word;
+}
+.border-black td {
+ border-top: 1px solid black !important;
+}
+
+/* See https://github.com/wkhtmltopdf/wkhtmltopdf/issues/1524 */
+.table-condensed {
+ > thead > tr > th {
+ border-bottom: none !important;
+ }
+ > tbody > tr {
+ page-break-inside: avoid !important;
+ border-top: none !important;
+ }
+}
+.zero_min_height {
+ min-height: 0px !important;
+}
+
+/* To avoid broken snippets in report rendering */
+.jumbotron, .panel, .carousel, section{
+ page-break-inside: avoid;
+}
+
+/* Wkhtmltopdf doesn't support very well the media-print CSS (depends on the version) */
+.hidden-print{
+ display: none;
+}
diff --git a/addons/web/static/src/less/report_backend.less b/addons/web/static/src/less/report_backend.less
new file mode 100644
index 00000000000..62252c2d9f7
--- /dev/null
+++ b/addons/web/static/src/less/report_backend.less
@@ -0,0 +1,14 @@
+.o_report_iframe {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border: none;
+}
+
+.o_report_no_edit_mode, .o_edit_mode_available, .o_report_edit_mode {
+ display: inline-block;
+}
diff --git a/addons/web/static/src/xml/report.xml b/addons/web/static/src/xml/report.xml
new file mode 100644
index 00000000000..c578add8ce9
--- /dev/null
+++ b/addons/web/static/src/xml/report.xml
@@ -0,0 +1,21 @@
+<template>
+
+ <!-- Main layout (iframe) -->
+ <div t-name="report.client_action">
+ <iframe class="o_report_iframe"/>
+ </div>
+
+ <!-- Buttons of the Control Panel -->
+ <t t-name="report.client_action.ControlButtons">
+ <div class="o_report_no_edit_mode">
+ <button type="button" class="btn btn-primary btn-sm o_report_print" title="Print">Print</button>
+ </div>
+ <div class="o_edit_mode_available">
+ <button type="button" class="btn btn-default btn-sm o_report_edit" title="Edit">Edit</button>
+ </div>
+ <div class="o_report_edit_mode">
+ <button type="button" class="btn btn-primary btn-sm o_report_save" title="Save">Save</button>
+ <button type="button" class="btn btn-default btn-sm o_report_discard" title="Discard">Discard</button>
+ </div>
+ </t>
+</template> \ No newline at end of file