From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- addons/lunch/static/description/icon.png | Bin 0 -> 8870 bytes addons/lunch/static/description/icon.svg | 24 + addons/lunch/static/img/4formaggio.png | Bin 0 -> 8362 bytes addons/lunch/static/img/Coke.png | Bin 0 -> 7469 bytes addons/lunch/static/img/bacon_burger.png | Bin 0 -> 8027 bytes addons/lunch/static/img/brie.png | Bin 0 -> 6794 bytes addons/lunch/static/img/burger.png | Bin 0 -> 2820 bytes addons/lunch/static/img/cheeseburger.png | Bin 0 -> 7020 bytes addons/lunch/static/img/chicken_curry.png | Bin 0 -> 7147 bytes addons/lunch/static/img/chirashi.png | Bin 0 -> 7221 bytes addons/lunch/static/img/club.png | Bin 0 -> 7011 bytes addons/lunch/static/img/coke_zero.png | Bin 0 -> 7153 bytes addons/lunch/static/img/drink.png | Bin 0 -> 2427 bytes addons/lunch/static/img/fanta.png | Bin 0 -> 5855 bytes addons/lunch/static/img/fuze_black.png | Bin 0 -> 5623 bytes addons/lunch/static/img/fuze_green.png | Bin 0 -> 4724 bytes addons/lunch/static/img/gouda.png | Bin 0 -> 7864 bytes addons/lunch/static/img/italiana.png | Bin 0 -> 11549 bytes addons/lunch/static/img/lipton.png | Bin 0 -> 6784 bytes addons/lunch/static/img/lunch.png | Bin 0 -> 2124 bytes addons/lunch/static/img/maki.png | Bin 0 -> 8806 bytes addons/lunch/static/img/mozza.png | Bin 0 -> 6191 bytes addons/lunch/static/img/napoli.png | Bin 0 -> 9099 bytes addons/lunch/static/img/pasta_bolognese.png | Bin 0 -> 6727 bytes addons/lunch/static/img/pizza.png | Bin 0 -> 3891 bytes addons/lunch/static/img/pizza_funghi.png | Bin 0 -> 9213 bytes addons/lunch/static/img/pizza_margherita.png | Bin 0 -> 8597 bytes addons/lunch/static/img/pizza_veggie.png | Bin 0 -> 7354 bytes addons/lunch/static/img/salmon_sushi.png | Bin 0 -> 6885 bytes addons/lunch/static/img/temaki.png | Bin 0 -> 9244 bytes addons/lunch/static/img/tuna_sandwich.png | Bin 0 -> 6425 bytes .../lunch/static/src/js/lunch_controller_common.js | 218 +++++ .../lunch/static/src/js/lunch_kanban_controller.js | 18 + addons/lunch/static/src/js/lunch_kanban_record.js | 36 + .../lunch/static/src/js/lunch_kanban_renderer.js | 29 + addons/lunch/static/src/js/lunch_kanban_view.js | 33 + .../lunch/static/src/js/lunch_list_controller.js | 18 + addons/lunch/static/src/js/lunch_list_renderer.js | 56 ++ addons/lunch/static/src/js/lunch_list_view.js | 38 + addons/lunch/static/src/js/lunch_mobile.js | 78 ++ addons/lunch/static/src/js/lunch_model.js | 130 +++ .../lunch/static/src/js/lunch_model_extension.js | 100 +++ addons/lunch/static/src/js/lunch_payment_dialog.js | 20 + addons/lunch/static/src/js/lunch_widget.js | 168 ++++ addons/lunch/static/src/scss/lunch_kanban.scss | 8 + addons/lunch/static/src/scss/lunch_list.scss | 11 + addons/lunch/static/src/scss/lunch_view.scss | 81 ++ addons/lunch/static/src/xml/lunch.xml | 29 + addons/lunch/static/src/xml/lunch_templates.xml | 132 +++ .../static/tests/lunch_kanban_mobile_tests.js | 212 +++++ addons/lunch/static/tests/lunch_kanban_tests.js | 986 +++++++++++++++++++++ addons/lunch/static/tests/lunch_list_tests.js | 267 ++++++ addons/lunch/static/tests/lunch_test_utils.js | 59 ++ 53 files changed, 2751 insertions(+) create mode 100644 addons/lunch/static/description/icon.png create mode 100644 addons/lunch/static/description/icon.svg create mode 100644 addons/lunch/static/img/4formaggio.png create mode 100644 addons/lunch/static/img/Coke.png create mode 100644 addons/lunch/static/img/bacon_burger.png create mode 100644 addons/lunch/static/img/brie.png create mode 100644 addons/lunch/static/img/burger.png create mode 100644 addons/lunch/static/img/cheeseburger.png create mode 100644 addons/lunch/static/img/chicken_curry.png create mode 100644 addons/lunch/static/img/chirashi.png create mode 100644 addons/lunch/static/img/club.png create mode 100644 addons/lunch/static/img/coke_zero.png create mode 100644 addons/lunch/static/img/drink.png create mode 100644 addons/lunch/static/img/fanta.png create mode 100644 addons/lunch/static/img/fuze_black.png create mode 100644 addons/lunch/static/img/fuze_green.png create mode 100644 addons/lunch/static/img/gouda.png create mode 100644 addons/lunch/static/img/italiana.png create mode 100644 addons/lunch/static/img/lipton.png create mode 100644 addons/lunch/static/img/lunch.png create mode 100644 addons/lunch/static/img/maki.png create mode 100644 addons/lunch/static/img/mozza.png create mode 100644 addons/lunch/static/img/napoli.png create mode 100644 addons/lunch/static/img/pasta_bolognese.png create mode 100644 addons/lunch/static/img/pizza.png create mode 100644 addons/lunch/static/img/pizza_funghi.png create mode 100644 addons/lunch/static/img/pizza_margherita.png create mode 100644 addons/lunch/static/img/pizza_veggie.png create mode 100644 addons/lunch/static/img/salmon_sushi.png create mode 100644 addons/lunch/static/img/temaki.png create mode 100644 addons/lunch/static/img/tuna_sandwich.png create mode 100644 addons/lunch/static/src/js/lunch_controller_common.js create mode 100644 addons/lunch/static/src/js/lunch_kanban_controller.js create mode 100644 addons/lunch/static/src/js/lunch_kanban_record.js create mode 100644 addons/lunch/static/src/js/lunch_kanban_renderer.js create mode 100644 addons/lunch/static/src/js/lunch_kanban_view.js create mode 100644 addons/lunch/static/src/js/lunch_list_controller.js create mode 100644 addons/lunch/static/src/js/lunch_list_renderer.js create mode 100644 addons/lunch/static/src/js/lunch_list_view.js create mode 100644 addons/lunch/static/src/js/lunch_mobile.js create mode 100644 addons/lunch/static/src/js/lunch_model.js create mode 100644 addons/lunch/static/src/js/lunch_model_extension.js create mode 100644 addons/lunch/static/src/js/lunch_payment_dialog.js create mode 100644 addons/lunch/static/src/js/lunch_widget.js create mode 100644 addons/lunch/static/src/scss/lunch_kanban.scss create mode 100644 addons/lunch/static/src/scss/lunch_list.scss create mode 100644 addons/lunch/static/src/scss/lunch_view.scss create mode 100644 addons/lunch/static/src/xml/lunch.xml create mode 100644 addons/lunch/static/src/xml/lunch_templates.xml create mode 100644 addons/lunch/static/tests/lunch_kanban_mobile_tests.js create mode 100644 addons/lunch/static/tests/lunch_kanban_tests.js create mode 100644 addons/lunch/static/tests/lunch_list_tests.js create mode 100644 addons/lunch/static/tests/lunch_test_utils.js (limited to 'addons/lunch/static') diff --git a/addons/lunch/static/description/icon.png b/addons/lunch/static/description/icon.png new file mode 100644 index 00000000..28870a1d Binary files /dev/null and b/addons/lunch/static/description/icon.png 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/lunch/static/img/4formaggio.png b/addons/lunch/static/img/4formaggio.png new file mode 100644 index 00000000..f19c95ac Binary files /dev/null and b/addons/lunch/static/img/4formaggio.png differ diff --git a/addons/lunch/static/img/Coke.png b/addons/lunch/static/img/Coke.png new file mode 100644 index 00000000..e822a265 Binary files /dev/null and b/addons/lunch/static/img/Coke.png 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 Binary files /dev/null and b/addons/lunch/static/img/bacon_burger.png differ diff --git a/addons/lunch/static/img/brie.png b/addons/lunch/static/img/brie.png new file mode 100644 index 00000000..5b76bfbb Binary files /dev/null and b/addons/lunch/static/img/brie.png differ diff --git a/addons/lunch/static/img/burger.png b/addons/lunch/static/img/burger.png new file mode 100644 index 00000000..064e3f98 Binary files /dev/null and b/addons/lunch/static/img/burger.png differ diff --git a/addons/lunch/static/img/cheeseburger.png b/addons/lunch/static/img/cheeseburger.png new file mode 100644 index 00000000..f9d5a3e5 Binary files /dev/null and b/addons/lunch/static/img/cheeseburger.png 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 Binary files /dev/null and b/addons/lunch/static/img/chicken_curry.png differ diff --git a/addons/lunch/static/img/chirashi.png b/addons/lunch/static/img/chirashi.png new file mode 100644 index 00000000..65143711 Binary files /dev/null and b/addons/lunch/static/img/chirashi.png differ diff --git a/addons/lunch/static/img/club.png b/addons/lunch/static/img/club.png new file mode 100644 index 00000000..5f3e36f3 Binary files /dev/null and b/addons/lunch/static/img/club.png 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 Binary files /dev/null and b/addons/lunch/static/img/coke_zero.png differ diff --git a/addons/lunch/static/img/drink.png b/addons/lunch/static/img/drink.png new file mode 100644 index 00000000..4d4eed49 Binary files /dev/null and b/addons/lunch/static/img/drink.png differ diff --git a/addons/lunch/static/img/fanta.png b/addons/lunch/static/img/fanta.png new file mode 100644 index 00000000..b7a48332 Binary files /dev/null and b/addons/lunch/static/img/fanta.png 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 Binary files /dev/null and b/addons/lunch/static/img/fuze_black.png 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 Binary files /dev/null and b/addons/lunch/static/img/fuze_green.png differ diff --git a/addons/lunch/static/img/gouda.png b/addons/lunch/static/img/gouda.png new file mode 100644 index 00000000..c50d6d8d Binary files /dev/null and b/addons/lunch/static/img/gouda.png differ diff --git a/addons/lunch/static/img/italiana.png b/addons/lunch/static/img/italiana.png new file mode 100644 index 00000000..2f8c4244 Binary files /dev/null and b/addons/lunch/static/img/italiana.png differ diff --git a/addons/lunch/static/img/lipton.png b/addons/lunch/static/img/lipton.png new file mode 100644 index 00000000..a1f8ef21 Binary files /dev/null and b/addons/lunch/static/img/lipton.png differ diff --git a/addons/lunch/static/img/lunch.png b/addons/lunch/static/img/lunch.png new file mode 100644 index 00000000..ed2fdb30 Binary files /dev/null and b/addons/lunch/static/img/lunch.png differ diff --git a/addons/lunch/static/img/maki.png b/addons/lunch/static/img/maki.png new file mode 100644 index 00000000..c92dd8c8 Binary files /dev/null and b/addons/lunch/static/img/maki.png differ diff --git a/addons/lunch/static/img/mozza.png b/addons/lunch/static/img/mozza.png new file mode 100644 index 00000000..8820fbb9 Binary files /dev/null and b/addons/lunch/static/img/mozza.png differ diff --git a/addons/lunch/static/img/napoli.png b/addons/lunch/static/img/napoli.png new file mode 100644 index 00000000..c7ecd65d Binary files /dev/null and b/addons/lunch/static/img/napoli.png 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 Binary files /dev/null and b/addons/lunch/static/img/pasta_bolognese.png differ diff --git a/addons/lunch/static/img/pizza.png b/addons/lunch/static/img/pizza.png new file mode 100644 index 00000000..ad358b77 Binary files /dev/null and b/addons/lunch/static/img/pizza.png 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 Binary files /dev/null and b/addons/lunch/static/img/pizza_funghi.png 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 Binary files /dev/null and b/addons/lunch/static/img/pizza_margherita.png 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 Binary files /dev/null and b/addons/lunch/static/img/pizza_veggie.png 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 Binary files /dev/null and b/addons/lunch/static/img/salmon_sushi.png differ diff --git a/addons/lunch/static/img/temaki.png b/addons/lunch/static/img/temaki.png new file mode 100644 index 00000000..8f47e7c0 Binary files /dev/null and b/addons/lunch/static/img/temaki.png 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 Binary files /dev/null and b/addons/lunch/static/img/tuna_sandwich.png 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($('
').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 @@ + + +
+

This is the first time you order a meal

+

Select a product and put your order comments on the note.

+

Your favorite meals will be created based on your last orders.

+

Don't forget the alerts displayed in the reddish area

+
+
+
+

+
+ +
+ + + + +
+
+ +
+
+
+
+
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 @@ + + + + + + +
+
+
+
+
+ +
+
+
+
+
+
+ Your Account + + + + +
+
+
+
+
+
+ + + + + + + +
+

+ Your order +

+
    +
  • +
    +
    +
    +
    +
    +
    + + + + +
    +
    +
    +
    + + +
    +
    + + + + +
    +
    + +
  • +
+
+
+
+
+ +
+

+ Total + + + + +

+ +
+
+
+
+
+ + + + + ctx.value = _.str.sprintf('%.2f', parseFloat(ctx.value)); + + + + + + + + + + + + + + +
+ +
+ + +
+ + + Your cart + ( + + + ) + + +
+
+ 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + +
+
+
+
+ `, + 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: ` + + + + `, + 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: ` + + + + `, + 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: ` + + + + `, + 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: ` + + + + `, + 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`] || ''; + var searchPanelArch = ` + + + + + `; + searchArch = searchArch.split('')[0] + searchPanelArch + ''; + 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, +}; + +}); -- cgit v1.2.3