Initial commit: Vertical Industry packages
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 6 KiB |
|
|
@ -0,0 +1,24 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70">
|
||||
<defs>
|
||||
<path id="icon-a" d="M4,5.35309892e-14 C36.4160122,9.87060235e-15 58.0836068,-3.97961823e-14 65,5.07020818e-14 C69,6.733808e-14 70,1 70,5 C70,43.0488877 70,62.4235458 70,65 C70,69 69,70 65,70 C61,70 9,70 4,70 C1,70 7.10542736e-15,69 7.10542736e-15,65 C7.25721566e-15,62.4676575 3.83358709e-14,41.8005206 3.60818146e-14,5 C-1.13686838e-13,1 1,5.75716207e-14 4,5.35309892e-14 Z"/>
|
||||
<linearGradient id="icon-c" x1="100%" x2="0%" y1="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#CDC484"/>
|
||||
<stop offset="100%" stop-color="#B5AA59"/>
|
||||
</linearGradient>
|
||||
<path id="icon-d" d="M34.9928486,19.5391573 C35.0500601,19.8604454 36.1442308,25.9991002 36.1442308,28.3438202 C36.1442308,31.9190055 34.1561298,34.4688031 31.216887,35.4941909 L32.1394231,51.7705126 C32.1894832,52.7070335 31.409976,53.5 30.4230769,53.5 L25.8461538,53.5 C24.8664063,53.5 24.0797476,52.7138694 24.1298077,51.7705126 L25.0523437,35.4941909 C22.1059495,34.4688031 20.125,31.9121696 20.125,28.3438202 C20.125,25.9922643 21.2191707,19.8604454 21.2763822,19.5391573 C21.5052284,18.1514658 24.5159856,18.1309581 24.7019231,19.6143524 L24.7019231,29.2666692 C24.7948918,29.4990904 25.7817909,29.4854186 25.8461538,29.2666692 C25.946274,27.5371818 26.4111178,19.7510707 26.4182692,19.5733369 C26.6542668,18.1514658 29.6149639,18.1514658 29.8438101,19.5733369 C29.858113,19.7579067 30.3158053,27.5371818 30.4159255,29.2666692 C30.4802885,29.4854186 31.4743389,29.4990904 31.5601563,29.2666692 L31.5601563,19.6143524 C31.7460938,18.137794 34.7640024,18.1514658 34.9928486,19.5391573 Z M43.5173678,39.0693762 L42.4446514,51.7226612 C42.3588341,52.6796898 43.1526442,53.5 44.1538462,53.5 L48.1586538,53.5 C49.1097957,53.5 49.875,52.7685567 49.875,51.8593796 L49.875,20.1407181 C49.875,19.2383769 49.1097957,18.5000977 48.1586538,18.5000977 C42.2587139,18.5000977 32.3253606,30.7022121 43.5173678,39.0693762 Z"/>
|
||||
<path id="icon-e" d="M34.9928486,17.5391573 C35.0500601,17.8604454 36.1442308,23.9991002 36.1442308,26.3438202 C36.1442308,29.9190055 34.1561298,32.4688031 31.216887,33.4941909 L32.1394231,49.7705126 C32.1894832,50.7070335 31.409976,51.5 30.4230769,51.5 L25.8461538,51.5 C24.8664063,51.5 24.0797476,50.7138694 24.1298077,49.7705126 L25.0523437,33.4941909 C22.1059495,32.4688031 20.125,29.9121696 20.125,26.3438202 C20.125,23.9922643 21.2191707,17.8604454 21.2763822,17.5391573 C21.5052284,16.1514658 24.5159856,16.1309581 24.7019231,17.6143524 L24.7019231,27.2666692 C24.7948918,27.4990904 25.7817909,27.4854186 25.8461538,27.2666692 C25.946274,25.5371818 26.4111178,17.7510707 26.4182692,17.5733369 C26.6542668,16.1514658 29.6149639,16.1514658 29.8438101,17.5733369 C29.858113,17.7579067 30.3158053,25.5371818 30.4159255,27.2666692 C30.4802885,27.4854186 31.4743389,27.4990904 31.5601563,27.2666692 L31.5601563,17.6143524 C31.7460938,16.137794 34.7640024,16.1514658 34.9928486,17.5391573 Z M43.5173678,37.0693762 L42.4446514,49.7226612 C42.3588341,50.6796898 43.1526442,51.5 44.1538462,51.5 L48.1586538,51.5 C49.1097957,51.5 49.875,50.7685567 49.875,49.8593796 L49.875,18.1407181 C49.875,17.2383769 49.1097957,16.5000977 48.1586538,16.5000977 C42.2587139,16.5000977 32.3253606,28.7022121 43.5173678,37.0693762 Z"/>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<mask id="icon-b" fill="#fff">
|
||||
<use xlink:href="#icon-a"/>
|
||||
</mask>
|
||||
<g mask="url(#icon-b)">
|
||||
<rect width="70" height="70" fill="url(#icon-c)"/>
|
||||
<path fill="#FFF" fill-opacity=".383" d="M4,1.8 L65,1.8 C67.6666667,1.8 69.3333333,1.13333333 70,-0.2 C70,2.46666667 70,3.46666667 70,2.8 L1.10547097e-14,2.8 C-1.65952376e-14,3.46666667 -2.9161925e-14,2.46666667 -2.66453526e-14,-0.2 C0.666666667,1.13333333 2,1.8 4,1.8 Z" transform="matrix(1 0 0 -1 0 2.8)"/>
|
||||
<path fill="#393939" d="M37.1559742,53 L4,52 C2,52 -7.10542736e-15,51.8543417 0,47.9215686 L2.11942169e-16,24.8800004 L21.6437579,0 L21,12.2352941 L26.7780762,0 L29.5940312,3.47275252 L33,0 L35,12.2352941 L41.1357671,3.93586958 L49,0 L49.7896212,33.2154878 L37.1559742,53 Z" opacity=".324" transform="translate(0 17)"/>
|
||||
<path fill="#000" fill-opacity=".383" d="M4,4 L65,4 C67.6666667,4 69.3333333,3 70,1 C70,3.66666667 70,5 70,5 L1.77635684e-15,5 C1.77635684e-15,5 1.77635684e-15,3.66666667 1.77635684e-15,1 C0.666666667,3 2,4 4,4 Z" transform="translate(0 65)"/>
|
||||
<use fill="#000" fill-rule="nonzero" opacity=".3" xlink:href="#icon-d"/>
|
||||
<use fill="#FFF" fill-rule="nonzero" xlink:href="#icon-e"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/4formaggio.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/Coke.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/bacon_burger.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/brie.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/burger.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/cheeseburger.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/chicken_curry.png
Normal file
|
After Width: | Height: | Size: 7 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/chirashi.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/club.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/coke_zero.png
Normal file
|
After Width: | Height: | Size: 7 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/drink.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/fanta.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/fuze_black.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/fuze_green.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/gouda.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/italiana.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/lipton.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/lunch.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/maki.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/mozza.png
Normal file
|
After Width: | Height: | Size: 6 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/napoli.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/pasta_bolognese.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/pizza.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/pizza_funghi.png
Normal file
|
After Width: | Height: | Size: 9 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/pizza_veggie.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/salmon_sushi.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/temaki.png
Normal file
|
After Width: | Height: | Size: 9 KiB |
BIN
odoo-bringout-oca-ocb-lunch/lunch/static/img/tuna_sandwich.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
|
|
@ -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': 'warning', 'confirmed': 'success', 'sent': 'info', 'ordered': '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,142 @@
|
|||
<?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 align-items-center pe-3">
|
||||
<div class="btn-group">
|
||||
<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-2 py-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 text-700">
|
||||
<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 text-bg-#{badgeClass} border-#{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 pe-3">
|
||||
<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 pb-1">
|
||||
<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 pb-1">
|
||||
<t t-if="props.location">
|
||||
<Many2XAutocomplete
|
||||
value="props.location"
|
||||
resModel="'lunch.location'"
|
||||
getDomain="getDomain"
|
||||
activeActions="{}"
|
||||
update.bind="props.onUpdateLunchLocation"
|
||||
/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<p>No lunch location available.</p>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="lunch.LunchDashboardOrder" owl="1">
|
||||
<LunchAlerts alerts="state.infos.alerts"/>
|
||||
|
||||
<div class="o_lunch_banner container-fluid p-4 border-bottom bg-view">
|
||||
<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 pb-1">
|
||||
<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-5" t-if="hasLines">
|
||||
<h4 class="mb-0">
|
||||
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="o_lunch_widget_lines overflow-auto 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-3 d-flex flex-column justify-content-between" t-if="hasLines">
|
||||
<h4 class="d-flex flex-row mt-1">
|
||||
<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-view 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>
|
||||
|
|
@ -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')
|
||||
});
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.o_lunch_content {
|
||||
.o_kanban_renderer {
|
||||
&.o_kanban_grouped, &.o_kanban_ungrouped {
|
||||
min-height: auto; // override min-height: 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.o_lunch_widget_lines{
|
||||
//calculation to display the number of lines to show before scrolling
|
||||
$-entry-height: calc(#{$font-size-base * $line-height-base} + (#{map-get($spacers, 1)} * 2));
|
||||
max-height: calc(#{$-entry-height} * 4.5); // if we want 4 entries with the 5th row visible
|
||||
}
|
||||
46
odoo-bringout-oca-ocb-lunch/lunch/static/src/views/kanban.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/** @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 {
|
||||
getGroupsOrRecords() {
|
||||
const {locationId} = this.env.searchModel.lunchState;
|
||||
if (!locationId) {
|
||||
return [];
|
||||
} else {
|
||||
return super.getGroupsOrRecords(...arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?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 d-flex flex-column h-100">
|
||||
<LunchDashboard openOrderLine.bind="openOrderLine"/>
|
||||
|
||||
<div class="overflow-auto flex-grow-1">
|
||||
<t t-call="lunch.WebKanbanRenderer"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="lunch.WebKanbanRenderer" t-inherit="web.KanbanRenderer" t-inherit-mode="primary" owl="1">
|
||||
<t t-if="showNoContentHelper" position="after">
|
||||
<t t-call="lunch.NoContentHelper"/>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
40
odoo-bringout-oca-ocb-lunch/lunch/static/src/views/list.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/** @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 {
|
||||
setup() {
|
||||
super.setup();
|
||||
const {locationId} = this.env.searchModel.lunchState;
|
||||
if (!locationId) {
|
||||
this.props.list.records = [];
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
18
odoo-bringout-oca-ocb-lunch/lunch/static/src/views/list.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?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 d-flex flex-column h-100">
|
||||
<LunchDashboard openOrderLine.bind="openOrderLine"/>
|
||||
|
||||
<div class="overflow-auto flex-grow-1">
|
||||
<t t-call="lunch.WebListRenderer"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="lunch.WebListRenderer" t-inherit="web.ListRenderer" t-inherit-mode="primary" owl="1">
|
||||
<t t-call="web.ActionHelper" position="after">
|
||||
<t t-call="lunch.NoContentHelper"/>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="lunch.NoContentHelper" owl="1">
|
||||
<t t-set="showNoLocationHelper" t-value="!this.env.searchModel.lunchState.locationId"/>
|
||||
|
||||
<div t-if="showNoLocationHelper and !showNoContentHelper" class="o_view_nocontent" style="pointer-events: all">
|
||||
<div class="o_nocontent_help">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No location found
|
||||
</p>
|
||||
<p>
|
||||
Please create a location to start ordering.
|
||||
</p>
|
||||
</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;
|
||||
}
|
||||
const result = Domain.and([
|
||||
domain,
|
||||
[['is_available_at', '=', this.lunchState.locationId]]
|
||||
]);
|
||||
return params.raw ? result : result.toList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
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() {
|
||||
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: [],
|
||||
},
|
||||
'lunch.location': {
|
||||
fields: {
|
||||
name: {string: 'Name', type: 'char'},
|
||||
},
|
||||
records: [
|
||||
{id: 1, name: "Office 1"},
|
||||
{id: 2, name: "Office 2"},
|
||||
],
|
||||
},
|
||||
};
|
||||
this.regularInfos = {
|
||||
user_location: [2, "Office 2"],
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
QUnit.test('basic rendering', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const kanban = await createLunchView({
|
||||
View: LunchKanbanView,
|
||||
model: 'product',
|
||||
data: this.data,
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div><field name="name"/></div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
mockRPC: mockLunchRPC({
|
||||
infos: this.regularInfos,
|
||||
userLocation: this.data['lunch.location'].records[0].id,
|
||||
}),
|
||||
});
|
||||
|
||||
assert.containsOnce(kanban, '.o_legacy_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_legacy_kanban_view',
|
||||
"should have a 'classical kanban view' column");
|
||||
assert.hasClass(kanban.$('.o_legacy_kanban_view'), 'o_lunch_kanban_view',
|
||||
"should have classname 'o_lunch_kanban_view'");
|
||||
assert.containsOnce($('.o_lunch_content'), '> details',
|
||||
"should have a 'lunch kanban' details/summary discolure panel");
|
||||
assert.hasClass($('.o_lunch_content > details'), 'fixed-bottom',
|
||||
"should have classname 'fixed-bottom'");
|
||||
assert.isNotVisible($('.o_lunch_content > details .o_lunch_banner'),
|
||||
"shouldn't have a visible 'lunch kanban' banner");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('LunchWidget', function () {
|
||||
QUnit.test('toggle', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const kanban = await createLunchView({
|
||||
View: LunchKanbanView,
|
||||
model: 'product',
|
||||
data: this.data,
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div><field name="name"/></div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
mockRPC: mockLunchRPC({
|
||||
infos: Object.assign({}, this.regularInfos, {
|
||||
total: "3.00",
|
||||
}),
|
||||
userLocation: this.data['lunch.location'].records[0].id,
|
||||
}),
|
||||
});
|
||||
|
||||
const $details = $('.o_lunch_content > details');
|
||||
assert.isNotVisible($details.find('.o_lunch_banner'),
|
||||
"shouldn't have a visible 'lunch kanban' banner");
|
||||
assert.isVisible($details.find('> summary'),
|
||||
"should hava a visible cart toggle button");
|
||||
assert.containsOnce($details, '> summary:contains(Your cart)',
|
||||
"should have 'Your cart' in the button text");
|
||||
assert.containsOnce($details, '> summary:contains(3.00)',
|
||||
"should have '3.00' in the button text");
|
||||
|
||||
await testUtils.dom.click($details.find('> summary'));
|
||||
assert.isVisible($details.find('.o_lunch_banner'),
|
||||
"should have a visible 'lunch kanban' banner");
|
||||
|
||||
await testUtils.dom.click($details.find('> summary'));
|
||||
assert.isNotVisible($details.find('.o_lunch_banner'),
|
||||
"shouldn't have a visible 'lunch kanban' banner");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('keep open when adding quantities', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const kanban = await createLunchView({
|
||||
View: LunchKanbanView,
|
||||
model: 'product',
|
||||
data: this.data,
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div><field name="name"/></div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
mockRPC: mockLunchRPC({
|
||||
infos: Object.assign({}, this.regularInfos, {
|
||||
lines: [
|
||||
{
|
||||
id: 6,
|
||||
product: [1, "Tuna sandwich", "3.00"],
|
||||
toppings: [],
|
||||
quantity: 1.0,
|
||||
},
|
||||
],
|
||||
}),
|
||||
userLocation: this.data['lunch.location'].records[0].id,
|
||||
}),
|
||||
});
|
||||
|
||||
const $details = $('.o_lunch_content > details');
|
||||
assert.isNotVisible($details.find('.o_lunch_banner'),
|
||||
"shouldn't have a visible 'lunch kanban' banner");
|
||||
assert.isVisible($details.find('> summary'),
|
||||
"should hava a visible cart toggle button");
|
||||
|
||||
await testUtils.dom.click($details.find('> summary'));
|
||||
assert.isVisible($details.find('.o_lunch_banner'),
|
||||
"should have a visible 'lunch kanban' banner");
|
||||
|
||||
const $widgetSecondColumn = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(1)');
|
||||
|
||||
assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_lines > li',
|
||||
"should have 1 order line");
|
||||
|
||||
let $firstLine = $widgetSecondColumn.find('.o_lunch_widget_lines > li:first');
|
||||
|
||||
await testUtils.dom.click($firstLine.find('button.o_add_product'));
|
||||
assert.isVisible($('.o_lunch_content > details .o_lunch_banner'),
|
||||
"add quantity should keep 'lunch kanban' banner open");
|
||||
|
||||
$firstLine = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(1) .o_lunch_widget_lines > li:first');
|
||||
|
||||
await testUtils.dom.click($firstLine.find('button.o_remove_product'));
|
||||
assert.isVisible($('.o_lunch_content > details .o_lunch_banner'),
|
||||
"remove quantity should keep 'lunch kanban' banner open");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,415 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { click, getFixture, nextTick, patchWithCleanup } from '@web/../tests/helpers/utils';
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
import { LunchKanbanRenderer } from '@lunch/views/kanban';
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
let lunchInfos;
|
||||
|
||||
async function makeLunchView(extraArgs = {}) {
|
||||
return await makeView(
|
||||
Object.assign({
|
||||
serverData,
|
||||
type: "kanban",
|
||||
resModel: "lunch.product",
|
||||
arch: `
|
||||
<kanban js_class="lunch_kanban">
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="name"/>
|
||||
<field name="price"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
mockRPC: (route, args) => {
|
||||
if (route == '/lunch/user_location_get') {
|
||||
return Promise.resolve(serverData.models['lunch.location'].records[0].id);
|
||||
} else if (route == '/lunch/infos') {
|
||||
return Promise.resolve(lunchInfos);
|
||||
}
|
||||
}
|
||||
}, extraArgs
|
||||
));
|
||||
}
|
||||
|
||||
QUnit.module('Lunch', {}, function() {
|
||||
QUnit.module('LunchKanban', (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
'lunch.product': {
|
||||
fields: {
|
||||
id: { string: "ID", type: "integer" },
|
||||
name: { string: 'Name', type: 'char' },
|
||||
is_available_at: { string: 'Available', type: 'integer' },
|
||||
price: { string: 'Price', type: 'float', },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "Big Plate", is_available_at: 1, price: 4.95, },
|
||||
{ id: 2, name: "Small Plate", is_available_at: 2, price: 6.99, },
|
||||
{ id: 3, name: "Just One Plate", is_available_at: 2, price: 5.87, },
|
||||
]
|
||||
},
|
||||
'lunch.location': {
|
||||
fields: {
|
||||
name: { string: 'Name', type: 'char' },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "Old Office" },
|
||||
{ id: 2, name: "New Office" },
|
||||
]
|
||||
},
|
||||
'lunch.order': {
|
||||
fields: {
|
||||
product_id: { string: 'Product', type: 'many2one', relation: 'lunch.product', },
|
||||
}
|
||||
},
|
||||
'res.users': {
|
||||
fields: {
|
||||
share: { type: 'boolean', },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: 'Johnny Hache', share: false, },
|
||||
{ id: 2, name: 'David Elora', share: false, }
|
||||
]
|
||||
}
|
||||
},
|
||||
views: {
|
||||
'lunch.order,false,form': `<form>
|
||||
<sheet>
|
||||
<field name="product_id" readonly="1"/>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="add_to_cart" type="object" string="Add to cart" />
|
||||
<button string="Discard" special="cancel"/>
|
||||
</footer>
|
||||
</form>`
|
||||
}
|
||||
};
|
||||
lunchInfos = {
|
||||
username: "Johnny Hache",
|
||||
wallet: 12.05,
|
||||
is_manager: false,
|
||||
currency: {
|
||||
symbol: "€",
|
||||
position: "after",
|
||||
},
|
||||
user_location: [1, "Old Office"],
|
||||
alerts: [],
|
||||
lines: [],
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.test("Basic rendering", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
await makeLunchView();
|
||||
|
||||
assert.containsOnce(target, '.o_lunch_banner');
|
||||
assert.containsNone(target, '.o_lunch_content .alert');
|
||||
assert.containsOnce(target, '.o_kanban_record:not(.o_kanban_ghost)', 1);
|
||||
|
||||
const lunchDashboard = target.querySelector('.o_lunch_banner');
|
||||
const lunchUser = lunchDashboard.querySelector('.lunch_user span');
|
||||
assert.equal(lunchUser.innerText, 'Johnny Hache');
|
||||
});
|
||||
|
||||
QUnit.test("Open product", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
await makeLunchView();
|
||||
|
||||
patchWithCleanup(LunchKanbanRenderer.prototype, {
|
||||
openOrderLine(productId, orderId) {
|
||||
assert.equal(productId, 1);
|
||||
}
|
||||
});
|
||||
|
||||
assert.containsOnce(target, '.o_kanban_record:not(.o_kanban_ghost)');
|
||||
|
||||
click(target, '.o_kanban_record:not(.o_kanban_ghost)');
|
||||
});
|
||||
|
||||
QUnit.test("Basic rendering with alerts", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
let userInfos = {
|
||||
...lunchInfos,
|
||||
alerts: [
|
||||
{
|
||||
id: 1,
|
||||
message: '<b>free boudin compote for everyone</b>',
|
||||
}
|
||||
]
|
||||
};
|
||||
await makeLunchView({
|
||||
mockRPC: (route, args) => {
|
||||
if (route == '/lunch/user_location_get') {
|
||||
return Promise.resolve(userInfos.user_location[0]);
|
||||
} else if (route == '/lunch/infos') {
|
||||
return Promise.resolve(userInfos);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.containsOnce(target, '.o_lunch_content .alert');
|
||||
assert.equal(target.querySelector('.o_lunch_content .alert').innerText, 'free boudin compote for everyone');
|
||||
});
|
||||
|
||||
QUnit.test("Open product", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
await makeLunchView();
|
||||
|
||||
patchWithCleanup(LunchKanbanRenderer.prototype, {
|
||||
openOrderLine(productId, orderId) {
|
||||
assert.equal(productId, 1);
|
||||
}
|
||||
});
|
||||
|
||||
assert.containsOnce(target, '.o_kanban_record:not(.o_kanban_ghost)');
|
||||
|
||||
click(target, '.o_kanban_record:not(.o_kanban_ghost)');
|
||||
});
|
||||
|
||||
QUnit.test("Location change", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
let userInfos = { ...lunchInfos };
|
||||
await makeLunchView({
|
||||
mockRPC: (route, args) => {
|
||||
if (route == '/lunch/user_location_get') {
|
||||
return Promise.resolve(userInfos.user_location[0]);
|
||||
} else if (route == '/lunch/infos') {
|
||||
return Promise.resolve(userInfos);
|
||||
} else if (route == '/lunch/user_location_set') {
|
||||
assert.equal(args.location_id, 2);
|
||||
userInfos.user_location = [2, "New Office"];
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
click(target, '.lunch_location input');
|
||||
|
||||
await nextTick();
|
||||
assert.containsOnce(target, '.lunch_location .dropdown-item:contains(New Office)');
|
||||
|
||||
click(target, '.lunch_location .dropdown-item:not(.ui-state-active)');
|
||||
|
||||
await nextTick();
|
||||
assert.containsN(target, 'div[role=article].o_kanban_record', 2);
|
||||
});
|
||||
|
||||
QUnit.test("Manager: user change", async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
let userInfos = { ...lunchInfos, is_manager: true };
|
||||
let expectedUserId = false; // false as we are requesting for the current user
|
||||
await makeLunchView({
|
||||
mockRPC: (route, args) => {
|
||||
if (route == '/lunch/user_location_get') {
|
||||
return Promise.resolve(userInfos.user_location[0]);
|
||||
} else if (route == '/lunch/infos') {
|
||||
assert.equal(expectedUserId, args.user_id);
|
||||
|
||||
if (expectedUserId === 2) {
|
||||
userInfos = {
|
||||
...userInfos,
|
||||
username: 'David Elora',
|
||||
wallet: -10000,
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.resolve(userInfos);
|
||||
} else if (route == '/lunch/user_location_set') {
|
||||
assert.equal(args.location_id, 2);
|
||||
userInfos.user_location = [2, "New Office"];
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.containsOnce(target, '.lunch_user input');
|
||||
click(target, '.lunch_user input');
|
||||
|
||||
await nextTick();
|
||||
assert.containsOnce(target, '.lunch_user .dropdown-item:contains(David Elora)');
|
||||
|
||||
expectedUserId = 2;
|
||||
click(target, '.lunch_user .dropdown-item:not(.ui-state-active)');
|
||||
|
||||
await nextTick();
|
||||
const wallet = target.querySelector('.o_lunch_banner .col-9 > .d-flex > span:nth-child(2)');
|
||||
assert.equal(wallet.innerText, '-10000.00€', 'David Elora is poor')
|
||||
|
||||
click(target, '.lunch_location input');
|
||||
await nextTick();
|
||||
click(target, '.lunch_location .dropdown-item:not(.ui-state-active)');
|
||||
|
||||
await nextTick();
|
||||
const user = target.querySelector('.lunch_user input');
|
||||
assert.equal(user.value, 'David Elora', 'changing location should not reset user');
|
||||
});
|
||||
|
||||
QUnit.test("Trash existing order", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
let userInfos = {
|
||||
...lunchInfos,
|
||||
lines: [
|
||||
{
|
||||
id: 1,
|
||||
product: [1, "Big Plate", "4.95"],
|
||||
toppings: [],
|
||||
quantity: 1,
|
||||
price: 4.95,
|
||||
raw_state: "new",
|
||||
state: "To Order",
|
||||
note: false
|
||||
}
|
||||
],
|
||||
raw_state: "new",
|
||||
total: "4.95",
|
||||
};
|
||||
await makeLunchView({
|
||||
mockRPC: (route, args) => {
|
||||
if (route == '/lunch/user_location_get') {
|
||||
return Promise.resolve(userInfos.user_location[0]);
|
||||
} else if (route == '/lunch/infos') {
|
||||
return Promise.resolve(userInfos);
|
||||
} else if (route == '/lunch/trash') {
|
||||
userInfos = {
|
||||
...userInfos,
|
||||
lines: [],
|
||||
raw_state: false,
|
||||
total: 0,
|
||||
};
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.containsN(target, 'div.o_lunch_banner > .row > div', 3);
|
||||
assert.containsOnce(target, 'div.o_lunch_banner > .row > div:nth-child(2) button.fa-trash', 'should have trash icon');
|
||||
assert.containsOnce(target, 'div.o_lunch_banner > .row > div:nth-child(2) ul > li', 'should have one order line');
|
||||
|
||||
assert.containsOnce(target, 'div.o_lunch_banner > .row > div:nth-child(3) button:contains(Order Now)');
|
||||
|
||||
click(target, 'div.o_lunch_banner > .row > div:nth-child(2) button.fa-trash');
|
||||
await nextTick();
|
||||
assert.containsN(target, 'div.o_lunch_banner > .row > div', 1);
|
||||
});
|
||||
|
||||
QUnit.test("Change existing order", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
let userInfos = {
|
||||
...lunchInfos,
|
||||
lines: [
|
||||
{
|
||||
id: 1,
|
||||
product: [1, "Big Plate", "4.95"],
|
||||
toppings: [],
|
||||
quantity: 1,
|
||||
price: 4.95,
|
||||
raw_state: "new",
|
||||
state: "To Order",
|
||||
note: false
|
||||
}
|
||||
],
|
||||
raw_state: "new",
|
||||
total: "4.95",
|
||||
};
|
||||
await makeLunchView({
|
||||
mockRPC: (route, args) => {
|
||||
if (route == '/lunch/user_location_get') {
|
||||
return Promise.resolve(userInfos.user_location[0]);
|
||||
} else if (route == '/lunch/infos') {
|
||||
return Promise.resolve(userInfos);
|
||||
} else if (route == '/web/dataset/call_kw/lunch.order/update_quantity') {
|
||||
assert.equal(args.args[1], 1, 'should increment order quantity by 1');
|
||||
userInfos = {
|
||||
...userInfos,
|
||||
lines: [
|
||||
{
|
||||
...userInfos.lines[0],
|
||||
product: [1, "Big Plate", "9.9"],
|
||||
quantity: 2,
|
||||
price: 4.95 * 2,
|
||||
}
|
||||
],
|
||||
total: 4.95 * 2,
|
||||
};
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
click(target, 'div.o_lunch_banner > .row > div:nth-child(2) button.fa-plus-circle');
|
||||
});
|
||||
|
||||
QUnit.test("Confirm existing order", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
let userInfos = {
|
||||
...lunchInfos,
|
||||
lines: [
|
||||
{
|
||||
id: 1,
|
||||
product: [1, "Big Plate", "4.95"],
|
||||
toppings: [],
|
||||
quantity: 1,
|
||||
price: 4.95,
|
||||
raw_state: "new",
|
||||
state: "To Order",
|
||||
note: false
|
||||
}
|
||||
],
|
||||
raw_state: "new",
|
||||
total: "4.95",
|
||||
};
|
||||
await makeLunchView({
|
||||
mockRPC: (route, args) => {
|
||||
if (route == '/lunch/user_location_get') {
|
||||
return Promise.resolve(userInfos.user_location[0]);
|
||||
} else if (route == '/lunch/infos') {
|
||||
return Promise.resolve(userInfos);
|
||||
} else if (route == '/lunch/pay') {
|
||||
assert.equal(args.user_id, false); // Should confirm order of current user
|
||||
userInfos = {
|
||||
...userInfos,
|
||||
lines: [
|
||||
{
|
||||
...userInfos.lines[0],
|
||||
raw_state: 'ordered',
|
||||
state: 'Ordered,'
|
||||
}
|
||||
],
|
||||
raw_state: 'ordered',
|
||||
wallet: userInfos.wallet - 4.95,
|
||||
};
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const wallet = target.querySelector('.o_lunch_banner .col-9 > .d-flex > span:nth-child(2)');
|
||||
assert.equal(wallet.innerText, '12.05€');
|
||||
|
||||
click(target, 'div.o_lunch_banner > .row > div:nth-child(3) button');
|
||||
|
||||
await nextTick();
|
||||
assert.equal(wallet.innerText, '7.10€', 'Wallet should update');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
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: [],
|
||||
},
|
||||
'lunch.location': {
|
||||
fields: {
|
||||
name: {string: 'Name', type: 'char'},
|
||||
company_id: {string: 'Company', type: 'many2one', relation: 'res.company'},
|
||||
},
|
||||
records: [
|
||||
{id: 1, name: "Office 1", company_id: false},
|
||||
{id: 2, name: "Office 2", company_id: false},
|
||||
],
|
||||
},
|
||||
'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]},
|
||||
],
|
||||
},
|
||||
'res.company': {
|
||||
fields: {
|
||||
name: {string: 'Name', type: 'char'},
|
||||
}, records: [
|
||||
{id: 1, name: "Dunder Trade Company"},
|
||||
]
|
||||
}
|
||||
};
|
||||
this.regularInfos = {
|
||||
username: "Marc Demo",
|
||||
wallet: 36.5,
|
||||
is_manager: false,
|
||||
group_portal_id: PORTAL_GROUP_ID,
|
||||
currency: {
|
||||
symbol: "\u20ac",
|
||||
position: "after"
|
||||
},
|
||||
user_location: [2, "Office 2"],
|
||||
alerts: [{id: 42, message: '<b>Warning! Neurotoxin pressure has reached dangerously unlethal levels.</b>'}]
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
QUnit.test('basic rendering', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
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_legacy_list_view',
|
||||
"should have a 'classical list view' column");
|
||||
assert.hasClass(list.$('.o_legacy_list_view'), 'o_lunch_list_view',
|
||||
"should have classname 'o_lunch_list_view'");
|
||||
assert.containsOnce(list, '.o_lunch_content > .o_lunch_banner',
|
||||
"should have a 'lunch' banner");
|
||||
|
||||
const $alertMessage = list.$('.alert > *');
|
||||
assert.equal($alertMessage.length, 1);
|
||||
assert.equal($alertMessage.prop('tagName'), 'B');
|
||||
assert.equal($alertMessage.text(), "Warning! Neurotoxin pressure has reached dangerously unlethal levels.")
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('LunchWidget', function () {
|
||||
|
||||
QUnit.test('search panel domain location', async function (assert) {
|
||||
assert.expect(18);
|
||||
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',
|
||||
// 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',
|
||||
]);
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('search panel domain location false: fetch products in all locations', async function (assert) {
|
||||
assert.expect(9);
|
||||
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',
|
||||
])
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { _t } from 'web.core';
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
tour.register('order_lunch_tour', {
|
||||
url: "/web",
|
||||
test: true,
|
||||
}, [
|
||||
tour.stepUtils.showAppsMenuItem(),
|
||||
{
|
||||
trigger: '.o_app[data-menu-xmlid="lunch.menu_lunch"]',
|
||||
content: _t("Start by accessing the lunch app."),
|
||||
position: 'bottom',
|
||||
},
|
||||
{
|
||||
content:"click on location",
|
||||
trigger: ".lunch_location .o_input_dropdown input",
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Pick 'Farm 1' option",
|
||||
trigger: '.o_input_dropdown li:contains(Farm 1)',
|
||||
},
|
||||
{
|
||||
trigger: '.lunch_location input:propValueContains(Farm 1)',
|
||||
run: () => {}, // wait for article to be correctly loaded
|
||||
},
|
||||
{
|
||||
trigger: "div[role='article']",
|
||||
content: _t("Click on a product you want to order and is available."),
|
||||
run: 'click'
|
||||
},
|
||||
{
|
||||
trigger: 'textarea[id="note"]',
|
||||
content: _t("Add additionnal information about your order."),
|
||||
position: 'bottom',
|
||||
run: 'text allergy to peanuts',
|
||||
},
|
||||
{
|
||||
trigger: 'button[name="add_to_cart"]',
|
||||
content: _t("Add your order to the cart."),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'button:contains("Order Now")',
|
||||
content: _t("Validate your order"),
|
||||
position: 'left',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: '.o_lunch_widget_lines .badge:contains("Ordered")',
|
||||
content: 'Check that order is ordered',
|
||||
run: () => {}
|
||||
}]);
|
||||