[IMP] lunch: migrate to Owl

Migrate lunch to Owl.

task-2916338

Part-of: odoo/odoo#99021
This commit is contained in:
Kevin Baptiste
2022-08-30 08:18:36 +02:00
committed by Yannick Tivisse
parent 727fd4f208
commit 503c0d6f8f
30 changed files with 904 additions and 2086 deletions
+6 -19
View File
@@ -42,36 +42,23 @@ If you want to save your employees' time and avoid them to always have coins in
'application': True,
'assets': {
'web.assets_backend': [
'lunch/static/src/components/*.js',
'lunch/static/src/components/*.scss',
'lunch/static/src/mixins/*.js',
'lunch/static/src/views/*.js',
'lunch/static/src/scss/lunch_view.scss',
'lunch/static/src/scss/lunch_kanban.scss',
'lunch/static/src/scss/lunch_list.scss',
'lunch/static/src/js/lunch_controller_common.js',
'lunch/static/src/js/lunch_widget.js',
'lunch/static/src/js/lunch_mobile.js',
'lunch/static/src/js/lunch_payment_dialog.js',
'lunch/static/src/js/lunch_kanban_view.js',
'lunch/static/src/js/lunch_kanban_controller.js',
'lunch/static/src/js/lunch_kanban_renderer.js',
'lunch/static/src/js/lunch_kanban_record.js',
'lunch/static/src/js/lunch_model_extension.js',
'lunch/static/src/js/lunch_list_view.js',
'lunch/static/src/js/lunch_list_controller.js',
'lunch/static/src/js/lunch_list_renderer.js',
],
'web.assets_tests': [
'lunch/static/tests/tours/*.js',
],
'web.qunit_suite_tests': [
'lunch/static/tests/lunch_test_utils.js',
'lunch/static/tests/lunch_kanban_tests.js',
'lunch/static/tests/lunch_list_tests.js',
],
'web.qunit_mobile_suite_tests': [
'lunch/static/tests/lunch_test_utils.js',
'lunch/static/tests/lunch_kanban_mobile_tests.js',
],
'web.assets_qweb': [
'lunch/static/src/xml/lunch_templates.xml',
'lunch/static/src/components/*.xml',
'lunch/static/src/views/*.xml',
],
},
'license': 'LGPL-3',
+2
View File
@@ -50,6 +50,7 @@
<field name="name">Coca Cola</field>
<field name="category_id" ref="categ_drinks"/>
<field name="price">2.9</field>
<field name="description"></field>
<field name="supplier_id" ref="supplier_hungry_dog"/>
<field name="company_id" ref="base.main_company"/>
</record>
@@ -214,6 +215,7 @@
<field name="category_id" ref="categ_sandwich"/>
<field name="price">3.0</field>
<field name="supplier_id" ref="supplier_corner"/>
<field name="description"></field>
<field name="image_1920" type="base64" file="lunch/static/img/chicken_curry.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
@@ -0,0 +1,179 @@
/** @odoo-module */
import { useBus, useService } from "@web/core/utils/hooks";
import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
const { Component, useState, onWillStart, markup, xml } = owl;
export class LunchCurrency extends Component {
get amount() {
return parseFloat(this.props.amount).toFixed(2);
}
}
LunchCurrency.template = 'lunch.LunchCurrency';
LunchCurrency.props = ["currency", "amount"];
export class LunchOrderLine extends Component {
setup() {
super.setup();
this.orm = useService('orm');
this.state = useState({ mobileOpen: false });
}
get line() {
return this.props.line;
}
get canEdit() {
return !['sent', 'confirmed'].includes(this.line.raw_state);
}
get badgeClass() {
const mapping = {'new': 'bg-warning', 'confirmed': 'bg-success', 'sent': 'bg-info', 'ordered': 'bg-danger'};
return mapping[this.line.raw_state];
}
get hasToppings() {
return this.line.toppings.length !== 0;
}
async updateQuantity(increment) {
await this.orm.call('lunch.order', 'update_quantity', [
this.props.line.id,
increment
]);
await this.props.onUpdateQuantity();
}
}
LunchOrderLine.template = 'lunch.LunchOrderLine';
LunchOrderLine.props = ["line", "currency", "onUpdateQuantity", "openOrderLine"];
LunchOrderLine.components = {
LunchCurrency,
};
export class LunchAlert extends Component {
get message() {
return markup(this.props.message);
}
}
LunchAlert.props = ["message"];
LunchAlert.template = xml`<t t-out="message"/>`
export class LunchAlerts extends Component {}
LunchAlerts.components = {
LunchAlert,
}
LunchAlerts.props = ["alerts"];
LunchAlerts.template = 'lunch.LunchAlerts';
export class LunchUser extends Component {
getDomain() {
return [['share', '=', false]];
}
}
LunchUser.components = {
Many2XAutocomplete,
}
LunchUser.props = ["username", "isManager", "onUpdateUser"];
LunchUser.template = "lunch.LunchUser";
export class LunchLocation extends Component {
getDomain() {
return [];
}
}
LunchLocation.components = {
Many2XAutocomplete,
}
LunchLocation.props = ["location", "onUpdateLunchLocation"];
LunchLocation.template = "lunch.LunchLocation";
export class LunchDashboard extends Component {
setup() {
super.setup();
this.rpc = useService("rpc");
this.user = useService("user");
this.state = useState({
infos: {},
});
useBus(this.env.bus, 'lunch_update_dashboard', () => this._fetchLunchInfos());
onWillStart(async () => {
await this._fetchLunchInfos()
this.env.searchModel.updateLocationId(this.state.infos.user_location[0]);
});
}
async lunchRpc(route, args = {}) {
return await this.rpc(route, {
...args,
context: this.user.context,
user_id: this.env.searchModel.lunchState.userId,
})
}
async _fetchLunchInfos() {
this.state.infos = await this.lunchRpc('/lunch/infos');
}
async emptyCart() {
await this.lunchRpc('/lunch/trash');
await this._fetchLunchInfos();
}
get hasLines() {
return this.state.infos.lines && this.state.infos.lines.length !== 0;
}
get canOrder() {
return this.state.infos.raw_state === 'new';
}
get location() {
return this.state.infos.user_location && this.state.infos.user_location[1];
}
async orderNow() {
if (!this.canOrder) {
return;
}
await this.lunchRpc('/lunch/pay');
await this._fetchLunchInfos();
}
async onUpdateQuantity() {
await this._fetchLunchInfos();
}
async onUpdateUser(value) {
if (!value) {
return;
}
this.env.searchModel.updateUserId(value[0].id);
await this._fetchLunchInfos();
}
async onUpdateLunchLocation(value) {
if (!value) {
return;
}
await this.lunchRpc('/lunch/user_location_set', {
location_id: value[0].id,
});
await this._fetchLunchInfos();
this.env.searchModel.updateLocationId(value[0].id);
}
}
LunchDashboard.components = {
LunchAlerts,
LunchCurrency,
LunchLocation,
LunchOrderLine,
LunchUser,
Many2XAutocomplete,
};
LunchDashboard.props = ["openOrderLine"];
LunchDashboard.template = 'lunch.LunchDashboard';
@@ -0,0 +1,9 @@
.lunch_topping:before {
content: '+ ';
}
.o_lunch_content {
.o-autocomplete--input {
cursor: pointer;
}
}
@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="lunch.LunchCurrency" owl="1">
<span>
<t t-if="props.currency.position == 'before'" t-esc="props.currency.symbol"/>
<t t-esc="amount"/>
<t t-if="props.currency.position == 'after'" t-esc="props.currency.symbol"/>
</span>
</t>
<t t-name="lunch.LunchOrderLine" owl="1">
<div class="d-flex">
<div>
<button class="btn btn-sm btn-icon btn-link fa fa-minus-circle p-0" t-if="canEdit" t-on-click="() => this.updateQuantity(-1)"/>
<span t-esc="line.quantity" class="px-1"/>
<button class="btn btn-sm btn-icon btn-link fa fa-plus-circle p-0" t-if="canEdit" t-on-click="() => this.updateQuantity(1)"/>
</div>
<span class="flex-grow-1 ps-2">
<a t-on-click="() => props.openOrderLine(line.product[0], line.id)" t-if="canEdit" role="button" title="Edit order">
<t t-esc="line.product[1]"/>
</a>
<t t-else="" t-esc="line.product[1]"/>
<span t-esc="line.state" t-attf-class="badge ms-2 rounded-pill #{badgeClass}"/>
</span>
<LunchCurrency currency="props.currency" amount="line.product[2]"/>
</div>
<ul t-if="hasToppings" class="list-unstyled ps-4">
<li t-foreach="line.toppings" t-as="topping" t-key="topping" class="d-flex">
<span class="flex-grow-1 lunch_topping" t-esc="topping[0]"/>
<LunchCurrency currency="props.currency" amount="topping[1]"/>
</li>
</ul>
<div t-if="line.note" t-esc="line.note" class="text-muted ps-4"/>
</t>
<t t-name="lunch.LunchAlerts" owl="1">
<div class="alert alert-warning mb-0" t-if="props.alerts.length !== 0" role="alert">
<t t-foreach="props.alerts" t-as="alert" t-key="alert.id">
<LunchAlert message="alert.message" />
</t>
</div>
</t>
<t t-name="lunch.LunchUser" owl="1">
<div class="lunch_user">
<span t-if="!props.isManager" t-esc="props.username"/>
<Many2XAutocomplete
t-else=""
value="props.username"
resModel="'res.users'"
getDomain="getDomain"
activeActions="{}"
update.bind="props.onUpdateUser"
/>
</div>
</t>
<t t-name="lunch.LunchLocation" owl="1">
<div class="lunch_location">
<Many2XAutocomplete
value="props.location"
resModel="'lunch.location'"
getDomain="getDomain"
activeActions="{}"
update.bind="props.onUpdateLunchLocation"
/>
</div>
</t>
<t t-name="lunch.LunchDashboardOrder" owl="1">
<LunchAlerts alerts="state.infos.alerts"/>
<div class="o_lunch_banner container-fluid py-2">
<div class="row h-100">
<div class="col-12 col-md-4">
<div class="row h-100 align-content-center">
<div class="col-3">
<img class="o_image_64_cover rounded-circle" t-att-src="state.infos.userimage"/>
</div>
<div class="col-9">
<LunchUser
isManager="state.infos.is_manager"
username="state.infos.username"
onUpdateUser.bind="onUpdateUser"/>
<LunchLocation
location="location"
onUpdateLunchLocation.bind="onUpdateLunchLocation"/>
<div class="d-flex flex-row">
<span class="flex-grow-1">Your Account</span>
<LunchCurrency currency="currency" amount="state.infos.wallet"/>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-4" t-if="hasLines">
<h4>
Your Order
<button t-if="state.infos.raw_state != 'confirmed'" class="btn btn-sm btn-icon btn-link fa fa-trash" t-on-click.prevent="emptyCart"/>
</h4>
<ul class="list-unstyled">
<li t-foreach="state.infos.lines" t-as="line" t-key="line.id">
<LunchOrderLine line="line" currency="currency" onUpdateQuantity.bind="onUpdateQuantity" openOrderLine.bind="props.openOrderLine"/>
</li>
</ul>
</div>
<div class="col-12 col-md-4 d-flex flex-column justify-content-between" t-if="hasLines">
<h4 class="d-flex flex-row">
<span class="flex-grow-1">
Total
</span>
<LunchCurrency currency="currency" amount="state.infos.total"/>
</h4>
<button class="btn btn-primary" t-if="canOrder" t-on-click="orderNow">Order Now</button>
</div>
</div>
</div>
</t>
<t t-name="lunch.LunchDashboard" owl="1">
<t t-set="currency" t-value="state.infos.currency"/>
<t t-if="!env.isSmall">
<t t-call="lunch.LunchDashboardOrder"/>
</t>
<t t-else="">
<details class="fixed-bottom bg-white p-2" t-att-open="state.mobileOpen">
<summary class="btn btn-primary w-100" t-on-click="() => state.mobileOpen = !state.mobileOpen">
<i class="fa fa-fw fa-shopping-cart"/>
Your Cart (<LunchCurrency currency="currency" amount="state.infos.total || 0"/>)
</summary>
<t t-call="lunch.LunchDashboardOrder"/>
</details>
</t>
</t>
</templates>
@@ -1,219 +0,0 @@
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');
const {Markup} = require('web.utils');
var LunchWidget = require('lunch.LunchWidget');
var LunchPaymentDialog = require('lunch.LunchPaymentDialog');
var _t = core._t;
var LunchControllerCommon = {
custom_events: {
add_product: '_onAddProduct',
change_location: '_onLocationChanged',
change_user: '_onUserChanged',
open_wizard: '_onOpenWizard',
order_now: '_onOrderNow',
remove_product: '_onRemoveProduct',
unlink_order: '_onUnlinkOrder',
},
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
this.editMode = false;
this.updated = false;
this.widgetData = null;
this.context = session.user_context;
this.archiveEnabled = false;
},
/**
* @override
*/
start: function () {
// create a div inside o_content that will be used to wrap the lunch
// banner and renderer (this is required to get the desired
// layout with the searchPanel to the left)
var self = this;
this.$('.o_content').append($('<div>').addClass('o_lunch_content'));
return this._super.apply(this, arguments).then(function () {
self.$('.o_lunch_content').append(self.$('.o_lunch_view'));
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_fetchPaymentInfo: function () {
return this._rpc({
route: '/lunch/payment_message',
params: {
context: this.context,
},
});
},
async _fetchWidgetData() {
const widgetData = await this._rpc({
route: '/lunch/infos',
params: {
user_id: this.searchModel.get('userId'),
context: this.context,
},
});
widgetData.wallet = parseFloat(widgetData.wallet).toFixed(2);
(widgetData.alerts || []).forEach(alert => { alert.message = Markup(alert.message); });
this.widgetData = widgetData;
},
/**
* Renders and appends the lunch banner widget.
*
* @private
*/
_renderLunchWidget: function () {
var oldWidget = this.widget;
this.widget = new LunchWidget(this, Object.assign(this.widgetData, {edit: this.editMode}));
return this.widget.appendTo(document.createDocumentFragment()).then(() => {
this.$('.o_lunch_content').prepend(this.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();
},
};
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;
});
@@ -1,18 +0,0 @@
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;
});
@@ -1,38 +0,0 @@
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});
}
},
_openRecord() {},
});
return LunchKanbanRecord;
});
@@ -1,29 +0,0 @@
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;
});
@@ -1,33 +0,0 @@
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;
});
@@ -1,18 +0,0 @@
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;
});
@@ -1,56 +0,0 @@
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;
});
@@ -1,38 +0,0 @@
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;
});
@@ -1,78 +0,0 @@
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);
});
-130
View File
@@ -1,130 +0,0 @@
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;
});
@@ -1,100 +0,0 @@
odoo.define("lunch/static/src/js/lunch_model_extension.js", function (require) {
"use strict";
const ActionModel = require("web.ActionModel");
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;
});
@@ -1,20 +0,0 @@
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;
});
-164
View File
@@ -1,164 +0,0 @@
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.group_portal_id = params.group_portal_id || false;
this.userimage = params.userimage || '';
this.username = params.username || '';
this.lunchUserField = null;
this.locations = params.locations || [];
this.userLocation = params.user_location[1] || '';
const company_ids = [false].concat(session.user_context.allowed_company_ids || []);
this.lunchLocationField = this._createMany2One('locations', 'lunch.location', this.userLocation, () => [
['company_id', 'in', company_ids]
]);
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 superDef = this._super.apply(this, arguments);
if (this.is_manager) {
this.lunchUserField = this._createMany2One('users', 'res.users', this.username,
() => [['groups_id', 'not in', [this.group_portal_id]]]
);
}
return superDef;
},
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);
}
if (this.userLocation) {
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;
});
@@ -0,0 +1,40 @@
/** @odoo-module */
import { useBus, useService } from "@web/core/utils/hooks";
export const LunchRendererMixin = {
setup() {
this._super(...arguments);
this.action = useService("action");
useBus(this.env.bus, 'lunch_open_order', (ev) => this.openOrderLine(ev.detail.productId));
},
openOrderLine(productId, orderId) {
let context = {};
if (this.env.searchModel.lunchState.userId) {
context['default_user_id'] = this.env.searchModel.lunchState.userId;
}
let action = {
res_model: 'lunch.order',
name: this.env._t('Configure Your Order'),
type: 'ir.actions.act_window',
views: [[false, 'form']],
target: 'new',
context: {
...context,
default_product_id: productId,
},
};
if (orderId) {
action['res_id'] = orderId;
}
this.action.doAction(action, {
onClose: () => this.env.bus.trigger('lunch_update_dashboard')
});
},
}
@@ -1,5 +1,5 @@
.o_lunch_content {
.o_legacy_kanban_view {
.o_kanban_renderer {
flex: 1 1 100%;
&.o_kanban_grouped {
min-height: auto; // override min-height: 100%
+36
View File
@@ -0,0 +1,36 @@
/** @odoo-module */
import { patch } from '@web/core/utils/patch';
import { registry } from '@web/core/registry';
import { kanbanView } from '@web/views/kanban/kanban_view';
import { KanbanRecord } from '@web/views/kanban/kanban_record';
import { KanbanRenderer } from '@web/views/kanban/kanban_renderer';
import { LunchDashboard } from '../components/lunch_dashboard';
import { LunchRendererMixin } from '../mixins/lunch_renderer_mixin';
import { LunchSearchModel } from './search_model';
export class LunchKanbanRecord extends KanbanRecord {
onGlobalClick(ev) {
this.env.bus.trigger('lunch_open_order', {productId: this.props.record.resId});
}
}
export class LunchKanbanRenderer extends KanbanRenderer {}
patch(LunchKanbanRenderer.prototype, 'lunch_kanban_renderer_mixin', LunchRendererMixin);
LunchKanbanRenderer.template = 'lunch.KanbanRenderer';
LunchKanbanRenderer.components = {
...LunchKanbanRenderer.components,
LunchDashboard,
KanbanRecord: LunchKanbanRecord,
}
registry.category('views').add('lunch_kanban', {
...kanbanView,
Renderer: LunchKanbanRenderer,
SearchModel: LunchSearchModel,
});
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="lunch.KanbanRenderer" owl="1">
<div class="o_lunch_content">
<LunchDashboard openOrderLine.bind="openOrderLine"/>
<div class="overflow-auto">
<t t-call="web.KanbanRenderer"/>
</div>
</div>
</t>
</templates>
+32
View File
@@ -0,0 +1,32 @@
/** @odoo-module */
import { patch } from '@web/core/utils/patch';
import { registry } from '@web/core/registry';
import { listView } from '@web/views/list/list_view';
import { ListRenderer } from '@web/views/list/list_renderer';
import { LunchDashboard } from '../components/lunch_dashboard';
import { LunchRendererMixin } from '../mixins/lunch_renderer_mixin';
import { LunchSearchModel } from './search_model';
export class LunchListRenderer extends ListRenderer {
onCellClicked(record, column) {
this.openOrderLine(record.resId);
}
}
patch(LunchListRenderer.prototype, 'lunch_list_renderer_mixin', LunchRendererMixin);
LunchListRenderer.template = 'lunch.ListRenderer';
LunchListRenderer.components = {
...LunchListRenderer.components,
LunchDashboard,
}
registry.category('views').add('lunch_list', {
...listView,
Renderer: LunchListRenderer,
SearchModel: LunchSearchModel,
});
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="lunch.ListRenderer" owl="1">
<div class="o_lunch_content">
<LunchDashboard openOrderLine.bind="openOrderLine"/>
<div class="overflow-auto">
<t t-call="web.ListRenderer"/>
</div>
</div>
</t>
</templates>
@@ -0,0 +1,67 @@
/** @odoo-module */
import { useService } from "@web/core/utils/hooks";
import { Domain } from '@web/core/domain';
import { SearchModel } from '@web/search/search_model';
const { useState, onWillStart } = owl;
export class LunchSearchModel extends SearchModel {
setup() {
super.setup(...arguments);
this.rpc = useService('rpc');
this.lunchState = useState({
locationId: false,
userId: false,
});
onWillStart(async () => {
const locationId = await this.rpc('/lunch/user_location_get', {});
this.updateLocationId(locationId);
});
}
exportState() {
const state = super.exportState();
state.locationId = this.lunchState.locationId;
state.userId = this.lunchState.userId;
return state;
}
_importState(state) {
super._importState(...arguments);
if (state.locationId) {
this.lunchState.locationId = state.locationId;
}
if (state.userId) {
this.lunchState.userId = state.userId;
}
}
updateUserId(userId) {
this.lunchState.userId = userId;
this._notify();
}
updateLocationId(locationId) {
this.lunchState.locationId = locationId;
this._notify();
}
_getDomain(params = {}) {
const domain = super._getDomain(params);
if (!this.lunchState.locationId) {
return domain;
}
return Domain.and([
domain,
[['is_available_at', '=', this.lunchState.locationId]]
]).toList();
}
}
@@ -1,130 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<span t-name="LunchWidget">
<t t-foreach="widget.alerts" t-as="alert" t-key="alert_index">
<div class="alert alert-warning mb-0" role="alert">
<t t-out="alert.message"/>
</div>
</t>
<div class="o_lunch_banner container-fluid">
<div class="o_lunch_widget row py-3 py-md-0">
<div class="o_lunch_widget_info col-12 col-md-4 card border-0">
<div class="card-body row g-0 align-items-center">
<div class="col-3 col-md-6 col-lg-3">
<img class="o_image_64_cover rounded-circle" t-attf-src="{{ widget.userimage }}"/>
</div>
<div class="col-9 col-md-6 col-lg-9">
<div class="ps-3">
<div class="o_lunch_user_field py-1"/>
<div class="o_lunch_location_field py-1"/>
<div class="d-flex flex-row py-1">
<span class="flex-grow-1">Your Account</span>
<t t-call="currency_field">
<t t-set="value" t-value="widget.wallet"/>
<t t-set="currency" t-value="widget.currency"/>
</t>
</div>
</div>
</div>
</div>
</div>
<div class="o_lunch_widget_info col-12 col-md-4 card border-0">
<t t-if="!_.isEmpty(widget.lines)">
<div class="card-body">
<h4 class="card-title">
Your order
<button t-if="widget.raw_state != 'confirmed'" class="btn btn-sm btn-icon btn-link fa fa-trash o_lunch_widget_unlink"/>
</h4>
<ul class="list-unstyled o_lunch_widget_lines">
<t t-set="states_mapping" t-value="{'new': 'warning', 'confirmed': 'success', 'sent': 'info', 'ordered': 'danger'}" />
<li t-foreach="widget.lines" t-as="line" t-key="line_index">
<t t-set="can_modify" t-value="!['sent', 'confirmed'].includes(line.raw_state)" />
<div class="d-flex align-items-center">
<div class="flex-grow-0 flex-shrink-0 o_lunch_product_quantity">
<button class="btn btn-sm btn-icon btn-link fa fa-minus-circle o_remove_product" t-if="can_modify" t-attf-data-id="{{ line.id }}"/>
<span t-esc="line.quantity"/>
<button class="btn btn-sm btn-icon btn-link fa fa-plus-circle o_add_product" t-if="can_modify" t-attf-data-id="{{ line.id }}"/>
</div>
<div class="flex-grow-1 ps-2">
<button t-if="can_modify" t-esc="line.product[1]" t-attf-class="btn btn-link o_lunch_open_wizard" t-attf-data-product-id="{{ line.product[0] }}" t-attf-data-id="{{ line.id }}"/>
<t t-else="" t-esc="line.product[1]" />
<t t-set="state_pill" t-value="states_mapping[line.raw_state]" />
<span t-esc="line.state" t-attf-class="badge rounded-pill bg-{{ state_pill }}" />
</div>
<div class="flex-grow-0">
<t t-call="currency_field">
<t t-set="value" t-value="line.product[2]"/>
<t t-set="currency" t-value="widget.currency"/>
</t>
</div>
</div>
<div t-foreach="line.toppings" t-as="topping" t-key="topping_index" class="d-flex flex-row">
<div class="flex-grow-1 ps-5">
<span>+ <t t-esc="topping[0]"/></span>
</div>
<div class="flex-grow-0">
<t t-call="currency_field">
<t t-set="value" t-value="topping[1]"/>
<t t-set="currency" t-value="widget.currency"/>
</t>
</div>
</div>
<span t-if="line.note" t-esc="line.note" class="text-muted ps-5"/>
</li>
</ul>
</div>
</t>
</div>
<div class="o_lunch_widget_info col-12 col-md-4 card border-0">
<t t-if="!_.isEmpty(widget.lines) &amp;&amp; widget.raw_state == 'new'">
<div class="card-body d-flex flex-column justify-content-between">
<h4 class="card-title d-flex py-1">
<span class="flex-grow-1">Total</span>
<t t-call="currency_field">
<t t-set="value" t-value="widget.total"/>
<t t-set="currency" t-value="widget.currency"/>
</t>
</h4>
<button t-if="widget.raw_state == 'new'" class="btn btn-primary w-100 o_lunch_widget_order_button">Order now</button>
</div>
</t>
</div>
</div>
</div>
</span>
<span t-name="currency_field" class="o_field_monetary o_field_number o_field_widget">
<t t-js="ctx">
ctx.value = _.str.sprintf('%.2f', parseFloat(ctx.value));
</t>
<t t-if="currency">
<t t-if="currency.position == 'after'">
<t t-esc="value"/><t t-esc="currency.symbol"/>
</t>
<t t-else="">
<t t-esc="currency.symbol"/><t t-esc="value"/>
</t>
</t>
<t t-else="">
<t t-esc="value"/>
</t>
</span>
<div t-name="lunch.LunchPaymentDialog">
<span t-esc="widget.message"/>
</div>
<t t-name="LunchWidgetMobile">
<details class="fixed-bottom" t-attf-open="#{widget.keepOpen}">
<summary class="o_lunch_toggle_cart btn btn-primary w-100">
<i class="fa fa-fw fa-shopping-cart"/>
Your cart
(<t t-call="currency_field">
<t t-set="value" t-value="widget.total"/>
<t t-set="currency" t-value="widget.currency"/>
</t>)
</summary>
<t t-call="LunchWidget"/>
</details>
</t>
</templates>
File diff suppressed because it is too large Load Diff
@@ -1,59 +0,0 @@
odoo.define('lunch.test_utils', function (require) {
"use strict";
const AbstractStorageService = require('web.AbstractStorageService');
const RamStorage = require('web.RamStorage');
const {createView} = require('web.test_utils');
/**
* Helper to create a lunch view with searchpanel
*
* @param {object} params
*/
async function createLunchView(params) {
params.archs = params.archs || {};
var searchArch = params.archs[`${params.model},false,search`] || '<search></search>';
var searchPanelArch = `
<searchpanel>
<field name="category_id" select="multi" string="Categories" enable_counters="1"/>
<field name="supplier_id" select="multi" string="Vendors" enable_counters="1"/>
</searchpanel>
`;
searchArch = searchArch.split('</search>')[0] + searchPanelArch + '</search>';
params.archs[`${params.model},false,search`] = searchArch;
if (!params.services || !params.services.local_storage) {
// the searchPanel uses the localStorage to store/retrieve default
// active category value
params.services = params.services || {};
const RamStorageService = AbstractStorageService.extend({
storage: new RamStorage(),
});
params.services.local_storage = RamStorageService;
}
return createView(params);
}
/**
* Helper to generate a mockRPC function for the mandatory lunch routes (prefixed by '/lunch')
*
* @param {object} infos
* @param {integer} userLocation
*/
function mockLunchRPC({infos, userLocation}) {
return async function (route) {
if (route === '/lunch/infos') {
return Promise.resolve(infos);
}
if (route === '/lunch/user_location_get') {
return Promise.resolve(userLocation);
}
return this._super.apply(this, arguments);
};
}
return {
createLunchView,
mockLunchRPC,
};
});
+1 -1
View File
@@ -200,7 +200,7 @@
<field name="name">lunch.order.view.form</field>
<field name="model">lunch.order</field>
<field name="arch" type="xml">
<form>
<form class="flex-column">
<field name="company_id" invisible="1"/>
<field name="date" invisible="1"/>
<field name="currency_id" invisible="1"/>
+3 -3
View File
@@ -113,8 +113,8 @@
<field name="is_new"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click o_kanban_record_has_image_fill o_kanban_record">
<field name="image_128" class="o_lunch_image o_kanban_image_fill_left d-none d-md-block" options="{'placeholder': '/lunch/static/img/lunch.png', 'size': [94, 94]}" widget="image"/>
<div class="oe_kanban_global_click o_kanban_record_has_image_fill">
<field name="image_128" class="o_lunch_image o_kanban_image_fill_left" options="{'placeholder': '/lunch/static/img/lunch.png', 'size': [94, 94]}" widget="image"/>
<div class="oe_kanban_details ml8">
<div class="o_kanban_record_top">
<div class="o_kanban_record_headings">
@@ -160,7 +160,7 @@
<field name="currency_id"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click o_kanban_record_has_image_fill o_kanban_record">
<div class="oe_kanban_global_click o_kanban_record_has_image_fill">
<div class="o_kanban_image_fill_left d-none d-md-block"
t-attf-style="background-image:url('#{kanban_image('lunch.product', 'image_128', record.id.raw_value)}')"/>
<div class="oe_kanban_details">