summaryrefslogtreecommitdiff
path: root/addons/lunch/static
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
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/lunch/static')
-rw-r--r--addons/lunch/static/description/icon.pngbin0 -> 8870 bytes
-rw-r--r--addons/lunch/static/description/icon.svg24
-rw-r--r--addons/lunch/static/img/4formaggio.pngbin0 -> 8362 bytes
-rw-r--r--addons/lunch/static/img/Coke.pngbin0 -> 7469 bytes
-rw-r--r--addons/lunch/static/img/bacon_burger.pngbin0 -> 8027 bytes
-rw-r--r--addons/lunch/static/img/brie.pngbin0 -> 6794 bytes
-rw-r--r--addons/lunch/static/img/burger.pngbin0 -> 2820 bytes
-rw-r--r--addons/lunch/static/img/cheeseburger.pngbin0 -> 7020 bytes
-rw-r--r--addons/lunch/static/img/chicken_curry.pngbin0 -> 7147 bytes
-rw-r--r--addons/lunch/static/img/chirashi.pngbin0 -> 7221 bytes
-rw-r--r--addons/lunch/static/img/club.pngbin0 -> 7011 bytes
-rw-r--r--addons/lunch/static/img/coke_zero.pngbin0 -> 7153 bytes
-rw-r--r--addons/lunch/static/img/drink.pngbin0 -> 2427 bytes
-rw-r--r--addons/lunch/static/img/fanta.pngbin0 -> 5855 bytes
-rw-r--r--addons/lunch/static/img/fuze_black.pngbin0 -> 5623 bytes
-rw-r--r--addons/lunch/static/img/fuze_green.pngbin0 -> 4724 bytes
-rw-r--r--addons/lunch/static/img/gouda.pngbin0 -> 7864 bytes
-rw-r--r--addons/lunch/static/img/italiana.pngbin0 -> 11549 bytes
-rw-r--r--addons/lunch/static/img/lipton.pngbin0 -> 6784 bytes
-rw-r--r--addons/lunch/static/img/lunch.pngbin0 -> 2124 bytes
-rw-r--r--addons/lunch/static/img/maki.pngbin0 -> 8806 bytes
-rw-r--r--addons/lunch/static/img/mozza.pngbin0 -> 6191 bytes
-rw-r--r--addons/lunch/static/img/napoli.pngbin0 -> 9099 bytes
-rw-r--r--addons/lunch/static/img/pasta_bolognese.pngbin0 -> 6727 bytes
-rw-r--r--addons/lunch/static/img/pizza.pngbin0 -> 3891 bytes
-rw-r--r--addons/lunch/static/img/pizza_funghi.pngbin0 -> 9213 bytes
-rw-r--r--addons/lunch/static/img/pizza_margherita.pngbin0 -> 8597 bytes
-rw-r--r--addons/lunch/static/img/pizza_veggie.pngbin0 -> 7354 bytes
-rw-r--r--addons/lunch/static/img/salmon_sushi.pngbin0 -> 6885 bytes
-rw-r--r--addons/lunch/static/img/temaki.pngbin0 -> 9244 bytes
-rw-r--r--addons/lunch/static/img/tuna_sandwich.pngbin0 -> 6425 bytes
-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
-rw-r--r--addons/lunch/static/tests/lunch_kanban_mobile_tests.js212
-rw-r--r--addons/lunch/static/tests/lunch_kanban_tests.js986
-rw-r--r--addons/lunch/static/tests/lunch_list_tests.js267
-rw-r--r--addons/lunch/static/tests/lunch_test_utils.js59
53 files changed, 2751 insertions, 0 deletions
diff --git a/addons/lunch/static/description/icon.png b/addons/lunch/static/description/icon.png
new file mode 100644
index 00000000..28870a1d
--- /dev/null
+++ b/addons/lunch/static/description/icon.png
Binary files differ
diff --git a/addons/lunch/static/description/icon.svg b/addons/lunch/static/description/icon.svg
new file mode 100644
index 00000000..64a5d86e
--- /dev/null
+++ b/addons/lunch/static/description/icon.svg
@@ -0,0 +1,24 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70">
+ <defs>
+ <path id="icon-a" d="M4,5.35309892e-14 C36.4160122,9.87060235e-15 58.0836068,-3.97961823e-14 65,5.07020818e-14 C69,6.733808e-14 70,1 70,5 C70,43.0488877 70,62.4235458 70,65 C70,69 69,70 65,70 C61,70 9,70 4,70 C1,70 7.10542736e-15,69 7.10542736e-15,65 C7.25721566e-15,62.4676575 3.83358709e-14,41.8005206 3.60818146e-14,5 C-1.13686838e-13,1 1,5.75716207e-14 4,5.35309892e-14 Z"/>
+ <linearGradient id="icon-c" x1="100%" x2="0%" y1="0%" y2="100%">
+ <stop offset="0%" stop-color="#CDC484"/>
+ <stop offset="100%" stop-color="#B5AA59"/>
+ </linearGradient>
+ <path id="icon-d" d="M34.9928486,19.5391573 C35.0500601,19.8604454 36.1442308,25.9991002 36.1442308,28.3438202 C36.1442308,31.9190055 34.1561298,34.4688031 31.216887,35.4941909 L32.1394231,51.7705126 C32.1894832,52.7070335 31.409976,53.5 30.4230769,53.5 L25.8461538,53.5 C24.8664063,53.5 24.0797476,52.7138694 24.1298077,51.7705126 L25.0523437,35.4941909 C22.1059495,34.4688031 20.125,31.9121696 20.125,28.3438202 C20.125,25.9922643 21.2191707,19.8604454 21.2763822,19.5391573 C21.5052284,18.1514658 24.5159856,18.1309581 24.7019231,19.6143524 L24.7019231,29.2666692 C24.7948918,29.4990904 25.7817909,29.4854186 25.8461538,29.2666692 C25.946274,27.5371818 26.4111178,19.7510707 26.4182692,19.5733369 C26.6542668,18.1514658 29.6149639,18.1514658 29.8438101,19.5733369 C29.858113,19.7579067 30.3158053,27.5371818 30.4159255,29.2666692 C30.4802885,29.4854186 31.4743389,29.4990904 31.5601563,29.2666692 L31.5601563,19.6143524 C31.7460938,18.137794 34.7640024,18.1514658 34.9928486,19.5391573 Z M43.5173678,39.0693762 L42.4446514,51.7226612 C42.3588341,52.6796898 43.1526442,53.5 44.1538462,53.5 L48.1586538,53.5 C49.1097957,53.5 49.875,52.7685567 49.875,51.8593796 L49.875,20.1407181 C49.875,19.2383769 49.1097957,18.5000977 48.1586538,18.5000977 C42.2587139,18.5000977 32.3253606,30.7022121 43.5173678,39.0693762 Z"/>
+ <path id="icon-e" d="M34.9928486,17.5391573 C35.0500601,17.8604454 36.1442308,23.9991002 36.1442308,26.3438202 C36.1442308,29.9190055 34.1561298,32.4688031 31.216887,33.4941909 L32.1394231,49.7705126 C32.1894832,50.7070335 31.409976,51.5 30.4230769,51.5 L25.8461538,51.5 C24.8664063,51.5 24.0797476,50.7138694 24.1298077,49.7705126 L25.0523437,33.4941909 C22.1059495,32.4688031 20.125,29.9121696 20.125,26.3438202 C20.125,23.9922643 21.2191707,17.8604454 21.2763822,17.5391573 C21.5052284,16.1514658 24.5159856,16.1309581 24.7019231,17.6143524 L24.7019231,27.2666692 C24.7948918,27.4990904 25.7817909,27.4854186 25.8461538,27.2666692 C25.946274,25.5371818 26.4111178,17.7510707 26.4182692,17.5733369 C26.6542668,16.1514658 29.6149639,16.1514658 29.8438101,17.5733369 C29.858113,17.7579067 30.3158053,25.5371818 30.4159255,27.2666692 C30.4802885,27.4854186 31.4743389,27.4990904 31.5601563,27.2666692 L31.5601563,17.6143524 C31.7460938,16.137794 34.7640024,16.1514658 34.9928486,17.5391573 Z M43.5173678,37.0693762 L42.4446514,49.7226612 C42.3588341,50.6796898 43.1526442,51.5 44.1538462,51.5 L48.1586538,51.5 C49.1097957,51.5 49.875,50.7685567 49.875,49.8593796 L49.875,18.1407181 C49.875,17.2383769 49.1097957,16.5000977 48.1586538,16.5000977 C42.2587139,16.5000977 32.3253606,28.7022121 43.5173678,37.0693762 Z"/>
+ </defs>
+ <g fill="none" fill-rule="evenodd">
+ <mask id="icon-b" fill="#fff">
+ <use xlink:href="#icon-a"/>
+ </mask>
+ <g mask="url(#icon-b)">
+ <rect width="70" height="70" fill="url(#icon-c)"/>
+ <path fill="#FFF" fill-opacity=".383" d="M4,1.8 L65,1.8 C67.6666667,1.8 69.3333333,1.13333333 70,-0.2 C70,2.46666667 70,3.46666667 70,2.8 L1.10547097e-14,2.8 C-1.65952376e-14,3.46666667 -2.9161925e-14,2.46666667 -2.66453526e-14,-0.2 C0.666666667,1.13333333 2,1.8 4,1.8 Z" transform="matrix(1 0 0 -1 0 2.8)"/>
+ <path fill="#393939" d="M37.1559742,53 L4,52 C2,52 -7.10542736e-15,51.8543417 0,47.9215686 L2.11942169e-16,24.8800004 L21.6437579,0 L21,12.2352941 L26.7780762,0 L29.5940312,3.47275252 L33,0 L35,12.2352941 L41.1357671,3.93586958 L49,0 L49.7896212,33.2154878 L37.1559742,53 Z" opacity=".324" transform="translate(0 17)"/>
+ <path fill="#000" fill-opacity=".383" d="M4,4 L65,4 C67.6666667,4 69.3333333,3 70,1 C70,3.66666667 70,5 70,5 L1.77635684e-15,5 C1.77635684e-15,5 1.77635684e-15,3.66666667 1.77635684e-15,1 C0.666666667,3 2,4 4,4 Z" transform="translate(0 65)"/>
+ <use fill="#000" fill-rule="nonzero" opacity=".3" xlink:href="#icon-d"/>
+ <use fill="#FFF" fill-rule="nonzero" xlink:href="#icon-e"/>
+ </g>
+ </g>
+</svg>
diff --git a/addons/lunch/static/img/4formaggio.png b/addons/lunch/static/img/4formaggio.png
new file mode 100644
index 00000000..f19c95ac
--- /dev/null
+++ b/addons/lunch/static/img/4formaggio.png
Binary files differ
diff --git a/addons/lunch/static/img/Coke.png b/addons/lunch/static/img/Coke.png
new file mode 100644
index 00000000..e822a265
--- /dev/null
+++ b/addons/lunch/static/img/Coke.png
Binary files differ
diff --git a/addons/lunch/static/img/bacon_burger.png b/addons/lunch/static/img/bacon_burger.png
new file mode 100644
index 00000000..ad4177d8
--- /dev/null
+++ b/addons/lunch/static/img/bacon_burger.png
Binary files differ
diff --git a/addons/lunch/static/img/brie.png b/addons/lunch/static/img/brie.png
new file mode 100644
index 00000000..5b76bfbb
--- /dev/null
+++ b/addons/lunch/static/img/brie.png
Binary files differ
diff --git a/addons/lunch/static/img/burger.png b/addons/lunch/static/img/burger.png
new file mode 100644
index 00000000..064e3f98
--- /dev/null
+++ b/addons/lunch/static/img/burger.png
Binary files differ
diff --git a/addons/lunch/static/img/cheeseburger.png b/addons/lunch/static/img/cheeseburger.png
new file mode 100644
index 00000000..f9d5a3e5
--- /dev/null
+++ b/addons/lunch/static/img/cheeseburger.png
Binary files differ
diff --git a/addons/lunch/static/img/chicken_curry.png b/addons/lunch/static/img/chicken_curry.png
new file mode 100644
index 00000000..09e49448
--- /dev/null
+++ b/addons/lunch/static/img/chicken_curry.png
Binary files differ
diff --git a/addons/lunch/static/img/chirashi.png b/addons/lunch/static/img/chirashi.png
new file mode 100644
index 00000000..65143711
--- /dev/null
+++ b/addons/lunch/static/img/chirashi.png
Binary files differ
diff --git a/addons/lunch/static/img/club.png b/addons/lunch/static/img/club.png
new file mode 100644
index 00000000..5f3e36f3
--- /dev/null
+++ b/addons/lunch/static/img/club.png
Binary files differ
diff --git a/addons/lunch/static/img/coke_zero.png b/addons/lunch/static/img/coke_zero.png
new file mode 100644
index 00000000..72dbba55
--- /dev/null
+++ b/addons/lunch/static/img/coke_zero.png
Binary files differ
diff --git a/addons/lunch/static/img/drink.png b/addons/lunch/static/img/drink.png
new file mode 100644
index 00000000..4d4eed49
--- /dev/null
+++ b/addons/lunch/static/img/drink.png
Binary files differ
diff --git a/addons/lunch/static/img/fanta.png b/addons/lunch/static/img/fanta.png
new file mode 100644
index 00000000..b7a48332
--- /dev/null
+++ b/addons/lunch/static/img/fanta.png
Binary files differ
diff --git a/addons/lunch/static/img/fuze_black.png b/addons/lunch/static/img/fuze_black.png
new file mode 100644
index 00000000..1daac338
--- /dev/null
+++ b/addons/lunch/static/img/fuze_black.png
Binary files differ
diff --git a/addons/lunch/static/img/fuze_green.png b/addons/lunch/static/img/fuze_green.png
new file mode 100644
index 00000000..9a274ab6
--- /dev/null
+++ b/addons/lunch/static/img/fuze_green.png
Binary files differ
diff --git a/addons/lunch/static/img/gouda.png b/addons/lunch/static/img/gouda.png
new file mode 100644
index 00000000..c50d6d8d
--- /dev/null
+++ b/addons/lunch/static/img/gouda.png
Binary files differ
diff --git a/addons/lunch/static/img/italiana.png b/addons/lunch/static/img/italiana.png
new file mode 100644
index 00000000..2f8c4244
--- /dev/null
+++ b/addons/lunch/static/img/italiana.png
Binary files differ
diff --git a/addons/lunch/static/img/lipton.png b/addons/lunch/static/img/lipton.png
new file mode 100644
index 00000000..a1f8ef21
--- /dev/null
+++ b/addons/lunch/static/img/lipton.png
Binary files differ
diff --git a/addons/lunch/static/img/lunch.png b/addons/lunch/static/img/lunch.png
new file mode 100644
index 00000000..ed2fdb30
--- /dev/null
+++ b/addons/lunch/static/img/lunch.png
Binary files differ
diff --git a/addons/lunch/static/img/maki.png b/addons/lunch/static/img/maki.png
new file mode 100644
index 00000000..c92dd8c8
--- /dev/null
+++ b/addons/lunch/static/img/maki.png
Binary files differ
diff --git a/addons/lunch/static/img/mozza.png b/addons/lunch/static/img/mozza.png
new file mode 100644
index 00000000..8820fbb9
--- /dev/null
+++ b/addons/lunch/static/img/mozza.png
Binary files differ
diff --git a/addons/lunch/static/img/napoli.png b/addons/lunch/static/img/napoli.png
new file mode 100644
index 00000000..c7ecd65d
--- /dev/null
+++ b/addons/lunch/static/img/napoli.png
Binary files differ
diff --git a/addons/lunch/static/img/pasta_bolognese.png b/addons/lunch/static/img/pasta_bolognese.png
new file mode 100644
index 00000000..64f9972e
--- /dev/null
+++ b/addons/lunch/static/img/pasta_bolognese.png
Binary files differ
diff --git a/addons/lunch/static/img/pizza.png b/addons/lunch/static/img/pizza.png
new file mode 100644
index 00000000..ad358b77
--- /dev/null
+++ b/addons/lunch/static/img/pizza.png
Binary files differ
diff --git a/addons/lunch/static/img/pizza_funghi.png b/addons/lunch/static/img/pizza_funghi.png
new file mode 100644
index 00000000..00bceba9
--- /dev/null
+++ b/addons/lunch/static/img/pizza_funghi.png
Binary files differ
diff --git a/addons/lunch/static/img/pizza_margherita.png b/addons/lunch/static/img/pizza_margherita.png
new file mode 100644
index 00000000..0e317265
--- /dev/null
+++ b/addons/lunch/static/img/pizza_margherita.png
Binary files differ
diff --git a/addons/lunch/static/img/pizza_veggie.png b/addons/lunch/static/img/pizza_veggie.png
new file mode 100644
index 00000000..950afb54
--- /dev/null
+++ b/addons/lunch/static/img/pizza_veggie.png
Binary files differ
diff --git a/addons/lunch/static/img/salmon_sushi.png b/addons/lunch/static/img/salmon_sushi.png
new file mode 100644
index 00000000..1467b54f
--- /dev/null
+++ b/addons/lunch/static/img/salmon_sushi.png
Binary files differ
diff --git a/addons/lunch/static/img/temaki.png b/addons/lunch/static/img/temaki.png
new file mode 100644
index 00000000..8f47e7c0
--- /dev/null
+++ b/addons/lunch/static/img/temaki.png
Binary files differ
diff --git a/addons/lunch/static/img/tuna_sandwich.png b/addons/lunch/static/img/tuna_sandwich.png
new file mode 100644
index 00000000..b616d615
--- /dev/null
+++ b/addons/lunch/static/img/tuna_sandwich.png
Binary files differ
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>
diff --git a/addons/lunch/static/tests/lunch_kanban_mobile_tests.js b/addons/lunch/static/tests/lunch_kanban_mobile_tests.js
new file mode 100644
index 00000000..1f2e61ff
--- /dev/null
+++ b/addons/lunch/static/tests/lunch_kanban_mobile_tests.js
@@ -0,0 +1,212 @@
+odoo.define('lunch.lunchKanbanMobileTests', function (require) {
+"use strict";
+
+const LunchKanbanView = require('lunch.LunchKanbanView');
+
+const testUtils = require('web.test_utils');
+const {createLunchView, mockLunchRPC} = require('lunch.test_utils');
+
+QUnit.module('Views');
+
+QUnit.module('LunchKanbanView Mobile', {
+ beforeEach() {
+ const PORTAL_GROUP_ID = 1234;
+
+ this.data = {
+ 'product': {
+ fields: {
+ is_available_at: {string: 'Product Availability', type: 'many2one', relation: 'lunch.location'},
+ category_id: {string: 'Product Category', type: 'many2one', relation: 'lunch.product.category'},
+ supplier_id: {string: 'Vendor', type: 'many2one', relation: 'lunch.supplier'},
+ },
+ records: [
+ {id: 1, name: 'Tuna sandwich', is_available_at: 1},
+ ],
+ },
+ 'lunch.order': {
+ fields: {},
+ update_quantity() {
+ return Promise.resolve();
+ },
+ },
+ 'lunch.product.category': {
+ fields: {},
+ records: [],
+ },
+ 'lunch.supplier': {
+ fields: {},
+ records: [],
+ },
+ 'ir.model.data': {
+ fields: {},
+ xmlid_to_res_id() {
+ return Promise.resolve(PORTAL_GROUP_ID);
+ },
+ },
+ 'lunch.location': {
+ fields: {
+ name: {string: 'Name', type: 'char'},
+ },
+ records: [
+ {id: 1, name: "Office 1"},
+ {id: 2, name: "Office 2"},
+ ],
+ },
+ };
+ this.regularInfos = {
+ user_location: [2, "Office 2"],
+ };
+ },
+}, function () {
+ QUnit.test('basic rendering', async function (assert) {
+ assert.expect(7);
+
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: mockLunchRPC({
+ infos: this.regularInfos,
+ userLocation: this.data['lunch.location'].records[0].id,
+ }),
+ });
+
+ assert.containsOnce(kanban, '.o_kanban_view .o_kanban_record:not(.o_kanban_ghost)',
+ "should have 1 records in the renderer");
+
+ // check view layout
+ assert.containsOnce(kanban, '.o_content > .o_lunch_content',
+ "should have a 'kanban lunch wrapper' column");
+ assert.containsOnce(kanban, '.o_lunch_content > .o_kanban_view',
+ "should have a 'classical kanban view' column");
+ assert.hasClass(kanban.$('.o_kanban_view'), 'o_lunch_kanban_view',
+ "should have classname 'o_lunch_kanban_view'");
+ assert.containsOnce($('.o_lunch_content'), '> details',
+ "should have a 'lunch kanban' details/summary discolure panel");
+ assert.hasClass($('.o_lunch_content > details'), 'fixed-bottom',
+ "should have classname 'fixed-bottom'");
+ assert.isNotVisible($('.o_lunch_content > details .o_lunch_banner'),
+ "shouldn't have a visible 'lunch kanban' banner");
+
+ kanban.destroy();
+ });
+
+ QUnit.module('LunchWidget', function () {
+ QUnit.test('toggle', async function (assert) {
+ assert.expect(6);
+
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: mockLunchRPC({
+ infos: Object.assign({}, this.regularInfos, {
+ total: "3.00",
+ }),
+ userLocation: this.data['lunch.location'].records[0].id,
+ }),
+ });
+
+ const $details = $('.o_lunch_content > details');
+ assert.isNotVisible($details.find('.o_lunch_banner'),
+ "shouldn't have a visible 'lunch kanban' banner");
+ assert.isVisible($details.find('> summary'),
+ "should hava a visible cart toggle button");
+ assert.containsOnce($details, '> summary:contains(Your cart)',
+ "should have 'Your cart' in the button text");
+ assert.containsOnce($details, '> summary:contains(3.00)',
+ "should have '3.00' in the button text");
+
+ await testUtils.dom.click($details.find('> summary'));
+ assert.isVisible($details.find('.o_lunch_banner'),
+ "should have a visible 'lunch kanban' banner");
+
+ await testUtils.dom.click($details.find('> summary'));
+ assert.isNotVisible($details.find('.o_lunch_banner'),
+ "shouldn't have a visible 'lunch kanban' banner");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('keep open when adding quantities', async function (assert) {
+ assert.expect(6);
+
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: mockLunchRPC({
+ infos: Object.assign({}, this.regularInfos, {
+ lines: [
+ {
+ id: 6,
+ product: [1, "Tuna sandwich", "3.00"],
+ toppings: [],
+ quantity: 1.0,
+ },
+ ],
+ }),
+ userLocation: this.data['lunch.location'].records[0].id,
+ }),
+ });
+
+ const $details = $('.o_lunch_content > details');
+ assert.isNotVisible($details.find('.o_lunch_banner'),
+ "shouldn't have a visible 'lunch kanban' banner");
+ assert.isVisible($details.find('> summary'),
+ "should hava a visible cart toggle button");
+
+ await testUtils.dom.click($details.find('> summary'));
+ assert.isVisible($details.find('.o_lunch_banner'),
+ "should have a visible 'lunch kanban' banner");
+
+ const $widgetSecondColumn = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(1)');
+
+ assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_lines > li',
+ "should have 1 order line");
+
+ let $firstLine = $widgetSecondColumn.find('.o_lunch_widget_lines > li:first');
+
+ await testUtils.dom.click($firstLine.find('button.o_add_product'));
+ assert.isVisible($('.o_lunch_content > details .o_lunch_banner'),
+ "add quantity should keep 'lunch kanban' banner open");
+
+ $firstLine = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(1) .o_lunch_widget_lines > li:first');
+
+ await testUtils.dom.click($firstLine.find('button.o_remove_product'));
+ assert.isVisible($('.o_lunch_content > details .o_lunch_banner'),
+ "remove quantity should keep 'lunch kanban' banner open");
+
+ kanban.destroy();
+ });
+ });
+});
+
+});
diff --git a/addons/lunch/static/tests/lunch_kanban_tests.js b/addons/lunch/static/tests/lunch_kanban_tests.js
new file mode 100644
index 00000000..d4deade7
--- /dev/null
+++ b/addons/lunch/static/tests/lunch_kanban_tests.js
@@ -0,0 +1,986 @@
+odoo.define('lunch.lunchKanbanTests', function (require) {
+"use strict";
+
+const LunchKanbanView = require('lunch.LunchKanbanView');
+
+const testUtils = require('web.test_utils');
+const {createLunchView, mockLunchRPC} = require('lunch.test_utils');
+
+QUnit.module('Views');
+
+QUnit.module('LunchKanbanView', {
+ beforeEach() {
+ const PORTAL_GROUP_ID = 1234;
+
+ this.data = {
+ 'product': {
+ fields: {
+ is_available_at: {string: 'Product Availability', type: 'many2one', relation: 'lunch.location'},
+ category_id: {string: 'Product Category', type: 'many2one', relation: 'lunch.product.category'},
+ supplier_id: {string: 'Vendor', type: 'many2one', relation: 'lunch.supplier'},
+ },
+ records: [
+ {id: 1, name: 'Tuna sandwich', is_available_at: 1},
+ ],
+ },
+ 'lunch.order': {
+ fields: {},
+ update_quantity() {
+ return Promise.resolve();
+ },
+ },
+ 'lunch.product.category': {
+ fields: {},
+ records: [],
+ },
+ 'lunch.supplier': {
+ fields: {},
+ records: [],
+ },
+ 'ir.model.data': {
+ fields: {},
+ xmlid_to_res_id() {
+ return Promise.resolve(PORTAL_GROUP_ID);
+ },
+ },
+ 'lunch.location': {
+ fields: {
+ name: {string: 'Name', type: 'char'},
+ },
+ records: [
+ {id: 1, name: "Office 1"},
+ {id: 2, name: "Office 2"},
+ ],
+ },
+ 'res.users': {
+ fields: {
+ name: {string: 'Name', type: 'char'},
+ groups_id: {string: 'Groups', type: 'many2many'},
+ },
+ records: [
+ {id: 1, name: "Mitchell Admin", groups_id: []},
+ {id: 2, name: "Marc Demo", groups_id: []},
+ {id: 3, name: "Jean-Luc Portal", groups_id: [PORTAL_GROUP_ID]},
+ ],
+ },
+ };
+ this.regularInfos = {
+ username: "Marc Demo",
+ wallet: 36.5,
+ is_manager: false,
+ currency: {
+ symbol: "\u20ac",
+ position: "after"
+ },
+ user_location: [2, "Office 2"],
+ };
+ this.managerInfos = {
+ username: "Mitchell Admin",
+ wallet: 47.6,
+ is_manager: true,
+ currency: {
+ symbol: "\u20ac",
+ position: "after"
+ },
+ user_location: [2, "Office 2"],
+ };
+ },
+}, function () {
+ QUnit.test('basic rendering', async function (assert) {
+ assert.expect(7);
+
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: mockLunchRPC({
+ infos: this.regularInfos,
+ userLocation: this.data['lunch.location'].records[0].id,
+ }),
+ });
+
+ assert.containsOnce(kanban, '.o_kanban_view .o_kanban_record:not(.o_kanban_ghost)',
+ "should have 1 records in the renderer");
+
+ // check view layout
+ assert.containsN(kanban, '.o_content > div', 2,
+ "should have 2 columns");
+ assert.containsOnce(kanban, '.o_content > div.o_search_panel',
+ "should have a 'lunch filters' column");
+ assert.containsOnce(kanban, '.o_content > .o_lunch_content',
+ "should have a 'lunch wrapper' column");
+ assert.containsOnce(kanban, '.o_lunch_content > .o_kanban_view',
+ "should have a 'classical kanban view' column");
+ assert.hasClass(kanban.$('.o_kanban_view'), 'o_lunch_kanban_view',
+ "should have classname 'o_lunch_kanban_view'");
+ assert.containsOnce(kanban, '.o_lunch_content > span > .o_lunch_banner',
+ "should have a 'lunch' banner");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('no flickering at reload', async function (assert) {
+ assert.expect(2);
+
+ const self = this;
+ let infosProm = Promise.resolve();
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: function (route, args) {
+ if (route === '/lunch/user_location_get') {
+ return Promise.resolve(self.data['lunch.location'].records[0].id);
+ }
+ if (route === '/lunch/infos') {
+ return Promise.resolve(self.regularInfos);
+ }
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'xmlid_to_res_id') {
+ // delay the rendering of the lunch widget
+ return infosProm.then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ infosProm = testUtils.makeTestPromise();
+ kanban.reload();
+
+ assert.strictEqual(kanban.$('.o_lunch_widget').length, 1,
+ "old widget should still be present");
+
+ await infosProm.resolve();
+
+ assert.strictEqual(kanban.$('.o_lunch_widget').length, 1);
+
+ kanban.destroy();
+ });
+
+ QUnit.module('LunchWidget', function () {
+
+ QUnit.test('empty cart', async function (assert) {
+ assert.expect(3);
+
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: mockLunchRPC({
+ infos: this.regularInfos,
+ userLocation: this.data['lunch.location'].records[0].id,
+ }),
+ });
+
+ const $kanbanWidget = kanban.$('.o_lunch_widget');
+
+ assert.containsN($kanbanWidget, '> .o_lunch_widget_info', 3,
+ "should have 3 columns");
+ assert.isVisible($kanbanWidget.find('> .o_lunch_widget_info:first'),
+ "should have the first column visible");
+ assert.strictEqual($kanbanWidget.find('> .o_lunch_widget_info:not(:first)').html().trim(), "",
+ "all columns but the first should be empty");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('search panel domain location', async function (assert) {
+ assert.expect(20);
+ let expectedLocation = 1;
+ let locationId = this.data['lunch.location'].records[0].id;
+ const regularInfos = _.extend({}, this.regularInfos);
+
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: function (route, args) {
+ assert.step(route);
+
+ if (route.startsWith('/lunch')) {
+ if (route === '/lunch/user_location_set') {
+ locationId = args.location_id;
+ return Promise.resolve(true);
+ }
+ return mockLunchRPC({
+ infos: regularInfos,
+ userLocation: locationId,
+ }).apply(this, arguments);
+ }
+ if (args.method === 'search_panel_select_multi_range') {
+ assert.deepEqual(args.kwargs.search_domain, [["is_available_at", "in", [expectedLocation]]],
+ 'The initial domain of the search panel must contain the user location');
+ }
+ if (route === '/web/dataset/search_read') {
+ assert.deepEqual(args.domain, [["is_available_at", "in", [expectedLocation]]],
+ 'The domain for fetching actual data should be correct');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ expectedLocation = 2;
+ await testUtils.fields.many2one.clickOpenDropdown('locations');
+ await testUtils.fields.many2one.clickItem('locations', "Office 2");
+
+ assert.verifySteps([
+ // Initial state
+ '/lunch/user_location_get',
+ '/web/dataset/call_kw/product/search_panel_select_multi_range',
+ '/web/dataset/call_kw/product/search_panel_select_multi_range',
+ '/web/dataset/search_read',
+ '/lunch/infos',
+ '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id',
+ // Click m2o
+ '/web/dataset/call_kw/lunch.location/name_search',
+ // Click new location
+ '/lunch/user_location_set',
+ '/web/dataset/call_kw/product/search_panel_select_multi_range',
+ '/web/dataset/call_kw/product/search_panel_select_multi_range',
+ '/web/dataset/search_read',
+ '/lunch/infos',
+ '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id',
+ ]);
+
+ kanban.destroy();
+ });
+
+ QUnit.test('search panel domain location false: fetch products in all locations', async function (assert) {
+ assert.expect(10);
+ const regularInfos = _.extend({}, this.regularInfos);
+
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: function (route, args) {
+ assert.step(route);
+
+ if (route.startsWith('/lunch')) {
+ return mockLunchRPC({
+ infos: regularInfos,
+ userLocation: false,
+ }).apply(this, arguments);
+ }
+ if (args.method === 'search_panel_select_multi_range') {
+ assert.deepEqual(args.kwargs.search_domain, [],
+ 'The domain should not exist since the location is false.');
+ }
+ if (route === '/web/dataset/search_read') {
+ assert.deepEqual(args.domain, [],
+ 'The domain for fetching actual data should be correct');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+ assert.verifySteps([
+ '/lunch/user_location_get',
+ '/web/dataset/call_kw/product/search_panel_select_multi_range',
+ '/web/dataset/call_kw/product/search_panel_select_multi_range',
+ '/web/dataset/search_read',
+ '/lunch/infos',
+ '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id',
+ ])
+
+ kanban.destroy();
+ });
+
+ QUnit.test('non-empty cart', async function (assert) {
+ assert.expect(17);
+
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: mockLunchRPC({
+ infos: Object.assign({}, this.regularInfos, {
+ total: "3.00",
+ lines: [
+ {
+ product: [1, "Tuna sandwich", "3.00"],
+ toppings: [],
+ quantity: 1.0,
+ },
+ ],
+ }),
+ userLocation: this.data['lunch.location'].records[0].id,
+ }),
+ });
+
+ const $kanbanWidget = kanban.$('.o_lunch_widget');
+
+ assert.containsN($kanbanWidget, '> .o_lunch_widget_info', 3,
+ "should have 3 columns");
+
+ assert.containsOnce($kanbanWidget, '.o_lunch_widget_info:eq(1)',
+ "should have a second column");
+
+ const $widgetSecondColumn = $kanbanWidget.find('.o_lunch_widget_info:eq(1)');
+
+ assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_unlink',
+ "should have a button to clear the order");
+
+ assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_lines > li',
+ "should have 1 order line");
+
+ const $firstLine = $widgetSecondColumn.find('.o_lunch_widget_lines > li:first');
+ assert.containsOnce($firstLine, 'button.o_remove_product',
+ "should have a button to remove a product quantity on each line");
+ assert.containsOnce($firstLine, 'button.o_add_product',
+ "should have a button to add a product quantity on each line");
+ assert.containsOnce($firstLine, '.o_lunch_product_quantity > :eq(1)',
+ "should have the line's quantity");
+ assert.strictEqual($firstLine.find('.o_lunch_product_quantity > :eq(1)').text().trim(), "1",
+ "should have 1 as the line's quantity");
+ assert.containsOnce($firstLine, '.o_lunch_open_wizard',
+ "should have the line's product name to open the wizard");
+ assert.strictEqual($firstLine.find('.o_lunch_open_wizard').text().trim(), "Tuna sandwich",
+ "should have 'Tuna sandwich' as the line's product name");
+ assert.containsOnce($firstLine, '.o_field_monetary',
+ "should have the line's amount");
+ assert.strictEqual($firstLine.find('.o_field_monetary').text().trim(), "3.00€",
+ "should have '3.00€' as the line's amount");
+
+ assert.containsOnce($kanbanWidget, '.o_lunch_widget_info:eq(2)',
+ "should have a third column");
+
+ const $widgetThirdColumn = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(2)');
+
+ assert.containsOnce($widgetThirdColumn, '.o_field_monetary',
+ "should have an account balance");
+ assert.strictEqual($widgetThirdColumn.find('.o_field_monetary').text().trim(), "3.00€",
+ "should have '3.00€' in the account balance");
+ assert.containsOnce($widgetThirdColumn, '.o_lunch_widget_order_button',
+ "should have a button to validate the order");
+ assert.strictEqual($widgetThirdColumn.find('.o_lunch_widget_order_button').text().trim(), "Order now",
+ "should have 'Order now' as the validate order button text");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('ordered cart', async function (assert) {
+ assert.expect(15);
+
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: mockLunchRPC({
+ infos: Object.assign({}, this.regularInfos, {
+ raw_state: "ordered",
+ state: "Ordered",
+ lines: [
+ {
+ product: [1, "Tuna sandwich", "3.00"],
+ toppings: [],
+ quantity: 1.0,
+ },
+ ],
+ }),
+ userLocation: this.data['lunch.location'].records[0].id,
+ }),
+ });
+
+ const $kanbanWidget = kanban.$('.o_lunch_widget');
+
+ assert.containsN($kanbanWidget, '> .o_lunch_widget_info', 3,
+ "should have 3 columns");
+
+ assert.containsOnce($kanbanWidget, '.o_lunch_widget_info:eq(1)',
+ "should have a second column");
+
+ const $widgetSecondColumn = $kanbanWidget.find('.o_lunch_widget_info:eq(1)');
+
+ assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_unlink',
+ "should have a button to clear the order");
+ assert.containsOnce($widgetSecondColumn, '.badge.badge-warning.o_lunch_ordered',
+ "should have an ordered state badge");
+ assert.strictEqual($widgetSecondColumn.find('.o_lunch_ordered').text().trim(), "Ordered",
+ "should have 'Ordered' in the state badge");
+
+ assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_lines > li',
+ "should have 1 order line");
+
+ const $firstLine = $widgetSecondColumn.find('.o_lunch_widget_lines > li:first');
+ assert.containsOnce($firstLine, 'button.o_remove_product',
+ "should have a button to remove a product quantity on each line");
+ assert.containsOnce($firstLine, 'button.o_add_product',
+ "should have a button to add a product quantity on each line");
+ assert.containsOnce($firstLine, '.o_lunch_product_quantity > :eq(1)',
+ "should have the line's quantity");
+ assert.strictEqual($firstLine.find('.o_lunch_product_quantity > :eq(1)').text().trim(), "1",
+ "should have 1 as the line's quantity");
+ assert.containsOnce($firstLine, '.o_lunch_open_wizard',
+ "should have the line's product name to open the wizard");
+ assert.strictEqual($firstLine.find('.o_lunch_open_wizard').text().trim(), "Tuna sandwich",
+ "should have 'Tuna sandwich' as the line's product name");
+ assert.containsOnce($firstLine, '.o_field_monetary',
+ "should have the line's amount");
+ assert.strictEqual($firstLine.find('.o_field_monetary').text().trim(), "3.00€",
+ "should have '3.00€' as the line's amount");
+
+ assert.strictEqual($kanbanWidget.find('> .o_lunch_widget_info:eq(2)').html().trim(), "",
+ "third column should be empty");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('confirmed cart', async function (assert) {
+ assert.expect(15);
+
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: mockLunchRPC({
+ infos: Object.assign({}, this.regularInfos, {
+ raw_state: "confirmed",
+ state: "Received",
+ lines: [
+ {
+ product: [1, "Tuna sandwich", "3.00"],
+ toppings: [],
+ quantity: 1.0,
+ },
+ ],
+ }),
+ userLocation: this.data['lunch.location'].records[0].id,
+ }),
+ });
+
+ const $kanbanWidget = kanban.$('.o_lunch_widget');
+
+ assert.containsN($kanbanWidget, '> .o_lunch_widget_info', 3,
+ "should have 3 columns");
+
+ assert.containsOnce($kanbanWidget, '.o_lunch_widget_info:eq(1)',
+ "should have a second column");
+
+ const $widgetSecondColumn = $kanbanWidget.find('.o_lunch_widget_info:eq(1)');
+
+ assert.containsNone($widgetSecondColumn, '.o_lunch_widget_unlink',
+ "shouldn't have a button to clear the order");
+ assert.containsOnce($widgetSecondColumn, '.badge.badge-success.o_lunch_confirmed',
+ "should have a confirmed state badge");
+ assert.strictEqual($widgetSecondColumn.find('.o_lunch_confirmed').text().trim(), "Received",
+ "should have 'Received' in the state badge");
+
+ assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_lines > li',
+ "should have 1 order line");
+
+ const $firstLine = $widgetSecondColumn.find('.o_lunch_widget_lines > li:first');
+ assert.containsNone($firstLine, 'button.o_remove_product',
+ "shouldn't have a button to remove a product quantity on each line");
+ assert.containsNone($firstLine, 'button.o_add_product',
+ "shouldn't have a button to add a product quantity on each line");
+ assert.containsOnce($firstLine, '.o_lunch_product_quantity',
+ "should have the line's quantity");
+ assert.strictEqual($firstLine.find('.o_lunch_product_quantity').text().trim(), "1",
+ "should have 1 as the line's quantity");
+ assert.containsOnce($firstLine, '.o_lunch_open_wizard',
+ "should have the line's product name to open the wizard");
+ assert.strictEqual($firstLine.find('.o_lunch_open_wizard').text().trim(), "Tuna sandwich",
+ "should have 'Tuna sandwich' as the line's product name");
+ assert.containsOnce($firstLine, '.o_field_monetary',
+ "should have the line's amount");
+ assert.strictEqual($firstLine.find('.o_field_monetary').text().trim(), "3.00€",
+ "should have '3.00€' as the line's amount");
+
+ assert.strictEqual($kanbanWidget.find('> .o_lunch_widget_info:eq(2)').html().trim(), "",
+ "third column should be empty");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('regular user', async function (assert) {
+ assert.expect(11);
+
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: mockLunchRPC({
+ infos: this.regularInfos,
+ userLocation: this.data['lunch.location'].records[0].id,
+ }),
+ });
+
+ const $kanbanWidget = kanban.$('.o_lunch_widget');
+
+ assert.containsOnce($kanbanWidget, '.o_lunch_widget_info:first',
+ "should have a first column");
+
+ const $widgetFirstColumn = $kanbanWidget.find('.o_lunch_widget_info:first');
+
+ assert.containsOnce($widgetFirstColumn, 'img.rounded-circle',
+ "should have a rounded avatar image");
+
+ assert.containsOnce($widgetFirstColumn, '.o_lunch_user_field',
+ "should have a user field");
+ assert.containsNone($widgetFirstColumn, '.o_lunch_user_field > .o_field_widget',
+ "shouldn't have a field widget in the user field");
+ assert.strictEqual($widgetFirstColumn.find('.o_lunch_user_field').text().trim(), "Marc Demo",
+ "should have 'Marc Demo' in the user field");
+
+ assert.containsOnce($widgetFirstColumn, '.o_lunch_location_field',
+ "should have a location field");
+ assert.containsOnce($widgetFirstColumn, '.o_lunch_location_field > .o_field_many2one[name="locations"]',
+ "should have a many2one in the location field");
+
+ await testUtils.fields.many2one.clickOpenDropdown('locations');
+ const $input = $widgetFirstColumn.find('.o_field_many2one[name="locations"] input');
+ assert.containsN($input.autocomplete('widget'), 'li', 2,
+ "autocomplete dropdown should have 2 entries");
+ assert.strictEqual($input.val(), "Office 2",
+ "locations input should have 'Office 2' as value");
+
+ assert.containsOnce($widgetFirstColumn, '.o_lunch_location_field + div',
+ "should have an account balance");
+ assert.strictEqual($widgetFirstColumn.find('.o_lunch_location_field + div .o_field_monetary').text().trim(), "36.50€",
+ "should have '36.50€' in the account balance");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('manager user', async function (assert) {
+ assert.expect(12);
+
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: mockLunchRPC({
+ infos: this.managerInfos,
+ userLocation: this.data['lunch.location'].records[0].id,
+ }),
+ });
+
+ const $kanbanWidget = kanban.$('.o_lunch_widget');
+
+ assert.containsOnce($kanbanWidget, '.o_lunch_widget_info:first',
+ "should have a first column");
+
+ const $widgetFirstColumn = $kanbanWidget.find('.o_lunch_widget_info:first');
+
+ assert.containsOnce($widgetFirstColumn, 'img.rounded-circle',
+ "should have a rounded avatar image");
+
+ assert.containsOnce($widgetFirstColumn, '.o_lunch_user_field',
+ "should have a user field");
+ assert.containsOnce($widgetFirstColumn, '.o_lunch_user_field > .o_field_many2one[name="users"]',
+ "shouldn't have a field widget in the user field");
+
+ await testUtils.fields.many2one.clickOpenDropdown('users');
+ const $userInput = $widgetFirstColumn.find('.o_field_many2one[name="users"] input');
+ assert.containsN($userInput.autocomplete('widget'), 'li', 2,
+ "users autocomplete dropdown should have 2 entries");
+ assert.strictEqual($userInput.val(), "Mitchell Admin",
+ "should have 'Mitchell Admin' as value in user field");
+
+ assert.containsOnce($widgetFirstColumn, '.o_lunch_location_field',
+ "should have a location field");
+ assert.containsOnce($widgetFirstColumn, '.o_lunch_location_field > .o_field_many2one[name="locations"]',
+ "should have a many2one in the location field");
+
+ await testUtils.fields.many2one.clickOpenDropdown('locations');
+ const $locationInput = $widgetFirstColumn.find('.o_field_many2one[name="locations"] input');
+ assert.containsN($locationInput.autocomplete('widget'), 'li', 2,
+ "locations autocomplete dropdown should have 2 entries");
+ assert.strictEqual($locationInput.val(), "Office 2",
+ "should have 'Office 2' as value");
+
+ assert.containsOnce($widgetFirstColumn, '.o_lunch_location_field + div',
+ "should have an account balance");
+ assert.strictEqual($widgetFirstColumn.find('.o_lunch_location_field + div .o_field_monetary').text().trim(), "47.60€",
+ "should have '47.60€' in the account balance");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('add a product', async function (assert) {
+ assert.expect(1);
+
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: mockLunchRPC({
+ infos: this.regularInfos,
+ userLocation: this.data['lunch.location'].records[0].id,
+ }),
+ intercepts: {
+ do_action: function (ev) {
+ assert.deepEqual(ev.data.action, {
+ name: "Configure Your Order",
+ res_model: 'lunch.order',
+ type: 'ir.actions.act_window',
+ views: [[false, 'form']],
+ target: 'new',
+ context: {
+ default_product_id: 1,
+ },
+ },
+ "should open the wizard");
+ },
+ },
+ });
+
+ await testUtils.dom.click(kanban.$('.o_kanban_record:first'));
+
+ kanban.destroy();
+ });
+
+ QUnit.test('add product quantity', async function (assert) {
+ assert.expect(3);
+
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: Object.assign({}, this.data, {
+ 'lunch.order': {
+ fields: {},
+ update_quantity([lineIds, increment]) {
+ assert.deepEqual(lineIds, [6], "should have [6] as lineId to update quantity");
+ assert.strictEqual(increment, 1, "should have +1 as increment to update quantity");
+ return Promise.resolve();
+ },
+ },
+ }),
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: mockLunchRPC({
+ infos: Object.assign({}, this.regularInfos, {
+ lines: [
+ {
+ id: 6,
+ product: [1, "Tuna sandwich", "3.00"],
+ toppings: [],
+ quantity: 1.0,
+ },
+ ],
+ }),
+ userLocation: this.data['lunch.location'].records[0].id,
+ }),
+ });
+
+ const $widgetSecondColumn = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(1)');
+
+ assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_lines > li',
+ "should have 1 order line");
+
+ const $firstLine = $widgetSecondColumn.find('.o_lunch_widget_lines > li:first');
+
+ await testUtils.dom.click($firstLine.find('button.o_add_product'));
+
+ kanban.destroy();
+ });
+
+ QUnit.test('remove product quantity', async function (assert) {
+ assert.expect(3);
+
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: Object.assign({}, this.data, {
+ 'lunch.order': {
+ fields: {},
+ update_quantity([lineIds, increment]) {
+ assert.deepEqual(lineIds, [6], "should have [6] as lineId to update quantity");
+ assert.strictEqual(increment, -1, "should have -1 as increment to update quantity");
+ return Promise.resolve();
+ },
+ },
+ }),
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: mockLunchRPC({
+ infos: Object.assign({}, this.regularInfos, {
+ lines: [
+ {
+ id: 6,
+ product: [1, "Tuna sandwich", "3.00"],
+ toppings: [],
+ quantity: 1.0,
+ },
+ ],
+ }),
+ userLocation: this.data['lunch.location'].records[0].id,
+ }),
+ });
+
+ const $widgetSecondColumn = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(1)');
+
+ assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_lines > li',
+ "should have 1 order line");
+
+ const $firstLine = $widgetSecondColumn.find('.o_lunch_widget_lines > li:first');
+
+ await testUtils.dom.click($firstLine.find('button.o_remove_product'));
+
+ kanban.destroy();
+ });
+
+ QUnit.test('clear order', async function (assert) {
+ assert.expect(1);
+
+ const self = this;
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: function (route) {
+ if (route.startsWith('/lunch')) {
+ if (route === '/lunch/trash') {
+ assert.ok('should perform clear order RPC call');
+ return Promise.resolve();
+ }
+ return mockLunchRPC({
+ infos: Object.assign({}, self.regularInfos, {
+ lines: [
+ {
+ product: [1, "Tuna sandwich", "3.00"],
+ toppings: [],
+ },
+ ],
+ }),
+ userLocation: self.data['lunch.location'].records[0].id,
+ }).apply(this, arguments);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ const $widgetSecondColumn = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(1)');
+
+ await testUtils.dom.click($widgetSecondColumn.find('button.o_lunch_widget_unlink'));
+
+ kanban.destroy();
+ });
+
+ QUnit.test('validate order: success', async function (assert) {
+ assert.expect(1);
+
+ const self = this;
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: function (route) {
+ if (route.startsWith('/lunch')) {
+ if (route === '/lunch/pay') {
+ assert.ok("should perform pay order RPC call");
+ return Promise.resolve(true);
+ }
+ return mockLunchRPC({
+ infos: Object.assign({}, self.regularInfos, {
+ lines: [
+ {
+ product: [1, "Tuna sandwich", "3.00"],
+ toppings: [],
+ },
+ ],
+ }),
+ userLocation: self.data['lunch.location'].records[0].id,
+ }).apply(this, arguments);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ const $widgetThirdColumn = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(2)');
+
+ await testUtils.dom.click($widgetThirdColumn.find('button.o_lunch_widget_order_button'));
+
+ kanban.destroy();
+ });
+
+ QUnit.test('validate order: failure', async function (assert) {
+ assert.expect(5);
+
+ const self = this;
+ const kanban = await createLunchView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ `,
+ mockRPC: function (route) {
+ if (route.startsWith('/lunch')) {
+ if (route === '/lunch/pay') {
+ assert.ok('should perform pay order RPC call');
+ return Promise.resolve(false);
+ }
+ if (route === '/lunch/payment_message') {
+ assert.ok('should perform payment message RPC call');
+ return Promise.resolve({ message: 'This is a payment message.'});
+ }
+ return mockLunchRPC({
+ infos: Object.assign({}, self.regularInfos, {
+ lines: [
+ {
+ product: [1, "Tuna sandwich", "3.00"],
+ toppings: [],
+ },
+ ],
+ }),
+ userLocation: self.data['lunch.location'].records[0].id,
+ }).apply(this, arguments);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ const $widgetThirdColumn = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(2)');
+
+ await testUtils.dom.click($widgetThirdColumn.find('button.o_lunch_widget_order_button'));
+
+ assert.containsOnce(document.body, '.modal', "should open a Dialog box");
+ assert.strictEqual($('.modal-title').text().trim(),
+ "Not enough money in your wallet", "should have a Dialog's title");
+ assert.strictEqual($('.modal-body').text().trim(),
+ "This is a payment message.", "should have a Dialog's message");
+
+ kanban.destroy();
+ });
+ });
+});
+
+});
diff --git a/addons/lunch/static/tests/lunch_list_tests.js b/addons/lunch/static/tests/lunch_list_tests.js
new file mode 100644
index 00000000..cca644a5
--- /dev/null
+++ b/addons/lunch/static/tests/lunch_list_tests.js
@@ -0,0 +1,267 @@
+odoo.define('lunch.lunchListTests', function (require) {
+"use strict";
+
+const LunchListView = require('lunch.LunchListView');
+
+const testUtils = require('web.test_utils');
+const {createLunchView, mockLunchRPC} = require('lunch.test_utils');
+
+QUnit.module('Views');
+
+QUnit.module('LunchListView', {
+ beforeEach() {
+ const PORTAL_GROUP_ID = 1234;
+
+ this.data = {
+ 'product': {
+ fields: {
+ is_available_at: {string: 'Product Availability', type: 'many2one', relation: 'lunch.location'},
+ category_id: {string: 'Product Category', type: 'many2one', relation: 'lunch.product.category'},
+ supplier_id: {string: 'Vendor', type: 'many2one', relation: 'lunch.supplier'},
+ },
+ records: [
+ {id: 1, name: 'Tuna sandwich', is_available_at: 1},
+ ],
+ },
+ 'lunch.order': {
+ fields: {},
+ update_quantity() {
+ return Promise.resolve();
+ },
+ },
+ 'lunch.product.category': {
+ fields: {},
+ records: [],
+ },
+ 'lunch.supplier': {
+ fields: {},
+ records: [],
+ },
+ 'ir.model.data': {
+ fields: {},
+ xmlid_to_res_id() {
+ return Promise.resolve(PORTAL_GROUP_ID);
+ },
+ },
+ 'lunch.location': {
+ fields: {
+ name: {string: 'Name', type: 'char'},
+ },
+ records: [
+ {id: 1, name: "Office 1"},
+ {id: 2, name: "Office 2"},
+ ],
+ },
+ 'res.users': {
+ fields: {
+ name: {string: 'Name', type: 'char'},
+ groups_id: {string: 'Groups', type: 'many2many'},
+ },
+ records: [
+ {id: 1, name: "Mitchell Admin", groups_id: []},
+ {id: 2, name: "Marc Demo", groups_id: []},
+ {id: 3, name: "Jean-Luc Portal", groups_id: [PORTAL_GROUP_ID]},
+ ],
+ },
+ };
+ this.regularInfos = {
+ username: "Marc Demo",
+ wallet: 36.5,
+ is_manager: false,
+ currency: {
+ symbol: "\u20ac",
+ position: "after"
+ },
+ user_location: [2, "Office 2"],
+ };
+ },
+}, function () {
+ QUnit.test('basic rendering', async function (assert) {
+ assert.expect(6);
+
+ const list = await createLunchView({
+ View: LunchListView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <tree>
+ <field name="name"/>
+ </tree>
+ `,
+ mockRPC: mockLunchRPC({
+ infos: this.regularInfos,
+ userLocation: this.data['lunch.location'].records[0].id,
+ }),
+ });
+
+ // check view layout
+ assert.containsN(list, '.o_content > div', 2,
+ "should have 2 columns");
+ assert.containsOnce(list, '.o_content > div.o_search_panel',
+ "should have a 'lunch filters' column");
+ assert.containsOnce(list, '.o_content > .o_lunch_content',
+ "should have a 'lunch wrapper' column");
+ assert.containsOnce(list, '.o_lunch_content > .o_list_view',
+ "should have a 'classical list view' column");
+ assert.hasClass(list.$('.o_list_view'), 'o_lunch_list_view',
+ "should have classname 'o_lunch_list_view'");
+ assert.containsOnce(list, '.o_lunch_content > span > .o_lunch_banner',
+ "should have a 'lunch' banner");
+
+ list.destroy();
+ });
+
+ QUnit.module('LunchWidget', function () {
+
+ QUnit.test('search panel domain location', async function (assert) {
+ assert.expect(20);
+ let expectedLocation = 1;
+ let locationId = this.data['lunch.location'].records[0].id;
+ const regularInfos = _.extend({}, this.regularInfos);
+
+ const list = await createLunchView({
+ View: LunchListView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <tree>
+ <field name="name"/>
+ </tree>
+ `,
+ mockRPC: function (route, args) {
+ assert.step(route);
+
+ if (route.startsWith('/lunch')) {
+ if (route === '/lunch/user_location_set') {
+ locationId = args.location_id;
+ return Promise.resolve(true);
+ }
+ return mockLunchRPC({
+ infos: regularInfos,
+ userLocation: locationId,
+ }).apply(this, arguments);
+ }
+ if (args.method === 'search_panel_select_multi_range') {
+ assert.deepEqual(args.kwargs.search_domain, [["is_available_at", "in", [expectedLocation]]],
+ 'The initial domain of the search panel must contain the user location');
+ }
+ if (route === '/web/dataset/search_read') {
+ assert.deepEqual(args.domain, [["is_available_at", "in", [expectedLocation]]],
+ 'The domain for fetching actual data should be correct');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ expectedLocation = 2;
+ await testUtils.fields.many2one.clickOpenDropdown('locations');
+ await testUtils.fields.many2one.clickItem('locations', "Office 2");
+
+ assert.verifySteps([
+ // Initial state
+ '/lunch/user_location_get',
+ '/web/dataset/call_kw/product/search_panel_select_multi_range',
+ '/web/dataset/call_kw/product/search_panel_select_multi_range',
+ '/web/dataset/search_read',
+ '/lunch/infos',
+ '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id',
+ // Click m2o
+ '/web/dataset/call_kw/lunch.location/name_search',
+ // Click new location
+ '/lunch/user_location_set',
+ '/web/dataset/call_kw/product/search_panel_select_multi_range',
+ '/web/dataset/call_kw/product/search_panel_select_multi_range',
+ '/web/dataset/search_read',
+ '/lunch/infos',
+ '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id',
+ ]);
+
+ list.destroy();
+ });
+
+ QUnit.test('search panel domain location false: fetch products in all locations', async function (assert) {
+ assert.expect(10);
+ const regularInfos = _.extend({}, this.regularInfos);
+
+ const list = await createLunchView({
+ View: LunchListView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <tree>
+ <field name="name"/>
+ </tree>
+ `,
+ mockRPC: function (route, args) {
+ assert.step(route);
+
+ if (route.startsWith('/lunch')) {
+ return mockLunchRPC({
+ infos: regularInfos,
+ userLocation: false,
+ }).apply(this, arguments);
+ }
+ if (args.method === 'search_panel_select_multi_range') {
+ assert.deepEqual(args.kwargs.search_domain, [],
+ 'The domain should not exist since the location is false.');
+ }
+ if (route === '/web/dataset/search_read') {
+ assert.deepEqual(args.domain, [],
+ 'The domain for fetching actual data should be correct');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+ assert.verifySteps([
+ '/lunch/user_location_get',
+ '/web/dataset/call_kw/product/search_panel_select_multi_range',
+ '/web/dataset/call_kw/product/search_panel_select_multi_range',
+ '/web/dataset/search_read',
+ '/lunch/infos',
+ '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id',
+ ])
+
+ list.destroy();
+ });
+
+ QUnit.test('add a product', async function (assert) {
+ assert.expect(1);
+
+ const list = await createLunchView({
+ View: LunchListView,
+ model: 'product',
+ data: this.data,
+ arch: `
+ <tree>
+ <field name="name"/>
+ </tree>
+ `,
+ mockRPC: mockLunchRPC({
+ infos: this.regularInfos,
+ userLocation: this.data['lunch.location'].records[0].id,
+ }),
+ intercepts: {
+ do_action: function (ev) {
+ assert.deepEqual(ev.data.action, {
+ name: "Configure Your Order",
+ res_model: 'lunch.order',
+ type: 'ir.actions.act_window',
+ views: [[false, 'form']],
+ target: 'new',
+ context: {
+ default_product_id: 1,
+ },
+ },
+ "should open the wizard");
+ },
+ },
+ });
+
+ await testUtils.dom.click(list.$('.o_data_row:first'));
+
+ list.destroy();
+ });
+ });
+});
+
+});
diff --git a/addons/lunch/static/tests/lunch_test_utils.js b/addons/lunch/static/tests/lunch_test_utils.js
new file mode 100644
index 00000000..baa7f189
--- /dev/null
+++ b/addons/lunch/static/tests/lunch_test_utils.js
@@ -0,0 +1,59 @@
+odoo.define('lunch.test_utils', function (require) {
+"use strict";
+
+const AbstractStorageService = require('web.AbstractStorageService');
+const RamStorage = require('web.RamStorage');
+const {createView} = require('web.test_utils');
+
+/**
+ * Helper to create a lunch view with searchpanel
+ *
+ * @param {object} params
+ */
+async function createLunchView(params) {
+ params.archs = params.archs || {};
+ var searchArch = params.archs[`${params.model},false,search`] || '<search></search>';
+ var searchPanelArch = `
+ <searchpanel>
+ <field name="category_id" select="multi" string="Categories" enable_counters="1"/>
+ <field name="supplier_id" select="multi" string="Vendors" enable_counters="1"/>
+ </searchpanel>
+ `;
+ searchArch = searchArch.split('</search>')[0] + searchPanelArch + '</search>';
+ params.archs[`${params.model},false,search`] = searchArch;
+ if (!params.services || !params.services.local_storage) {
+ // the searchPanel uses the localStorage to store/retrieve default
+ // active category value
+ params.services = params.services || {};
+ const RamStorageService = AbstractStorageService.extend({
+ storage: new RamStorage(),
+ });
+ params.services.local_storage = RamStorageService;
+ }
+ return createView(params);
+}
+
+/**
+ * Helper to generate a mockRPC function for the mandatory lunch routes (prefixed by '/lunch')
+ *
+ * @param {object} infos
+ * @param {integer} userLocation
+ */
+function mockLunchRPC({infos, userLocation}) {
+ return async function (route) {
+ if (route === '/lunch/infos') {
+ return Promise.resolve(infos);
+ }
+ if (route === '/lunch/user_location_get') {
+ return Promise.resolve(userLocation);
+ }
+ return this._super.apply(this, arguments);
+ };
+}
+
+return {
+ createLunchView,
+ mockLunchRPC,
+};
+
+});