diff --git a/addons/web/models/models.py b/addons/web/models/models.py
index fe01ffe6938..31e5834d200 100644
--- a/addons/web/models/models.py
+++ b/addons/web/models/models.py
@@ -1,11 +1,14 @@
# -*- coding: utf-8 -*-
-from datetime import datetime
import babel.dates
import pytz
-from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT
from odoo import _, api, fields, models
+from odoo.tools import lazy
+class IrActionsActWindowView(models.Model):
+ _inherit = 'ir.actions.act_window.view'
+
+ view_mode = fields.Selection(selection_add=[('qweb', 'QWeb')])
class Base(models.AbstractModel):
_inherit = 'base'
@@ -78,3 +81,35 @@ class Base(models.AbstractModel):
data[group_by_value][field_value] += 1
return data
+
+ ##### qweb view hooks #####
+ @api.model
+ def qweb_render_view(self, view_id, domain):
+ assert view_id
+ return self.env['ir.qweb'].render(
+ view_id, {
+ **self.env['ir.ui.view']._prepare_qcontext(),
+ **self._qweb_prepare_qcontext(view_id, domain),
+ })
+
+ def _qweb_prepare_qcontext(self, view_id, domain):
+ """
+ Base qcontext for rendering qweb views bound to this model
+ """
+ return {
+ 'model': self,
+ 'domain': domain,
+ # not necessarily necessary as env is already part of the
+ # non-minimal qcontext
+ 'context': self.env.context,
+ 'records': lazy(self.search, domain),
+ }
+
+ @api.model
+ def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
+ r = super().fields_view_get(view_id, view_type, toolbar, submenu)
+ # avoid leaking the raw (un-rendered) template, also avoids bloating
+ # the response payload for no reason
+ if r['type'] == 'qweb':
+ r['arch'] = ''
+ return r
diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js
index 57b3771427b..ba00b4dbab7 100644
--- a/addons/web/static/src/js/views/abstract_controller.js
+++ b/addons/web/static/src/js/views/abstract_controller.js
@@ -456,7 +456,10 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
}
});
} else if ($target.attr('name')) {
- this.do_action($target.attr('name'));
+ this.do_action(
+ $target.attr('name'),
+ data.context && {additional_context: data.context}
+ );
} else {
this.do_action({
name: $target.attr('title') || _.str.strip($target.text()),
diff --git a/addons/web/static/src/js/views/qweb/qweb_view.js b/addons/web/static/src/js/views/qweb/qweb_view.js
new file mode 100644
index 00000000000..9e79ecf19d2
--- /dev/null
+++ b/addons/web/static/src/js/views/qweb/qweb_view.js
@@ -0,0 +1,202 @@
+/**
+ * Client-side implementation of a qweb view.
+ */
+odoo.define('web.qweb', function (require) {
+"use strict";
+
+var core = require('web.core');
+var AbstractView = require('web.AbstractView');
+var AbstractModel = require('web.AbstractModel');
+var AbstractRenderer = require('web.AbstractRenderer');
+var AbstractController = require('web.AbstractController');
+var registry = require('web.view_registry');
+
+var _lt = core._lt;
+
+/**
+ * model
+ */
+var Model = AbstractModel.extend({
+ /**
+ * init
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this._state = {
+ viewId: false,
+ modelName: false,
+ body: '',
+ context: {},
+ domain: [],
+ };
+ },
+ /**
+ * fetches the rendered qweb view
+ */
+ _fetch: function () {
+ var state = this._state;
+ return this._rpc({
+ model: state.modelName,
+ method: 'qweb_render_view',
+ kwargs: {
+ view_id: state.viewId,
+ domain: state.domain,
+ context: state.context
+ }
+ }).then(function (r) {
+ state.body = r;
+ return state.viewId;
+ });
+ },
+ /**
+ * get
+ */
+ get: function () {
+ return this._state;
+ },
+ /**
+ * load
+ */
+ load: function (params) {
+ _.extend(this._state, _.pick(params, ['viewId', 'modelName', 'domain', 'context']));
+
+ return this._fetch();
+ },
+ /**
+ * reload
+ */
+ reload: function (_id, _params) {
+ // FIXME: update this._state or something?
+ return this._fetch();
+ }
+});
+/**
+ * renderer
+ */
+var Renderer = AbstractRenderer.extend({
+ /**
+ * render
+ */
+ _render: function () {
+ var self = this;
+ return this._super.apply(this, arguments).then(function () {
+ self.$el.html(self.state.body);
+ });
+ }
+});
+/**
+ * controller
+ */
+var Controller = AbstractController.extend({
+ events: _.extend({}, AbstractController.prototype.events, {
+ 'click [type="toggle"]': '_onLazyToggle',
+ }),
+
+ init: function () {
+ this._super.apply(this, arguments);
+ this._$buttons = $('