summaryrefslogtreecommitdiff
path: root/addons/lunch/static/src
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/lunch/static/src
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/lunch/static/src')
-rw-r--r--addons/lunch/static/src/js/lunch_controller_common.js218
-rw-r--r--addons/lunch/static/src/js/lunch_kanban_controller.js18
-rw-r--r--addons/lunch/static/src/js/lunch_kanban_record.js36
-rw-r--r--addons/lunch/static/src/js/lunch_kanban_renderer.js29
-rw-r--r--addons/lunch/static/src/js/lunch_kanban_view.js33
-rw-r--r--addons/lunch/static/src/js/lunch_list_controller.js18
-rw-r--r--addons/lunch/static/src/js/lunch_list_renderer.js56
-rw-r--r--addons/lunch/static/src/js/lunch_list_view.js38
-rw-r--r--addons/lunch/static/src/js/lunch_mobile.js78
-rw-r--r--addons/lunch/static/src/js/lunch_model.js130
-rw-r--r--addons/lunch/static/src/js/lunch_model_extension.js100
-rw-r--r--addons/lunch/static/src/js/lunch_payment_dialog.js20
-rw-r--r--addons/lunch/static/src/js/lunch_widget.js168
-rw-r--r--addons/lunch/static/src/scss/lunch_kanban.scss8
-rw-r--r--addons/lunch/static/src/scss/lunch_list.scss11
-rw-r--r--addons/lunch/static/src/scss/lunch_view.scss81
-rw-r--r--addons/lunch/static/src/xml/lunch.xml29
-rw-r--r--addons/lunch/static/src/xml/lunch_templates.xml132
18 files changed, 1203 insertions, 0 deletions
diff --git a/addons/lunch/static/src/js/lunch_controller_common.js b/addons/lunch/static/src/js/lunch_controller_common.js
new file mode 100644
index 00000000..8cd09316
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_controller_common.js
@@ -0,0 +1,218 @@
+odoo.define('lunch.LunchControllerCommon', function (require) {
+"use strict";
+
+/**
+ * This file defines the common events and functions used by Controllers for the Lunch view.
+ */
+
+var session = require('web.session');
+var core = require('web.core');
+var LunchWidget = require('lunch.LunchWidget');
+var LunchPaymentDialog = require('lunch.LunchPaymentDialog');
+
+var _t = core._t;
+
+var LunchControllerCommon = {
+ custom_events: {
+ add_product: '_onAddProduct',
+ change_location: '_onLocationChanged',
+ change_user: '_onUserChanged',
+ open_wizard: '_onOpenWizard',
+ order_now: '_onOrderNow',
+ remove_product: '_onRemoveProduct',
+ unlink_order: '_onUnlinkOrder',
+ },
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this.editMode = false;
+ this.updated = false;
+ this.widgetData = null;
+ this.context = session.user_context;
+ this.archiveEnabled = false;
+ },
+ /**
+ * @override
+ */
+ start: function () {
+ // create a div inside o_content that will be used to wrap the lunch
+ // banner and renderer (this is required to get the desired
+ // layout with the searchPanel to the left)
+ var self = this;
+ this.$('.o_content').append($('<div>').addClass('o_lunch_content'));
+ return this._super.apply(this, arguments).then(function () {
+ self.$('.o_lunch_content').append(self.$('.o_lunch_view'));
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ _fetchPaymentInfo: function () {
+ return this._rpc({
+ route: '/lunch/payment_message',
+ params: {
+ context: this.context,
+ },
+ });
+ },
+ _fetchWidgetData: async function () {
+ this.widgetData = await this._rpc({
+ route: '/lunch/infos',
+ params: {
+ user_id: this.searchModel.get('userId'),
+ context: this.context,
+ },
+ });
+ },
+ /**
+ * Renders and appends the lunch banner widget.
+ *
+ * @private
+ */
+ _renderLunchWidget: function () {
+ var self = this;
+ var oldWidget = this.widget;
+ this.widgetData.wallet = parseFloat(this.widgetData.wallet).toFixed(2);
+ this.widget = new LunchWidget(this, _.extend(this.widgetData, {edit: this.editMode}));
+ return this.widget.appendTo(document.createDocumentFragment()).then(function () {
+ self.$('.o_lunch_content').prepend(self.widget.$el);
+ if (oldWidget) {
+ oldWidget.destroy();
+ }
+ });
+ },
+ _showPaymentDialog: function (title) {
+ var self = this;
+
+ title = title || '';
+
+ this._fetchPaymentInfo().then(function (data) {
+ var paymentDialog = new LunchPaymentDialog(self, _.extend(data, {title: title}));
+ paymentDialog.open();
+ });
+ },
+ /**
+ * Override to fetch and display the lunch data. Because of the presence of
+ * the searchPanel, also wrap the lunch widget and the renderer into
+ * a div, to get the desired layout.
+ *
+ * @override
+ * @private
+ */
+ _update: function () {
+ var def = this._fetchWidgetData().then(this._renderLunchWidget.bind(this));
+ return Promise.all([def, this._super.apply(this, arguments)]);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ _onAddProduct: function (ev) {
+ var self = this;
+ ev.stopPropagation();
+
+ this._rpc({
+ model: 'lunch.order',
+ method: 'update_quantity',
+ args: [[ev.data.lineId], 1],
+ }).then(function () {
+ self.reload();
+ });
+ },
+ _onLocationChanged: function (ev) {
+ ev.stopPropagation();
+ this.searchModel.dispatch('setLocationId', ev.data.locationId);
+ },
+ _onOpenWizard: function (ev) {
+ var self = this;
+ ev.stopPropagation();
+
+ var ctx = this.searchModel.get('userId') ? {default_user_id: this.searchModel.get('userId')} : {};
+
+ var options = {
+ on_close: function () {
+ self.reload();
+ },
+ };
+
+ // YTI TODO Maybe don't always pass the default_product_id
+ var action = {
+ res_model: 'lunch.order',
+ name: _t('Configure Your Order'),
+ type: 'ir.actions.act_window',
+ views: [[false, 'form']],
+ target: 'new',
+ context: _.extend(ctx, {default_product_id: ev.data.productId}),
+ };
+
+ if (ev.data.lineId) {
+ action = _.extend(action, {
+ res_id: ev.data.lineId,
+ context: _.extend(action.context, {
+ active_id: ev.data.lineId,
+ }),
+ });
+ }
+
+ this.do_action(action, options);
+ },
+ _onOrderNow: function (ev) {
+ var self = this;
+ ev.stopPropagation();
+
+ this._rpc({
+ route: '/lunch/pay',
+ params: {
+ user_id: this.searchModel.get('userId'),
+ context: this.context,
+ },
+ }).then(function (isPaid) {
+ if (isPaid) {
+ // TODO: feedback?
+ self.reload();
+ } else {
+ self._showPaymentDialog(_t("Not enough money in your wallet"));
+ self.reload();
+ }
+ });
+ },
+ _onRemoveProduct: function (ev) {
+ var self = this;
+ ev.stopPropagation();
+
+ this._rpc({
+ model: 'lunch.order',
+ method: 'update_quantity',
+ args: [[ev.data.lineId], -1],
+ }).then(function () {
+ self.reload();
+ });
+ },
+ _onUserChanged: function (ev) {
+ ev.stopPropagation();
+ this.searchModel.dispatch('updateUserId', ev.data.userId);
+ },
+ _onUnlinkOrder: function (ev) {
+ var self = this;
+ ev.stopPropagation();
+
+ this._rpc({
+ route: '/lunch/trash',
+ params: {
+ user_id: this.searchModel.get('userId'),
+ context: this.context,
+ },
+ }).then(function () {
+ self.reload();
+ });
+ },
+};
+
+return LunchControllerCommon;
+
+});
diff --git a/addons/lunch/static/src/js/lunch_kanban_controller.js b/addons/lunch/static/src/js/lunch_kanban_controller.js
new file mode 100644
index 00000000..dc370a46
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_kanban_controller.js
@@ -0,0 +1,18 @@
+odoo.define('lunch.LunchKanbanController', function (require) {
+"use strict";
+
+/**
+ * This file defines the Controller for the Lunch Kanban view, which is an
+ * override of the KanbanController.
+ */
+
+var KanbanController = require('web.KanbanController');
+var LunchControllerCommon = require('lunch.LunchControllerCommon');
+
+var LunchKanbanController = KanbanController.extend(LunchControllerCommon , {
+ custom_events: _.extend({}, KanbanController.prototype.custom_events, LunchControllerCommon.custom_events),
+});
+
+return LunchKanbanController;
+
+});
diff --git a/addons/lunch/static/src/js/lunch_kanban_record.js b/addons/lunch/static/src/js/lunch_kanban_record.js
new file mode 100644
index 00000000..9031192a
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_kanban_record.js
@@ -0,0 +1,36 @@
+odoo.define('lunch.LunchKanbanRecord', function (require) {
+ "use strict";
+
+ /**
+ * This file defines the KanbanRecord for the Lunch Kanban view.
+ */
+
+ var KanbanRecord = require('web.KanbanRecord');
+
+ var LunchKanbanRecord = KanbanRecord.extend({
+ events: _.extend({}, KanbanRecord.prototype.events, {
+ 'click': '_onSelectRecord',
+ }),
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Open the add product wizard
+ *
+ * @private
+ * @param {MouseEvent} ev Click event
+ */
+ _onSelectRecord: function (ev) {
+ ev.preventDefault();
+ // ignore clicks on oe_kanban_action elements
+ if (!$(ev.target).hasClass('oe_kanban_action')) {
+ this.trigger_up('open_wizard', {productId: this.recordData.product_id ? this.recordData.product_id.res_id: this.recordData.id});
+ }
+ },
+ });
+
+ return LunchKanbanRecord;
+
+ });
diff --git a/addons/lunch/static/src/js/lunch_kanban_renderer.js b/addons/lunch/static/src/js/lunch_kanban_renderer.js
new file mode 100644
index 00000000..bea87992
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_kanban_renderer.js
@@ -0,0 +1,29 @@
+odoo.define('lunch.LunchKanbanRenderer', function (require) {
+"use strict";
+
+/**
+ * This file defines the Renderer for the Lunch Kanban view, which is an
+ * override of the KanbanRenderer.
+ */
+
+var LunchKanbanRecord = require('lunch.LunchKanbanRecord');
+
+var KanbanRenderer = require('web.KanbanRenderer');
+
+var LunchKanbanRenderer = KanbanRenderer.extend({
+ config: _.extend({}, KanbanRenderer.prototype.config, {
+ KanbanRecord: LunchKanbanRecord,
+ }),
+
+ /**
+ * @override
+ */
+ start: function () {
+ this.$el.addClass('o_lunch_view o_lunch_kanban_view position-relative align-content-start flex-grow-1 flex-shrink-1');
+ return this._super.apply(this, arguments);
+ },
+});
+
+return LunchKanbanRenderer;
+
+});
diff --git a/addons/lunch/static/src/js/lunch_kanban_view.js b/addons/lunch/static/src/js/lunch_kanban_view.js
new file mode 100644
index 00000000..50d347de
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_kanban_view.js
@@ -0,0 +1,33 @@
+odoo.define('lunch.LunchKanbanView', function (require) {
+"use strict";
+
+var LunchKanbanController = require('lunch.LunchKanbanController');
+var LunchKanbanRenderer = require('lunch.LunchKanbanRenderer');
+
+var core = require('web.core');
+var KanbanView = require('web.KanbanView');
+var view_registry = require('web.view_registry');
+
+var _lt = core._lt;
+
+var LunchKanbanView = KanbanView.extend({
+ config: _.extend({}, KanbanView.prototype.config, {
+ Controller: LunchKanbanController,
+ Renderer: LunchKanbanRenderer,
+ }),
+ display_name: _lt('Lunch Kanban'),
+
+ /**
+ * @override
+ */
+ _createSearchModel(params, extraExtensions = {}) {
+ Object.assign(extraExtensions, { Lunch: {} });
+ return this._super(params, extraExtensions);
+ },
+});
+
+view_registry.add('lunch_kanban', LunchKanbanView);
+
+return LunchKanbanView;
+
+});
diff --git a/addons/lunch/static/src/js/lunch_list_controller.js b/addons/lunch/static/src/js/lunch_list_controller.js
new file mode 100644
index 00000000..bdfbab6b
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_list_controller.js
@@ -0,0 +1,18 @@
+odoo.define('lunch.LunchListController', function (require) {
+"use strict";
+
+/**
+ * This file defines the Controller for the Lunch List view, which is an
+ * override of the ListController.
+ */
+
+var ListController = require('web.ListController');
+var LunchControllerCommon = require('lunch.LunchControllerCommon');
+
+var LunchListController = ListController.extend(LunchControllerCommon, {
+ custom_events: _.extend({}, ListController.prototype.custom_events, LunchControllerCommon.custom_events),
+});
+
+return LunchListController;
+
+});
diff --git a/addons/lunch/static/src/js/lunch_list_renderer.js b/addons/lunch/static/src/js/lunch_list_renderer.js
new file mode 100644
index 00000000..0ab0b5c9
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_list_renderer.js
@@ -0,0 +1,56 @@
+odoo.define('lunch.LunchListRenderer', function (require) {
+"use strict";
+
+/**
+ * This file defines the Renderer for the Lunch List view, which is an
+ * override of the ListRenderer.
+ */
+
+var ListRenderer = require('web.ListRenderer');
+
+var LunchListRenderer = ListRenderer.extend({
+ events: _.extend({}, ListRenderer.prototype.events, {
+ 'click .o_data_row': '_onClickListRow',
+ }),
+
+ /**
+ * @override
+ */
+ start: function () {
+ this.$el.addClass('o_lunch_view o_lunch_list_view');
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * Override to add id of product_id in dataset.
+ *
+ * @override
+ */
+ _renderRow: function (record) {
+ var tr = this._super.apply(this, arguments);
+ tr.attr('data-product-id', record.data.id);
+ return tr;
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Open the add product wizard
+ *
+ * @private
+ * @param {MouseEvent} ev Click event
+ */
+ _onClickListRow: function (ev) {
+ ev.preventDefault();
+ var productId = ev.currentTarget.dataset && ev.currentTarget.dataset.productId ? parseInt(ev.currentTarget.dataset.productId) : null;
+
+ if (productId) {
+ this.trigger_up('open_wizard', {productId: productId});
+ }
+ },
+});
+
+return LunchListRenderer;
+
+});
diff --git a/addons/lunch/static/src/js/lunch_list_view.js b/addons/lunch/static/src/js/lunch_list_view.js
new file mode 100644
index 00000000..00e8efa5
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_list_view.js
@@ -0,0 +1,38 @@
+odoo.define('lunch.LunchListView', function (require) {
+"use strict";
+
+var LunchListController = require('lunch.LunchListController');
+var LunchListRenderer = require('lunch.LunchListRenderer');
+
+var core = require('web.core');
+var ListView = require('web.ListView');
+var view_registry = require('web.view_registry');
+
+var _lt = core._lt;
+
+var LunchListView = ListView.extend({
+ config: _.extend({}, ListView.prototype.config, {
+ Controller: LunchListController,
+ Renderer: LunchListRenderer,
+ }),
+ display_name: _lt('Lunch List'),
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _createSearchModel(params, extraExtensions = {}) {
+ Object.assign(extraExtensions, { Lunch: {} });
+ return this._super(params, extraExtensions);
+ },
+
+});
+
+view_registry.add('lunch_list', LunchListView);
+
+return LunchListView;
+
+});
diff --git a/addons/lunch/static/src/js/lunch_mobile.js b/addons/lunch/static/src/js/lunch_mobile.js
new file mode 100644
index 00000000..f6ad1e92
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_mobile.js
@@ -0,0 +1,78 @@
+odoo.define('lunch.LunchMobile', function (require) {
+"use strict";
+
+var config = require('web.config');
+var LunchWidget = require('lunch.LunchWidget');
+var LunchKanbanController = require('lunch.LunchKanbanController');
+var LunchListController = require('lunch.LunchListController');
+
+if (!config.device.isMobile) {
+ return;
+}
+
+LunchWidget.include({
+ template: "LunchWidgetMobile",
+
+ /**
+ * Override to set the toggle state allowing initially open it.
+ *
+ * @override
+ */
+ init: function (parent, params) {
+ this._super.apply(this, arguments);
+ this.keepOpen = params.keepOpen || undefined;
+ },
+});
+
+var mobileFunctions = {
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this.openWidget = false;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Override to add the widget's toggle state to its data.
+ *
+ * @override
+ * @private
+ */
+ _renderLunchWidget: function () {
+ this.widgetData.keepOpen = this.openWidget;
+ this.openWidget = false;
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ * @private
+ */
+ _onAddProduct: function () {
+ this.openWidget = true;
+ this._super.apply(this, arguments);
+ },
+
+ /**
+ * @override
+ * @private
+ */
+ _onRemoveProduct: function () {
+ this.openWidget = true;
+ this._super.apply(this, arguments);
+ },
+};
+
+LunchKanbanController.include(mobileFunctions);
+LunchListController.include(mobileFunctions);
+
+});
diff --git a/addons/lunch/static/src/js/lunch_model.js b/addons/lunch/static/src/js/lunch_model.js
new file mode 100644
index 00000000..720b4356
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_model.js
@@ -0,0 +1,130 @@
+odoo.define('lunch.LunchModel', function (require) {
+"use strict";
+
+/**
+ * This file defines the Model for the Lunch Kanban view, which is an
+ * override of the KanbanModel.
+ */
+
+var session = require('web.session');
+var BasicModel = require('web.BasicModel');
+
+var LunchModel = BasicModel.extend({
+ init: function () {
+ this.locationId = false;
+ this.userId = false;
+ this._promInitLocation = null;
+
+ this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @return {Promise} resolved with the location domain
+ */
+ getLocationDomain: function () {
+ var self = this;
+ return this._initUserLocation().then(function () {
+ return self._buildLocationDomainLeaf() ? [self._buildLocationDomainLeaf()]: [];
+ });
+ },
+ __load: function () {
+ var self = this;
+ var args = arguments;
+ var _super = this._super;
+
+ return this._initUserLocation().then(function () {
+ var params = args[0];
+ self._addOrUpdate(params.domain, self._buildLocationDomainLeaf());
+
+ return _super.apply(self, args);
+ });
+ },
+ __reload: function (id, options) {
+ var domain = options && options.domain || this.localData[id].domain;
+
+ this._addOrUpdate(domain, this._buildLocationDomainLeaf());
+ options = _.extend(options, {domain: domain});
+
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ _addOrUpdate: function (domain, subDomain) {
+ if (subDomain && subDomain.length) {
+ var key = subDomain[0];
+ var index = _.findIndex(domain, function (val) {
+ return val[0] === key;
+ });
+
+ if (index < 0) {
+ domain.push(subDomain);
+ } else {
+ domain[index] = subDomain;
+ }
+
+ return domain;
+ }
+
+ return domain;
+ },
+ /**
+ * Builds the domain leaf corresponding to the current user's location
+ *
+ * @private
+ * @return {(Array[])|undefined}
+ */
+ _buildLocationDomainLeaf: function () {
+ if (this.locationId) {
+ return ['is_available_at', 'in', [this.locationId]];
+ }
+ },
+ _getUserLocation: function () {
+ return this._rpc({
+ route: '/lunch/user_location_get',
+ params: {
+ context: session.user_context,
+ user_id: this.userId,
+ },
+ });
+ },
+ /**
+ * Gets the user location once.
+ * Can be triggered from anywhere
+ * Useful to inject the location domain in the search panel
+ *
+ * @private
+ * @return {Promise}
+ */
+ _initUserLocation: function () {
+ var self = this;
+ if (!this._promInitLocation) {
+ this._promInitLocation = new Promise(function (resolve) {
+ self._getUserLocation().then(function (locationId) {
+ self.locationId = locationId;
+ resolve();
+ });
+ });
+ }
+ return this._promInitLocation;
+ },
+ _updateLocation: function (locationId) {
+ this.locationId = locationId;
+ return Promise.resolve();
+ },
+ _updateUser: function (userId) {
+ this.userId = userId;
+ this._promInitLocation = null;
+ return this._initUserLocation();
+ }
+});
+
+return LunchModel;
+
+});
diff --git a/addons/lunch/static/src/js/lunch_model_extension.js b/addons/lunch/static/src/js/lunch_model_extension.js
new file mode 100644
index 00000000..aec64b97
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_model_extension.js
@@ -0,0 +1,100 @@
+odoo.define("lunch/static/src/js/lunch_model_extension.js", function (require) {
+ "use strict";
+
+ const ActionModel = require("web/static/src/js/views/action_model.js");
+
+ class LunchModelExtension extends ActionModel.Extension {
+
+ //---------------------------------------------------------------------
+ // Public
+ //---------------------------------------------------------------------
+
+ /**
+ * @override
+ * @returns {any}
+ */
+ get(property) {
+ switch (property) {
+ case "domain": return this.getDomain();
+ case "userId": return this.state.userId;
+ }
+ }
+
+ /**
+ * @override
+ */
+ async load() {
+ await this._updateLocationId();
+ }
+
+ /**
+ * @override
+ */
+ prepareState() {
+ Object.assign(this.state, {
+ locationId: null,
+ userId: null,
+ });
+ }
+
+ //---------------------------------------------------------------------
+ // Actions / Getters
+ //---------------------------------------------------------------------
+
+ /**
+ * @returns {Array[] | null}
+ */
+ getDomain() {
+ if (this.state.locationId) {
+ return [["is_available_at", "in", [this.state.locationId]]];
+ }
+ return null;
+ }
+
+ /**
+ * @param {number} locationId
+ * @returns {Promise}
+ */
+ setLocationId(locationId) {
+ this.state.locationId = locationId;
+ this.env.services.rpc({
+ route: "/lunch/user_location_set",
+ params: {
+ context: this.env.session.user_context,
+ location_id: this.state.locationId,
+ user_id: this.state.userId,
+ },
+ });
+ }
+
+ /**
+ * @param {number} userId
+ * @returns {Promise}
+ */
+ updateUserId(userId) {
+ this.state.userId = userId;
+ this.shouldLoad = true;
+ }
+
+ //---------------------------------------------------------------------
+ // Private
+ //---------------------------------------------------------------------
+
+ /**
+ * @returns {Promise}
+ */
+ async _updateLocationId() {
+ this.state.locationId = await this.env.services.rpc({
+ route: "/lunch/user_location_get",
+ params: {
+ context: this.env.session.user_context,
+ user_id: this.state.userId,
+ },
+ });
+ }
+ }
+
+ ActionModel.registry.add("Lunch", LunchModelExtension, 20);
+
+ return LunchModelExtension;
+});
diff --git a/addons/lunch/static/src/js/lunch_payment_dialog.js b/addons/lunch/static/src/js/lunch_payment_dialog.js
new file mode 100644
index 00000000..ae624403
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_payment_dialog.js
@@ -0,0 +1,20 @@
+odoo.define('lunch.LunchPaymentDialog', function (require) {
+"use strict";
+
+var Dialog = require('web.Dialog');
+
+var LunchPaymentDialog = Dialog.extend({
+ template: 'lunch.LunchPaymentDialog',
+
+ init: function (parent, options) {
+ this._super.apply(this, arguments);
+
+ options = options || {};
+
+ this.message = options.message || '';
+ },
+});
+
+return LunchPaymentDialog;
+
+});
diff --git a/addons/lunch/static/src/js/lunch_widget.js b/addons/lunch/static/src/js/lunch_widget.js
new file mode 100644
index 00000000..ca0c497c
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_widget.js
@@ -0,0 +1,168 @@
+odoo.define('lunch.LunchWidget', function (require) {
+"use strict";
+
+var core = require('web.core');
+var relationalFields = require('web.relational_fields');
+var session = require('web.session');
+var Widget = require('web.Widget');
+
+var _t = core._t;
+var FieldMany2One = relationalFields.FieldMany2One;
+
+
+var LunchMany2One = FieldMany2One.extend({
+ start: function () {
+ this.$el.addClass('w-100');
+ return this._super.apply(this, arguments);
+ }
+});
+
+var LunchWidget = Widget.extend({
+ template: 'LunchWidget',
+ custom_events: {
+ field_changed: '_onFieldChanged',
+ },
+ events: {
+ 'click .o_add_product': '_onAddProduct',
+ 'click .o_lunch_widget_order_button': '_onOrderNow',
+ 'click .o_remove_product': '_onRemoveProduct',
+ 'click .o_lunch_widget_unlink': '_onUnlinkOrder',
+ 'click .o_lunch_open_wizard': '_onLunchOpenWizard',
+ },
+
+ init: function (parent, params) {
+ this._super.apply(this, arguments);
+
+ this.is_manager = params.is_manager || false;
+ this.userimage = params.userimage || '';
+ this.username = params.username || '';
+
+ this.lunchUserField = null;
+
+ this.group_portal_id = undefined;
+
+ this.locations = params.locations || [];
+ this.userLocation = params.user_location[1] || '';
+
+ this.lunchLocationField = this._createMany2One('locations', 'lunch.location', this.userLocation);
+
+ this.wallet = params.wallet || 0;
+ this.raw_state = params.raw_state || 'new';
+ this.state = params.state || _t('To Order');
+ this.lines = params.lines || [];
+ this.total = params.total || 0;
+
+ this.alerts = params.alerts || [];
+
+ this.currency = params.currency || session.get_currency(session.company_currency_id);
+ },
+ willStart: function () {
+ var self = this;
+ var superDef = this._super.apply(this, arguments);
+
+ var def = this._rpc({
+ model: 'ir.model.data',
+ method: 'xmlid_to_res_id',
+ kwargs: {xmlid: 'base.group_portal'},
+ }).then(function (id) {
+ self.group_portal_id = id;
+
+ if (self.is_manager) {
+ self.lunchUserField = self._createMany2One('users', 'res.users', self.username, function () {
+ return [['groups_id', 'not in', [self.group_portal_id]]];
+ });
+ }
+ });
+ return Promise.all([superDef, def]);
+ },
+ renderElement: function () {
+ this._super.apply(this, arguments);
+ if (this.lunchUserField) {
+ this.lunchUserField.appendTo(this.$('.o_lunch_user_field'));
+ } else {
+ this.$('.o_lunch_user_field').text(this.username);
+ }
+ this.lunchLocationField.appendTo(this.$('.o_lunch_location_field'));
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ _createMany2One: function (name, model, value, domain, context) {
+ var fields = {};
+ fields[name] = {type: 'many2one', relation: model, string: name};
+ var data = {};
+ data[name] = {data: {display_name: value}};
+
+ var record = {
+ id: name,
+ res_id: 1,
+ model: 'dummy',
+ fields: fields,
+ fieldsInfo: {
+ default: fields,
+ },
+ data: data,
+ getDomain: domain || function () { return []; },
+ getContext: context || function () { return {}; },
+ };
+ var options = {
+ mode: 'edit',
+ noOpen: true,
+ attrs: {
+ can_create: false,
+ can_write: false,
+ }
+ };
+ return new LunchMany2One(this, name, record, options);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ _onAddProduct: function (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ this.trigger_up('add_product', {lineId: $(ev.currentTarget).data('id')});
+ },
+ _onOrderNow: function (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ this.trigger_up('order_now', {});
+ },
+ _onLunchOpenWizard: function (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ var target = $(ev.currentTarget);
+ this.trigger_up('open_wizard', {productId: target.data('product-id'), lineId: target.data('id')});
+ },
+ _onFieldChanged: function (ev) {
+ ev.stopPropagation();
+
+ if (ev.data.dataPointID === 'users') {
+ this.trigger_up('change_user', {userId: ev.data.changes.users.id});
+ } else if (ev.data.dataPointID === 'locations') {
+ this.trigger_up('change_location', {locationId: ev.data.changes.locations.id});
+ }
+ },
+ _onRemoveProduct: function (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ this.trigger_up('remove_product', {lineId: $(ev.currentTarget).data('id')});
+ },
+ _onUnlinkOrder: function (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ this.trigger_up('unlink_order', {});
+ },
+});
+
+return LunchWidget;
+
+});
diff --git a/addons/lunch/static/src/scss/lunch_kanban.scss b/addons/lunch/static/src/scss/lunch_kanban.scss
new file mode 100644
index 00000000..b4ffa97c
--- /dev/null
+++ b/addons/lunch/static/src/scss/lunch_kanban.scss
@@ -0,0 +1,8 @@
+.o_lunch_content {
+ .o_kanban_view {
+ flex: 1 1 100%;
+ &.o_kanban_grouped {
+ min-height: auto; // override min-height: 100%
+ }
+ }
+}
diff --git a/addons/lunch/static/src/scss/lunch_list.scss b/addons/lunch/static/src/scss/lunch_list.scss
new file mode 100644
index 00000000..b0afa58a
--- /dev/null
+++ b/addons/lunch/static/src/scss/lunch_list.scss
@@ -0,0 +1,11 @@
+.o_lunch_content {
+ .o_list_button {
+ width: 1px;
+ }
+
+ .o_lunch_list_view {
+ td:last-child {
+ padding-right: 16px;
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/lunch/static/src/scss/lunch_view.scss b/addons/lunch/static/src/scss/lunch_view.scss
new file mode 100644
index 00000000..6b7056f9
--- /dev/null
+++ b/addons/lunch/static/src/scss/lunch_view.scss
@@ -0,0 +1,81 @@
+.o_lunch_content {
+ display: flex;
+ flex-direction: column; // display lunch widget above kanban renderer
+ flex: 1 1 100%; // displayed to the right of the searchPanel
+ min-width: 0; // prevent grouped kanban from horizontally overflowing
+ max-width: 100%;
+ height: 100%;
+ .o_lunch_banner {
+ flex: 0 0 auto;
+ border-bottom: 1px solid #CED4DA;
+ background-color: white;
+ }
+
+ .o_lunch_purple {
+ color: $o-brand-odoo;
+ }
+
+ .o_flex_basis_0 {
+ flex-basis: 0;
+ }
+
+ .o_lunch_widget {
+ min-height: 90px;
+ max-height: 33vh;
+ overflow-y: auto;
+
+ .o_lunch_widget_info.card {
+ &, .card-title, .card-body {
+ color: $o-main-text-color;
+ background-color: inherit !important;
+ }
+
+ .card-title {
+ font-weight: bold;
+ margin-bottom: 0;
+ }
+
+ .card-body {
+ padding: 0.5rem 1rem;
+ }
+
+ .btn-link {
+ padding: 0;
+ &.o_lunch_open_wizard {
+ color: $o-main-text-color;
+ font-weight: normal;
+ }
+ }
+ }
+ }
+}
+
+.o_lunch_image {
+ img {
+ max-width: 128px;
+ max-height: 128px !important;
+ }
+}
+
+@include media-breakpoint-down(sm) {
+ .o_lunch_content {
+ details summary {
+ // Hide the caret. For details see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary
+ list-style-type: none;
+ &::-webkit-details-marker {
+ display: none
+ }
+ }
+ .o_lunch_widget {
+ max-height: 100%
+ }
+ }
+}
+
+.o_lunch_wizard {
+ .col-10 {
+ .o_form_label {
+ font-weight: normal !important;
+ }
+ }
+}
diff --git a/addons/lunch/static/src/xml/lunch.xml b/addons/lunch/static/src/xml/lunch.xml
new file mode 100644
index 00000000..d0415545
--- /dev/null
+++ b/addons/lunch/static/src/xml/lunch.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates>
+ <div t-name="LunchPreviousOrdersWidgetNoOrder" class="col-lg-12">
+ <h3>This is the first time you order a meal</h3>
+ <p class="text-muted">Select a product and put your order comments on the note.</p>
+ <p class="text-muted">Your favorite meals will be created based on your last orders.</p>
+ <p class="text-muted">Don't forget the alerts displayed in the reddish area</p>
+ </div>
+ <div t-name="LunchPreviousOrdersWidgetList" class="row">
+ <div t-foreach="categories" t-as="supplier" class="col-lg-4">
+ <h3><t t-esc="supplier"/></h3>
+ <div t-foreach='categories[supplier]' t-as='order' class="o_lunch_vignette">
+ <button type="button" class="float-right o_add_button oe_edit_only oe_link" t-att-data-id="order.line_id">
+ <span class="fa fa-plus-square"></span>
+ <span>Add</span>
+ </button>
+ <div>
+ <t t-esc="order.product_name"/>
+ <span class="badge badge-pill float-right">
+ <span class="o_lunch_price" t-raw="formatValue(order)"/>
+ </span>
+ </div>
+ <div class="text-muted">
+ <t t-if="order.note != false" t-esc="order.note"/>
+ </div>
+ </div>
+ </div>
+ </div>
+</templates>
diff --git a/addons/lunch/static/src/xml/lunch_templates.xml b/addons/lunch/static/src/xml/lunch_templates.xml
new file mode 100644
index 00000000..6cc78721
--- /dev/null
+++ b/addons/lunch/static/src/xml/lunch_templates.xml
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates>
+ <span t-name="LunchWidget">
+ <t t-foreach="widget.alerts" t-as="alert">
+ <div class="alert alert-warning mb-0" role="alert">
+ <t t-raw="alert.message"/> <!-- alert.message is coming from a fields.Html so it should be safe -->
+ </div>
+ </t>
+ <div class="o_lunch_banner container-fluid">
+ <div class="o_lunch_widget row py-3 py-md-0">
+ <div class="o_lunch_widget_info col-12 col-md-4 card border-0">
+ <div class="card-body row no-gutters align-items-center">
+ <div class="col-3 col-md-6 col-lg-3">
+ <img class="o_image_64_cover rounded-circle" t-attf-src="{{ widget.userimage }}"/>
+ </div>
+ <div class="col-9 col-md-6 col-lg-9">
+ <div class="pl-3">
+ <div class="o_lunch_user_field py-1"/>
+ <div class="o_lunch_location_field py-1"/>
+ <div class="d-flex flex-row py-1">
+ <span class="flex-grow-1">Your Account</span>
+ <t t-call="currency_field">
+ <t t-set="value" t-value="widget.wallet"/>
+ <t t-set="currency" t-value="widget.currency"/>
+ </t>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="o_lunch_widget_info col-12 col-md-4 card border-0">
+ <t t-if="!_.isEmpty(widget.lines)">
+ <t t-if="widget.raw_state == 'ordered'">
+ <t t-set="state_class" t-value="'badge-warning o_lunch_ordered'"/>
+ </t>
+ <t t-else="widget.raw_state == 'confirmed'">
+ <t t-set="state_class" t-value="'badge-success o_lunch_confirmed'"/>
+ </t>
+ <div class="card-body">
+ <h4 class="card-title">
+ Your order
+ <button t-if="widget.raw_state != 'confirmed'" class="btn btn-sm btn-icon btn-link fa fa-trash o_lunch_widget_unlink"/>
+ <span t-if="widget.raw_state != 'new'" t-esc="widget.state" t-attf-class="badge badge-pill {{ state_class }}"/>
+ </h4>
+ <ul class="list-unstyled o_lunch_widget_lines">
+ <li t-foreach="widget.lines" t-as="line">
+ <div class="d-flex align-items-center">
+ <div class="flex-grow-0 flex-shrink-0 o_lunch_product_quantity">
+ <button class="btn btn-sm btn-icon btn-link fa fa-minus-circle o_remove_product" t-if="widget.raw_state != 'confirmed'" t-attf-data-id="{{ line.id }}"/>
+ <span t-esc="line.quantity"/>
+ <button class="btn btn-sm btn-icon btn-link fa fa-plus-circle o_add_product" t-if="widget.raw_state != 'confirmed'" t-attf-data-id="{{ line.id }}"/>
+ </div>
+ <div class="flex-grow-1 pl-2">
+ <button t-esc="line.product[1]" class="btn btn-link o_lunch_open_wizard" t-attf-data-product-id="{{ line.product[0] }}" t-attf-data-id="{{ line.id }}"/>
+ </div>
+ <div class="flex-grow-0">
+ <t t-call="currency_field">
+ <t t-set="value" t-value="line.product[2]"/>
+ <t t-set="currency" t-value="widget.currency"/>
+ </t>
+ </div>
+ </div>
+ <div t-foreach="line.toppings" t-as="topping" class="d-flex flex-row">
+ <div class="flex-grow-1 pl-5">
+ <span>+ <t t-esc="topping[0]"/></span>
+ </div>
+ <div class="flex-grow-0">
+ <t t-call="currency_field">
+ <t t-set="value" t-value="topping[1]"/>
+ <t t-set="currency" t-value="widget.currency"/>
+ </t>
+ </div>
+ </div>
+ <span t-if="line.note" t-esc="line.note" class="text-muted pl-5"/>
+ </li>
+ </ul>
+ </div>
+ </t>
+ </div>
+ <div class="o_lunch_widget_info col-12 col-md-4 card border-0">
+ <t t-if="!_.isEmpty(widget.lines) &amp;&amp; widget.raw_state == 'new'">
+ <div class="card-body d-flex flex-column justify-content-between">
+ <h4 class="card-title d-flex py-1">
+ <span class="flex-grow-1">Total</span>
+ <t t-call="currency_field">
+ <t t-set="value" t-value="widget.total"/>
+ <t t-set="currency" t-value="widget.currency"/>
+ </t>
+ </h4>
+ <button t-if="widget.raw_state == 'new'" class="btn btn-primary w-100 o_lunch_widget_order_button">Order now</button>
+ </div>
+ </t>
+ </div>
+ </div>
+ </div>
+ </span>
+
+ <span t-name="currency_field" class="o_field_monetary o_field_number o_field_widget">
+ <t t-js="ctx">
+ ctx.value = _.str.sprintf('%.2f', parseFloat(ctx.value));
+ </t>
+ <t t-if="currency">
+ <t t-if="currency.position == 'after'">
+ <t t-esc="value"/><t t-esc="currency.symbol"/>
+ </t>
+ <t t-else="">
+ <t t-esc="currency.symbol"/><t t-esc="value"/>
+ </t>
+ </t>
+ <t t-else="">
+ <t t-esc="value"/>
+ </t>
+ </span>
+
+ <div t-name="lunch.LunchPaymentDialog">
+ <span t-esc="widget.message"/>
+ </div>
+
+ <t t-name="LunchWidgetMobile">
+ <details class="fixed-bottom" t-attf-open="#{widget.keepOpen}">
+ <summary class="o_lunch_toggle_cart btn btn-primary w-100">
+ <i class="fa fa-fw fa-shopping-cart"/>
+ Your cart
+ (<t t-call="currency_field">
+ <t t-set="value" t-value="widget.total"/>
+ <t t-set="currency" t-value="widget.currency"/>
+ </t>)
+ </summary>
+ <t t-call="LunchWidget"/>
+ </details>
+ </t>
+</templates>