From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- addons/website_sale/static/src/img/AZERTY.jpg | Bin 0 -> 27543 bytes addons/website_sale/static/src/img/accessory1.jpg | Bin 0 -> 7136 bytes .../static/src/img/accessory1_features.png | Bin 0 -> 68094 bytes .../static/src/img/anywhere_anything.png | Bin 0 -> 157256 bytes addons/website_sale/static/src/img/apps.png | Bin 0 -> 229328 bytes addons/website_sale/static/src/img/bluetooth.jpg | Bin 0 -> 14195 bytes .../website_sale/static/src/img/buds_closeup.png | Bin 0 -> 52725 bytes addons/website_sale/static/src/img/design.png | Bin 0 -> 253345 bytes addons/website_sale/static/src/img/imac1.png | Bin 0 -> 282790 bytes addons/website_sale/static/src/img/imac2.png | Bin 0 -> 825682 bytes .../static/src/img/ipad_experience.png | Bin 0 -> 198111 bytes addons/website_sale/static/src/img/ipad_why.png | Bin 0 -> 316562 bytes addons/website_sale/static/src/img/keyboard.png | Bin 0 -> 87402 bytes addons/website_sale/static/src/img/mighty.png | Bin 0 -> 321536 bytes .../website_sale/static/src/img/more_features.png | Bin 0 -> 59933 bytes .../static/src/img/overview_design_silver.png | Bin 0 -> 179281 bytes .../website_sale/static/src/img/overview_hero.png | Bin 0 -> 395471 bytes .../static/src/img/planner_product_page.png | Bin 0 -> 331907 bytes .../static/src/img/play_where_you_play.jpg | Bin 0 -> 79148 bytes .../static/src/img/promo_headphones.png | Bin 0 -> 7794 bytes addons/website_sale/static/src/img/purple.png | Bin 0 -> 90150 bytes addons/website_sale/static/src/img/redcross.png | Bin 0 -> 669 bytes .../snippets_thumbs/s_products_recently_viewed.svg | 99 +++ .../img/snippets_thumbs/s_products_searchbar.svg | 40 + .../s_products_searchbar_inline.svg | 35 + .../static/src/img/website_sale_chart_demo.png | Bin 0 -> 14944 bytes .../src/img/website_sale_dashboard_sales_demo.png | Bin 0 -> 56319 bytes addons/website_sale/static/src/img/wireless.png | Bin 0 -> 181057 bytes .../static/src/js/tours/website_sale_shop.js | 75 ++ .../src/js/tours/website_sale_shop_backend.js | 8 + .../src/js/tours/website_sale_shop_frontend.js | 11 + addons/website_sale/static/src/js/variant_mixin.js | 23 + .../static/src/js/website_sale.editor.js | 698 +++++++++++++++++ addons/website_sale/static/src/js/website_sale.js | 827 +++++++++++++++++++++ .../static/src/js/website_sale_backend.js | 127 ++++ .../static/src/js/website_sale_form_editor.js | 28 + .../static/src/js/website_sale_payment.js | 48 ++ .../static/src/js/website_sale_recently_viewed.js | 243 ++++++ .../static/src/js/website_sale_tracking.js | 113 +++ .../static/src/js/website_sale_utils.js | 59 ++ .../static/src/js/website_sale_validate.js | 51 ++ .../src/js/website_sale_video_field_preview.js | 28 + .../static/src/scss/primary_variables.scss | 1 + .../website_sale/static/src/scss/website_mail.scss | 48 ++ .../static/src/scss/website_sale.editor.scss | 40 + .../website_sale/static/src/scss/website_sale.scss | 582 +++++++++++++++ .../static/src/scss/website_sale_backend.scss | 72 ++ .../static/src/scss/website_sale_dashboard.scss | 81 ++ .../static/src/scss/website_sale_frontend.scss | 140 ++++ .../src/snippets/s_dynamic_snippet_products/000.js | 58 ++ .../snippets/s_dynamic_snippet_products/000.xml | 8 + .../snippets/s_dynamic_snippet_products/options.js | 109 +++ .../src/snippets/s_products_searchbar/000.js | 136 ++++ .../website_sale/static/src/xml/website_sale.xml | 11 + .../static/src/xml/website_sale_dashboard.xml | 162 ++++ .../src/xml/website_sale_recently_viewed.xml | 49 ++ .../static/src/xml/website_sale_utils.xml | 49 ++ 57 files changed, 4059 insertions(+) create mode 100644 addons/website_sale/static/src/img/AZERTY.jpg create mode 100644 addons/website_sale/static/src/img/accessory1.jpg create mode 100644 addons/website_sale/static/src/img/accessory1_features.png create mode 100644 addons/website_sale/static/src/img/anywhere_anything.png create mode 100644 addons/website_sale/static/src/img/apps.png create mode 100644 addons/website_sale/static/src/img/bluetooth.jpg create mode 100644 addons/website_sale/static/src/img/buds_closeup.png create mode 100644 addons/website_sale/static/src/img/design.png create mode 100644 addons/website_sale/static/src/img/imac1.png create mode 100644 addons/website_sale/static/src/img/imac2.png create mode 100644 addons/website_sale/static/src/img/ipad_experience.png create mode 100644 addons/website_sale/static/src/img/ipad_why.png create mode 100644 addons/website_sale/static/src/img/keyboard.png create mode 100644 addons/website_sale/static/src/img/mighty.png create mode 100644 addons/website_sale/static/src/img/more_features.png create mode 100644 addons/website_sale/static/src/img/overview_design_silver.png create mode 100644 addons/website_sale/static/src/img/overview_hero.png create mode 100644 addons/website_sale/static/src/img/planner_product_page.png create mode 100644 addons/website_sale/static/src/img/play_where_you_play.jpg create mode 100644 addons/website_sale/static/src/img/promo_headphones.png create mode 100644 addons/website_sale/static/src/img/purple.png create mode 100644 addons/website_sale/static/src/img/redcross.png create mode 100644 addons/website_sale/static/src/img/snippets_thumbs/s_products_recently_viewed.svg create mode 100644 addons/website_sale/static/src/img/snippets_thumbs/s_products_searchbar.svg create mode 100644 addons/website_sale/static/src/img/snippets_thumbs/s_products_searchbar_inline.svg create mode 100644 addons/website_sale/static/src/img/website_sale_chart_demo.png create mode 100644 addons/website_sale/static/src/img/website_sale_dashboard_sales_demo.png create mode 100644 addons/website_sale/static/src/img/wireless.png create mode 100644 addons/website_sale/static/src/js/tours/website_sale_shop.js create mode 100644 addons/website_sale/static/src/js/tours/website_sale_shop_backend.js create mode 100644 addons/website_sale/static/src/js/tours/website_sale_shop_frontend.js create mode 100644 addons/website_sale/static/src/js/variant_mixin.js create mode 100644 addons/website_sale/static/src/js/website_sale.editor.js create mode 100644 addons/website_sale/static/src/js/website_sale.js create mode 100644 addons/website_sale/static/src/js/website_sale_backend.js create mode 100644 addons/website_sale/static/src/js/website_sale_form_editor.js create mode 100644 addons/website_sale/static/src/js/website_sale_payment.js create mode 100644 addons/website_sale/static/src/js/website_sale_recently_viewed.js create mode 100644 addons/website_sale/static/src/js/website_sale_tracking.js create mode 100644 addons/website_sale/static/src/js/website_sale_utils.js create mode 100644 addons/website_sale/static/src/js/website_sale_validate.js create mode 100644 addons/website_sale/static/src/js/website_sale_video_field_preview.js create mode 100644 addons/website_sale/static/src/scss/primary_variables.scss create mode 100644 addons/website_sale/static/src/scss/website_mail.scss create mode 100644 addons/website_sale/static/src/scss/website_sale.editor.scss create mode 100644 addons/website_sale/static/src/scss/website_sale.scss create mode 100644 addons/website_sale/static/src/scss/website_sale_backend.scss create mode 100644 addons/website_sale/static/src/scss/website_sale_dashboard.scss create mode 100644 addons/website_sale/static/src/scss/website_sale_frontend.scss create mode 100644 addons/website_sale/static/src/snippets/s_dynamic_snippet_products/000.js create mode 100644 addons/website_sale/static/src/snippets/s_dynamic_snippet_products/000.xml create mode 100644 addons/website_sale/static/src/snippets/s_dynamic_snippet_products/options.js create mode 100644 addons/website_sale/static/src/snippets/s_products_searchbar/000.js create mode 100644 addons/website_sale/static/src/xml/website_sale.xml create mode 100644 addons/website_sale/static/src/xml/website_sale_dashboard.xml create mode 100644 addons/website_sale/static/src/xml/website_sale_recently_viewed.xml create mode 100644 addons/website_sale/static/src/xml/website_sale_utils.xml (limited to 'addons/website_sale/static/src') diff --git a/addons/website_sale/static/src/img/AZERTY.jpg b/addons/website_sale/static/src/img/AZERTY.jpg new file mode 100644 index 00000000..26f107a7 Binary files /dev/null and b/addons/website_sale/static/src/img/AZERTY.jpg differ diff --git a/addons/website_sale/static/src/img/accessory1.jpg b/addons/website_sale/static/src/img/accessory1.jpg new file mode 100644 index 00000000..145c5761 Binary files /dev/null and b/addons/website_sale/static/src/img/accessory1.jpg differ diff --git a/addons/website_sale/static/src/img/accessory1_features.png b/addons/website_sale/static/src/img/accessory1_features.png new file mode 100644 index 00000000..14492050 Binary files /dev/null and b/addons/website_sale/static/src/img/accessory1_features.png differ diff --git a/addons/website_sale/static/src/img/anywhere_anything.png b/addons/website_sale/static/src/img/anywhere_anything.png new file mode 100644 index 00000000..22c0ac65 Binary files /dev/null and b/addons/website_sale/static/src/img/anywhere_anything.png differ diff --git a/addons/website_sale/static/src/img/apps.png b/addons/website_sale/static/src/img/apps.png new file mode 100644 index 00000000..f2057b8c Binary files /dev/null and b/addons/website_sale/static/src/img/apps.png differ diff --git a/addons/website_sale/static/src/img/bluetooth.jpg b/addons/website_sale/static/src/img/bluetooth.jpg new file mode 100644 index 00000000..18e5681c Binary files /dev/null and b/addons/website_sale/static/src/img/bluetooth.jpg differ diff --git a/addons/website_sale/static/src/img/buds_closeup.png b/addons/website_sale/static/src/img/buds_closeup.png new file mode 100644 index 00000000..4c842a5d Binary files /dev/null and b/addons/website_sale/static/src/img/buds_closeup.png differ diff --git a/addons/website_sale/static/src/img/design.png b/addons/website_sale/static/src/img/design.png new file mode 100644 index 00000000..ce8a21fb Binary files /dev/null and b/addons/website_sale/static/src/img/design.png differ diff --git a/addons/website_sale/static/src/img/imac1.png b/addons/website_sale/static/src/img/imac1.png new file mode 100644 index 00000000..59fc3de8 Binary files /dev/null and b/addons/website_sale/static/src/img/imac1.png differ diff --git a/addons/website_sale/static/src/img/imac2.png b/addons/website_sale/static/src/img/imac2.png new file mode 100644 index 00000000..894ed174 Binary files /dev/null and b/addons/website_sale/static/src/img/imac2.png differ diff --git a/addons/website_sale/static/src/img/ipad_experience.png b/addons/website_sale/static/src/img/ipad_experience.png new file mode 100644 index 00000000..387fc2d2 Binary files /dev/null and b/addons/website_sale/static/src/img/ipad_experience.png differ diff --git a/addons/website_sale/static/src/img/ipad_why.png b/addons/website_sale/static/src/img/ipad_why.png new file mode 100644 index 00000000..11015696 Binary files /dev/null and b/addons/website_sale/static/src/img/ipad_why.png differ diff --git a/addons/website_sale/static/src/img/keyboard.png b/addons/website_sale/static/src/img/keyboard.png new file mode 100644 index 00000000..308d5c55 Binary files /dev/null and b/addons/website_sale/static/src/img/keyboard.png differ diff --git a/addons/website_sale/static/src/img/mighty.png b/addons/website_sale/static/src/img/mighty.png new file mode 100644 index 00000000..c9270a24 Binary files /dev/null and b/addons/website_sale/static/src/img/mighty.png differ diff --git a/addons/website_sale/static/src/img/more_features.png b/addons/website_sale/static/src/img/more_features.png new file mode 100644 index 00000000..4cf5a8c6 Binary files /dev/null and b/addons/website_sale/static/src/img/more_features.png differ diff --git a/addons/website_sale/static/src/img/overview_design_silver.png b/addons/website_sale/static/src/img/overview_design_silver.png new file mode 100644 index 00000000..50e37e9e Binary files /dev/null and b/addons/website_sale/static/src/img/overview_design_silver.png differ diff --git a/addons/website_sale/static/src/img/overview_hero.png b/addons/website_sale/static/src/img/overview_hero.png new file mode 100644 index 00000000..f6e4fd3e Binary files /dev/null and b/addons/website_sale/static/src/img/overview_hero.png differ diff --git a/addons/website_sale/static/src/img/planner_product_page.png b/addons/website_sale/static/src/img/planner_product_page.png new file mode 100644 index 00000000..ec8e6106 Binary files /dev/null and b/addons/website_sale/static/src/img/planner_product_page.png differ diff --git a/addons/website_sale/static/src/img/play_where_you_play.jpg b/addons/website_sale/static/src/img/play_where_you_play.jpg new file mode 100644 index 00000000..0364ed84 Binary files /dev/null and b/addons/website_sale/static/src/img/play_where_you_play.jpg differ diff --git a/addons/website_sale/static/src/img/promo_headphones.png b/addons/website_sale/static/src/img/promo_headphones.png new file mode 100644 index 00000000..21bc935f Binary files /dev/null and b/addons/website_sale/static/src/img/promo_headphones.png differ diff --git a/addons/website_sale/static/src/img/purple.png b/addons/website_sale/static/src/img/purple.png new file mode 100644 index 00000000..b7f33aa4 Binary files /dev/null and b/addons/website_sale/static/src/img/purple.png differ diff --git a/addons/website_sale/static/src/img/redcross.png b/addons/website_sale/static/src/img/redcross.png new file mode 100644 index 00000000..92a87401 Binary files /dev/null and b/addons/website_sale/static/src/img/redcross.png differ diff --git a/addons/website_sale/static/src/img/snippets_thumbs/s_products_recently_viewed.svg b/addons/website_sale/static/src/img/snippets_thumbs/s_products_recently_viewed.svg new file mode 100644 index 00000000..a9bf77fe --- /dev/null +++ b/addons/website_sale/static/src/img/snippets_thumbs/s_products_recently_viewed.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/website_sale/static/src/img/snippets_thumbs/s_products_searchbar.svg b/addons/website_sale/static/src/img/snippets_thumbs/s_products_searchbar.svg new file mode 100644 index 00000000..e6317a00 --- /dev/null +++ b/addons/website_sale/static/src/img/snippets_thumbs/s_products_searchbar.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/website_sale/static/src/img/snippets_thumbs/s_products_searchbar_inline.svg b/addons/website_sale/static/src/img/snippets_thumbs/s_products_searchbar_inline.svg new file mode 100644 index 00000000..bfa9a706 --- /dev/null +++ b/addons/website_sale/static/src/img/snippets_thumbs/s_products_searchbar_inline.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/website_sale/static/src/img/website_sale_chart_demo.png b/addons/website_sale/static/src/img/website_sale_chart_demo.png new file mode 100644 index 00000000..0cb7a5cf Binary files /dev/null and b/addons/website_sale/static/src/img/website_sale_chart_demo.png differ diff --git a/addons/website_sale/static/src/img/website_sale_dashboard_sales_demo.png b/addons/website_sale/static/src/img/website_sale_dashboard_sales_demo.png new file mode 100644 index 00000000..4338a722 Binary files /dev/null and b/addons/website_sale/static/src/img/website_sale_dashboard_sales_demo.png differ diff --git a/addons/website_sale/static/src/img/wireless.png b/addons/website_sale/static/src/img/wireless.png new file mode 100644 index 00000000..a379ffcf Binary files /dev/null and b/addons/website_sale/static/src/img/wireless.png differ diff --git a/addons/website_sale/static/src/js/tours/website_sale_shop.js b/addons/website_sale/static/src/js/tours/website_sale_shop.js new file mode 100644 index 00000000..6edb36dd --- /dev/null +++ b/addons/website_sale/static/src/js/tours/website_sale_shop.js @@ -0,0 +1,75 @@ +odoo.define("website_sale.tour_shop", function (require) { + "use strict"; + + var core = require("web.core"); + var _t = core._t; + + // return the steps, used for backend and frontend + + return [{ + trigger: "#new-content-menu > a", + content: _t("Let's create your first product."), + extra_trigger: ".js_sale", + position: "bottom", + }, { + trigger: "a[data-action=new_product]", + content: _t("Select New Product to create it and manage its properties to boost your sales."), + position: "bottom", + }, { + trigger: ".modal-dialog #editor_new_product input[type=text]", + content: _t("Enter a name for your new product"), + position: "right", + }, { + trigger: ".modal-footer button.btn-primary.btn-continue", + content: _t("Click on Continue to create the product."), + position: "right", + }, { + trigger: ".product_price .oe_currency_value:visible", + extra_trigger: ".editor_enable", + content: _t("Edit the price of this product by clicking on the amount."), + position: "bottom", + run: "text 1.99", + }, { + trigger: "#wrap img.product_detail_img", + extra_trigger: ".product_price .o_dirty .oe_currency_value:not(:containsExact(1.00))", + content: _t("Double click here to set an image describing your product."), + position: "top", + run: function (actions) { + actions.dblclick(); + }, + }, { + trigger: ".o_select_media_dialog .o_upload_media_button", + content: _t("Upload a file from your local library."), + position: "bottom", + run: function (actions) { + actions.auto(".modal-footer .btn-secondary"); + }, + }, { + trigger: "button.o_we_add_snippet_btn", + auto: true, + }, { + trigger: "#snippet_structure .oe_snippet:eq(3) .oe_snippet_thumbnail", + extra_trigger: "body:not(.modal-open)", + content: _t("Drag this website block and drop it in your page."), + position: "bottom", + run: "drag_and_drop", + }, { + trigger: "button[data-action=save]", + content: _t("Once you click on Save, your product is updated."), + position: "bottom", + }, { + trigger: ".js_publish_management .js_publish_btn .css_publish", + extra_trigger: "body:not(.editor_enable)", + content: _t("Click on this button so your customers can see it."), + position: "bottom", + }, { + trigger: ".o_main_navbar .o_menu_toggle, #oe_applications .dropdown-toggle", + content: _t("Let's now take a look at your administration dashboard to get your eCommerce website ready in no time."), + position: "bottom", + }, { // backend + trigger: '.o_apps > a[data-menu-xmlid="website.menu_website_configuration"], #oe_main_menu_navbar a[data-menu-xmlid="website.menu_website_configuration"]', + content: _t("Open your website app here."), + extra_trigger: ".o_apps,#oe_applications", + position: "bottom", + }]; +}); diff --git a/addons/website_sale/static/src/js/tours/website_sale_shop_backend.js b/addons/website_sale/static/src/js/tours/website_sale_shop_backend.js new file mode 100644 index 00000000..ab605852 --- /dev/null +++ b/addons/website_sale/static/src/js/tours/website_sale_shop_backend.js @@ -0,0 +1,8 @@ +odoo.define("website_sale.tour_shop_backend", function (require) { +"use strict"; + +var tour = require("web_tour.tour"); +var steps = require("website_sale.tour_shop"); +tour.register("shop", {url: "/shop"}, steps); + +}); diff --git a/addons/website_sale/static/src/js/tours/website_sale_shop_frontend.js b/addons/website_sale/static/src/js/tours/website_sale_shop_frontend.js new file mode 100644 index 00000000..afefddef --- /dev/null +++ b/addons/website_sale/static/src/js/tours/website_sale_shop_frontend.js @@ -0,0 +1,11 @@ +odoo.define("website_sale.tour_shop_frontend", function (require) { +"use strict"; + +var tour = require("web_tour.tour"); +var steps = require("website_sale.tour_shop"); +tour.register("shop", { + url: "/shop", + sequence: 130, +}, steps); + +}); diff --git a/addons/website_sale/static/src/js/variant_mixin.js b/addons/website_sale/static/src/js/variant_mixin.js new file mode 100644 index 00000000..0e92564b --- /dev/null +++ b/addons/website_sale/static/src/js/variant_mixin.js @@ -0,0 +1,23 @@ +odoo.define('website_sale.VariantMixin', function (require) { +'use strict'; + +var VariantMixin = require('sale.VariantMixin'); + +/** + * Website behavior is slightly different from backend so we append + * "_website" to URLs to lead to a different route + * + * @private + * @param {string} uri The uri to adapt + */ +VariantMixin._getUri = function (uri) { + if (this.isWebsite){ + return uri + '_website'; + } else { + return uri; + } +}; + +return VariantMixin; + +}); diff --git a/addons/website_sale/static/src/js/website_sale.editor.js b/addons/website_sale/static/src/js/website_sale.editor.js new file mode 100644 index 00000000..b60d15f2 --- /dev/null +++ b/addons/website_sale/static/src/js/website_sale.editor.js @@ -0,0 +1,698 @@ +odoo.define('website_sale.add_product', function (require) { +'use strict'; + +var core = require('web.core'); +var wUtils = require('website.utils'); +var WebsiteNewMenu = require('website.newMenu'); + +var _t = core._t; + +WebsiteNewMenu.include({ + actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, { + new_product: '_createNewProduct', + }), + + //-------------------------------------------------------------------------- + // Actions + //-------------------------------------------------------------------------- + + /** + * Asks the user information about a new product to create, then creates it + * and redirects the user to this new product. + * + * @private + * @returns {Promise} Unresolved if there is a redirection + */ + _createNewProduct: function () { + var self = this; + return wUtils.prompt({ + id: "editor_new_product", + window_title: _t("New Product"), + input: _t("Name"), + }).then(function (result) { + if (!result.val) { + return; + } + return self._rpc({ + route: '/shop/add_product', + params: { + name: result.val, + }, + }).then(function (url) { + window.location.href = url; + return new Promise(function () {}); + }); + }); + }, +}); +}); + +//============================================================================== + +odoo.define('website_sale.editor', function (require) { +'use strict'; + +var options = require('web_editor.snippets.options'); +var publicWidget = require('web.public.widget'); +const {Class: EditorMenuBar} = require('web_editor.editor'); +const {qweb} = require('web.core'); + +EditorMenuBar.include({ + custom_events: Object.assign(EditorMenuBar.prototype.custom_events, { + get_ribbons: '_onGetRibbons', + get_ribbon_classes: '_onGetRibbonClasses', + delete_ribbon: '_onDeleteRibbon', + set_ribbon: '_onSetRibbon', + set_product_ribbon: '_onSetProductRibbon', + }), + + /** + * @override + */ + async willStart() { + const _super = this._super.bind(this); + let ribbons = []; + if (this._isProductListPage()) { + ribbons = await this._rpc({ + model: 'product.ribbon', + method: 'search_read', + fields: ['id', 'html', 'bg_color', 'text_color', 'html_class'], + }); + } + this.ribbons = Object.fromEntries(ribbons.map(ribbon => [ribbon.id, ribbon])); + this.originalRibbons = Object.assign({}, this.ribbons); + this.productTemplatesRibbons = []; + this.deletedRibbonClasses = ''; + return _super(...arguments); + }, + /** + * @override + */ + async save() { + const _super = this._super.bind(this); + await this._saveRibbons(); + return _super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Saves the ribbons in the database. + * + * @private + */ + async _saveRibbons() { + if (!this._isProductListPage()) { + return; + } + const originalIds = Object.keys(this.originalRibbons).map(id => parseInt(id)); + const currentIds = Object.keys(this.ribbons).map(id => parseInt(id)); + + const ribbons = Object.values(this.ribbons); + const created = ribbons.filter(ribbon => !originalIds.includes(ribbon.id)); + const deletedIds = originalIds.filter(id => !currentIds.includes(id)); + const modified = ribbons.filter(ribbon => { + if (created.includes(ribbon)) { + return false; + } + const original = this.originalRibbons[ribbon.id]; + return Object.entries(ribbon).some(([key, value]) => value !== original[key]); + }); + + const proms = []; + let createdRibbonIds; + if (created.length > 0) { + proms.push(this._rpc({ + method: 'create', + model: 'product.ribbon', + args: [created.map(ribbon => { + ribbon = Object.assign({}, ribbon); + delete ribbon.id; + return ribbon; + })], + }).then(ids => createdRibbonIds = ids)); + } + + modified.forEach(ribbon => proms.push(this._rpc({ + method: 'write', + model: 'product.ribbon', + args: [[ribbon.id], ribbon], + }))); + + if (deletedIds.length > 0) { + proms.push(this._rpc({ + method: 'unlink', + model: 'product.ribbon', + args: [deletedIds], + })); + } + + await Promise.all(proms); + const localToServer = Object.assign( + this.ribbons, + Object.fromEntries(created.map((ribbon, index) => [ribbon.id, {id: createdRibbonIds[index]}])), + {'false': {id: false}}, + ); + + // Building the final template to ribbon-id map + const finalTemplateRibbons = this.productTemplatesRibbons.reduce((acc, {templateId, ribbonId}) => { + acc[templateId] = ribbonId; + return acc; + }, {}); + // Inverting the relationship so that we have all templates that have the same ribbon to reduce RPCs + const ribbonTemplates = Object.entries(finalTemplateRibbons).reduce((acc, [templateId, ribbonId]) => { + if (!acc[ribbonId]) { + acc[ribbonId] = []; + } + acc[ribbonId].push(parseInt(templateId)); + return acc; + }, {}); + const setProductTemplateRibbons = Object.entries(ribbonTemplates) + // If the ribbonId that the template had no longer exists, remove the ribbon (id = false) + .map(([ribbonId, templateIds]) => { + const id = currentIds.includes(parseInt(ribbonId)) ? ribbonId : false; + return [id, templateIds]; + }).map(([ribbonId, templateIds]) => this._rpc({ + method: 'write', + model: 'product.template', + args: [templateIds, {'website_ribbon_id': localToServer[ribbonId].id}], + })); + return Promise.all(setProductTemplateRibbons); + }, + /** + * Checks whether the current page is the product list. + * + * @private + */ + _isProductListPage() { + return $('#products_grid').length !== 0; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Returns a copy of this.ribbons through a callback. + * + * @private + */ + _onGetRibbons(ev) { + ev.data.callback(Object.assign({}, this.ribbons)); + }, + /** + * Returns all ribbon classes, current and deleted, so they can be removed. + * + * @private + */ + _onGetRibbonClasses(ev) { + const classes = Object.values(this.ribbons).reduce((classes, ribbon) => { + return classes + ` ${ribbon.html_class}`; + }, '') + this.deletedRibbonClasses; + ev.data.callback(classes); + }, + /** + * Deletes a ribbon. + * + * @private + */ + _onDeleteRibbon(ev) { + this.deletedRibbonClasses += ` ${this.ribbons[ev.data.id].html_class}`; + delete this.ribbons[ev.data.id]; + }, + /** + * Sets a ribbon; + * + * @private + */ + _onSetRibbon(ev) { + const {ribbon} = ev.data; + const previousRibbon = this.ribbons[ribbon.id]; + if (previousRibbon) { + this.deletedRibbonClasses += ` ${previousRibbon.html_class}`; + } + this.ribbons[ribbon.id] = ribbon; + }, + /** + * Sets which ribbon is used by a product template. + * + * @private + */ + _onSetProductRibbon(ev) { + const {templateId, ribbonId} = ev.data; + this.productTemplatesRibbons.push({templateId, ribbonId}); + }, +}); + +publicWidget.registry.websiteSaleCurrency = publicWidget.Widget.extend({ + selector: '.oe_website_sale', + disabledInEditableMode: false, + edit_events: { + 'click .oe_currency_value:o_editable': '_onCurrencyValueClick', + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onCurrencyValueClick: function (ev) { + $(ev.currentTarget).selectContent(); + }, +}); + +function reload() { + if (window.location.href.match(/\?enable_editor/)) { + window.location.reload(); + } else { + window.location.href = window.location.href.replace(/\?(enable_editor=1&)?|#.*|$/, '?enable_editor=1&'); + } +} + +options.registry.WebsiteSaleGridLayout = options.Class.extend({ + + /** + * @override + */ + start: function () { + this.ppg = parseInt(this.$target.closest('[data-ppg]').data('ppg')); + this.ppr = parseInt(this.$target.closest('[data-ppr]').data('ppr')); + return this._super.apply(this, arguments); + }, + /** + * @override + */ + onFocus: function () { + var listLayoutEnabled = this.$target.closest('#products_grid').hasClass('o_wsale_layout_list'); + this.$el.filter('.o_wsale_ppr_submenu').toggleClass('d-none', listLayoutEnabled); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * @see this.selectClass for params + */ + setPpg: function (previewMode, widgetValue, params) { + const ppg = parseInt(widgetValue); + if (!ppg || ppg < 1) { + return false; + } + this.ppg = ppg; + return this._rpc({ + route: '/shop/change_ppg', + params: { + 'ppg': ppg, + }, + }).then(() => reload()); + }, + /** + * @see this.selectClass for params + */ + setPpr: function (previewMode, widgetValue, params) { + this.ppr = parseInt(widgetValue); + this._rpc({ + route: '/shop/change_ppr', + params: { + 'ppr': this.ppr, + }, + }).then(reload); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + switch (methodName) { + case 'setPpg': { + return this.ppg; + } + case 'setPpr': { + return this.ppr; + } + } + return this._super(...arguments); + }, +}); + +options.registry.WebsiteSaleProductsItem = options.Class.extend({ + xmlDependencies: (options.Class.prototype.xmlDependencies || []).concat(['/website_sale/static/src/xml/website_sale_utils.xml']), + events: _.extend({}, options.Class.prototype.events || {}, { + 'mouseenter .o_wsale_soptions_menu_sizes table': '_onTableMouseEnter', + 'mouseleave .o_wsale_soptions_menu_sizes table': '_onTableMouseLeave', + 'mouseover .o_wsale_soptions_menu_sizes td': '_onTableItemMouseEnter', + 'click .o_wsale_soptions_menu_sizes td': '_onTableItemClick', + }), + + /** + * @override + */ + willStart: async function () { + const _super = this._super.bind(this); + this.ppr = this.$target.closest('[data-ppr]').data('ppr'); + this.productTemplateID = parseInt(this.$target.find('[data-oe-model="product.template"]').data('oe-id')); + this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve})); + return _super(...arguments); + }, + /** + * @override + */ + start: function () { + this._resetRibbonDummy(); + return this._super(...arguments); + }, + /** + * @override + */ + onFocus: function () { + var listLayoutEnabled = this.$target.closest('#products_grid').hasClass('o_wsale_layout_list'); + this.$el.find('.o_wsale_soptions_menu_sizes') + .toggleClass('d-none', listLayoutEnabled); + // Ribbons may have been edited or deleted in another products' option, need to make sure they're up to date + this.rerender = true; + }, + /** + * @override + */ + onBlur: function () { + // Since changes will not be saved unless they are validated, reset the + // previewed ribbon onBlur to communicate that to the user + this._resetRibbonDummy(); + this._toggleEditingUI(false); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * @override + */ + selectStyle(previewMode, widgetValue, params) { + const proms = [this._super(...arguments)]; + if (params.cssProperty === 'background-color' && params.colorNames.includes(widgetValue)) { + // Reset text-color when choosing a background-color class, so it uses the automatic text-color of the class. + proms.push(this.selectStyle(previewMode, '', {applyTo: '.o_wsale_ribbon_dummy', cssProperty: 'color'})); + } + return Promise.all(proms); + }, + /** + * @see this.selectClass for params + */ + async setRibbon(previewMode, widgetValue, params) { + if (previewMode === 'reset') { + widgetValue = this.prevRibbonId; + } else { + this.prevRibbonId = this.$target[0].dataset.ribbonId; + } + this.$target[0].dataset.ribbonId = widgetValue; + this.trigger_up('set_product_ribbon', { + templateId: this.productTemplateID, + ribbonId: widgetValue || false, + }); + const ribbon = this.ribbons[widgetValue] || {html: '', bg_color: '', text_color: '', html_class: ''}; + const $ribbons = $(`[data-ribbon-id="${widgetValue}"] .o_ribbon:not(.o_wsale_ribbon_dummy)`); + $ribbons.html(ribbon.html); + let htmlClasses; + this.trigger_up('get_ribbon_classes', {callback: classes => htmlClasses = classes}); + $ribbons.removeClass(htmlClasses); + + $ribbons.addClass(ribbon.html_class || ''); + $ribbons.css('color', ribbon.text_color); + $ribbons.css('background-color', ribbon.bg_color || ''); + + if (!this.ribbons[widgetValue]) { + $(`[data-ribbon-id="${widgetValue}"]`).each((index, product) => delete product.dataset.ribbonId); + } + this._resetRibbonDummy(); + this._toggleEditingUI(false); + }, + /** + * @see this.selectClass for params + */ + editRibbon(previewMode, widgetValue, params) { + this.saveMethod = 'modify'; + this._toggleEditingUI(true); + }, + /** + * @see this.selectClass for params + */ + createRibbon(previewMode, widgetValue, params) { + this.saveMethod = 'create'; + this.$ribbon.html('Ribbon text'); + this.$ribbon.addClass('bg-primary o_ribbon_left'); + this._toggleEditingUI(true); + this.isCreating = true; + }, + /** + * @see this.selectClass for params + */ + async deleteRibbon(previewMode, widgetValue, params) { + if (this.isCreating) { + // Ribbon doesn't exist yet, simply discard. + this.isCreating = false; + this._resetRibbonDummy(); + return this._toggleEditingUI(false); + } + const {ribbonId} = this.$target[0].dataset; + this.trigger_up('delete_ribbon', {id: ribbonId}); + this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve})); + this.rerender = true; + await this.setRibbon(false, ribbonId); + }, + /** + * @see this.selectClass for params + */ + async saveRibbon(previewMode, widgetValue, params) { + const text = this.$ribbon.html().trim(); + if (!text) { + return; + } + const ribbon = { + 'html': text, + 'bg_color': this.$ribbon[0].style.backgroundColor, + 'text_color': this.$ribbon[0].style.color, + 'html_class': this.$ribbon.attr('class').split(' ') + .filter(c => !['d-none', 'o_wsale_ribbon_dummy', 'o_ribbon'].includes(c)) + .join(' '), + }; + ribbon.id = this.saveMethod === 'modify' ? parseInt(this.$target[0].dataset.ribbonId) : Date.now(); + this.trigger_up('set_ribbon', {ribbon: ribbon}); + this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve})); + this.rerender = true; + await this.setRibbon(false, ribbon.id); + }, + /** + * @see this.selectClass for params + */ + setRibbonHtml(previewMode, widgetValue, params) { + this.$ribbon.html(widgetValue); + }, + /** + * @see this.selectClass for params + */ + setRibbonMode(previewMode, widgetValue, params) { + this.$ribbon[0].className = this.$ribbon[0].className.replace(/o_(ribbon|tag)_(left|right)/, `o_${widgetValue}_$2`); + }, + /** + * @see this.selectClass for params + */ + setRibbonPosition(previewMode, widgetValue, params) { + this.$ribbon[0].className = this.$ribbon[0].className.replace(/o_(ribbon|tag)_(left|right)/, `o_$1_${widgetValue}`); + }, + /** + * @see this.selectClass for params + */ + changeSequence: function (previewMode, widgetValue, params) { + this._rpc({ + route: '/shop/change_sequence', + params: { + id: this.productTemplateID, + sequence: widgetValue, + }, + }).then(reload); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + updateUI: async function () { + await this._super.apply(this, arguments); + + var sizeX = parseInt(this.$target.attr('colspan') || 1); + var sizeY = parseInt(this.$target.attr('rowspan') || 1); + + var $size = this.$el.find('.o_wsale_soptions_menu_sizes'); + $size.find('tr:nth-child(-n + ' + sizeY + ') td:nth-child(-n + ' + sizeX + ')') + .addClass('selected'); + + // Adapt size array preview to fit ppr + $size.find('tr td:nth-child(n + ' + parseInt(this.ppr + 1) + ')').hide(); + if (this.rerender) { + this.rerender = false; + return this._rerenderXML(); + } + }, + /** + * @override + */ + updateUIVisibility: async function () { + // Main updateUIVisibility will remove the d-none class because there are visible widgets + // inside of it. TODO: update this once updateUIVisibility can be used to compute visibility + // of arbitrary DOM elements and not just widgets. + const isEditing = this.$el.find('[data-name="ribbon_options"]').hasClass('d-none'); + await this._super(...arguments); + this._toggleEditingUI(isEditing); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + async _renderCustomXML(uiFragment) { + const $select = $(uiFragment.querySelector('.o_wsale_ribbon_select')); + this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve})); + if (!this.$ribbon) { + this._resetRibbonDummy(); + } + const classes = this.$ribbon[0].className; + this.$ribbon[0].className = ''; + const defaultTextColor = window.getComputedStyle(this.$ribbon[0]).color; + this.$ribbon[0].className = classes; + Object.values(this.ribbons).forEach(ribbon => { + const colorClasses = ribbon.html_class + .split(' ') + .filter(className => !/^o_(ribbon|tag)_(left|right)$/.test(className)) + .join(' '); + $select.append(qweb.render('website_sale.ribbonSelectItem', { + ribbon, + colorClasses, + isTag: /o_tag_(left|right)/.test(ribbon.html_class), + isLeft: /o_(tag|ribbon)_left/.test(ribbon.html_class), + textColor: ribbon.text_color || colorClasses ? 'currentColor' : defaultTextColor, + })); + }); + }, + /** + * @override + */ + async _computeWidgetState(methodName, params) { + const classList = this.$ribbon[0].classList; + switch (methodName) { + case 'setRibbon': + return this.$target.attr('data-ribbon-id') || ''; + case 'setRibbonHtml': + return this.$ribbon.html(); + case 'setRibbonMode': { + if (classList.contains('o_ribbon_left') || classList.contains('o_ribbon_right')) { + return 'ribbon'; + } + return 'tag'; + } + case 'setRibbonPosition': { + if (classList.contains('o_tag_left') || classList.contains('o_ribbon_left')) { + return 'left'; + } + return 'right'; + } + } + return this._super(methodName, params); + }, + /** + * Toggles the UI mode between select and create/edit mode. + * + * @private + * @param {Boolean} state true to activate editing UI, false to deactivate. + */ + _toggleEditingUI(state) { + this.$el.find('[data-name="ribbon_options"]').toggleClass('d-none', state); + this.$el.find('[data-name="ribbon_customize_opt"]').toggleClass('d-none', !state); + this.$('.o_ribbon:not(.o_wsale_ribbon_dummy)').toggleClass('d-none', state); + this.$ribbon.toggleClass('d-none', !state); + }, + /** + * Creates a copy of current ribbon to manipulate for edition/creation. + * + * @private + */ + _resetRibbonDummy() { + if (this.$ribbon) { + this.$ribbon.remove(); + } + const $original = this.$('.o_ribbon'); + this.$ribbon = $original.clone().addClass('d-none o_wsale_ribbon_dummy').appendTo($original.parent()); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onTableMouseEnter: function (ev) { + $(ev.currentTarget).addClass('oe_hover'); + }, + /** + * @private + */ + _onTableMouseLeave: function (ev) { + $(ev.currentTarget).removeClass('oe_hover'); + }, + /** + * @private + */ + _onTableItemMouseEnter: function (ev) { + var $td = $(ev.currentTarget); + var $table = $td.closest("table"); + var x = $td.index() + 1; + var y = $td.parent().index() + 1; + + var tr = []; + for (var yi = 0; yi < y; yi++) { + tr.push("tr:eq(" + yi + ")"); + } + var $selectTr = $table.find(tr.join(",")); + var td = []; + for (var xi = 0; xi < x; xi++) { + td.push("td:eq(" + xi + ")"); + } + var $selectTd = $selectTr.find(td.join(",")); + + $table.find("td").removeClass("select"); + $selectTd.addClass("select"); + }, + /** + * @private + */ + _onTableItemClick: function (ev) { + var $td = $(ev.currentTarget); + var x = $td.index() + 1; + var y = $td.parent().index() + 1; + this._rpc({ + route: '/shop/change_size', + params: { + id: this.productTemplateID, + x: x, + y: y, + }, + }).then(reload); + }, +}); +}); diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js new file mode 100644 index 00000000..89bce50f --- /dev/null +++ b/addons/website_sale/static/src/js/website_sale.js @@ -0,0 +1,827 @@ +odoo.define('website_sale.cart', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); +var core = require('web.core'); +var _t = core._t; + +var timeout; + +publicWidget.registry.websiteSaleCartLink = publicWidget.Widget.extend({ + selector: '#top_menu a[href$="/shop/cart"]', + events: { + 'mouseenter': '_onMouseEnter', + 'mouseleave': '_onMouseLeave', + 'click': '_onClick', + }, + + /** + * @constructor + */ + init: function () { + this._super.apply(this, arguments); + this._popoverRPC = null; + }, + /** + * @override + */ + start: function () { + this.$el.popover({ + trigger: 'manual', + animation: true, + html: true, + title: function () { + return _t("My Cart"); + }, + container: 'body', + placement: 'auto', + template: '' + }); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onMouseEnter: function (ev) { + var self = this; + clearTimeout(timeout); + $(this.selector).not(ev.currentTarget).popover('hide'); + timeout = setTimeout(function () { + if (!self.$el.is(':hover') || $('.mycart-popover:visible').length) { + return; + } + self._popoverRPC = $.get("/shop/cart", { + type: 'popover', + }).then(function (data) { + self.$el.data("bs.popover").config.content = data; + self.$el.popover("show"); + $('.popover').on('mouseleave', function () { + self.$el.trigger('mouseleave'); + }); + }); + }, 300); + }, + /** + * @private + * @param {Event} ev + */ + _onMouseLeave: function (ev) { + var self = this; + setTimeout(function () { + if ($('.popover:hover').length) { + return; + } + if (!self.$el.is(':hover')) { + self.$el.popover('hide'); + } + }, 1000); + }, + /** + * @private + * @param {Event} ev + */ + _onClick: function (ev) { + // When clicking on the cart link, prevent any popover to show up (by + // clearing the related setTimeout) and, if a popover rpc is ongoing, + // wait for it to be completed before going to the link's href. Indeed, + // going to that page may perform the same computation the popover rpc + // is already doing. + clearTimeout(timeout); + if (this._popoverRPC && this._popoverRPC.state() === 'pending') { + ev.preventDefault(); + var href = ev.currentTarget.href; + this._popoverRPC.then(function () { + window.location.href = href; + }); + } + }, +}); +}); + +odoo.define('website_sale.website_sale_category', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); + +publicWidget.registry.websiteSaleCategory = publicWidget.Widget.extend({ + selector: '#o_shop_collapse_category', + events: { + 'click .fa-chevron-right': '_onOpenClick', + 'click .fa-chevron-down': '_onCloseClick', + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onOpenClick: function (ev) { + var $fa = $(ev.currentTarget); + $fa.parent().siblings().find('.fa-chevron-down:first').click(); + $fa.parents('li').find('ul:first').show('normal'); + $fa.toggleClass('fa-chevron-down fa-chevron-right'); + }, + /** + * @private + * @param {Event} ev + */ + _onCloseClick: function (ev) { + var $fa = $(ev.currentTarget); + $fa.parent().find('ul:first').hide('normal'); + $fa.toggleClass('fa-chevron-down fa-chevron-right'); + }, +}); +}); + +odoo.define('website_sale.website_sale', function (require) { +'use strict'; + +var core = require('web.core'); +var config = require('web.config'); +var publicWidget = require('web.public.widget'); +var VariantMixin = require('sale.VariantMixin'); +var wSaleUtils = require('website_sale.utils'); +const wUtils = require('website.utils'); +require("web.zoomodoo"); + + +publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, { + selector: '.oe_website_sale', + events: _.extend({}, VariantMixin.events || {}, { + 'change form .js_product:first input[name="add_qty"]': '_onChangeAddQuantity', + 'mouseup .js_publish': '_onMouseupPublish', + 'touchend .js_publish': '_onMouseupPublish', + 'change .oe_cart input.js_quantity[data-product-id]': '_onChangeCartQuantity', + 'click .oe_cart a.js_add_suggested_products': '_onClickSuggestedProduct', + 'click a.js_add_cart_json': '_onClickAddCartJSON', + 'click .a-submit': '_onClickSubmit', + 'change form.js_attributes input, form.js_attributes select': '_onChangeAttribute', + 'mouseup form.js_add_cart_json label': '_onMouseupAddCartLabel', + 'touchend form.js_add_cart_json label': '_onMouseupAddCartLabel', + 'click .show_coupon': '_onClickShowCoupon', + 'submit .o_wsale_products_searchbar_form': '_onSubmitSaleSearch', + 'change select[name="country_id"]': '_onChangeCountry', + 'change #shipping_use_same': '_onChangeShippingUseSame', + 'click .toggle_summary': '_onToggleSummary', + 'click #add_to_cart, #buy_now, #products_grid .o_wsale_product_btn .a-submit': 'async _onClickAdd', + 'click input.js_product_change': 'onChangeVariant', + 'change .js_main_product [data-attribute_exclusions]': 'onChangeVariant', + 'change oe_optional_products_modal [data-attribute_exclusions]': 'onChangeVariant', + }), + + /** + * @constructor + */ + init: function () { + this._super.apply(this, arguments); + + this._changeCartQuantity = _.debounce(this._changeCartQuantity.bind(this), 500); + this._changeCountry = _.debounce(this._changeCountry.bind(this), 500); + + this.isWebsite = true; + + delete this.events['change .main_product:not(.in_cart) input.js_quantity']; + delete this.events['change [data-attribute_exclusions]']; + }, + /** + * @override + */ + start() { + const def = this._super(...arguments); + + this._applyHashFromSearch(); + + _.each(this.$('div.js_product'), function (product) { + $('input.js_product_change', product).first().trigger('change'); + }); + + // This has to be triggered to compute the "out of stock" feature and the hash variant changes + this.triggerVariantChange(this.$el); + + this.$('select[name="country_id"]').change(); + + core.bus.on('resize', this, function () { + if (config.device.size_class === config.device.SIZES.XL) { + $('.toggle_summary_div').addClass('d-none d-xl-block'); + } + }); + + this._startZoom(); + + window.addEventListener('hashchange', () => { + this._applyHash(); + this.triggerVariantChange(this.$el); + }); + + return def; + }, + /** + * The selector is different when using list view of variants. + * + * @override + */ + getSelectedVariantValues: function ($container) { + var combination = $container.find('input.js_product_change:checked') + .data('combination'); + + if (combination) { + return combination; + } + return VariantMixin.getSelectedVariantValues.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _applyHash: function () { + var hash = window.location.hash.substring(1); + if (hash) { + var params = $.deparam(hash); + if (params['attr']) { + var attributeIds = params['attr'].split(','); + var $inputs = this.$('input.js_variant_change, select.js_variant_change option'); + _.each(attributeIds, function (id) { + var $toSelect = $inputs.filter('[data-value_id="' + id + '"]'); + if ($toSelect.is('input[type="radio"]')) { + $toSelect.prop('checked', true); + } else if ($toSelect.is('option')) { + $toSelect.prop('selected', true); + } + }); + this._changeColorAttribute(); + } + } + }, + + /** + * Sets the url hash from the selected product options. + * + * @private + */ + _setUrlHash: function ($parent) { + var $attributes = $parent.find('input.js_variant_change:checked, select.js_variant_change option:selected'); + var attributeIds = _.map($attributes, function (elem) { + return $(elem).data('value_id'); + }); + history.replaceState(undefined, undefined, '#attr=' + attributeIds.join(',')); + }, + /** + * Set the checked color active. + * + * @private + */ + _changeColorAttribute: function () { + $('.css_attribute_color').removeClass("active") + .filter(':has(input:checked)') + .addClass("active"); + }, + /** + * @private + */ + _changeCartQuantity: function ($input, value, $dom_optional, line_id, productIDs) { + _.each($dom_optional, function (elem) { + $(elem).find('.js_quantity').text(value); + productIDs.push($(elem).find('span[data-product-id]').data('product-id')); + }); + $input.data('update_change', true); + + this._rpc({ + route: "/shop/cart/update_json", + params: { + line_id: line_id, + product_id: parseInt($input.data('product-id'), 10), + set_qty: value + }, + }).then(function (data) { + $input.data('update_change', false); + var check_value = parseInt($input.val() || 0, 10); + if (isNaN(check_value)) { + check_value = 1; + } + if (value !== check_value) { + $input.trigger('change'); + return; + } + if (!data.cart_quantity) { + return window.location = '/shop/cart'; + } + wSaleUtils.updateCartNavBar(data); + $input.val(data.quantity); + $('.js_quantity[data-line-id='+line_id+']').val(data.quantity).html(data.quantity); + + if (data.warning) { + var cart_alert = $('.oe_cart').parent().find('#data_warning'); + if (cart_alert.length === 0) { + $('.oe_cart').prepend(''); + } + else { + cart_alert.html(' ' + data.warning); + } + $input.val(data.quantity); + } + }); + }, + /** + * @private + */ + _changeCountry: function () { + if (!$("#country_id").val()) { + return; + } + this._rpc({ + route: "/shop/country_infos/" + $("#country_id").val(), + params: { + mode: $("#country_id").attr('mode'), + }, + }).then(function (data) { + // placeholder phone_code + $("input[name='phone']").attr('placeholder', data.phone_code !== 0 ? '+'+ data.phone_code : ''); + + // populate states and display + var selectStates = $("select[name='state_id']"); + // dont reload state at first loading (done in qweb) + if (selectStates.data('init')===0 || selectStates.find('option').length===1) { + if (data.states.length || data.state_required) { + selectStates.html(''); + _.each(data.states, function (x) { + var opt = $('