summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/lunch/static/src/js/lunch_kanban_controller.js47
-rw-r--r--addons/lunch/static/src/js/lunch_kanban_mobile.js74
-rw-r--r--addons/lunch/static/src/js/lunch_kanban_record.js46
-rw-r--r--addons/lunch/static/src/js/lunch_kanban_view.js7
-rw-r--r--addons/lunch/static/src/js/lunch_kanban_widget.js13
-rw-r--r--addons/lunch/static/src/js/lunch_payment_dialog.js6
-rw-r--r--addons/lunch/static/src/scss/lunch_kanban.scss63
-rw-r--r--addons/lunch/static/src/xml/lunch_kanban.xml146
-rw-r--r--addons/lunch/static/tests/lunch_kanban_mobile_tests.js208
-rw-r--r--addons/lunch/static/tests/lunch_kanban_tests.js821
-rw-r--r--addons/lunch/static/tests/lunch_test_utils.js58
-rw-r--r--addons/lunch/static/tests/test_lunch_kanban.js259
-rw-r--r--addons/lunch/views/lunch_templates.xml13
-rw-r--r--addons/web/static/src/scss/search_view_mobile.scss3
14 files changed, 1335 insertions, 429 deletions
diff --git a/addons/lunch/static/src/js/lunch_kanban_controller.js b/addons/lunch/static/src/js/lunch_kanban_controller.js
index 8055f876d2e..5545d926be3 100644
--- a/addons/lunch/static/src/js/lunch_kanban_controller.js
+++ b/addons/lunch/static/src/js/lunch_kanban_controller.js
@@ -18,11 +18,9 @@ var LunchKanbanController = KanbanController.extend({
add_product: '_onAddProduct',
change_location: '_onLocationChanged',
change_user: '_onUserChanged',
- edit_order: '_onEditOrder',
open_wizard: '_onOpenWizard',
order_now: '_onOrderNow',
remove_product: '_onRemoveProduct',
- save_order: '_onSaveOrder',
unlink_order: '_onUnlinkOrder',
}),
@@ -69,7 +67,23 @@ var LunchKanbanController = KanbanController.extend({
},
}).then(function (data) {
self.widgetData = data;
- self.model._updateLocation(data.user_location[0]);
+ return self.model._updateLocation(data.user_location[0]);
+ });
+ },
+ /**
+ * Renders and appends the lunch banner widget.
+ *
+ * @private
+ */
+ _renderLunchKanbanWidget: function () {
+ var self = this;
+ if (this.widget) {
+ this.widget.destroy();
+ }
+ this.widgetData.wallet = parseFloat(this.widgetData.wallet).toFixed(2);
+ this.widget = new LunchKanbanWidget(this, _.extend(this.widgetData, {edit: this.editMode}));
+ return this.widget.appendTo(document.createDocumentFragment()).then(function () {
+ self.$('.o_lunch_kanban').prepend(self.widget.$el);
});
},
_showPaymentDialog: function (title) {
@@ -91,19 +105,8 @@ var LunchKanbanController = KanbanController.extend({
* @private
*/
_update: function () {
- var self = this;
-
- var def = this._fetchWidgetData().then(function () {
- if (self.widget) {
- self.widget.destroy();
- }
- self.widgetData.wallet = parseFloat(self.widgetData.wallet).toFixed(2);
- self.widget = new LunchKanbanWidget(self, _.extend(self.widgetData, {edit: self.editMode}));
- return self.widget.appendTo(document.createDocumentFragment()).then(function () {
- self.$('.o_lunch_kanban').prepend(self.widget.$el);
- });
- });
- return $.when(def, this._super.apply(self, arguments));
+ var def = this._fetchWidgetData().then(this._renderLunchKanbanWidget.bind(this));
+ return $.when(def, this._super.apply(this, arguments));
},
/**
* Override to add the location domain (coming from the lunchKanbanWidget)
@@ -134,12 +137,6 @@ var LunchKanbanController = KanbanController.extend({
self.reload();
});
},
- _onEditOrder: function (ev) {
- ev.stopPropagation();
-
- this.editMode = true;
- this.reload();
- },
_onLocationChanged: function (ev) {
var self = this;
@@ -208,12 +205,6 @@ var LunchKanbanController = KanbanController.extend({
self.reload();
});
},
- _onSaveOrder: function (ev) {
- ev.stopPropagation();
-
- this.editMode = false;
- this.reload();
- },
_onUserChanged: function (ev) {
ev.stopPropagation();
diff --git a/addons/lunch/static/src/js/lunch_kanban_mobile.js b/addons/lunch/static/src/js/lunch_kanban_mobile.js
new file mode 100644
index 00000000000..eddd22ac351
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_kanban_mobile.js
@@ -0,0 +1,74 @@
+odoo.define('lunch.LunchKanbanMobile', function (require) {
+"use strict";
+
+var config = require('web.config');
+var LunchKanbanWidget = require('lunch.LunchKanbanWidget');
+var LunchKanbanController = require('lunch.LunchKanbanController');
+
+if (!config.device.isMobile) {
+ return;
+}
+
+LunchKanbanWidget.include({
+ template: "LunchKanbanWidgetMobile",
+
+ /**
+ * 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;
+ },
+});
+
+LunchKanbanController.include({
+ /**
+ * @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
+ */
+ _renderLunchKanbanWidget: 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);
+ },
+});
+
+});
diff --git a/addons/lunch/static/src/js/lunch_kanban_record.js b/addons/lunch/static/src/js/lunch_kanban_record.js
index a0d60588fae..ce9272b34f0 100644
--- a/addons/lunch/static/src/js/lunch_kanban_record.js
+++ b/addons/lunch/static/src/js/lunch_kanban_record.js
@@ -1,22 +1,36 @@
odoo.define('lunch.LunchKanbanRecord', function (require) {
-"use strict";
+ "use strict";
-/**
- * This file defines the KanbanRecord for the Lunch Kanban view.
- */
+ /**
+ * This file defines the KanbanRecord for the Lunch Kanban view.
+ */
-var KanbanRecord = require('web.KanbanRecord');
+ var KanbanRecord = require('web.KanbanRecord');
-var LunchKanbanRecord = KanbanRecord.extend({
- _onGlobalClick: 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});
- }
- },
-});
+ var LunchKanbanRecord = KanbanRecord.extend({
+ events: _.extend({}, KanbanRecord.prototype.events, {
+ 'click': '_onSelectRecord',
+ }),
-return LunchKanbanRecord;
+ //--------------------------------------------------------------------------
+ // 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});
+ }
+ },
+ });
+
+ return LunchKanbanRecord;
+
+ });
diff --git a/addons/lunch/static/src/js/lunch_kanban_view.js b/addons/lunch/static/src/js/lunch_kanban_view.js
index 72b30634e87..a0cd6d3f3e0 100644
--- a/addons/lunch/static/src/js/lunch_kanban_view.js
+++ b/addons/lunch/static/src/js/lunch_kanban_view.js
@@ -5,19 +5,12 @@ var LunchKanbanController = require('lunch.LunchKanbanController');
var LunchKanbanModel = require('lunch.LunchKanbanModel');
var LunchKanbanRenderer = require('lunch.LunchKanbanRenderer');
-var config = require('web.config');
var core = require('web.core');
var KanbanView = require('web.KanbanView');
var view_registry = require('web.view_registry');
var _lt = core._lt;
-if (config.device.isMobile) {
- // use the classical KanbanView in mobile
- view_registry.add('lunch_kanban', KanbanView);
- return;
-}
-
var LunchKanbanView = KanbanView.extend({
config: _.extend({}, KanbanView.prototype.config, {
Controller: LunchKanbanController,
diff --git a/addons/lunch/static/src/js/lunch_kanban_widget.js b/addons/lunch/static/src/js/lunch_kanban_widget.js
index 73721c6def1..7bad4168b06 100644
--- a/addons/lunch/static/src/js/lunch_kanban_widget.js
+++ b/addons/lunch/static/src/js/lunch_kanban_widget.js
@@ -23,11 +23,9 @@ var LunchKanbanWidget = Widget.extend({
field_changed: '_onFieldChanged',
},
events: {
- 'click .o_add_money': '_onAddMoney',
'click .o_add_product': '_onAddProduct',
'click .o_lunch_widget_order_button': '_onOrderNow',
'click .o_remove_product': '_onRemoveProduct',
- 'click .o_lunch_widget_save': '_onSaveOrder',
'click .o_lunch_widget_unlink': '_onUnlinkOrder',
'click .o_lunch_open_wizard': '_onLunchOpenWizard',
},
@@ -124,11 +122,6 @@ var LunchKanbanWidget = Widget.extend({
// Handlers
//--------------------------------------------------------------------------
- _onAddMoney: function (ev) {
- ev.preventDefault();
- ev.stopPropagation();
- this.trigger_up('add_money', {});
- },
_onAddProduct: function (ev) {
ev.preventDefault();
ev.stopPropagation();
@@ -163,12 +156,6 @@ var LunchKanbanWidget = Widget.extend({
this.trigger_up('remove_product', {lineId: $(ev.currentTarget).data('id')});
},
- _onSaveOrder: function (ev) {
- ev.preventDefault();
- ev.stopPropagation();
-
- this.trigger_up('save_order', {});
- },
_onUnlinkOrder: function (ev) {
ev.preventDefault();
ev.stopPropagation();
diff --git a/addons/lunch/static/src/js/lunch_payment_dialog.js b/addons/lunch/static/src/js/lunch_payment_dialog.js
index 00e49299189..ae624403f12 100644
--- a/addons/lunch/static/src/js/lunch_payment_dialog.js
+++ b/addons/lunch/static/src/js/lunch_payment_dialog.js
@@ -1,19 +1,15 @@
odoo.define('lunch.LunchPaymentDialog', function (require) {
"use strict";
-var core = require('web.core');
var Dialog = require('web.Dialog');
-var qweb = core.qweb;
-
var LunchPaymentDialog = Dialog.extend({
template: 'lunch.LunchPaymentDialog',
init: function (parent, options) {
- var self = this;
this._super.apply(this, arguments);
- options = (options || {});
+ options = options || {};
this.message = options.message || '';
},
diff --git a/addons/lunch/static/src/scss/lunch_kanban.scss b/addons/lunch/static/src/scss/lunch_kanban.scss
index 7c50ae36358..51acfd2b3f8 100644
--- a/addons/lunch/static/src/scss/lunch_kanban.scss
+++ b/addons/lunch/static/src/scss/lunch_kanban.scss
@@ -6,6 +6,8 @@
height: 100%;
.o_lunch_kanban_banner {
flex: 0 0 auto;
+ border-bottom: 1px solid #CED4DA;
+ background-color: white;
}
.o_kanban_view {
flex: 1 1 100%;
@@ -19,52 +21,47 @@
}
.o_lunch_widget {
- display: flex;
- border-bottom: 1px solid #CED4DA;
- background-color: white;
min-height: 90px;
max-height: 33vh;
overflow-y: auto;
- .o_lunch_widget_info {
- padding: 5px 12px;
- margin: 4px 8px;
- width: 300px;
- flex: 1 1 auto;
- overflow-y: auto;
-
- .o_lunch_ordered {
- color: white;
- background-color: #F0D970;
- padding: 1px 3px;
- border-radius: .25em;
- }
-
- .o_lunch_confirmed {
- color: white;
- background-color: #00BA4E;
- padding: 1px 3px;
- border-radius: .25em;
+ .o_lunch_widget_info.card {
+ &, .card-title, .card-body {
+ color: $o-main-text-color;
+ background-color: inherit !important;
}
- .o_lunch_widget_title {
+ .card-title {
font-weight: bold;
- font-size: initial;
+ margin-bottom: 0;
}
- .o_lunch_widget_order_button {
- display: block;
- width: 100%;
- margin-bottom: 4px;
- margin-top: 12px;
+ .card-body {
+ padding: 0.5rem 1rem;
}
- .o_lunch_open_wizard {
- cursor: pointer;
- &:hover {
- font-weight: bolder;
+ .btn-link {
+ padding: 0;
+ &.o_lunch_open_wizard {
+ color: $o-main-text-color;
+ font-weight: normal;
}
}
}
}
}
+
+@include media-breakpoint-down(sm) {
+ .o_lunch_kanban {
+ 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%
+ }
+ }
+}
diff --git a/addons/lunch/static/src/xml/lunch_kanban.xml b/addons/lunch/static/src/xml/lunch_kanban.xml
index 62a2edd2900..3b3b319ce95 100644
--- a/addons/lunch/static/src/xml/lunch_kanban.xml
+++ b/addons/lunch/static/src/xml/lunch_kanban.xml
@@ -1,84 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
- <div t-name="LunchKanbanWidget" class="o_lunch_kanban_banner">
- <div class="o_lunch_widget">
- <div class="o_lunch_widget_info d-flex flex-row">
- <div>
- <img class="rounded-circle" t-attf-src="{{ widget.userimage }}"/>
- </div>
- <div class="w-50 ml-2 py-3">
- <div class="o_lunch_user_field"/>
- <div class="o_lunch_location_field"/>
- <div class="d-flex flex-row">
- <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 t-name="LunchKanbanWidget" class="o_lunch_kanban_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-4">
+ <img class="rounded-circle" t-attf-src="{{ widget.userimage }}"/>
+ </div>
+ <div class="col-9 col-md-6 col-lg-8">
+ <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">
+ <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="'o_lunch_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="'o_lunch_confirmed'"/>
+ <t t-set="state_class" t-value="'badge-success o_lunch_confirmed'"/>
</t>
- <div class="d-flex flex-row">
- <div class="flex-grow-1">
- <span class="o_lunch_widget_title">Your order</span>
- <a href="#"><i t-if="widget.raw_state != 'confirmed'" class="fa fa-trash o_lunch_widget_unlink"/></a>
- </div>
- <div t-if="widget.raw_state != 'new'" t-esc="widget.state" t-attf-class="flex-grow-0 {{ state_class }}"/>
- </div>
- <div t-foreach="widget.lines" t-as="line">
- <div class="d-flex flex-row">
- <div class="flex-grow-0">
- <a class="o_remove_product" t-if="widget.raw_state != 'confirmed'" t-attf-data-id="{{ line.id }}" href="#"><i class="fa fa-minus-circle"/></a>
- <span t-esc="line.quantity"/>
- <a class="o_add_product" t-if="widget.raw_state != 'confirmed'" t-attf-data-id="{{ line.id }}" href="#"><i class="fa fa-plus-circle"/></a>
- </div>
- <div class="flex-grow-1 pl-2">
- <a t-esc="line.product[1]" class="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"/>
+ <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">
+ <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="d-flex flex-row">
- <div class="o_lunch_widget_title flex-grow-1">
- Total
- </div>
- <div class="flex-grow-0 pr-1">
+ <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>
- </div>
+ </h4>
+ <button t-if="widget.raw_state == 'new'" class="btn btn-primary w-100 o_lunch_widget_order_button">Order now</button>
</div>
- <button t-if="widget.raw_state == 'new'" class="btn btn-primary o_lunch_widget_order_button">ORDER NOW</button>
</t>
</div>
</div>
@@ -89,7 +93,7 @@
</t>
</div>
- <t t-name="currency_field">
+ <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>
@@ -104,9 +108,23 @@
<t t-else="">
<t t-esc="value"/>
</t>
- </t>
+ </span>
<div t-name="lunch.LunchPaymentDialog">
<span t-esc="widget.message"/>
</div>
+
+ <t t-name="LunchKanbanWidgetMobile">
+ <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="LunchKanbanWidget"/>
+ </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 00000000000..216d830bb47
--- /dev/null
+++ b/addons/lunch/static/tests/lunch_kanban_mobile_tests.js
@@ -0,0 +1,208 @@
+odoo.define('lunch.lunchKanbanMobileTests', function (require) {
+"use strict";
+
+const LunchKanbanView = require('lunch.LunchKanbanView');
+
+const testUtils = require('web.test_utils');
+const {createLunchKanbanView, 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 $.when();
+ },
+ },
+ 'ir.model.data': {
+ fields: {},
+ xmlid_to_res_id() {
+ return $.when(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', function (assert) {
+ assert.expect(9);
+
+ const kanban = createLunchKanbanView({
+ 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 > div',
+ "should have 1 column");
+ assert.containsNone(kanban, '.o_content > div.o_search_panel',
+ "shouldn't have a 'lunch filters' column");
+ assert.containsOnce(kanban, '.o_content > .o_lunch_kanban',
+ "should have a 'kanban lunch wrapper' column");
+ assert.containsOnce(kanban, '.o_lunch_kanban > .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_kanban'), '> details',
+ "should have a 'lunch kanban' details/summary discolure panel");
+ assert.hasClass($('.o_lunch_kanban > details'), 'fixed-bottom',
+ "should have classname 'fixed-bottom'");
+ assert.isNotVisible($('.o_lunch_kanban > details .o_lunch_kanban_banner'),
+ "shouldn't have a visible 'lunch kanban' banner");
+
+ kanban.destroy();
+ });
+
+ QUnit.module('LunchKanbanWidget', function () {
+ QUnit.test('toggle', function (assert) {
+ assert.expect(6);
+
+ const kanban = createLunchKanbanView({
+ 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_kanban > details');
+ assert.isNotVisible($details.find('.o_lunch_kanban_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");
+
+ testUtils.dom.click($details.find('> summary'));
+ assert.isVisible($details.find('.o_lunch_kanban_banner'),
+ "should have a visible 'lunch kanban' banner");
+
+ testUtils.dom.click($details.find('> summary'));
+ assert.isNotVisible($details.find('.o_lunch_kanban_banner'),
+ "shouldn't have a visible 'lunch kanban' banner");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('keep open when adding quantities', function (assert) {
+ assert.expect(6);
+
+ const kanban = createLunchKanbanView({
+ 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_kanban > details');
+ assert.isNotVisible($details.find('.o_lunch_kanban_banner'),
+ "shouldn't have a visible 'lunch kanban' banner");
+ assert.isVisible($details.find('> summary'),
+ "should hava a visible cart toggle button");
+
+ testUtils.dom.click($details.find('> summary'));
+ assert.isVisible($details.find('.o_lunch_kanban_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');
+
+ testUtils.dom.click($firstLine.find('button.o_add_product'));
+ assert.isVisible($('.o_lunch_kanban > details .o_lunch_kanban_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');
+
+ testUtils.dom.click($firstLine.find('button.o_remove_product'));
+ assert.isVisible($('.o_lunch_kanban > details .o_lunch_kanban_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 00000000000..571b11ed52e
--- /dev/null
+++ b/addons/lunch/static/tests/lunch_kanban_tests.js
@@ -0,0 +1,821 @@
+odoo.define('lunch.lunchKanbanTests', function (require) {
+"use strict";
+
+const LunchKanbanView = require('lunch.LunchKanbanView');
+
+const testUtils = require('web.test_utils');
+const {createLunchKanbanView, 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 $.when();
+ },
+ },
+ 'lunch.product.category': {
+ fields: {},
+ records: [],
+ },
+ 'lunch.supplier': {
+ fields: {},
+ records: [],
+ },
+ 'ir.model.data': {
+ fields: {},
+ xmlid_to_res_id() {
+ return $.when(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', function (assert) {
+ assert.expect(7);
+
+ const kanban = createLunchKanbanView({
+ 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_kanban',
+ "should have a 'kanban lunch wrapper' column");
+ assert.containsOnce(kanban, '.o_lunch_kanban > .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_kanban > .o_lunch_kanban_banner',
+ "should have a 'lunch kanban' banner");
+
+ kanban.destroy();
+ });
+
+ QUnit.module('LunchKanbanWidget', function () {
+
+ QUnit.test('empty cart', function (assert) {
+ assert.expect(3);
+
+ const kanban = createLunchKanbanView({
+ 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('non-empty cart', function (assert) {
+ assert.expect(17);
+
+ const kanban = createLunchKanbanView({
+ 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', function (assert) {
+ assert.expect(15);
+
+ const kanban = createLunchKanbanView({
+ 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', function (assert) {
+ assert.expect(15);
+
+ const kanban = createLunchKanbanView({
+ 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', function (assert) {
+ assert.expect(11);
+
+ const kanban = createLunchKanbanView({
+ 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");
+
+ 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', function (assert) {
+ assert.expect(12);
+
+ const kanban = createLunchKanbanView({
+ 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");
+
+ 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");
+
+ 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', function (assert) {
+ assert.expect(1);
+
+ const kanban = createLunchKanbanView({
+ 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, {
+ res_model: 'lunch.order.temp',
+ type: 'ir.actions.act_window',
+ views: [[false, 'form']],
+ target: 'new',
+ context: {
+ default_product_id: undefined,
+ line_id: false,
+ },
+ },
+ "should open the wizard");
+ },
+ },
+ });
+
+ testUtils.dom.click(kanban.$('.o_kanban_record:first'));
+
+ kanban.destroy();
+ });
+
+ QUnit.test('add product quantity', function (assert) {
+ assert.expect(3);
+
+ const kanban = createLunchKanbanView({
+ 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 $.when();
+ },
+ },
+ }),
+ 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');
+
+ testUtils.dom.click($firstLine.find('button.o_add_product'));
+
+ kanban.destroy();
+ });
+
+ QUnit.test('remove product quantity', function (assert) {
+ assert.expect(3);
+
+ const kanban = createLunchKanbanView({
+ 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 $.when();
+ },
+ },
+ }),
+ 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');
+
+ testUtils.dom.click($firstLine.find('button.o_remove_product'));
+
+ kanban.destroy();
+ });
+
+ QUnit.test('clear order', function (assert) {
+ assert.expect(1);
+
+ const self = this;
+ const kanban = createLunchKanbanView({
+ 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 $.when();
+ }
+ 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)');
+
+ testUtils.dom.click($widgetSecondColumn.find('button.o_lunch_widget_unlink'));
+
+ kanban.destroy();
+ });
+
+ QUnit.test('validate order: success', function (assert) {
+ assert.expect(1);
+
+ const self = this;
+ const kanban = createLunchKanbanView({
+ 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 $.when(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)');
+
+ testUtils.dom.click($widgetThirdColumn.find('button.o_lunch_widget_order_button'));
+
+ kanban.destroy();
+ });
+
+ QUnit.test('validate order: failure', function (assert) {
+ assert.expect(5);
+
+ const self = this;
+ const kanban = createLunchKanbanView({
+ 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 $.when(false);
+ }
+ if (route === '/lunch/payment_message') {
+ assert.ok('should perform payment message RPC call');
+ return $.when({ 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)');
+
+ 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_test_utils.js b/addons/lunch/static/tests/lunch_test_utils.js
new file mode 100644
index 00000000000..3c8b52d18e3
--- /dev/null
+++ b/addons/lunch/static/tests/lunch_test_utils.js
@@ -0,0 +1,58 @@
+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 kanban view with searchpanel
+ *
+ * @param {object} params
+ */
+function createLunchKanbanView(params) {
+ const archPieces = params.arch.split('</templates>');
+ params.arch = `
+ ${archPieces[0]}</templates>
+ <searchpanel>
+ <field name="category_id" select="multi" string="Categories"/>
+ <field name="supplier_id" select="multi" string="Vendors"/>
+ </searchpanel>
+ ${archPieces[1]}
+ `;
+ 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 function (route) {
+ if (route === '/lunch/infos') {
+ return $.when(infos);
+ }
+ if (route === '/lunch/user_location_get') {
+ return $.when(userLocation);
+ }
+ return this._super.apply(this, arguments);
+ };
+}
+
+return {
+ createLunchKanbanView,
+ mockLunchRPC,
+};
+
+});
diff --git a/addons/lunch/static/tests/test_lunch_kanban.js b/addons/lunch/static/tests/test_lunch_kanban.js
deleted file mode 100644
index 020c3748150..00000000000
--- a/addons/lunch/static/tests/test_lunch_kanban.js
+++ /dev/null
@@ -1,259 +0,0 @@
-odoo.define('lunch.lunchKanbanTests', function (require) {
-"use strict";
-
-var LunchKanbanView = require('lunch.LunchKanbanView');
-var LunchKanbanWidget = require('lunch.LunchKanbanWidget');
-var testUtils = require('web.test_utils');
-
-var createView = testUtils.createView;
-
-QUnit.module('Views', {
- beforeEach: function () {
- this.data = {
- product: {
- fields: {
- id: {string: 'ID', type: 'integer'},
- is_available_at: {string: 'available_location_ids', type: 'integer'},
- },
- records: [
- {id: 1, name: 'Tuna sandwich', price: 3.0, is_available_at: 1},
- ]
- },
- };
- },
-}, function () {
- QUnit.module('LunchKanbanView');
-
- QUnit.test('Simple rendering of LunchKanbanView', function (assert) {
- assert.expect(7);
-
- var lunchKanban = createView({
- View: LunchKanbanView,
- model: 'product',
- data: this.data,
- arch: '<kanban><templates><t t-name="kanban-box">' +
- '<div>' +
- '</div>' +
- '</t></templates></kanban>',
- mockRPC: function(route, args) {
- if (route === '/lunch/infos') {
- return $.when({
- order: 1,
- wallet: 20,
- username: 'Marc Demo',
- userimage: '',
- is_manager: false,
- users: [
- {id: 1, name: 'Mitchell Admin'},
- {id: 2, name: 'Marc Demo'},
- ],
- total: 7.4,
- state: 'new',
- lines: [
- {id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4}
- ],
- alerts: [],
- locations: ['hello', 'hallo'],
- user_location: [1, 'hello'],
- });
- } else if (route === '/lunch/user_location_get') {
- return $.when(1);
- } else if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') {
- return $.when();
- } else if (route.startsWith('data:image/png;base64,')) {
- return $.when();
- }
- return this._super(route, args);
- },
- });
-
- var $section = $(lunchKanban.$('.o_lunch_widget_info')[0]);
- // username
- assert.strictEqual($section.find('div:eq(1) div:eq(0)').text().trim(), 'Marc Demo', 'Username should have been Marc Demo');
- // your order section
- $section = $(lunchKanban.$('.o_lunch_widget_info')[1]);
- // only one line
- assert.strictEqual($section.find('div:eq(2) div.d-flex').length, 1, 'There should be only one line');
- // quantity = 1
- assert.strictEqual($section.find('div:eq(2) div:eq(0) span:eq(0)').text().trim(), '1', 'The line should contain only one product');
- // buttons to remove and to add product
- assert.strictEqual($section.find('.o_remove_product').length, 1, 'There should be a remove product button');
- assert.strictEqual($section.find('.o_add_product').length, 1, 'There should be a add product button');
-
- $section = $(lunchKanban.$('.o_lunch_widget_info')[2]);
- // total
- assert.strictEqual($section.find('div:eq(0) div:eq(1)').text().trim(), '7.40', 'total should be of 7.40');
- // order now button
- assert.strictEqual($section.find('button').length, 1, 'order now button should be available');
-
- lunchKanban.destroy();
- });
-
- QUnit.test('User interactions', function (assert) {
- assert.expect(9);
-
- var state = 'new';
-
- var lunchKanban = createView({
- View: LunchKanbanView,
- model: 'product',
- data: this.data,
- arch: '<kanban><templates><t t-name="kanban-box">' +
- '<div>' +
- '</div>' +
- '</t></templates></kanban>',
- mockRPC: function(route, args) {
- if (route === '/lunch/infos') {
- return $.when({
- order: 1,
- wallet: 20,
- username: 'Marc Demo',
- userimage: '',
- is_manager: false,
- users: [
- {id: 1, name: 'Mitchell Admin'},
- {id: 2, name: 'Marc Demo'},
- ],
- total: 7.4,
- raw_state: state,
- state: 'New',
- lines: [
- {id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4}
- ],
- alerts: [],
- locations: ['hello', 'hallo'],
- user_location: [1, 'hello'],
- });
- } else if (route === '/lunch/payment_message') {
- return $.when({message: 'Hello'});
- } else if (route === '/lunch/pay') {
- return $.when(true);
- } else if (route === '/lunch/user_location_get') {
- return $.when(1);
- } else if (args.method === 'update_quantity') {
- assert.step(args.args);
- return $.when();
- } else if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') {
- return $.when();
- } else if (route.startsWith('data:image/png;base64,')) {
- return $.when();
- }
- return this._super(route, args);
- },
- });
-
- lunchKanban.$('.o_add_product').click();
- lunchKanban.$('.o_remove_product').click();
-
- state = 'ordered';
- lunchKanban.$('.o_lunch_widget_order_button').click();
- // state is shown
- assert.strictEqual(lunchKanban.$('.o_lunch_ordered').length, 1, 'state should be shown as ordered');
- // buttons
- assert.strictEqual(lunchKanban.$('.o_remove_product').length, 1, 'button to remove product should be shown');
- assert.strictEqual(lunchKanban.$('.o_add_product').length, 1, 'button to add product should be shown');
- state = 'confirmed';
- lunchKanban.reload();
- // state is updated
- assert.strictEqual(lunchKanban.$('.o_lunch_confirmed').length, 1, 'state should be shown as confirmed');
- // Buttons not shown anymore
- assert.strictEqual(lunchKanban.$('.o_remove_product').length, 0, 'button to remove product should not be shown');
- assert.strictEqual(lunchKanban.$('.o_add_product').length, 0, 'button to add product should not be shown');
-
-
- assert.verifySteps([
- [[1], 1],
- [[1], -1],
- ]);
-
- lunchKanban.destroy();
- });
-
- QUnit.test('Manager interactions', function (assert) {
- assert.expect(1);
-
- var lunchKanban = createView({
- View: LunchKanbanView,
- model: 'product',
- data: this.data,
- arch: '<kanban><templates><t t-name="kanban-box">' +
- '<div>' +
- '</div>' +
- '</t></templates></kanban>',
- mockRPC: function(route, args) {
- if (route === '/lunch/infos') {
- return $.when({
- order: 1,
- wallet: 20,
- username: 'Marc Demo',
- userimage: '',
- is_manager: true,
- users: [
- {id: 1, name: 'Mitchell Admin'},
- {id: 2, name: 'Marc Demo'},
- ],
- total: 7.4,
- raw_state: 'new',
- state: 'New',
- lines: [
- {id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4}
- ],
- alerts: [],
- locations: ['hello', 'hallo'],
- user_location: [1, 'hello'],
- });
- } else if (route === '/lunch/user_location_get') {
- return $.when(1);
- } else if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') {
- return $.when();
- } else if (route.startsWith('data:image/png;base64,')) {
- return $.when();
- }
- return this._super(route, args);
- },
- });
-
- var select = lunchKanban.$('div.o_lunch_user_field div.o_field_widget.o_field_many2one');
-
- assert.strictEqual(select.length, 1, 'There should be a user selection field');
-
- lunchKanban.destroy();
- });
-
- QUnit.module('LunchKanbanWidget');
-
- QUnit.test('Rpc calls should be performed after the init()', function (assert) {
- assert.expect(2);
-
- var parent = testUtils.createParent({});
-
- var widget = new LunchKanbanWidget(parent, {
- user_location: [1, 'hello'],
- });
-
- testUtils.mock.addMockEnvironment(widget, {
- data: {},
- mockRPC: function (route, args) {
- if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') {
- assert.ok('should perform xmlid_to_res_id RPC call in the willStart()');
- assert.deepEqual(args, {
- args: [],
- kwargs: {
- xmlid: "base.group_portal",
- },
- method: "xmlid_to_res_id",
- model: "ir.model.data",
- }, 'should have the right parameters for xmlid_to_res_id RPC call');
- return $.when();
- }
- return this._super.apply(this, arguments);
- },
- });
-
- widget.appendTo($("#qunit-fixture"));
-
- parent.destroy();
- });
-});
-
-});
diff --git a/addons/lunch/views/lunch_templates.xml b/addons/lunch/views/lunch_templates.xml
index a076bbb2261..40c57da0b79 100644
--- a/addons/lunch/views/lunch_templates.xml
+++ b/addons/lunch/views/lunch_templates.xml
@@ -11,16 +11,25 @@
<script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_renderer.js"></script>
<script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_record.js"></script>
<script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_model.js"></script>
+ <script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_mobile.js"></script>
</xpath>
</template>
<template id="qunit_suite" name="lunch tests" inherit_id="web.qunit_suite">
<xpath expr="//t[@t-set='head']" position="inside">
- <script type="text/javascript" src="/lunch/static/tests/test_lunch_kanban.js"></script>
+ <script type="text/javascript" src="/lunch/static/tests/lunch_test_utils.js"></script>
+ <script type="text/javascript" src="/lunch/static/tests/lunch_kanban_tests.js"></script>
+ </xpath>
+ </template>
+
+ <template id="qunit_mobile_suite" name="lunch mobile tests" inherit_id="web.qunit_mobile_suite">
+ <xpath expr="//t[@t-set='head']" position="inside">
+ <script type="text/javascript" src="/lunch/static/tests/lunch_test_utils.js"></script>
+ <script type="text/javascript" src="/lunch/static/tests/lunch_kanban_mobile_tests.js"></script>
</xpath>
</template>
<template id="lunch_payment_dialog" name="Lunch Payment Dialog">
- To add some money to your wallet, please contact your lunch manager.
+ To add some money to your wallet, please contact your lunch manager.
</template>
</odoo>
diff --git a/addons/web/static/src/scss/search_view_mobile.scss b/addons/web/static/src/scss/search_view_mobile.scss
index c3c264649ea..dd72aaae83b 100644
--- a/addons/web/static/src/scss/search_view_mobile.scss
+++ b/addons/web/static/src/scss/search_view_mobile.scss
@@ -7,7 +7,7 @@
padding: 0;
width: 100%;
background-color: white;
- z-index: 1000;
+ z-index: $zindex-modal;
overflow: auto;
.o_mobile_search_header {
height: 46px;
@@ -89,7 +89,6 @@
right: 0;
padding: 15px;
font-size: 17px;
- z-index: 1000;
}
}
}