[IMP] lunch: add list view for lunch.product.report

- Move code from kanban view that is common with list view in common files
- Add action to add selected products to cart
- Add/update tests

closes odoo/odoo#47556

Taskid: 2160048
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
Anh Thao Pham (pta)
2020-04-30 11:40:39 +00:00
committed by Yannick Tivisse
parent 918a5f2cc0
commit 8b6b09fc26
21 changed files with 822 additions and 393 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ If you want to save your employees' time and avoid them to always have coins in
'data/ir_cron_data.xml'
],
'demo': ['data/lunch_demo.xml'],
'qweb': ['static/src/xml/lunch_kanban.xml', ],
'qweb': ['static/src/xml/lunch_templates.xml', ],
'installable': True,
'application': True,
'certificate': '001292377792581874189',
@@ -0,0 +1,253 @@
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.userId = null;
this.editMode = false;
this.updated = false;
this.widgetData = null;
this.context = session.user_context;
this.archiveEnabled = false;
},
/**
* @override
*/
start: function () {
// create a div inside o_content that will be used to wrap the lunch
// banner and renderer (this is required to get the desired
// layout with the searchPanel to the left)
var self = this;
this.$('.o_content').append($('<div>').addClass('o_lunch_content'));
return this._super.apply(this, arguments).then(function () {
self.$('.o_lunch_content').append(self.$('.o_lunch_view'));
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_fetchPaymentInfo: function () {
return this._rpc({
route: '/lunch/payment_message',
params: {
context: this.context,
},
});
},
_fetchWidgetData: function () {
var self = this;
return this._rpc({
route: '/lunch/infos',
params: {
user_id: this.userId,
context: this.context,
},
}).then(function (data) {
self.widgetData = data;
return self.model._updateLocation(data.user_location[0]);
});
},
/**
* Override to add the location domain (coming from the LunchWidget)
* to the searchDomain (coming from the controlPanel).
*
* @override
*/
_getViewDomain: async function () {
return this.model.getLocationDomain();
},
/**
* 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) {
var self = this;
ev.stopPropagation();
this._rpc({
route: '/lunch/user_location_set',
params: {
user_id: this.userId,
location_id: ev.data.locationId,
context: this.context,
},
}).then(function () {
self.model._updateLocation(ev.data.locationId).then(function () {
self.reload();
});
});
},
_onOpenWizard: function (ev) {
var self = this;
ev.stopPropagation();
var ctx = this.userId ? {default_user_id: this.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.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();
var self = this;
this.userId = ev.data.userId;
this.model._updateUser(ev.data.userId).then(function () {
self.reload();
});
},
_onUnlinkOrder: function (ev) {
var self = this;
ev.stopPropagation();
this._rpc({
route: '/lunch/trash',
params: {
user_id: this.userId,
context: this.context,
},
}).then(function () {
self.reload();
});
},
};
return LunchControllerCommon;
});
@@ -6,248 +6,11 @@ odoo.define('lunch.LunchKanbanController', function (require) {
* override of the KanbanController.
*/
var session = require('web.session');
var core = require('web.core');
var KanbanController = require('web.KanbanController');
var LunchKanbanWidget = require('lunch.LunchKanbanWidget');
var LunchPaymentDialog = require('lunch.LunchPaymentDialog');
var LunchControllerCommon = require('lunch.LunchControllerCommon');
var _t = core._t;
var LunchKanbanController = KanbanController.extend({
custom_events: _.extend({}, KanbanController.prototype.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.userId = null;
this.editMode = false;
this.updated = false;
this.widgetData = null;
this.context = session.user_context;
return this._super.apply(this, arguments);
},
/**
* @override
*/
start: function () {
// create a div inside o_content that will be used to wrap the lunch
// banner and kanban renderer (this is required to get the desired
// layout with the searchPanel to the left)
var self = this;
this.$('.o_content').append($('<div>').addClass('o_lunch_kanban'));
return this._super.apply(this, arguments).then(function () {
self.$('.o_lunch_kanban').append(self.$('.o_kanban_view'));
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_fetchPaymentInfo: function () {
return this._rpc({
route: '/lunch/payment_message',
params: {
context: this.context,
},
});
},
_fetchWidgetData: function () {
var self = this;
return this._rpc({
route: '/lunch/infos',
params: {
user_id: this.userId,
context: this.context,
},
}).then(function (data) {
self.widgetData = data;
return self.model._updateLocation(data.user_location[0]);
});
},
/**
* Override to add the location domain (coming from the lunchKanbanWidget)
* to the searchDomain (coming from the controlPanel).
*
* @override
*/
_getViewDomain: async function () {
return this.model.getLocationDomain();
},
/**
* Renders and appends the lunch banner widget.
*
* @private
*/
_renderLunchKanbanWidget: function () {
var self = this;
var oldWidget = this.widget;
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);
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 kanban renderer into
* a div, to get the desired layout.
*
* @override
* @private
*/
_update: function () {
var def = this._fetchWidgetData().then(this._renderLunchKanbanWidget.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) {
var self = this;
ev.stopPropagation();
this._rpc({
route: '/lunch/user_location_set',
params: {
user_id: this.userId,
location_id: ev.data.locationId,
context: this.context,
},
}).then(function () {
self.model._updateLocation(ev.data.locationId).then(function () {
self.reload();
});
});
},
_onOpenWizard: function (ev) {
var self = this;
ev.stopPropagation();
var ctx = this.userId ? {default_user_id: this.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.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();
var self = this;
this.userId = ev.data.userId;
this.model._updateUser(ev.data.userId).then(function () {
self.reload();
});
},
_onUnlinkOrder: function (ev) {
var self = this;
ev.stopPropagation();
this._rpc({
route: '/lunch/trash',
params: {
user_id: this.userId,
context: this.context,
},
}).then(function () {
self.reload();
});
},
var LunchKanbanController = KanbanController.extend(LunchControllerCommon , {
custom_events: _.extend({}, KanbanController.prototype.custom_events, LunchControllerCommon.custom_events),
});
return LunchKanbanController;
@@ -19,7 +19,7 @@ var LunchKanbanRenderer = KanbanRenderer.extend({
* @override
*/
start: function () {
this.$el.addClass('o_lunch_kanban_view position-relative align-content-start flex-grow-1 flex-shrink-1');
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);
},
});
@@ -2,7 +2,7 @@ odoo.define('lunch.LunchKanbanView', function (require) {
"use strict";
var LunchKanbanController = require('lunch.LunchKanbanController');
var LunchKanbanModel = require('lunch.LunchKanbanModel');
var LunchModel = require('lunch.LunchModel');
var LunchKanbanRenderer = require('lunch.LunchKanbanRenderer');
var core = require('web.core');
@@ -14,7 +14,7 @@ var _lt = core._lt;
var LunchKanbanView = KanbanView.extend({
config: _.extend({}, KanbanView.prototype.config, {
Controller: LunchKanbanController,
Model: LunchKanbanModel,
Model: LunchModel,
Renderer: LunchKanbanRenderer,
}),
display_name: _lt('Lunch Kanban'),
@@ -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;
});
@@ -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;
});
@@ -0,0 +1,39 @@
odoo.define('lunch.LunchListView', function (require) {
"use strict";
var LunchListController = require('lunch.LunchListController');
var LunchModel = require('lunch.LunchModel');
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,
Model: LunchModel,
Renderer: LunchListRenderer,
}),
display_name: _lt('Lunch List'),
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_getViewDomain: function (parent) {
const model = this.getModel(parent);
return model.getLocationDomain();
},
});
view_registry.add('lunch_list', LunchListView);
return LunchListView;
});
@@ -1,16 +1,17 @@
odoo.define('lunch.LunchKanbanMobile', function (require) {
odoo.define('lunch.LunchMobile', function (require) {
"use strict";
var config = require('web.config');
var LunchKanbanWidget = require('lunch.LunchKanbanWidget');
var LunchWidget = require('lunch.LunchWidget');
var LunchKanbanController = require('lunch.LunchKanbanController');
var LunchListController = require('lunch.LunchListController');
if (!config.device.isMobile) {
return;
}
LunchKanbanWidget.include({
template: "LunchKanbanWidgetMobile",
LunchWidget.include({
template: "LunchWidgetMobile",
/**
* Override to set the toggle state allowing initially open it.
@@ -23,7 +24,7 @@ LunchKanbanWidget.include({
},
});
LunchKanbanController.include({
var mobileFunctions = {
/**
* @override
*/
@@ -42,7 +43,7 @@ LunchKanbanController.include({
* @override
* @private
*/
_renderLunchKanbanWidget: function () {
_renderLunchWidget: function () {
this.widgetData.keepOpen = this.openWidget;
this.openWidget = false;
return this._super.apply(this, arguments);
@@ -69,6 +70,9 @@ LunchKanbanController.include({
this.openWidget = true;
this._super.apply(this, arguments);
},
});
};
LunchKanbanController.include(mobileFunctions);
LunchListController.include(mobileFunctions);
});
@@ -1,4 +1,4 @@
odoo.define('lunch.LunchKanbanModel', function (require) {
odoo.define('lunch.LunchModel', function (require) {
"use strict";
/**
@@ -7,9 +7,9 @@ odoo.define('lunch.LunchKanbanModel', function (require) {
*/
var session = require('web.session');
var KanbanModel = require('web.KanbanModel');
var BasicModel = require('web.BasicModel');
var LunchKanbanModel = KanbanModel.extend({
var LunchModel = BasicModel.extend({
init: function () {
this.locationId = false;
this.userId = false;
@@ -125,6 +125,6 @@ var LunchKanbanModel = KanbanModel.extend({
}
});
return LunchKanbanModel;
return LunchModel;
});
@@ -1,4 +1,4 @@
odoo.define('lunch.LunchKanbanWidget', function (require) {
odoo.define('lunch.LunchWidget', function (require) {
"use strict";
var core = require('web.core');
@@ -17,8 +17,8 @@ var LunchMany2One = FieldMany2One.extend({
}
});
var LunchKanbanWidget = Widget.extend({
template: 'LunchKanbanWidget',
var LunchWidget = Widget.extend({
template: 'LunchWidget',
custom_events: {
field_changed: '_onFieldChanged',
},
@@ -125,7 +125,6 @@ var LunchKanbanWidget = Widget.extend({
_onAddProduct: function (ev) {
ev.preventDefault();
ev.stopPropagation();
this.trigger_up('add_product', {lineId: $(ev.currentTarget).data('id')});
},
_onOrderNow: function (ev) {
@@ -164,6 +163,6 @@ var LunchKanbanWidget = Widget.extend({
},
});
return LunchKanbanWidget;
return LunchWidget;
});
+1 -79
View File
@@ -1,86 +1,8 @@
.o_lunch_kanban {
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
height: 100%;
.o_lunch_kanban_banner {
flex: 0 0 auto;
border-bottom: 1px solid #CED4DA;
background-color: white;
}
.o_lunch_content {
.o_kanban_view {
flex: 1 1 100%;
&.o_kanban_grouped {
min-height: auto; // override min-height: 100%
}
}
.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_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%
}
}
}
.o_lunch_wizard {
.col-10 {
.o_form_label {
font-weight: normal !important;
}
}
}
+7 -1
View File
@@ -1,5 +1,11 @@
.o_lunch_list {
.o_lunch_content {
.o_list_button {
width: 1px;
}
.o_lunch_list_view {
td:last-child {
padding-right: 16px;
}
}
}
@@ -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;
}
}
}
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<span t-name="LunchKanbanWidget">
<span t-name="LunchWidget">
<t t-foreach="widget.alerts" t-as="alert">
<div class="alert alert-warning mb-0" role="alert">
<t t-raw="alert.message"/> <!-- alert.message is coming from a fields.Html so it should be safe -->
</div>
</t>
<div class="o_lunch_kanban_banner container-fluid">
<div class="o_lunch_banner container-fluid">
<div class="o_lunch_widget row py-3 py-md-0">
<div class="o_lunch_widget_info col-12 col-md-4 card border-0">
<div class="card-body row no-gutters align-items-center">
@@ -116,7 +116,7 @@
<span t-esc="widget.message"/>
</div>
<t t-name="LunchKanbanWidgetMobile">
<t t-name="LunchWidgetMobile">
<details class="fixed-bottom" t-attf-open="#{widget.keepOpen}">
<summary class="o_lunch_toggle_cart btn btn-primary w-100">
<i class="fa fa-fw fa-shopping-cart"/>
@@ -126,7 +126,7 @@
<t t-set="currency" t-value="widget.currency"/>
</t>)
</summary>
<t t-call="LunchKanbanWidget"/>
<t t-call="LunchWidget"/>
</details>
</t>
</templates>
@@ -4,7 +4,7 @@ odoo.define('lunch.lunchKanbanMobileTests', function (require) {
const LunchKanbanView = require('lunch.LunchKanbanView');
const testUtils = require('web.test_utils');
const {createLunchKanbanView, mockLunchRPC} = require('lunch.test_utils');
const {createLunchView, mockLunchRPC} = require('lunch.test_utils');
QUnit.module('Views');
@@ -61,7 +61,7 @@ QUnit.module('LunchKanbanView Mobile', {
QUnit.test('basic rendering', async function (assert) {
assert.expect(7);
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -84,27 +84,27 @@ QUnit.module('LunchKanbanView Mobile', {
"should have 1 records in the renderer");
// check view layout
assert.containsOnce(kanban, '.o_content > .o_lunch_kanban',
assert.containsOnce(kanban, '.o_content > .o_lunch_content',
"should have a 'kanban lunch wrapper' column");
assert.containsOnce(kanban, '.o_lunch_kanban > .o_kanban_view',
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_kanban'), '> details',
assert.containsOnce($('.o_lunch_content'), '> details',
"should have a 'lunch kanban' details/summary discolure panel");
assert.hasClass($('.o_lunch_kanban > details'), 'fixed-bottom',
assert.hasClass($('.o_lunch_content > details'), 'fixed-bottom',
"should have classname 'fixed-bottom'");
assert.isNotVisible($('.o_lunch_kanban > details .o_lunch_kanban_banner'),
assert.isNotVisible($('.o_lunch_content > details .o_lunch_banner'),
"shouldn't have a visible 'lunch kanban' banner");
kanban.destroy();
});
QUnit.module('LunchKanbanWidget', function () {
QUnit.module('LunchWidget', function () {
QUnit.test('toggle', async function (assert) {
assert.expect(6);
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -125,8 +125,8 @@ QUnit.module('LunchKanbanView Mobile', {
}),
});
const $details = $('.o_lunch_kanban > details');
assert.isNotVisible($details.find('.o_lunch_kanban_banner'),
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");
@@ -136,11 +136,11 @@ QUnit.module('LunchKanbanView Mobile', {
"should have '3.00' in the button text");
await testUtils.dom.click($details.find('> summary'));
assert.isVisible($details.find('.o_lunch_kanban_banner'),
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_kanban_banner'),
assert.isNotVisible($details.find('.o_lunch_banner'),
"shouldn't have a visible 'lunch kanban' banner");
kanban.destroy();
@@ -149,7 +149,7 @@ QUnit.module('LunchKanbanView Mobile', {
QUnit.test('keep open when adding quantities', async function (assert) {
assert.expect(6);
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -177,14 +177,14 @@ QUnit.module('LunchKanbanView Mobile', {
}),
});
const $details = $('.o_lunch_kanban > details');
assert.isNotVisible($details.find('.o_lunch_kanban_banner'),
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_kanban_banner'),
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)');
@@ -195,13 +195,13 @@ QUnit.module('LunchKanbanView Mobile', {
let $firstLine = $widgetSecondColumn.find('.o_lunch_widget_lines > li:first');
await testUtils.dom.click($firstLine.find('button.o_add_product'));
assert.isVisible($('.o_lunch_kanban > details .o_lunch_kanban_banner'),
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_kanban > details .o_lunch_kanban_banner'),
assert.isVisible($('.o_lunch_content > details .o_lunch_banner'),
"remove quantity should keep 'lunch kanban' banner open");
kanban.destroy();
+23 -23
View File
@@ -4,7 +4,7 @@ odoo.define('lunch.lunchKanbanTests', function (require) {
const LunchKanbanView = require('lunch.LunchKanbanView');
const testUtils = require('web.test_utils');
const {createLunchKanbanView, mockLunchRPC} = require('lunch.test_utils');
const {createLunchView, mockLunchRPC} = require('lunch.test_utils');
QUnit.module('Views');
@@ -89,7 +89,7 @@ QUnit.module('LunchKanbanView', {
QUnit.test('basic rendering', async function (assert) {
assert.expect(7);
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -116,14 +116,14 @@ QUnit.module('LunchKanbanView', {
"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',
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_kanban > span > .o_lunch_kanban_banner',
"should have a 'lunch kanban' banner");
assert.containsOnce(kanban, '.o_lunch_content > span > .o_lunch_banner',
"should have a 'lunch' banner");
kanban.destroy();
});
@@ -133,7 +133,7 @@ QUnit.module('LunchKanbanView', {
const self = this;
let infosProm = Promise.resolve();
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -175,12 +175,12 @@ QUnit.module('LunchKanbanView', {
kanban.destroy();
});
QUnit.module('LunchKanbanWidget', function () {
QUnit.module('LunchWidget', function () {
QUnit.test('empty cart', async function (assert) {
assert.expect(3);
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -217,7 +217,7 @@ QUnit.module('LunchKanbanView', {
let locationId = this.data['lunch.location'].records[0].id;
const regularInfos = _.extend({}, this.regularInfos);
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -285,7 +285,7 @@ QUnit.module('LunchKanbanView', {
assert.expect(10);
const regularInfos = _.extend({}, this.regularInfos);
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -333,7 +333,7 @@ QUnit.module('LunchKanbanView', {
QUnit.test('non-empty cart', async function (assert) {
assert.expect(17);
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -415,7 +415,7 @@ QUnit.module('LunchKanbanView', {
QUnit.test('ordered cart', async function (assert) {
assert.expect(15);
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -491,7 +491,7 @@ QUnit.module('LunchKanbanView', {
QUnit.test('confirmed cart', async function (assert) {
assert.expect(15);
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -567,7 +567,7 @@ QUnit.module('LunchKanbanView', {
QUnit.test('regular user', async function (assert) {
assert.expect(11);
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -626,7 +626,7 @@ QUnit.module('LunchKanbanView', {
QUnit.test('manager user', async function (assert) {
assert.expect(12);
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -690,7 +690,7 @@ QUnit.module('LunchKanbanView', {
QUnit.test('add a product', async function (assert) {
assert.expect(1);
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -732,7 +732,7 @@ QUnit.module('LunchKanbanView', {
QUnit.test('add product quantity', async function (assert) {
assert.expect(3);
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: Object.assign({}, this.data, {
@@ -784,7 +784,7 @@ QUnit.module('LunchKanbanView', {
QUnit.test('remove product quantity', async function (assert) {
assert.expect(3);
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: Object.assign({}, this.data, {
@@ -837,7 +837,7 @@ QUnit.module('LunchKanbanView', {
assert.expect(1);
const self = this;
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -883,7 +883,7 @@ QUnit.module('LunchKanbanView', {
assert.expect(1);
const self = this;
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -929,7 +929,7 @@ QUnit.module('LunchKanbanView', {
assert.expect(5);
const self = this;
const kanban = await createLunchKanbanView({
const kanban = await createLunchView({
View: LunchKanbanView,
model: 'product',
data: this.data,
@@ -0,0 +1,267 @@
odoo.define('lunch.lunchListTests', function (require) {
"use strict";
const LunchListView = require('lunch.LunchListView');
const testUtils = require('web.test_utils');
const {createLunchView, mockLunchRPC} = require('lunch.test_utils');
QUnit.module('Views');
QUnit.module('LunchListView', {
beforeEach() {
const PORTAL_GROUP_ID = 1234;
this.data = {
'product': {
fields: {
is_available_at: {string: 'Product Availability', type: 'many2one', relation: 'lunch.location'},
category_id: {string: 'Product Category', type: 'many2one', relation: 'lunch.product.category'},
supplier_id: {string: 'Vendor', type: 'many2one', relation: 'lunch.supplier'},
},
records: [
{id: 1, name: 'Tuna sandwich', is_available_at: 1},
],
},
'lunch.order': {
fields: {},
update_quantity() {
return Promise.resolve();
},
},
'lunch.product.category': {
fields: {},
records: [],
},
'lunch.supplier': {
fields: {},
records: [],
},
'ir.model.data': {
fields: {},
xmlid_to_res_id() {
return Promise.resolve(PORTAL_GROUP_ID);
},
},
'lunch.location': {
fields: {
name: {string: 'Name', type: 'char'},
},
records: [
{id: 1, name: "Office 1"},
{id: 2, name: "Office 2"},
],
},
'res.users': {
fields: {
name: {string: 'Name', type: 'char'},
groups_id: {string: 'Groups', type: 'many2many'},
},
records: [
{id: 1, name: "Mitchell Admin", groups_id: []},
{id: 2, name: "Marc Demo", groups_id: []},
{id: 3, name: "Jean-Luc Portal", groups_id: [PORTAL_GROUP_ID]},
],
},
};
this.regularInfos = {
username: "Marc Demo",
wallet: 36.5,
is_manager: false,
currency: {
symbol: "\u20ac",
position: "after"
},
user_location: [2, "Office 2"],
};
},
}, function () {
QUnit.test('basic rendering', async function (assert) {
assert.expect(6);
const list = await createLunchView({
View: LunchListView,
model: 'product',
data: this.data,
arch: `
<tree>
<field name="name"/>
</tree>
`,
mockRPC: mockLunchRPC({
infos: this.regularInfos,
userLocation: this.data['lunch.location'].records[0].id,
}),
});
// check view layout
assert.containsN(list, '.o_content > div', 2,
"should have 2 columns");
assert.containsOnce(list, '.o_content > div.o_search_panel',
"should have a 'lunch filters' column");
assert.containsOnce(list, '.o_content > .o_lunch_content',
"should have a 'lunch wrapper' column");
assert.containsOnce(list, '.o_lunch_content > .o_list_view',
"should have a 'classical list view' column");
assert.hasClass(list.$('.o_list_view'), 'o_lunch_list_view',
"should have classname 'o_lunch_list_view'");
assert.containsOnce(list, '.o_lunch_content > span > .o_lunch_banner',
"should have a 'lunch' banner");
list.destroy();
});
QUnit.module('LunchWidget', function () {
QUnit.test('search panel domain location', async function (assert) {
assert.expect(20);
let expectedLocation = 1;
let locationId = this.data['lunch.location'].records[0].id;
const regularInfos = _.extend({}, this.regularInfos);
const list = await createLunchView({
View: LunchListView,
model: 'product',
data: this.data,
arch: `
<tree>
<field name="name"/>
</tree>
`,
mockRPC: function (route, args) {
assert.step(route);
if (route.startsWith('/lunch')) {
if (route === '/lunch/user_location_set') {
locationId = args.location_id;
return Promise.resolve(true);
}
return mockLunchRPC({
infos: regularInfos,
userLocation: locationId,
}).apply(this, arguments);
}
if (args.method === 'search_panel_select_multi_range') {
assert.deepEqual(args.kwargs.search_domain, [["is_available_at", "in", [expectedLocation]]],
'The initial domain of the search panel must contain the user location');
}
if (route === '/web/dataset/search_read') {
assert.deepEqual(args.domain, [["is_available_at", "in", [expectedLocation]]],
'The domain for fetching actual data should be correct');
}
return this._super.apply(this, arguments);
},
});
expectedLocation = 2;
await testUtils.fields.many2one.clickOpenDropdown('locations');
await testUtils.fields.many2one.clickItem('locations', "Office 2");
assert.verifySteps([
// Initial state
'/lunch/user_location_get',
'/web/dataset/call_kw/product/search_panel_select_multi_range',
'/web/dataset/call_kw/product/search_panel_select_multi_range',
'/web/dataset/search_read',
'/lunch/infos',
'/web/dataset/call_kw/ir.model.data/xmlid_to_res_id',
// Click m2o
'/web/dataset/call_kw/lunch.location/name_search',
// Click new location
'/lunch/user_location_set',
'/web/dataset/call_kw/product/search_panel_select_multi_range',
'/web/dataset/call_kw/product/search_panel_select_multi_range',
'/web/dataset/search_read',
'/lunch/infos',
'/web/dataset/call_kw/ir.model.data/xmlid_to_res_id',
]);
list.destroy();
});
QUnit.test('search panel domain location false: fetch products in all locations', async function (assert) {
assert.expect(10);
const regularInfos = _.extend({}, this.regularInfos);
const list = await createLunchView({
View: LunchListView,
model: 'product',
data: this.data,
arch: `
<tree>
<field name="name"/>
</tree>
`,
mockRPC: function (route, args) {
assert.step(route);
if (route.startsWith('/lunch')) {
return mockLunchRPC({
infos: regularInfos,
userLocation: false,
}).apply(this, arguments);
}
if (args.method === 'search_panel_select_multi_range') {
assert.deepEqual(args.kwargs.search_domain, [],
'The domain should not exist since the location is false.');
}
if (route === '/web/dataset/search_read') {
assert.deepEqual(args.domain, [],
'The domain for fetching actual data should be correct');
}
return this._super.apply(this, arguments);
}
});
assert.verifySteps([
'/lunch/user_location_get',
'/web/dataset/call_kw/product/search_panel_select_multi_range',
'/web/dataset/call_kw/product/search_panel_select_multi_range',
'/web/dataset/search_read',
'/lunch/infos',
'/web/dataset/call_kw/ir.model.data/xmlid_to_res_id',
])
list.destroy();
});
QUnit.test('add a product', async function (assert) {
assert.expect(1);
const list = await createLunchView({
View: LunchListView,
model: 'product',
data: this.data,
arch: `
<tree>
<field name="name"/>
</tree>
`,
mockRPC: mockLunchRPC({
infos: this.regularInfos,
userLocation: this.data['lunch.location'].records[0].id,
}),
intercepts: {
do_action: function (ev) {
assert.deepEqual(ev.data.action, {
name: "Configure Your Order",
res_model: 'lunch.order',
type: 'ir.actions.act_window',
views: [[false, 'form']],
target: 'new',
context: {
default_product_id: 1,
},
},
"should open the wizard");
},
},
});
await testUtils.dom.click(list.$('.o_data_row:first'));
list.destroy();
});
});
});
});
@@ -6,11 +6,11 @@ const RamStorage = require('web.RamStorage');
const {createView} = require('web.test_utils');
/**
* Helper to create a lunch kanban view with searchpanel
* Helper to create a lunch view with searchpanel
*
* @param {object} params
*/
async function createLunchKanbanView(params) {
async function createLunchView(params) {
params.archs = params.archs || {};
var searchArch = params.archs[`${params.model},false,search`] || '<search></search>';
var searchPanelArch = `
@@ -52,7 +52,7 @@ function mockLunchRPC({infos, userLocation}) {
}
return {
createLunchKanbanView,
createLunchView,
mockLunchRPC,
};
+16 -1
View File
@@ -91,6 +91,21 @@
</form>
</field>
</record>
<record id="lunch_product_report_view_tree" model="ir.ui.view">
<field name="name">lunch.product.tree</field>
<field name="model">lunch.product.report</field>
<field name="priority">999</field>
<field name="arch" type="xml">
<tree js_class="lunch_list">
<field name="name"/>
<field name="category_id"/>
<field name="supplier_id"/>
<field name="description"/>
<field name="product_id" invisible="1"/>
<field name="price" widget="monetary"/>
</tree>
</field>
</record>
<record id="view_lunch_product_kanban_order" model="ir.ui.view">
<field name="name">lunch.product.kanban</field>
@@ -330,7 +345,7 @@
<record id="lunch_product_action_order" model="ir.actions.act_window">
<field name="name">Order Your Lunch</field>
<field name="res_model">lunch.product.report</field>
<field name="view_mode">kanban</field>
<field name="view_mode">kanban,tree</field>
<field name="view_id" ref="view_lunch_product_kanban_order"/>
<field name="search_view_id" ref="lunch_product_report_view_search"/>
<field name="domain">[('supplier_id.available_today', '=', True), '|', ('user_id', '=', False), ('user_id', '=', uid)]</field>
+9 -3
View File
@@ -2,17 +2,22 @@
<odoo>
<template id="assets_backend" name="lunch assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/lunch/static/src/scss/lunch_view.scss"/>
<link rel="stylesheet" type="text/scss" href="/lunch/static/src/scss/lunch_kanban.scss"/>
<link rel="stylesheet" type="text/scss" href="/lunch/static/src/scss/lunch_list.scss"/>
<script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_widget.js"></script>
<script type="text/javascript" src="/lunch/static/src/js/lunch_controller_common.js"></script>
<script type="text/javascript" src="/lunch/static/src/js/lunch_model.js"></script>
<script type="text/javascript" src="/lunch/static/src/js/lunch_widget.js"></script>
<script type="text/javascript" src="/lunch/static/src/js/lunch_mobile.js"></script>
<script type="text/javascript" src="/lunch/static/src/js/lunch_payment_dialog.js"></script>
<script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_view.js"></script>
<script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_controller.js"></script>
<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>
<script type="text/javascript" src="/lunch/static/src/js/lunch_list_view.js"></script>
<script type="text/javascript" src="/lunch/static/src/js/lunch_list_controller.js"></script>
<script type="text/javascript" src="/lunch/static/src/js/lunch_list_renderer.js"></script>
</xpath>
</template>
@@ -20,6 +25,7 @@
<xpath expr="." 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_tests.js"></script>
<script type="text/javascript" src="/lunch/static/tests/lunch_list_tests.js"></script>
</xpath>
</template>