[IMP] sale_product_configurator: redesign of the modal

This commit
- Cleans and optimize the SCSS
- Improves the layout and the spacing of the existing modal
- Improves the input's style (`.o_input`, better pills)
- Sets a fixed height to the modal so it doesn't grow/shrink when
fields are shown/hidden

task-3289240

closes odoo/odoo#133056

closes odoo/odoo#140096

Related: odoo/enterprise#49636
Related: odoo/enterprise#49733
Signed-off-by: Valentin Chevalier <vcr@odoo.com>
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
Antoine (anso)
2023-10-27 21:36:46 +00:00
committed by Antoine Vandevenne (anv)
parent bd187c35c2
commit 5f7aa80c73
20 changed files with 324 additions and 304 deletions
@@ -1,9 +0,0 @@
.o_product_badge_extra_price {
display: inline-block;
width: 1rem;
height: 1rem;
color: map-get($theme-colors, 'primary');
background: white;
line-height: 1rem;
border-radius: 50%;
}
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="product.badge_extra_price">
<span class="badge rounded-pill text-bg-light border ms-1 ps-1">
<span t-out="this.props.price > 0 ? '+' : '-'" class="o_product_badge_extra_price"/>
<span t-out="getFormattedPrice()" class="text-muted fst-italic"/>
<span class="badge ms-1 ps-1 text-bg-info rounded-pill">
<span t-out="this.props.price > 0 ? '+' : '-'" class="me-1"/>
<i t-out="getFormattedPrice()"/>
</span>
</t>
</templates>
@@ -1,47 +1,36 @@
.table.o_sale_product_configurator_table {
& tr:first-child > td {
padding-top: 0 !important;
}
&.o_sale_product_configurator_table_optional > :not(caption) > *:last-child > * {
border-bottom: 0;
}
}
.o_sale_product_configurator_img {
width: 100px;
width: 120px;
max-height: 240px;
}
.o_sale_product_configurator_qty {
width: 200px;
width: 160px;
.input-group {
display: inline-flex;
}
div {
width: initial; // We don't want the quantity form to be full-width
.btn, input {
border-color: $input-border-color;
input {
max-width: 4rem;
//removing input field=number arrows as their size might
//change depending on browser default styling and shift input's position
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input {
// Needs !important because themes customize btns' padding directly
// rather than change '$input-btn-padding-X' (shared with inputs).
height: auto !important;
max-width: 4rem;
text-align: center;
//removing input field=number arrows as their size might
//change depending on browser default styling and shift input's position
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
&[type=number] {
-moz-appearance: textfield;
}
&[type=number] {
-moz-appearance: textfield;
}
}
}
.o_sale_product_configurator_name {
word-wrap: break-word;
min-width: 140px;
}
.o_sale_product_configurator_price {
width: 120px;
width: 160px;
}
@@ -1,69 +1,77 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="sale_product_configurator.product">
<td class="o_sale_product_configurator_img">
<td class="o_sale_product_configurator_img py-3 px-0">
<img
t-if="this.props.id"
class="w-100"
t-att-src="'/web/image/product.product/'+this.props.id+'/image_128'"
alt="Product Image"/>
<img
t-else=""
class="w-100"
t-att-src="'/web/image/product.template/'+this.props.product_tmpl_id+'/image_128'"
alt="Product Image"/>
</td>
<td t-att-colspan="this.props.optional ? 2:false">
<div class="o_sale_product_configurator_name">
<strong t-out="this.props.display_name"/>
<td class="p-3" t-att-colspan="this.props.optional ? 2:false">
<div class="mb-4 text-break" name="o_sale_product_configurator_name">
<h5 t-out="this.props.display_name"/>
<div
t-if="this.props.description_sale"
t-out="this.props.description_sale"
class="text-muted small"/>
<div t-if="!this.env.isPossibleCombination(this.props)" class="alert alert-warning mt-3">
<span>This option or combination of options is not available</span>
</div>
</div>
<t t-foreach="this.props.attribute_lines" t-as="ptal" t-key="ptal.id">
<PTAL t-props="ptal" productTmplId="this.props.product_tmpl_id"/>
</t>
</td>
<td t-if="!this.props.optional" class="text-center o_sale_product_configurator_qty">
<div class="input-group">
<td class="o_sale_product_configurator_qty py-3 px-0 text-end">
<div t-if="!this.props.optional" class="input-group justify-content-end">
<button
class="btn btn-primary d-none d-md-inline-block"
class="btn btn-secondary d-none d-md-inline-block"
aria-label="Remove one"
t-on-click="decreaseQuantity">
<i class="fa fa-minus"></i>
<i class="fa fa-minus"/>
</button>
<input
class="form-control quantity border-bottom border-top text-center"
name="product_quantity"
type="number"
t-att-value="this.props.quantity"
t-on-change="setQuantity"/>
<button
class="btn btn-primary d-none d-md-inline-block"
class="btn btn-secondary d-none d-md-inline-block"
aria-label="Add one"
t-on-click="increaseQuantity">
<i class="fa fa-plus"></i>
<i class="fa fa-plus"/>
</button>
</div>
<div t-else="">
<h5 class="text-nowrap" t-out="getFormattedPrice()"/>
</div>
<a
class="d-block mt-2"
role="button"
t-if="!this.props.optional &amp;&amp; this.env.mainProductTmplId !== this.props.product_tmpl_id"
t-on-click="() => this.env.removeProduct(this.props.product_tmpl_id)">
Remove product
</a>
</td>
<td class="text-center o_sale_product_configurator_price" name="price">
<t t-if="this.env.isPossibleCombination(this.props)">
<span style="white-space: nowrap;" t-out="getFormattedPrice()"/>
<td class="o_sale_product_configurator_price py-3 px-0 text-end" name="price">
<div t-if="!this.props.optional" class="input-group justify-content-end">
<h5 class="text-nowrap" t-out="getFormattedPrice()"/>
</div>
<div t-else="">
<button
t-if="this.props.optional"
class="btn btn-primary btn-sm"
class="btn btn-secondary"
t-att-class="{'disabled': !this.env.isPossibleCombination(this.props)}"
t-on-click="() => this.env.addProduct(this.props.product_tmpl_id)">
<i class="fa fa-shopping-cart"></i> Add to cart
<i class="fa fa-plus"/> Add
</button>
</t>
<t t-else="">
<p class="alert alert-warning">Option not available</p>
</t>
<div>
<a
role="button"
t-if="!this.props.optional &amp;&amp; this.env.mainProductTmplId !== this.props.product_tmpl_id"
t-on-click="() => this.env.removeProduct(this.props.product_tmpl_id)">
<i class="fa fa-trash-o"/>
</a>
</div>
</td>
</t>
@@ -36,7 +36,7 @@ export class ProductConfiguratorDialog extends Component {
}
setup() {
this.title = _t("Configure");
this.title = _t("Configure your product");
this.rpc = useService("rpc");
this.state = useState({
products: [],
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="sale_product_configurator.dialog">
<Dialog size="size" title="title">
<Dialog size="size" title="title" contentClass="this.state.optionalProducts.length ? 'h-75' : ''">
<ProductList t-if="this.state.products.length" products="this.state.products"/>
<ProductList
t-if="this.state.optionalProducts.length"
@@ -15,7 +15,7 @@
Confirm
</button>
<button class="btn btn-secondary" t-on-click="onDiscard">
Back
Cancel
</button>
</t>
</Dialog>
@@ -1,31 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="saleProductConfigurator.productList">
<table class="table table-striped table-sm">
<thead>
<tr t-if="!this.props.areProductsOptional">
<th class="o_product_configurator_img"><span class="label">Product</span></th>
<th><span class="label"></span></th>
<th class="text-center o_product_configurator_qty">
<span class="label">Quantity</span>
</th>
<th class="text-center o_product_configurator_price">
<span class="label">Price</span>
</th>
<h4 class="mt-4 mb-3" t-if="this.props.areProductsOptional">Add optional products</h4>
<table class="o_sale_product_configurator_table table table-sm position-relative mb-0" t-att-class="{'o_sale_product_configurator_table_optional': this.props.areProductsOptional}">
<thead t-if="!this.props.areProductsOptional">
<tr>
<th class="px-0 border-bottom-0" colspan="2">Product</th>
<th class="px-0 text-end border-bottom-0">Quantity</th>
<th class="px-0 text-end border-bottom-0">Price</th>
</tr>
<tr t-else=""><th colspan="4"><h4>Available Options:</h4></th></tr>
</thead>
<tbody>
<tbody class="border-top-0">
<tr t-foreach="this.props.products" t-as="product" t-key="product.product_tmpl_id">
<Product t-props="product" optional="this.props.areProductsOptional"/>
</tr>
<tr t-if="!this.props.areProductsOptional">
<td colspan="4" class="text-end">
<strong>Total: </strong>
<span class="fw-bold" t-out="getFormattedTotal()"/>
<td colspan="4" class="border-bottom-0 text-end">
<h4>Total: <span t-out="getFormattedTotal()"/></h4>
</td>
</tr>
</tbody>
</tbody>
</table>
</t>
</templates>
@@ -133,4 +133,15 @@ export class ProductTemplateAttributeLine extends Component {
ptav => this.props.selected_attribute_value_ids.includes(ptav.id)
)?.is_custom;
}
/**
* Check if the line has a custom ptav or not.
*
* @return {Boolean} - Whether the line has a custom ptav or not.
*/
hasPTAVCustom() {
return this.props.attribute_values.some(
ptav => ptav.is_custom
);
}
}
@@ -1,24 +1,5 @@
.o_sale_product_configurator_ptal_name {
display: flex;
align-items: center;
font-size: 0.9rem;
text-transform: uppercase;
padding-bottom: 0.5rem;
&:after {
content: '';
margin-left: $spacer;
flex-grow: 1;
border-bottom: 1px solid map-get($grays, '400');
}
}
.o_sale_product_configurator_ptav_color {
position: relative;
display: inline-block;
border: 5px solid $input-border-color;
border-radius: 50%;
text-align: center;
border: 5px solid $border-color;
transition: $input-transition;
@include o-field-pointer();
@@ -27,7 +8,7 @@
content: "";
display: block;
@include o-position-absolute(-3px, -3px, -3px, -3px);
border: 4px solid white;
border: 4px solid $o-view-background-color;
border-radius: 50%;
box-shadow: inset 0 0 3px rgba(black, 0.3);
}
@@ -66,27 +47,12 @@
}
}
.o_sale_product_configurator_ptav_pills {
padding: $spacer/2 $spacer;
margin-right: 0.2rem;
border: none;
cursor: default !important;
&.btn.active {
color: $white;
background-color: map-get($theme-colors, 'primary');
}
&:not(.active) {
color: map-get($grays, '600');
background-color: map-get($grays, '200');
}
input {
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
opacity: 0;
}
.o_sale_product_configurator_ptav_pills.active label {
$-btn-secondary-design: map-get($o-btns-bs-override, "secondary");
background-color: map-get($-btn-secondary-design, active-background);
border-color: map-get($-btn-secondary-design, active-border);
color: map-get($-btn-secondary-design, active-color);
}
.css_not_available {
@@ -2,26 +2,31 @@
<templates xml:space="preserve">
<!-- Attributes line template -->
<t t-name="saleProductConfigurator.ptal">
<div name="ptal" t-attf-class="#{this.props.attribute_values.length === 1 &amp;&amp; hasPTAVCustom() ? 'd-flex' : ''}">
<!-- If the attribute line contains only one attribute value, we don't show the attribute
value template or the attribute line title unless the single attribute value is custom,
whereas in this case, only the title of the attribute line and the custom value
template are rendered. -->
<strong
t-if="this.props.attribute_values.length === 1 &amp;&amp; isSelectedPTAVCustom() || this.props.attribute_values.length &gt; 1"
t-out="this.props.attribute.name"
class="o_sale_product_configurator_ptal_name"/>
<t t-if="this.props.attribute_values.length > 1" t-call="{{getPTAVTemplate()}}"/>
<input
type="text"
placeholder="custom"
t-if="isSelectedPTAVCustom()"
t-att-value="this.props.customValue"
t-on-change="updateCustomValue"
/>
<div class="d-flex flex-column flex-lg-row gap-2 mb-2">
<label
t-if="this.props.attribute_values.length === 1 &amp;&amp; isSelectedPTAVCustom() || this.props.attribute_values.length &gt; 1"
t-out="this.props.attribute.name"
t-attf-class="fw-bold text-break #{this.props.attribute_values.length === 1 &amp;&amp; hasPTAVCustom() ? '' : 'w-lg-25'}"/>
<t t-if="this.props.attribute_values.length > 1" t-call="{{getPTAVTemplate()}}"/>
</div>
<input
class="o_input w-75 mb-4 ms-lg-auto"
type="text"
placeholder="Enter a customized value"
t-if="hasPTAVCustom &amp;&amp; isSelectedPTAVCustom()"
t-att-value="this.props.customValue"
t-on-change="updateCustomValue"
/>
</div>
</t>
<!-- Attributes value templates -->
<t t-name="saleProductConfigurator.ptav-select">
<select class="form-select"
<select class="o_input w-50 flex-grow-1"
t-on-change="updateSelectedPTAV"
t-att-name="'ptal-' + this.props.id">
<option
@@ -35,40 +40,12 @@
</select>
</t>
<t t-name="saleProductConfigurator.ptav-radio">
<ul class="list-inline list-unstyled">
<ul class="list-unstyled flex-grow-1 m-0">
<li t-foreach="this.props.attribute_values" t-as="ptav" t-key="ptav.id"
class="list-inline-item mb-3">
class="mb-2">
<div class="form-check">
<label
class="col-form-label"
t-att-class="{ 'css_not_available': ptav.excluded }"
t-att-for="ptav.id">
<span t-out="ptav.name"/>
<BadgeExtraPrice
t-if="ptav.price_extra"
price="ptav.price_extra"
currencyId="this.env.currencyId"/>
<input
type="radio"
class="form-check-input"
t-att-id="ptav.id"
t-att-value="ptav.id"
t-att-checked="this.props.selected_attribute_value_ids.includes(ptav.id)"
t-att-name="'ptal-' + this.props.id"
t-on-change="updateSelectedPTAV"/>
</label>
</div>
</li>
</ul>
</t>
<t t-name="saleProductConfigurator.ptav-pills">
<ul t-attf-class="list-inline list-unstyled">
<li t-foreach="this.props.attribute_values" t-as="ptav" t-key="ptav.id"
t-att-class="{'active': this.props.selected_attribute_value_ids.includes(ptav.id)}"
class="o_sale_product_configurator_ptav_pills btn btn-primary list-inline-item mb-3">
<div class="form-check">
<label
class="col-form-label"
class="form-check-label d-inline-flex align-items-center"
t-att-class="{ 'css_not_available': ptav.excluded }"
t-att-for="ptav.id">
<span t-out="ptav.name"/>
@@ -79,22 +56,50 @@
</label>
<input
type="radio"
class="form-check-input"
t-att-id="ptav.id"
t-att-value="ptav.id"
t-att-name="'ptal-' + this.props.id"
t-att-checked="this.props.selected_attribute_value_ids.includes(ptav.id)"
t-att-name="'ptal-' + this.props.id"
t-on-change="updateSelectedPTAV"/>
</div>
</li>
</ul>
</t>
<t t-name="saleProductConfigurator.ptav-color">
<ul class="list-inline">
<t t-name="saleProductConfigurator.ptav-pills">
<ul class="list-inline list-unstyled flex-grow-1 mb-0">
<li t-foreach="this.props.attribute_values" t-as="ptav" t-key="ptav.id"
class="list-inline-item me-1">
t-att-class="{'active': this.props.selected_attribute_value_ids.includes(ptav.id)}"
class="o_sale_product_configurator_ptav_pills list-inline-item">
<label
class="btn btn-outline-secondary"
t-att-class="{ 'css_not_available': ptav.excluded }"
t-att-for="ptav.id">
<span t-out="ptav.name"/>
<BadgeExtraPrice
t-if="ptav.price_extra"
price="ptav.price_extra"
currencyId="this.env.currencyId"/>
</label>
<input
class="btn-check"
type="radio"
t-att-id="ptav.id"
t-att-value="ptav.id"
t-att-name="'ptal-' + this.props.id"
t-att-checked="this.props.selected_attribute_value_ids.includes(ptav.id)"
t-on-change="updateSelectedPTAV"/>
</li>
</ul>
</t>
<t t-name="saleProductConfigurator.ptav-color">
<ul class="list-inline flex-grow-1 mb-0">
<li t-foreach="this.props.attribute_values" t-as="ptav" t-key="ptav.id"
class="list-inline-item me-2">
<t t-set="img_style" t-value="ptav.image ? 'background:url(/web/image/product.template.attribute.value/'+ptav.id+'/image); background-size:cover;' : ''"/>
<t t-set="color_style" t-value="ptav.is_custom ? '' : 'background-color:' + ptav.html_color"/>
<label
class="position-relative d-inline-block rounded-pill text-center"
t-att-title="ptav.name"
t-attf-style="#{img_style or color_style}"
t-att-class="{'o_sale_product_configurator_ptav_color': true,
@@ -114,20 +119,20 @@
</ul>
</t>
<t t-name="saleProductConfigurator.ptav-multi">
<ul t-attf-class="list-inline list-unstyled">
<ul class="list-unstyled flex-grow-1 m-0">
<li t-foreach="this.props.attribute_values" t-as="ptav" t-key="ptav.id"
class="list-inline-item mb-3">
class="mb-2">
<div class="form-check">
<input
type="checkbox"
class="me-1"
class="form-check-input"
t-att-id="ptav.id"
t-att-value="ptav.id"
t-att-name="'ptal-' + this.props.id"
t-att-checked="this.props.selected_attribute_value_ids.includes(ptav.id)"
t-on-change="updateSelectedPTAV"/>
<label
class="col-form-label"
class="form-check-label"
t-att-for="ptav.id">
<span t-out="ptav.name"/>
<BadgeExtraPrice
@@ -13,6 +13,7 @@
],
'assets': {
'web.assets_tests': [
'test_sale_product_configurators/static/src/js/tour_utils.js',
'test_sale_product_configurators/static/tests/tours/**/*',
],
},
@@ -0,0 +1,86 @@
/** @odoo-module **/
function addOptionalProduct(productName) {
return {
trigger: `table.o_sale_product_configurator_table_optional tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("${productName}")) td.o_sale_product_configurator_price button:contains("Add")`,
content: `Add ${productName}`,
};
}
function increaseProductQuantity(productName) {
return {
trigger: `table.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("${productName}")) td.o_sale_product_configurator_qty div>button:has(i.fa-plus)`,
content: `Increase quantity of ${productName}`,
};
}
function selectAttribute(productName, attributeName, attributeValue, attributeType='radio') {
switch (attributeType) {
case 'color':
return {
trigger: `table.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("${productName}")) td>div[name="ptal"]:has(div>label:contains("${attributeName}")) label[title="${attributeValue}"]`,
content: `Select ${attributeValue} for ${productName} ${attributeName}`,
};
case 'multi':
case 'pills':
case 'radio':
return {
trigger: `table.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("${productName}")) td>div[name="ptal"]:has(div>label:contains("${attributeName}")) span:contains("${attributeValue}")`,
content: `Select ${attributeValue} for ${productName} ${attributeName}`,
};
case 'select':
return {
trigger: `table.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("${productName}")) td>div[name="ptal"]:has(div>label:contains("${attributeName}"))`,
content: `Select ${attributeValue} for ${productName} ${attributeName}`,
run: () => {
let $selectElement = $(`table.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("${productName}")) td>div[name="ptal"]:has(div>label:contains("${attributeName}")) select`)
let inputValue = $selectElement.children(`option:contains(${attributeValue})`).val();
$selectElement.val(inputValue);
$selectElement[0].dispatchEvent(new Event("change"));
}
};
default:
return;
}
}
function setCustomAttribute(productName, attributeName, customValue) {
return {
trigger: `table.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("${productName}")) td>div[name="ptal"]:has(div>label:contains("${attributeName}")) input[type="text"]`,
content: `Set ${customValue} as a custom attribute for ${productName} ${attributeName}`,
run: `text ${customValue}`,
};
}
function selectAndSetCustomAttribute(productName, attributeName, attributeValue, customValue, attributeType=undefined) {
return [
selectAttribute(productName, attributeName, attributeValue, attributeType),
setCustomAttribute(productName, attributeName, customValue),
]
}
function assertPriceTotal(total) {
return {
trigger: `table.o_sale_product_configurator_table tr>td[colspan=4] span:contains("${total}")`,
content: `Assert that the total is ${total}`,
isCheck: true,
};
}
function assertProductNameContains(productName) {
return {
trigger: `table.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("${productName}"))`,
content: `Assert that the productName contains ${productName}`,
isCheck: true,
};
}
export default {
addOptionalProduct,
increaseProductQuantity,
selectAttribute,
setCustomAttribute,
selectAndSetCustomAttribute,
assertPriceTotal,
assertProductNameContains,
};
@@ -2,6 +2,7 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_service/tour_utils";
import configuratorTourUtils from "@test_sale_product_configurators/js/tour_utils";
registry.category("web_tour.tours").add('event_sale_with_product_configurator_tour', {
url: '/web',
@@ -25,7 +26,7 @@ registry.category("web_tour.tours").add('event_sale_with_product_configurator_to
}, {
trigger: 'ul.ui-autocomplete a:contains("Registration Event (TEST variants)")',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Memorabilia")) button:has(i.fa-shopping-cart)',
trigger: 'tr:has(div[name="o_sale_product_configurator_name"]:contains("Memorabilia")) button:has(i.fa-plus)',
}, {
trigger: 'button:contains(Confirm)',
}, {
@@ -54,13 +55,13 @@ registry.category("web_tour.tours").add('event_sale_with_product_configurator_to
}, {
trigger: 'ul.ui-autocomplete a:contains("Registration Event (TEST variants)")',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Registration Event (TEST variants)")) label:contains("Adult")',
trigger: 'tr:has(div[name="o_sale_product_configurator_name"]:contains("Registration Event (TEST variants)")) label:contains("Adult")',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Registration Event (TEST variants)"))>td:nth-child(3) input',
trigger: 'tr:has(div[name="o_sale_product_configurator_name"]:contains("Registration Event (TEST variants)")) .o_sale_product_configurator_qty input',
run: 'text 5',
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(2)>td>span:contains("150.00")', // to be sure the correct variant is set
}, {
},
configuratorTourUtils.assertPriceTotal("150.00"),
{
trigger: 'button:contains(Confirm)',
}, {
trigger: '.o_input_dropdown input',
@@ -88,10 +89,10 @@ registry.category("web_tour.tours").add('event_sale_with_product_configurator_to
}, {
trigger: 'ul.ui-autocomplete a:contains("Registration Event (TEST variants)")',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Registration Event (TEST variants)")) label:contains("VIP")',
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(2)>td>span:contains("60.00")', // to be sure the correct variant is set
}, {
trigger: 'tr:has(div[name="o_sale_product_configurator_name"]:contains("Registration Event (TEST variants)")) label:contains("VIP")',
},
configuratorTourUtils.assertPriceTotal(60.00),
{
trigger: 'button:contains(Confirm)',
}, {
trigger: '.o_input_dropdown input',
@@ -1,7 +1,8 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_service/tour_utils";
import { stepUtils, TourError } from "@web_tour/tour_service/tour_utils";
import configuratorTourUtils from "@test_sale_product_configurators/js/tour_utils";
let optionVariantImage;
@@ -27,48 +28,27 @@ registry.category("web_tour.tours").add('sale_product_configurator_advanced_tour
run: 'text Custo',
}, {
trigger: 'ul.ui-autocomplete a:contains("Customizable Desk (TEST)")',
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody span:contains("Custom")'
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(2)>input',
run: 'text Custom 1'
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(2)>ul:nth-child(8) span:contains("PAV9")',
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(2)>ul:nth-child(8) ~ input',
run: 'text Custom 2'
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(2)>ul:nth-child(11) span:contains("PAV5")',
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(2)>select:nth-child(15)',
run: function (){
let inputValue = $('main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(2)>select:nth-child(15) option:contains("PAV9")').val();
$('main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(2)>select:nth-child(15)').val(inputValue);
$('main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(2)>select:nth-child(15)')[0].dispatchEvent(new Event("change"));
}
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(2)>select:nth-child(15) ~ input',
run: 'text Custom 3'
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1) strong:contains("Custom, White, PAV9, PAV5, PAV1")',
isCheck: true,
}, {
trigger: 'main.modal-body>table:nth-child(2)>tbody>tr:nth-child(1)>td:nth-child(2):contains("Conference Chair (TEST) (Steel)")',
},
...configuratorTourUtils.selectAndSetCustomAttribute("Customizable Desk", "Legs", "Custom", "Custom 1"),
...configuratorTourUtils.selectAndSetCustomAttribute("Customizable Desk", "PA1", "PAV9", "Custom 2"),
configuratorTourUtils.selectAttribute("Customizable Desk", "PA2", "PAV5"),
...configuratorTourUtils.selectAndSetCustomAttribute("Customizable Desk", "PA4", "PAV9", "Custom 3", "select"),
configuratorTourUtils.assertProductNameContains("Custom, White, PAV9, PAV5, PAV1"),
{
trigger: 'table.o_sale_product_configurator_table_optional tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("Conference Chair (TEST) (Steel)"))',
run: function () {
optionVariantImage = $('main.modal-body>table:nth-child(2)>tbody>tr:nth-child(1)>td:nth-child(1)>img').attr('src');
optionVariantImage = $('table.o_sale_product_configurator_table_optional tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("Conference Chair (TEST) (Aluminium)")) td[name="o_sale_product_configurator_img"]>img').attr('src');
}
}, {
trigger: 'main.modal-body>table:nth-child(2)>tbody>tr:nth-child(1) label:contains("Aluminium")',
}, {
trigger: 'main.modal-body>table:nth-child(2)>tbody>tr:nth-child(1)>td:nth-child(2):contains("Conference Chair (TEST) (Aluminium)")',
},
configuratorTourUtils.selectAttribute("Conference Chair", "Legs", "Aluminium"),
{
trigger: 'table.o_sale_product_configurator_table_optional tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("Conference Chair (TEST) (Aluminium)"))',
run: function () {
let newVariantImage = $('main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(1)>img').attr('src');
let newVariantImage = $('table.o_sale_product_configurator_table_optional tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("Conference Chair (TEST) (Aluminium)")) td[name="o_sale_product_configurator_img"]>img').attr('src');
if (newVariantImage !== optionVariantImage) {
$('<p>').text('image variant option src changed').insertAfter('main.modal-body>table:nth-child(2)>tbody>tr:nth-child(1)>td:nth-child(2)>div>strong');
throw new TourError('image variant option src changed');
}
}
}, {
trigger: 'main.modal-body>table:nth-child(2)>tbody>tr:nth-child(1)>td:nth-child(2)>div:contains("image variant option src changed")',
}, {
trigger: 'button:contains(Confirm)',
}, {
@@ -2,6 +2,7 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_service/tour_utils";
import configuratorTourUtils from "@test_sale_product_configurators/js/tour_utils";
registry.category("web_tour.tours").add('sale_product_configurator_edition_tour', {
url: '/web',
@@ -25,9 +26,9 @@ registry.category("web_tour.tours").add('sale_product_configurator_edition_tour'
}, {
trigger: 'ul.ui-autocomplete a:contains("Customizable Desk (TEST)")',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Customizable Desk")) label:contains("Aluminium")',
trigger: 'tr:has(div[name="o_sale_product_configurator_name"]:contains("Customizable Desk")) label:contains("Aluminium")',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Customizable Desk (TEST) (Aluminium, White)"))',
trigger: 'tr:has(div[name="o_sale_product_configurator_name"]:contains("Customizable Desk (TEST) (Aluminium, White)"))',
isCheck: true,
}, {
trigger: 'button:contains(Confirm)',
@@ -40,20 +41,20 @@ registry.category("web_tour.tours").add('sale_product_configurator_edition_tour'
}, {
trigger: '.fa-pencil',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Customizable Desk"))>td:nth-child(2)>ul:nth-child(3)>li:nth-child(2)>div label:contains("Aluminium") > input:checked',
trigger: 'table.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("Customizable Desk")) td>div[name="ptal"]:has(div>label:contains("Legs")) label:has(span:contains("Aluminium")) ~ input:checked',
isCheck: true, // check updated legs
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Customizable Desk"))>td:nth-child(4) span:contains("800.40")',
trigger: 'table.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("Customizable Desk")) td[name="price"] h5:contains("800.40")',
isCheck: true, // check updated price
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Customizable Desk")) label:contains("Custom") > input',
trigger: 'table.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("Customizable Desk")) td>div[name="ptal"]:has(div>label:contains("Legs")) label:has(span:contains("Custom")) ~ input',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Customizable Desk"))>td:nth-child(2)>input',
trigger: 'table.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("Customizable Desk")) td>div[name="ptal"]:has(div>label:contains("Legs")) input[type="text"]',
run: 'text nice custom value'
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Customizable Desk")) label[style="background-color:#000000"] input',
trigger: 'tr:has(div[name="o_sale_product_configurator_name"]:contains("Customizable Desk")) label[style="background-color:#000000"] input',
}, {
trigger: '.o_sale_product_configurator_name:contains("Customizable Desk (TEST) (Custom, Black)")',
trigger: 'div[name="o_sale_product_configurator_name"]:contains("Customizable Desk (TEST) (Custom, Black)")',
isCheck: true, // used to sync with server
}, {
trigger: 'button:contains(Confirm)',
@@ -69,10 +70,9 @@ registry.category("web_tour.tours").add('sale_product_configurator_edition_tour'
trigger: 'div[name="product_template_id"]',
}, {
trigger: '.fa-pencil',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Customizable Desk"))>td:nth-child(2)>input',
run: 'text another nice custom value'
}, {
},
configuratorTourUtils.setCustomAttribute("Customizable Desk", "Legs", "another nice custom value"),
{
trigger: 'button:contains(Confirm)',
}, {
trigger: 'td.o_data_cell:contains("Custom: another nice custom value")',
@@ -83,15 +83,13 @@ registry.category("web_tour.tours").add('sale_product_configurator_edition_tour'
}, {
trigger: '.fa-pencil',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Customizable Desk")) label:contains("Steel") > input',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Customizable Desk (TEST) (Steel, Black)"))',
isCheck: true,
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Customizable Desk"))>td:nth-child(3)>div>button:has(i.fa-plus)',
}, { // Mr Tajine Saucisse uses the pricelist that has a rule when 2 or more products. Price is 600
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(2)>td>span:contains("1,200.00")',
}, {
trigger: 'table.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("Customizable Desk")) td>div[name="ptal"]:has(div>label:contains("Legs")) label:has(span:contains("Steel")) ~ input',
},
configuratorTourUtils.assertProductNameContains("Customizable Desk (TEST) (Steel, Black)"),
configuratorTourUtils.increaseProductQuantity("Customizable Desk"),
// Mr Tajine Saucisse uses the pricelist that has a rule when 2 or more products. Price is 600
configuratorTourUtils.assertPriceTotal("1,200.00"),
{
trigger: 'button:contains(Confirm)',
}, {
trigger: 'td.o_data_cell:contains("2.00")',
@@ -25,17 +25,17 @@ registry.category("web_tour.tours").add('sale_product_configurator_optional_prod
}, {
trigger: 'ul.ui-autocomplete a:contains("Customizable Desk (TEST)")',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Office Chair Black")) button:has(i.fa-shopping-cart)',
trigger: 'tr:has(div[name="o_sale_product_configurator_name"]:contains("Office Chair Black")) button:has(i.fa-plus)',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Customizable Desk")) button:has(i.fa-plus)'
trigger: 'tr:has(div[name="o_sale_product_configurator_name"]:contains("Customizable Desk")) button:has(i.fa-plus)'
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Chair floor protection")) button:has(i.fa-shopping-cart)',
trigger: 'tr:has(div[name="o_sale_product_configurator_name"]:contains("Chair floor protection")) button:has(i.fa-plus)',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Conference Chair")) button:has(i.fa-shopping-cart)',
trigger: 'tr:has(div[name="o_sale_product_configurator_name"]:contains("Conference Chair")) button:has(i.fa-plus)',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Conference Chair")) a:has(i.fa-trash-o)',
trigger: 'tr:has(div[name="o_sale_product_configurator_name"]:contains("Conference Chair")) a:contains("Remove product")',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Conference Chair")) button:has(i.fa-shopping-cart)',
trigger: 'tr:has(div[name="o_sale_product_configurator_name"]:contains("Conference Chair")) button:has(i.fa-plus)',
}, {
trigger: 'button:contains(Confirm)',
}, {
@@ -2,6 +2,7 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_service/tour_utils";
import configuratorTourUtils from "@test_sale_product_configurators/js/tour_utils";
registry.category("web_tour.tours").add('sale_product_configurator_pricelist_tour', {
url: '/web',
@@ -44,29 +45,22 @@ stepUtils.showAppsMenuItem(),
trigger: 'ul.ui-autocomplete a:contains("Customizable Desk (TEST)")',
}, {
content: "check price is correct (USD)",
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(4) span:contains("750.00")',
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(4) h5:contains("750.00")',
isCheck: true,
}, {
content: "add one more",
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(3)>div>button:has(i.fa-plus)',
}, {
content: "check price for 2",
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(4) span:contains("600.00")',
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(4) h5:contains("600.00")',
isCheck: true,
}, {
content: "add conference chair",
trigger: 'main.modal-body>table:nth-child(2)>tbody>tr:nth-child(1)>td:nth-child(3)>button',
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(2)>td:nth-child(3)>div>button:has(i.fa-plus)',
}, {
content: "add chair floor protection",
trigger: 'main.modal-body>table:nth-child(2)>tbody>tr:nth-child(1)>td:nth-child(3)>button',
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(3)>td:nth-child(3)>div>button:has(i.fa-plus)',
}, {
content: "verify configurator final price", // tax excluded
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(4)>td>span:contains("1,257.00")',
}, {
},
configuratorTourUtils.addOptionalProduct("Conference Chair"),
configuratorTourUtils.increaseProductQuantity("Conference Chair"),
configuratorTourUtils.addOptionalProduct("Chair floor protection"),
configuratorTourUtils.increaseProductQuantity("Chair floor protection"),
configuratorTourUtils.assertPriceTotal("1,257.00"),
{
content: "add to SO",
trigger: 'button:contains(Confirm)',
}, {
@@ -2,6 +2,7 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_service/tour_utils";
import configuratorTourUtils from "@test_sale_product_configurators/js/tour_utils";
registry.category("web_tour.tours").add('sale_product_configurator_recursive_optional_products_tour', {
url: '/web',
@@ -18,13 +19,11 @@ registry.category("web_tour.tours").add('sale_product_configurator_recursive_opt
run: 'text Custo',
}, {
trigger: 'ul.ui-autocomplete a:contains("Customizable Desk (TEST)")',
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1) label:contains("Aluminium")',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Conference Chair")) button:has(i.fa-shopping-cart)',
}, {
trigger: 'tr:has(.o_sale_product_configurator_name:contains("Chair floor protection")) button:has(i.fa-shopping-cart)',
}, {
},
configuratorTourUtils.selectAttribute("Customizable Desk", "Legs", "Aluminium"),
configuratorTourUtils.addOptionalProduct("Conference Chair"),
configuratorTourUtils.addOptionalProduct("Chair floor protection"),
{
trigger: 'button:contains(Confirm)',
}, ...stepUtils.discardForm()
]});
@@ -1,7 +1,8 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_service/tour_utils";
import { stepUtils, TourError } from "@web_tour/tour_service/tour_utils";
import configuratorTourUtils from "@test_sale_product_configurators/js/tour_utils";
registry.category("web_tour.tours").add('sale_product_configurator_single_custom_attribute_tour', {
url: '/web',
@@ -18,32 +19,28 @@ registry.category("web_tour.tours").add('sale_product_configurator_single_custom
run: 'text Custo',
}, {
trigger: 'ul.ui-autocomplete a:contains("Customizable Desk (TEST)")',
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(2)>input:nth-child(7)',
run: 'text great single custom value'
}, {
},
configuratorTourUtils.setCustomAttribute("Customizable Desk (TEST)", "product attribute", "great single custom value"),
{
trigger: 'button:contains(Confirm)',
}, {
trigger: 'td.o_data_cell:contains("single product attribute value: great single custom value")',
extra_trigger: 'div[name="order_line"]',
run: function (){} // check custom value
isCheck: true,
}, {
trigger: 'div[name="product_template_id"]',
}, {
trigger: '.fa-pencil',
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(2)>input:nth-child(7)',
trigger: 'table.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("Customizable Desk (TEST)")) td>div[name="ptal"]:has(div>label:contains("product attribute")) input[type="text"]',
run: function () {
// check custom value initialized
if ($('main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(2)>input:nth-child(7)').val() === "great single custom value") {
$('main').addClass('tour_success_2');
if ($('table.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("Customizable Desk (TEST)")) td>div[name="ptal"]:has(div>label:contains("product attribute")) input[type="text"]').val() !== "great single custom value") {
throw new TourError("The value of custom product attribute should be 'great single custom value'.");
}
}
}, {
trigger: 'main.tour_success_2',
isCheck: true,
}, {
trigger: 'button:contains(Back)',
trigger: 'button:contains("Cancel")',
},
...stepUtils.discardForm()
]});
@@ -2,6 +2,7 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_service/tour_utils";
import configuratorTourUtils from "@test_sale_product_configurators/js/tour_utils";
// Note: please keep this test without pricelist for maximum coverage.
// The pricelist is tested on the other tours.
@@ -28,12 +29,12 @@ registry.category("web_tour.tours").add('sale_product_configurator_tour', {
}, {
trigger: 'ul.ui-autocomplete a:contains("Customizable Desk (TEST)")',
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1) label:contains("Steel")',
trigger: '.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("Customizable Desk")) label:contains("Steel")',
isCheck: true,
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1) label:contains("Aluminium")',
trigger: '.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("Customizable Desk")) label:contains("Aluminium")',
}, {
trigger: 'main.modal-body>table:nth-child(1)>tbody>tr:nth-child(1)>td:nth-child(4)>span:contains("800.40")',
trigger: '.o_sale_product_configurator_table tr:has(td>div[name="o_sale_product_configurator_name"] h5:contains("Customizable Desk")) td[name="price"] h5:contains("800.40")',
isCheck: true, // check updated price
}, {
trigger: 'label[style="background-color:#000000"] input'
@@ -48,11 +49,10 @@ registry.category("web_tour.tours").add('sale_product_configurator_tour', {
isCheck: true, // check confirm is available
}, {
trigger: 'span:contains("Aluminium"):eq(1)',
}, {
trigger: 'main.modal-body>table:nth-child(2)>tbody>tr:nth-child(1)>td:nth-child(3)>button',
}, {
trigger: 'main.modal-body>table:nth-child(2)>tbody>tr:nth-child(1)>td:nth-child(3)>button',
}, {
},
configuratorTourUtils.addOptionalProduct("Conference Chair"),
configuratorTourUtils.addOptionalProduct("Chair floor protection"),
{
trigger: 'button:contains(Confirm)',
id: 'quotation_product_selected',
},