[IMP] website_sale: add to cart button
This commit adds an 'Add To cart' snippet to the 'Web Editor'. The snippet works as follow: 1. You choose the `product template` that you wish your users to buy. 2. If the product has more than 1 `variant`, a widget will offer you to either let the visitor choose - this will open a modal for him to choose the variant by himself - or select a variant yourself. 3. if `visitor choice` is not selected, a `buy_now` or `add_to_cart` action can be configured; this will determine the behaviour of the button after the user clicks it. task-2824359 closes odoo/odoo#91882 Signed-off-by: William Braeckman (wbr) <wbr@odoo.com> Co-authored-by: Morgane <morm@odoo.com>
This commit is contained in:
committed by
William Braeckman
co-authored by
Morgane
parent
aaa7336a1f
commit
2b29fc2dc8
@@ -0,0 +1,20 @@
|
||||
<svg width="82px" height="60px" viewBox="0 0 82 60" fill="none" enable-background="new 0 0 82 60"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<rect fill="none" width="82" height="60"/>
|
||||
<g>
|
||||
<g transform="translate(1)">
|
||||
<rect x="19" y="23" width="44.6" height="13.118" fill="#00C0D9"/>
|
||||
<rect x="19" y="23" width="44.6" height="13.118" fill="url(#paint0_linear_8_6)"/>
|
||||
<rect x="34.2941" y="28.7353" width="24.0882" height="1.52941" fill="white"/>
|
||||
<path d="M29.1892 30.5037H25.4919L25.5745 30.9099H28.9599C29.1541 30.9099 29.2981 31.0915 29.255 31.2821L29.1854 31.5903C29.4212 31.7055 29.5837 31.9487 29.5837 32.2302C29.5837 32.6263 29.2619 32.9468 28.867 32.9411C28.4909 32.9356 28.1816 32.6284 28.1714 32.2499C28.1659 32.0431 28.2482 31.8557 28.3833 31.7224H25.7392C25.8701 31.8515 25.9514 32.0312 25.9514 32.2302C25.9514 32.6341 25.6168 32.9593 25.2114 32.9404C24.8515 32.9236 24.5587 32.6308 24.5398 32.2686C24.5252 31.9888 24.6714 31.7423 24.8936 31.6137L24.0076 27.2537H23.1262C22.959 27.2537 22.8235 27.1173 22.8235 26.949V26.7458C22.8235 26.5776 22.959 26.4412 23.1262 26.4412H24.4193C24.5631 26.4412 24.6871 26.543 24.7159 26.6848L24.8315 27.2537H29.7854C29.9796 27.2537 30.1236 27.4352 30.0806 27.6259L29.4843 30.2665C29.453 30.4052 29.3305 30.5037 29.1892 30.5037ZM27.9694 28.574H27.364V28.0662C27.364 27.954 27.2736 27.863 27.1622 27.863H26.9604C26.8489 27.863 26.7586 27.954 26.7586 28.0662V28.574H26.1532C26.0417 28.574 25.9514 28.6649 25.9514 28.7771V28.9802C25.9514 29.0924 26.0417 29.1833 26.1532 29.1833H26.7586V29.6912C26.7586 29.8033 26.8489 29.8943 26.9604 29.8943H27.1622C27.2736 29.8943 27.364 29.8033 27.364 29.6912V29.1833H27.9694C28.0808 29.1833 28.1711 29.0924 28.1711 28.9802V28.7771C28.1711 28.6649 28.0808 28.574 27.9694 28.574Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_8_6" x1="-341.4267" y1="261.9717" x2="-338.8032" y2="261.7448" gradientUnits="userSpaceOnUse" gradientTransform="matrix(17 0 0 -5 5817.1001 1322.4)">
|
||||
<stop offset="0" style="stop-color:#00A09D"/>
|
||||
<stop offset="1" style="stop-color:#00C0D9"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -13,6 +13,17 @@ function addMedia(position = "right") {
|
||||
run: "click",
|
||||
};
|
||||
}
|
||||
function assertPathName(pathName, trigger) {
|
||||
return {
|
||||
content: `Check if we have been redirected to ${pathName}`,
|
||||
trigger: trigger,
|
||||
run: () => {
|
||||
if (!window.location.pathname.startsWith(pathName)) {
|
||||
console.error(`We should be on ${pathName}.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function changeBackground(snippet, position = "bottom") {
|
||||
return {
|
||||
@@ -129,6 +140,19 @@ function clickOnEdit(position = "bottom") {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple click on an element in the page.
|
||||
* @param {*} elementName
|
||||
* @param {*} selector
|
||||
*/
|
||||
function clickOnElement(elementName, selector) {
|
||||
return {
|
||||
content: `Clicking on the ${elementName}`,
|
||||
trigger: selector,
|
||||
run: 'click'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple click on a snippet in the edition area
|
||||
* @param {*} snippet
|
||||
@@ -314,8 +338,31 @@ function registerBackendAndFrontend(name, options, steps) {
|
||||
}, tourSteps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects an element inside a we-select, if the we-select is from a m2o widget, searches for it.
|
||||
*
|
||||
* @param widgetName {string} The widget's data-name
|
||||
* @param elementName {string} the element to search
|
||||
* @param searchNeeded {Boolean} if the widget is a m2o widget and a search is needed
|
||||
*/
|
||||
function selectElementInWeSelectWidget(widgetName, elementName, searchNeeded = false) {
|
||||
const steps = [clickOnElement(`${widgetName} toggler`, `we-select[data-name=${widgetName}] we-toggler`)];
|
||||
|
||||
if (searchNeeded) {
|
||||
steps.push({
|
||||
content: `Inputing ${elementName} in m2o widget search`,
|
||||
trigger: `we-select[data-name=${widgetName}] div.o_we_m2o_search input`,
|
||||
run: `text ${elementName}`
|
||||
});
|
||||
}
|
||||
steps.push(clickOnElement(`${elementName} in the ${widgetName} widget`,
|
||||
`we-select[data-name=${widgetName}] we-button:contains(${elementName})`));
|
||||
return steps;
|
||||
}
|
||||
|
||||
return {
|
||||
addMedia,
|
||||
assertPathName,
|
||||
changeBackground,
|
||||
changeBackgroundColor,
|
||||
changeColumnSize,
|
||||
@@ -324,6 +371,7 @@ return {
|
||||
changeOption,
|
||||
changePaddingSize,
|
||||
clickOnEdit,
|
||||
clickOnElement,
|
||||
clickOnSave,
|
||||
clickOnSnippet,
|
||||
clickOnText,
|
||||
@@ -339,5 +387,6 @@ return {
|
||||
clickOnExtraMenuItem,
|
||||
registerEditionTour,
|
||||
registerBackendAndFrontend,
|
||||
selectElementInWeSelectWidget,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
</t>
|
||||
<t id="event_speaker_bio_hook"/>
|
||||
<t id="snippet_donation_button_hook"/>
|
||||
<t id="snippet_add_to_cart_hook"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
@@ -179,6 +180,7 @@
|
||||
<xpath expr="//div[@id='snippet_content']/div[hasclass('o_panel_body')]" position="inside">
|
||||
<t id="newsletter_snippet" t-install="mass_mailing" string="Newsletter" t-thumbnail="/website/static/src/img/snippets_thumbs/s_newsletter_subscribe_form.svg"/>
|
||||
<t t-install="website_payment" string="Donation Button" t-thumbnail="/website/static/src/img/snippets_thumbs/s_donation_button.svg"/>
|
||||
<t t-install="website_sale" string="Add to Cart Button" t-thumbnail="/website/static/src/img/snippets_thumbs/s_donation_button.svg"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
@@ -563,7 +565,7 @@
|
||||
<!-- /!\ drop-in here is partly duplicated for s_popup (see dedicated options) -->
|
||||
<!-- TODO should be improved -->
|
||||
|
||||
<t t-set="so_content_addition_selector" t-translation="off">blockquote, .s_card:not(.s_timeline_card), .s_alert, .o_facebook_page, .s_share, .s_social_media, .s_rating, .s_hr, .s_google_map, .s_map, .s_countdown, .s_chart, .s_text_highlight, .s_progress_bar, .s_badge, .s_embed_code, .s_donation</t>
|
||||
<t t-set="so_content_addition_selector" t-translation="off">blockquote, .s_card:not(.s_timeline_card), .s_alert, .o_facebook_page, .s_share, .s_social_media, .s_rating, .s_hr, .s_google_map, .s_map, .s_countdown, .s_chart, .s_text_highlight, .s_progress_bar, .s_badge, .s_embed_code, .s_donation, .s_add_to_cart</t>
|
||||
<div id="so_content_addition"
|
||||
t-att-data-selector="so_content_addition_selector"
|
||||
t-attf-data-drop-near="p, h1, h2, h3, ul, ol, .row > div > img, #{so_content_addition_selector}"
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
'views/crm_team_views.xml',
|
||||
'views/templates.xml',
|
||||
'views/snippets/snippets.xml',
|
||||
'views/snippets/s_add_to_cart.xml',
|
||||
'views/snippets/s_dynamic_snippet_products.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/digest_views.xml',
|
||||
@@ -72,6 +73,7 @@
|
||||
'website.assets_wysiwyg': [
|
||||
'website_sale/static/src/scss/website_sale.editor.scss',
|
||||
'website_sale/static/src/snippets/s_dynamic_snippet_products/options.js',
|
||||
'website_sale/static/src/snippets/s_add_to_cart/options.js',
|
||||
'website_sale/static/src/js/website_sale.editor.js',
|
||||
],
|
||||
'website.assets_editor': [
|
||||
|
||||
@@ -593,6 +593,11 @@ class WebsiteSale(http.Controller):
|
||||
if hasattr(image_to_resequence, 'video_url'):
|
||||
image_to_resequence._onchange_video_url()
|
||||
|
||||
@http.route(['/shop/product/is_add_to_cart_allowed'], type='json', auth="public", website=True)
|
||||
def is_add_to_cart_allowed(self, product_id, **kwargs):
|
||||
product = request.env['product.product'].browse(product_id)
|
||||
return product._is_add_to_cart_allowed()
|
||||
|
||||
def _prepare_product_values(self, product, category, search, **kwargs):
|
||||
ProductCategory = request.env['product.public.category']
|
||||
|
||||
|
||||
@@ -14,6 +14,13 @@ odoo.define("website_sale.tour_utils", function (require) {
|
||||
};
|
||||
}
|
||||
|
||||
function assertCartContains(productName, backend = false) {
|
||||
return {
|
||||
content: `Checking if ${productName} is in the cart`,
|
||||
trigger: `${backend ? "iframe" : ""} a:contains(${productName})`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to select a pricelist on the /shop view
|
||||
*/
|
||||
@@ -27,7 +34,7 @@ odoo.define("website_sale.tour_utils", function (require) {
|
||||
content: "Click on pricelist",
|
||||
trigger: `span:contains(${pricelist})`,
|
||||
},
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,15 +43,16 @@ odoo.define("website_sale.tour_utils", function (require) {
|
||||
function assertProductPrice(attribute, value, productName) {
|
||||
return {
|
||||
content: `The ${attribute} of the ${productName} is ${value}`,
|
||||
trigger: `div:contains("${productName}") [data-oe-expression="template_price_vals[\'${attribute}\']"] .oe_currency_value:contains("${value}")`,
|
||||
trigger: `div:contains("${productName}") [data-oe-expression="template_price_vals['${attribute}']"] .oe_currency_value:contains("${value}")`,
|
||||
run: () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
assertCartContains,
|
||||
assertProductPrice,
|
||||
goToCart,
|
||||
selectPriceList,
|
||||
assertProductPrice
|
||||
};
|
||||
});
|
||||
|
||||
@@ -56,6 +56,7 @@ function animateClone($cart, $elem, offsetTop, offsetLeft) {
|
||||
$(this).removeClass("o_red_highlight").dequeue();
|
||||
});
|
||||
return new Promise(function (resolve, reject) {
|
||||
if(!$elem) resolve();
|
||||
var $imgtodrag = $elem.find('img').eq(0);
|
||||
if ($imgtodrag.length) {
|
||||
var $imgclone = $imgtodrag.clone()
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import publicWidget from 'web.public.widget';
|
||||
import { cartHandlerMixin } from 'website_sale.utils';
|
||||
import { WebsiteSale } from 'website_sale.website_sale';
|
||||
import { _t } from 'web.core';
|
||||
|
||||
publicWidget.registry.AddToCartSnippet = WebsiteSale.extend(cartHandlerMixin, {
|
||||
selector: '.s_add_to_cart_btn',
|
||||
events: {
|
||||
'click': '_onClickAddToCartButton',
|
||||
},
|
||||
|
||||
_onClickAddToCartButton: async function (ev) {
|
||||
const dataset = ev.currentTarget.dataset;
|
||||
|
||||
const visitorChoice = dataset.visitorChoice === 'true';
|
||||
const action = dataset.action;
|
||||
const productId = parseInt(dataset.productVariantId);
|
||||
|
||||
if (!productId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (visitorChoice) {
|
||||
this._handleAdd($(ev.currentTarget.closest('div')));
|
||||
} else {
|
||||
const isAddToCartAllowed = await this._rpc({
|
||||
route: `/shop/product/is_add_to_cart_allowed`,
|
||||
params: {
|
||||
product_id: productId,
|
||||
},
|
||||
});
|
||||
if (!isAddToCartAllowed) {
|
||||
this.displayNotification({
|
||||
title: 'User Error',
|
||||
message: _t('This product does not exist therefore it cannot be added to cart.'),
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.isBuyNow = action === 'buy_now';
|
||||
this.stayOnPageOption = !this.isBuyNow;
|
||||
this.addToCart({product_id: productId, add_qty: 1});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default publicWidget.registry.AddToCartSnippet;
|
||||
@@ -0,0 +1,258 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import options from 'web_editor.snippets.options';
|
||||
import { _t } from 'web.core';
|
||||
|
||||
const Many2oneUserValueWidget = options.userValueWidgetsRegistry['we-many2one'];
|
||||
|
||||
const Many2oneDefaultMessageWidget = Many2oneUserValueWidget.extend({
|
||||
|
||||
// defaultMessage: default message to display when no records are selected
|
||||
configAttributes: [...Many2oneUserValueWidget.prototype.configAttributes, 'defaultMessage'],
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async setValue(value, methodName) {
|
||||
await this._super(...arguments);
|
||||
|
||||
if (value === '') {
|
||||
this.menuTogglerEl.textContent = this.options.defaultMessage;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
options.userValueWidgetsRegistry['we-many2one-default-message'] = Many2oneDefaultMessageWidget;
|
||||
|
||||
options.registry.AddToCart = options.Class.extend({
|
||||
events: _.extend({}, options.Class.prototype.events || {}, {
|
||||
'click .reset-variant-picker': '_onClickResetVariantPicker',
|
||||
'click .reset-product-picker': '_onClickResetProductPicker',
|
||||
}),
|
||||
|
||||
async updateUI() {
|
||||
if (this.rerender) {
|
||||
this.rerender = false;
|
||||
await this._rerenderXML();
|
||||
return;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
_setButtonDisabled: function (isDisabled) {
|
||||
const buttonEl = this._buttonEl();
|
||||
|
||||
if (isDisabled) {
|
||||
buttonEl.classList.add('disabled');
|
||||
} else {
|
||||
buttonEl.classList.remove('disabled');
|
||||
}
|
||||
},
|
||||
|
||||
async setProductTemplate(previewMode, widgetValue, params) {
|
||||
this.$target[0].dataset.productTemplate = widgetValue;
|
||||
this._resetVariantChoice();
|
||||
this._resetAction();
|
||||
this._setButtonDisabled(false);
|
||||
|
||||
await this._fetchVariants(widgetValue);
|
||||
this.rerender = true;
|
||||
this._updateButton();
|
||||
|
||||
},
|
||||
|
||||
setProductVariant(previewMode, widgetValue, params) {
|
||||
this.$target[0].dataset.productVariant = widgetValue;
|
||||
this._updateButton();
|
||||
},
|
||||
|
||||
setAction(previewMode, widgetValue, params) {
|
||||
this.$target[0].dataset.action = widgetValue;
|
||||
this._updateButton();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_onClickResetVariantPicker() {
|
||||
this._resetVariantChoice();
|
||||
this._resetAction();
|
||||
this._updateButton();
|
||||
},
|
||||
|
||||
_onClickResetProductPicker() {
|
||||
this._resetProductChoice();
|
||||
this._resetVariantChoice();
|
||||
this._resetAction();
|
||||
this._updateButton();
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches the variants ids from the server
|
||||
*/
|
||||
async _fetchVariants(productTemplateId) {
|
||||
const response = await this._rpc({
|
||||
model: 'product.product',
|
||||
method: 'search_read',
|
||||
domain: [
|
||||
["product_tmpl_id", "=", parseInt(productTemplateId)],
|
||||
],
|
||||
});
|
||||
this.$target[0].dataset.variants = response.map(variant => variant.id);
|
||||
},
|
||||
|
||||
|
||||
_resetProductChoice() {
|
||||
this.$target[0].dataset.productTemplate = '';
|
||||
this._buttonEl().classList.add('disabled');
|
||||
},
|
||||
|
||||
|
||||
_resetVariantChoice() {
|
||||
this.$target[0].dataset.productVariant = '';
|
||||
},
|
||||
|
||||
_resetAction: function () {
|
||||
this.$target[0].dataset.action = "add_to_cart";
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns an array of variant ids from the dom
|
||||
*/
|
||||
_variantIds() {
|
||||
return this.$target[0].dataset.variants.split(',').map(stringId => parseInt(stringId));
|
||||
},
|
||||
|
||||
_buttonEl() {
|
||||
const buttonEl = this.$target[0].querySelector('.s_add_to_cart_btn');
|
||||
// In case the button was deleted somehow, we rebuild it.
|
||||
if (!buttonEl) {
|
||||
return this._buildButtonEl();
|
||||
}
|
||||
return buttonEl;
|
||||
},
|
||||
|
||||
_buildButtonEl() {
|
||||
const buttonEl = document.createElement('button');
|
||||
buttonEl.classList.add("s_add_to_cart_btn", "btn", "btn-secondary", "mb-2");
|
||||
this.$target[0].append(buttonEl);
|
||||
return buttonEl;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the button's html
|
||||
*/
|
||||
_updateButton() {
|
||||
const variantIds = this._variantIds();
|
||||
const buttonEl = this._buttonEl();
|
||||
|
||||
let productVariantId = variantIds[0];
|
||||
buttonEl.dataset.visitorChoice = false;
|
||||
|
||||
if (variantIds.length > 1) {
|
||||
// If there is more than 1 variant, that means that there are variants for the product template
|
||||
// and we check if there is one selected and assign it. If not, visitorChoice is set to true
|
||||
if (this.$target[0].dataset.productVariant) {
|
||||
productVariantId = this.$target[0].dataset.productVariant;
|
||||
} else {
|
||||
buttonEl.dataset.visitorChoice = true;
|
||||
}
|
||||
}
|
||||
buttonEl.dataset.productVariantId = productVariantId;
|
||||
buttonEl.dataset.action = this.$target[0].dataset.action;
|
||||
this._updateButtonContent();
|
||||
this._createHiddenFormInput(productVariantId);
|
||||
},
|
||||
|
||||
_updateButtonContent() {
|
||||
let iconEl = document.createElement('i');
|
||||
const buttonContent = {
|
||||
add_to_cart: {classList: "fa fa-cart-plus me-2", text: _t("Add to Cart")},
|
||||
buy_now: {classList: "fa fa-credit-card me-2", text: _t("Buy now")},
|
||||
};
|
||||
let buttonContentElement = buttonContent[this.$target[0].dataset.action];
|
||||
|
||||
iconEl.classList = buttonContentElement.classList;
|
||||
|
||||
this._buttonEl().replaceChildren(iconEl, buttonContentElement.text);
|
||||
},
|
||||
/**
|
||||
* Because sale_product_configurator._handleAdd() requires a hidden input to retrieve the productId,
|
||||
* this method creates a hidden input in the form of the button to make the modal behaviour possible.
|
||||
*/
|
||||
_createHiddenFormInput(productVariantId) {
|
||||
const inputEl = this._buttonEl().querySelector('input[type="hidden"][name="product_id"]');
|
||||
if (inputEl) {
|
||||
// If the input already exists, we change its value
|
||||
inputEl.setAttribute('value', productVariantId);
|
||||
} else {
|
||||
// Otherwise, we create the input element
|
||||
let inputEl = document.createElement('input');
|
||||
inputEl.setAttribute('type', 'hidden');
|
||||
inputEl.setAttribute('name', 'product_id');
|
||||
inputEl.setAttribute('value', productVariantId);
|
||||
this._buttonEl().append(inputEl);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the template is chosen and that we want to update the m2o variant widget with the right variants.
|
||||
*/
|
||||
async _renderCustomXML(uiFragment) {
|
||||
if (this.$target[0].dataset.productTemplate) {
|
||||
// That means that a template was selected and we want to update the content of the variant picker based on the template id
|
||||
const productVariantPickerEl = uiFragment.querySelector('we-many2one-default-message[data-name="product_variant_picker_opt"]');
|
||||
productVariantPickerEl.dataset.domain = `[["product_tmpl_id", "=", ${this.$target[0].dataset.productTemplate}]]`;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_computeWidgetState(methodName, params) {
|
||||
switch (methodName) {
|
||||
case 'setProductTemplate': {
|
||||
return this.$target[0].dataset.productTemplate || '';
|
||||
}
|
||||
case 'setProductVariant': {
|
||||
return this.$target[0].dataset.productVariant || '';
|
||||
}
|
||||
case 'setAction': {
|
||||
return this.$target[0].dataset.action;
|
||||
}
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _computeWidgetVisibility(widgetName, params) {
|
||||
switch (widgetName) {
|
||||
case 'product_variant_picker_opt': {
|
||||
return this.$target[0].dataset.productTemplate && this._variantIds().length > 1;
|
||||
}
|
||||
case 'product_variant_reset_opt': {
|
||||
return this.$target[0].dataset.productVariant;
|
||||
}
|
||||
|
||||
case 'product_template_reset_opt': {
|
||||
return this.$target[0].dataset.productTemplate;
|
||||
}
|
||||
case 'action_picker_opt': {
|
||||
if (this.$target[0].dataset.productTemplate) {
|
||||
if (this._variantIds().length > 1) {
|
||||
return this.$target[0].dataset.productVariant;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
export default {
|
||||
AddToCart: options.registry.AddToCart,
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import wsTourUtils from 'website_sale.tour_utils';
|
||||
import wTourUtils from 'website.tour_utils';
|
||||
|
||||
function editAddToCartSnippet() {
|
||||
return [
|
||||
wTourUtils.clickOnEdit(),
|
||||
wTourUtils.clickOnSnippet({id: 's_add_to_cart'})
|
||||
]
|
||||
}
|
||||
|
||||
wTourUtils.registerEditionTour('add_to_cart_snippet_tour', {
|
||||
url: '/',
|
||||
edition: true,
|
||||
test: true,
|
||||
},
|
||||
[
|
||||
wTourUtils.dragNDrop({name: 'Add to Cart Button'}),
|
||||
|
||||
// Basic product with no variants
|
||||
wTourUtils.clickOnSnippet({id: 's_add_to_cart'}),
|
||||
...wTourUtils.selectElementInWeSelectWidget('product_template_picker_opt', 'Acoustic Bloc Screens', true),
|
||||
...wTourUtils.clickOnSave(),
|
||||
wTourUtils.clickOnElement('add to cart button', 'iframe .s_add_to_cart_btn'),
|
||||
|
||||
// Product with 2 variants with visitor choice (will open modal)
|
||||
...editAddToCartSnippet(),
|
||||
...wTourUtils.selectElementInWeSelectWidget('product_template_picker_opt', 'Conference Chair', true),
|
||||
...wTourUtils.clickOnSave(),
|
||||
wTourUtils.clickOnElement('add to cart button', 'iframe .s_add_to_cart_btn'),
|
||||
wTourUtils.clickOnElement('continue shopping', 'iframe span:contains(Continue Shopping)'),
|
||||
|
||||
// Product with 2 variants with a variant selected
|
||||
...editAddToCartSnippet(),
|
||||
...wTourUtils.selectElementInWeSelectWidget('product_template_picker_opt', 'Conference Chair', true),
|
||||
...wTourUtils.selectElementInWeSelectWidget('product_variant_picker_opt', 'Conference Chair (Aluminium)'),
|
||||
...wTourUtils.clickOnSave(),
|
||||
wTourUtils.clickOnElement('add to cart button', 'iframe .s_add_to_cart_btn'),
|
||||
|
||||
// Basic product with no variants and action=buy now
|
||||
...editAddToCartSnippet(),
|
||||
...wTourUtils.selectElementInWeSelectWidget('product_template_picker_opt', 'Acoustic Bloc Screens', true),
|
||||
...wTourUtils.selectElementInWeSelectWidget('action_picker_opt', 'Buy Now'),
|
||||
...wTourUtils.clickOnSave(),
|
||||
wTourUtils.clickOnElement('add to cart button', 'iframe .s_add_to_cart_btn'),
|
||||
wTourUtils.assertPathName('/shop/payment', 'button[name=o_payment_submit_button]'),
|
||||
|
||||
wsTourUtils.goToCart({quantity: 4, backend: false}),
|
||||
wsTourUtils.assertCartContains('Acoustic Bloc Screens', false),
|
||||
wsTourUtils.assertCartContains('Conference Chair (Steel)', false),
|
||||
wsTourUtils.assertCartContains('Conference Chair (Aluminium)', false),
|
||||
],
|
||||
);
|
||||
@@ -4,6 +4,7 @@
|
||||
from . import test_customize
|
||||
from . import test_sale_process
|
||||
from . import test_sitemap
|
||||
from . import test_website_sale_add_to_cart_snippet
|
||||
from . import test_website_sale_cart_abandoned
|
||||
from . import test_website_sale_cart_recovery
|
||||
from . import test_website_sale_cart
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAddToCartSnippet(HttpCase):
|
||||
def test_configure_product(self):
|
||||
self.start_tour("/", 'add_to_cart_snippet_tour', login="admin")
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template name="Add to Cart Button" id="s_add_to_cart">
|
||||
<div class="s_add_to_cart">
|
||||
<button class="s_add_to_cart_btn disabled btn btn-secondary mb-2">
|
||||
<i class="fa fa-cart-plus me-2"/>Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template id="s_add_to_cart_options" inherit_id="website.snippet_options">
|
||||
<xpath expr="." position="inside">
|
||||
<div data-js="AddToCart"
|
||||
data-selector=".s_add_to_cart">
|
||||
<we-row>
|
||||
<we-many2one string="Product"
|
||||
data-model="product.template"
|
||||
data-set-product-template=""
|
||||
data-name="product_template_picker_opt"
|
||||
data-no-preview="true"
|
||||
data-domain='[["is_published", "=", true], ["sale_ok", "=", true]]'
|
||||
/>
|
||||
<we-button data-name="product_template_reset_opt"
|
||||
class="reset-product-picker align-self-end fa fa-fw fa-times">
|
||||
</we-button>
|
||||
</we-row>
|
||||
<we-row>
|
||||
<we-many2one-default-message string="⌙ Variant"
|
||||
data-model="product.product"
|
||||
data-set-product-variant=""
|
||||
data-name="product_variant_picker_opt"
|
||||
data-no-preview="true"
|
||||
data-default-message="Visitor's Choice"
|
||||
/>
|
||||
<we-button data-name="product_variant_reset_opt"
|
||||
class="reset-variant-picker align-self-end fa fa-fw fa-times">
|
||||
</we-button>
|
||||
</we-row>
|
||||
<we-select data-name="action_picker_opt" string="Action" data-no-preview="true">
|
||||
<we-button data-set-action="add_to_cart">Add to Cart</we-button>
|
||||
<we-button data-set-action="buy_now">Buy Now</we-button>
|
||||
</we-select>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
<record id="website_sale.s_add_to_cart_000_js" model="ir.asset">
|
||||
<field name="name">Add to Cart 000 JS</field>
|
||||
<field name="bundle">web.assets_frontend</field>
|
||||
<field name="path">website_sale/static/src/snippets/s_add_to_cart/000.js</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -5,6 +5,9 @@
|
||||
<xpath expr="//t[@id='sale_products_hook']" position="replace">
|
||||
<t t-snippet="website_sale.s_dynamic_snippet_products" t-thumbnail="/website_sale/static/src/img/snippets_thumbs/s_dynamic_products.svg"/>
|
||||
</xpath>
|
||||
<xpath expr="//t[@id='snippet_add_to_cart_hook']" position="replace">
|
||||
<t t-snippet="website_sale.s_add_to_cart" t-thumbnail="/website/static/src/img/snippets_thumbs/s_add_to_cart.svg"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="snippet_options" inherit_id="website.snippet_options" name="e-commerce snippet options">
|
||||
|
||||
Reference in New Issue
Block a user