mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-26 01:12:02 +02:00
19.0 vanilla
This commit is contained in:
parent
79f83631d5
commit
73afc09215
6267 changed files with 1534193 additions and 1130106 deletions
|
|
@ -10,40 +10,21 @@ pip install odoo-bringout-oca-ocb-website_sale
|
|||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- website
|
||||
- sale
|
||||
- website_payment
|
||||
- website_mail
|
||||
- portal_rating
|
||||
- digest
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: eCommerce
|
||||
- **Version**: 1.1
|
||||
- **Category**: Website/Website
|
||||
- **License**: LGPL-3
|
||||
- **Installable**: True
|
||||
- delivery
|
||||
- html_builder
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `website_sale`.
|
||||
- Repository: https://github.com/OCA/OCB
|
||||
- Branch: 19.0
|
||||
- Path: addons/website_sale
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original LGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
This package preserves the original LGPL-3 license.
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
[project]
|
||||
name = "odoo-bringout-oca-ocb-website_sale"
|
||||
version = "16.0.0"
|
||||
description = "eCommerce - Sell your products online"
|
||||
description = "eCommerce -
|
||||
Sell your products online
|
||||
"
|
||||
authors = [
|
||||
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
|
||||
]
|
||||
dependencies = [
|
||||
"odoo-bringout-oca-ocb-website>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-sale>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-website_payment>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-website_mail>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-portal_rating>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-digest>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-website>=19.0.0",
|
||||
"odoo-bringout-oca-ocb-sale>=19.0.0",
|
||||
"odoo-bringout-oca-ocb-website_payment>=19.0.0",
|
||||
"odoo-bringout-oca-ocb-website_mail>=19.0.0",
|
||||
"odoo-bringout-oca-ocb-portal_rating>=19.0.0",
|
||||
"odoo-bringout-oca-ocb-digest>=19.0.0",
|
||||
"odoo-bringout-oca-ocb-delivery>=19.0.0",
|
||||
"TODO_MAP-html_builder>=19.0.0",
|
||||
"requests>=2.25.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
|
|
@ -21,14 +25,14 @@ classifiers = [
|
|||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Office/Business",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/bringout/odoo-bringout-oca-ocb-website_sale"
|
||||
repository = "https://github.com/bringout/odoo-bringout-oca-ocb-website_sale"
|
||||
homepage = "https://github.com/bringout/0"
|
||||
repository = "https://github.com/bringout/0"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ Fully Integrated With Other Apps
|
|||
|
||||
Easily create awesome websites with no technical knowledge required.
|
||||
|
||||
### Blogs
|
||||
### Blog
|
||||
|
||||
Write news, attract new visitors, build customer loyalty.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import api, SUPERUSER_ID, _
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import wizard
|
||||
from . import report
|
||||
|
||||
def _post_init_hook(cr, registry):
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
def _post_init_hook(env):
|
||||
terms_conditions = env['ir.config_parameter'].get_param('account.use_invoice_terms')
|
||||
if not terms_conditions:
|
||||
env['ir.config_parameter'].set_param('account.use_invoice_terms', True)
|
||||
|
|
@ -16,11 +14,14 @@ def _post_init_hook(cr, registry):
|
|||
company.terms_type = 'html'
|
||||
env['website'].search([]).auth_signup_uninvited = 'b2c'
|
||||
|
||||
def uninstall_hook(cr, registry):
|
||||
existing_websites = env['website'].search([])
|
||||
for website in existing_websites:
|
||||
website._create_checkout_steps()
|
||||
|
||||
def uninstall_hook(env):
|
||||
''' Need to reenable the `product` pricelist multi-company rule that were
|
||||
disabled to be 'overridden' for multi-website purpose
|
||||
'''
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
pl_rule = env.ref('product.product_pricelist_comp_rule', raise_if_not_found=False)
|
||||
pl_item_rule = env.ref('product.product_pricelist_item_comp_rule', raise_if_not_found=False)
|
||||
multi_company_rules = pl_rule or env['ir.rule']
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
|
|
@ -8,33 +7,75 @@
|
|||
'summary': 'Sell your products online',
|
||||
'website': 'https://www.odoo.com/app/ecommerce',
|
||||
'version': '1.1',
|
||||
'depends': ['website', 'sale', 'website_payment', 'website_mail', 'portal_rating', 'digest'],
|
||||
'depends': [
|
||||
'website', 'sale', 'website_payment', 'website_mail', 'portal_rating', 'digest', 'delivery', 'html_builder',
|
||||
],
|
||||
'data': [
|
||||
# Security
|
||||
'security/ir.model.access.csv',
|
||||
'security/website_sale.xml',
|
||||
'security/ir_rules.xml',
|
||||
'security/res_groups.xml',
|
||||
|
||||
# Record data
|
||||
'data/data.xml',
|
||||
'data/mail_template_data.xml',
|
||||
'data/product_snippet_template_data.xml',
|
||||
'data/snippet_category_template_data.xml',
|
||||
'data/digest_data.xml',
|
||||
'data/ir_cron_data.xml',
|
||||
'views/product_attribute_views.xml',
|
||||
'views/product_tag_views.xml',
|
||||
'views/product_views.xml',
|
||||
'views/account_views.xml',
|
||||
'views/sale_report_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
'views/crm_team_views.xml',
|
||||
'data/tour.xml',
|
||||
|
||||
# Reports
|
||||
'report/sale_report_views.xml',
|
||||
|
||||
# QWeb templates
|
||||
'views/product_tile_templates.xml',
|
||||
'views/delivery_form_templates.xml',
|
||||
'views/gmc_templates.xml',
|
||||
'views/sale_portal_templates.xml',
|
||||
'views/templates.xml',
|
||||
'views/templates_svg.xml',
|
||||
|
||||
# Model views.
|
||||
'views/account_move_views.xml',
|
||||
'views/delivery_carrier_views.xml',
|
||||
'views/digest_views.xml',
|
||||
'views/product_attribute_views.xml',
|
||||
'views/product_document_views.xml',
|
||||
'views/product_feed_views.xml',
|
||||
'views/product_image_views.xml',
|
||||
'views/product_pricelist_item_views.xml',
|
||||
'views/product_pricelist_views.xml',
|
||||
'views/product_product_add.xml',
|
||||
'views/product_public_category_views.xml',
|
||||
'views/product_ribbon_views.xml',
|
||||
'views/product_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
'views/website_base_unit_views.xml',
|
||||
'views/website_pages_views.xml',
|
||||
'views/website_sale_menus.xml',
|
||||
'views/website_sale_visitor_views.xml',
|
||||
'views/variant_templates.xml',
|
||||
'views/website_views.xml',
|
||||
|
||||
# Website snippets
|
||||
'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',
|
||||
'views/website_sale_visitor_views.xml',
|
||||
'views/base_unit_view.xml',
|
||||
'views/product_product_add.xml',
|
||||
'views/website_views.xml',
|
||||
'views/website_pages_views.xml',
|
||||
'views/snippets/s_dynamic_snippet_categories.xml',
|
||||
'views/snippets/s_dynamic_snippet_products_preview_data.xml',
|
||||
'views/snippets/s_dynamic_snippet_category_preview_data.xml',
|
||||
'views/snippets/s_mega_menu/big_icons_subtitles.xml',
|
||||
'views/snippets/s_mega_menu/cards.xml',
|
||||
'views/snippets/s_mega_menu/image_menu.xml',
|
||||
'views/snippets/s_mega_menu/images_subtitles.xml',
|
||||
'views/snippets/s_mega_menu/little_icons.xml',
|
||||
'views/snippets/s_mega_menu/logos.xml',
|
||||
'views/snippets/s_mega_menu/multi_menus.xml',
|
||||
'views/snippets/s_mega_menu/odoo_menu.xml',
|
||||
'views/snippets/s_mega_menu/thumbnails.xml',
|
||||
'views/generate_primary_template.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/demo.xml',
|
||||
|
|
@ -45,60 +86,110 @@
|
|||
'uninstall_hook': 'uninstall_hook',
|
||||
'assets': {
|
||||
'web.assets_frontend': [
|
||||
'website_sale/static/src/interactions/**/*',
|
||||
'website_sale/static/src/snippets/**/*.js',
|
||||
'website_sale/static/src/js/tours/tour_utils.js',
|
||||
'website_sale/static/src/scss/product_tile.scss',
|
||||
'website_sale/static/src/scss/website_sale.scss',
|
||||
'website_sale/static/src/scss/website_mail.scss',
|
||||
'website_sale/static/src/scss/website_sale_frontend.scss',
|
||||
'website_sale/static/src/scss/website_sale_delivery.scss',
|
||||
'website_sale/static/src/snippets/s_dynamic_snippet_categories/000.scss',
|
||||
'website_sale/static/src/snippets/s_dynamic_snippet_categories/000.xml',
|
||||
'website/static/lib/multirange/multirange_custom.scss',
|
||||
'sale/static/src/scss/sale_portal.scss',
|
||||
'sale/static/src/scss/product_configurator.scss',
|
||||
'sale/static/src/js/variant_mixin.js',
|
||||
|
||||
'website_sale/static/src/scss/product_configurator.scss',
|
||||
|
||||
'website_sale/static/src/js/cart_service.js',
|
||||
'website_sale/static/src/js/variant_mixin.js',
|
||||
'website_sale/static/src/js/website_sale.js',
|
||||
'website_sale/static/src/xml/website_sale.xml',
|
||||
'website_sale/static/src/js/website_sale_utils.js',
|
||||
'website_sale/static/src/xml/website_sale_utils.xml',
|
||||
'website_sale/static/src/js/website_sale_payment.js',
|
||||
'website_sale/static/src/js/website_sale_validate.js',
|
||||
'website_sale/static/src/js/website_sale_recently_viewed.js',
|
||||
'website_sale/static/src/js/website_sale_tracking.js',
|
||||
'website/static/lib/multirange/multirange_custom.js',
|
||||
'website_sale/static/src/js/website_sale_category_link.js',
|
||||
'website/static/src/interactions/multirange_input.js',
|
||||
'website_sale/static/src/xml/website_sale_image_viewer.xml',
|
||||
'website_sale/static/src/js/components/website_sale_image_viewer.js',
|
||||
'website_sale/static/src/xml/website_sale_reorder_modal.xml',
|
||||
'website_sale/static/src/js/website_sale_reorder.js',
|
||||
'website_sale/static/src/js/notification/add_to_cart_notification/add_to_cart_notification.js',
|
||||
'website_sale/static/src/js/notification/add_to_cart_notification/add_to_cart_notification.xml',
|
||||
'website_sale/static/src/js/notification/cart_notification/cart_notification.js',
|
||||
'website_sale/static/src/js/notification/cart_notification/cart_notification.xml',
|
||||
'website_sale/static/src/js/notification/warning_notification/warning_notification.js',
|
||||
'website_sale/static/src/js/notification/warning_notification/warning_notification.xml',
|
||||
'website_sale/static/src/js/notification/notification_service.js',
|
||||
'sale/static/src/js/badge_extra_price/*',
|
||||
'sale/static/src/js/combo_configurator_dialog/*',
|
||||
'sale/static/src/js/models/*',
|
||||
'sale/static/src/js/product/*',
|
||||
'sale/static/src/js/product_card/*',
|
||||
'sale/static/src/js/product_configurator_dialog/*',
|
||||
'sale/static/src/js/product_list/*',
|
||||
'sale/static/src/js/product_template_attribute_line/*',
|
||||
'sale/static/src/js/quantity_buttons/*',
|
||||
'sale/static/src/js/sale_utils.js',
|
||||
'website_sale/static/src/js/combo_configurator_dialog/*',
|
||||
'website_sale/static/src/js/product/*',
|
||||
'website_sale/static/src/js/product_configurator_dialog/*',
|
||||
'website_sale/static/src/js/product_list/*',
|
||||
'website_sale/static/src/js/product_template_attribute_line/*',
|
||||
'website_sale/static/src/js/quantity_buttons/*',
|
||||
|
||||
# Location selector components are defined in `delivery` to share the codebase with the
|
||||
# backend.
|
||||
'delivery/static/src/js/location_selector/**/*',
|
||||
'website_sale/static/src/js/location_selector/**/*',
|
||||
],
|
||||
'web._assets_primary_variables': [
|
||||
'website_sale/static/src/scss/primary_variables.scss',
|
||||
],
|
||||
'web.assets_backend': [
|
||||
'website_sale/static/src/js/client_actions/**/*',
|
||||
'website_sale/static/src/js/tours/tour_utils.js',
|
||||
'website_sale/static/src/js/website_sale_video_field_preview.js',
|
||||
'website_sale/static/src/js/website_sale_backend.js',
|
||||
'website_sale/static/src/scss/website_sale_dashboard.scss',
|
||||
'website_sale/static/src/scss/website_sale_backend.scss',
|
||||
'website_sale/static/src/xml/website_sale_dashboard.xml',
|
||||
'website_sale/static/src/js/tours/website_sale_shop.js',
|
||||
'website_sale/static/src/xml/website_sale.xml',
|
||||
'website_sale/static/src/scss/kanban_record.scss',
|
||||
],
|
||||
'website.website_builder_assets': [
|
||||
'website_sale/static/src/js/website_sale_form_editor.js',
|
||||
'website_sale/static/src/website_builder/**/*',
|
||||
'website_sale/static/src/js/website_sale_utils.js',
|
||||
('remove', 'website_sale/static/src/**/*.edit.*'),
|
||||
],
|
||||
'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_sale/static/src/js/website_sale_form_editor.js',
|
||||
],
|
||||
'website.assets_editor': [
|
||||
'website_sale/static/src/js/systray_items/*.js',
|
||||
'website_sale/static/src/js/components/wysiwyg_adapter/wysiwyg_adapter.js',
|
||||
'website_sale/static/src/js/website_sale_form_editor.js',
|
||||
'website_sale/static/src/xml/website_sale_utils.xml',
|
||||
'website_sale/static/src/xml/website_sale_editor_previews.xml',
|
||||
],
|
||||
'web.assets_common': [
|
||||
'website_sale/static/src/js/tours/tour_utils.js',
|
||||
'website.assets_inside_builder_iframe': [
|
||||
'web/static/lib/bootstrap/scss/_variables.scss',
|
||||
'website_sale/static/src/website_builder/**/*.edit.*',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'website_sale/static/tests/**/*',
|
||||
'website_sale/static/tests/tours/**/*',
|
||||
'website_sale/static/src/js/tours/product_configurator_tour_utils.js',
|
||||
],
|
||||
'web.assets_unit_tests': [
|
||||
'website_sale/static/tests/interactions/**/*',
|
||||
'website_sale/static/tests/builder/**/*',
|
||||
],
|
||||
'web.assets_unit_tests_setup': [
|
||||
'delivery/static/src/js/location_selector/**/*',
|
||||
'website_sale/static/src/interactions/**/*',
|
||||
'website_sale/static/src/snippets/s_dynamic_snippet_products/dynamic_snippet_products.js',
|
||||
'website_sale/static/src/js/variant_mixin.js',
|
||||
'website_sale/static/src/js/website_sale_utils.js',
|
||||
'website_sale/static/src/js/components/website_sale_image_viewer.js',
|
||||
# TODO Find out why these do not work:
|
||||
#'website_sale/static/src/snippets/**/*.js',
|
||||
# TODO Re-activate when testing edit mode
|
||||
#('remove', 'website_sale/static/src/snippets/**/*.edit.js'),
|
||||
],
|
||||
},
|
||||
'author': 'Odoo S.A.',
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
|
|
|||
438
odoo-bringout-oca-ocb-website_sale/website_sale/const.py
Normal file
438
odoo-bringout-oca-ocb-website_sale/website_sale/const.py
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
import re
|
||||
|
||||
from odoo.tools.translate import LazyTranslate
|
||||
|
||||
_lt = LazyTranslate(__name__, default_lang='en_US')
|
||||
|
||||
# Website configurator
|
||||
|
||||
SHOP_PAGE_STYLE_MAPPING = {
|
||||
'classic_grid': {
|
||||
'title': _lt("Classic Grid"),
|
||||
'img_src': '/website_sale/static/src/img/configurator/shop/classic_grid.jpg',
|
||||
'views': {
|
||||
'enable': [
|
||||
'website_sale.template_footer_website_sale', # Footer
|
||||
],
|
||||
'disable': [],
|
||||
},
|
||||
'website_fields': {
|
||||
'shop_opt_products_design_classes': 'o_wsale_products_opt_layout_catalog '
|
||||
'o_wsale_products_opt_design_thumbs '
|
||||
'o_wsale_products_opt_name_color_regular '
|
||||
'o_wsale_products_opt_thumb_cover '
|
||||
'o_wsale_products_opt_img_secondary_show '
|
||||
'o_wsale_products_opt_img_hover_zoom_out_light '
|
||||
'o_wsale_products_opt_has_cta '
|
||||
'o_wsale_products_opt_actions_onhover '
|
||||
'o_wsale_products_opt_has_wishlist '
|
||||
'o_wsale_products_opt_wishlist_fixed '
|
||||
'o_wsale_products_opt_cc1 '
|
||||
'o_wsale_products_opt_rounded_2 '
|
||||
'o_wsale_products_opt_has_comparison '
|
||||
'o_wsale_products_opt_actions_promote',
|
||||
},
|
||||
'category_fields': {
|
||||
'show_category_title': False,
|
||||
'show_category_description': True,
|
||||
'align_category_content': False,
|
||||
},
|
||||
},
|
||||
'modern_grid': {
|
||||
'title': _lt("Modern Grid"),
|
||||
'img_src': '/website_sale/static/src/img/configurator/shop/modern_grid.jpg',
|
||||
'views': {
|
||||
'enable': [
|
||||
'website.template_header_search', # Header menu with search bar
|
||||
'website.header_width_full', # Header width
|
||||
'website_sale.products_mobile_cols_single', # Mobile cols single
|
||||
'website_sale.products_attributes_top', # Filters
|
||||
'website_sale.filmstrip_categories_grid', # Category style
|
||||
'website_sale.template_footer_website_sale', # Footer
|
||||
'website.footer_copyright_content_width_fluid', # Footer width
|
||||
],
|
||||
'disable': [
|
||||
'website_sale.products_attributes', # Filters
|
||||
],
|
||||
},
|
||||
'website_fields': {
|
||||
'shop_ppr': 5,
|
||||
'shop_gap': '0px',
|
||||
'shop_page_container': 'fluid', # Content fullwidth
|
||||
'shop_opt_products_design_classes': 'o_wsale_products_opt_thumb_cover '
|
||||
'o_wsale_products_opt_img_hover_zoom_out_light '
|
||||
'o_wsale_products_opt_has_cta '
|
||||
'o_wsale_products_opt_has_wishlist '
|
||||
'o_wsale_products_opt_has_comparison '
|
||||
'o_wsale_products_opt_actions_onhover '
|
||||
'o_wsale_products_opt_wishlist_fixed '
|
||||
'o_wsale_products_opt_layout_catalog '
|
||||
'o_wsale_products_opt_design_grid '
|
||||
'o_wsale_products_opt_actions_theme '
|
||||
'o_wsale_products_opt_img_secondary_show '
|
||||
'o_wsale_products_opt_thumb_4_5 '
|
||||
'o_wsale_products_opt_text_align_center',
|
||||
},
|
||||
'category_fields': {
|
||||
'show_category_title': True,
|
||||
'show_category_description': True,
|
||||
'align_category_content': False,
|
||||
},
|
||||
'scss_customization_params': {
|
||||
'header-links-style': 'default',
|
||||
'header-template': 'search',
|
||||
},
|
||||
},
|
||||
'showcase': {
|
||||
'title': _lt("Showcase"),
|
||||
'img_src': '/website_sale/static/src/img/configurator/shop/showcase.jpg',
|
||||
'views': {
|
||||
'enable': [
|
||||
'website.template_header_sales_four', # Header
|
||||
'website.header_width_full', # Header width
|
||||
'website_sale.products_shop_title_align', # Shop title centered
|
||||
'website_sale.filmstrip_categories_pills', # Category style
|
||||
'website_sale.products_attributes_top', # Filters
|
||||
'website_sale.floating_bar', # Toolbar/floating
|
||||
'website_sale.template_footer_website_sale', # Footer
|
||||
'website.footer_copyright_content_width_fluid', # Footer width
|
||||
],
|
||||
'disable': [
|
||||
'website_sale.products_attributes', # Filters
|
||||
],
|
||||
},
|
||||
'website_fields': {
|
||||
'shop_gap': '0px',
|
||||
'shop_page_container': 'fluid', # Content fullwidth
|
||||
'shop_opt_products_design_classes': 'o_wsale_products_opt_name_color_regular '
|
||||
'o_wsale_products_opt_thumb_cover '
|
||||
'o_wsale_products_opt_has_cta '
|
||||
'o_wsale_products_opt_has_wishlist '
|
||||
'o_wsale_products_opt_has_description '
|
||||
'o_wsale_products_opt_actions_inline '
|
||||
'o_wsale_products_opt_cc o_wsale_products_opt_cc5 '
|
||||
' o_wsale_products_opt_actions_theme '
|
||||
'o_wsale_products_opt_thumb_4_3 '
|
||||
'o_wsale_products_opt_layout_list '
|
||||
'o_wsale_products_opt_design_showcase '
|
||||
'o_wsale_products_opt_rounded_0'
|
||||
},
|
||||
'category_fields': {
|
||||
'show_category_title': True,
|
||||
'show_category_description': True,
|
||||
'align_category_content': True,
|
||||
},
|
||||
'scss_customization_params': {
|
||||
'header-links-style': 'default',
|
||||
'header-template': 'sales_four',
|
||||
},
|
||||
},
|
||||
'chips_contained': {
|
||||
'title': _lt("Chips Contained"),
|
||||
'img_src': '/website_sale/static/src/img/configurator/shop/chips_contained.jpg',
|
||||
'views': {
|
||||
'enable': [
|
||||
'website.template_header_sales_one', # Header
|
||||
'website_sale.products_shop_title_align', # Shop title centered
|
||||
'website_sale.products_mobile_cols_single', # Mobile cols single
|
||||
'website_sale.filmstrip_categories_bordered', # Category style
|
||||
'website_sale.products_attributes_top', # Filters
|
||||
'website_sale.template_footer_website_sale', # Footer
|
||||
],
|
||||
'disable': [
|
||||
'website_sale.products_attributes', # Filters
|
||||
],
|
||||
},
|
||||
'website_fields': {
|
||||
'shop_ppr': 4,
|
||||
'shop_gap': '16px',
|
||||
'shop_opt_products_design_classes': 'o_wsale_products_opt_name_color_regular '
|
||||
'o_wsale_products_opt_thumb_cover '
|
||||
'o_wsale_products_opt_img_secondary_show '
|
||||
'o_wsale_products_opt_img_hover_zoom_out_light '
|
||||
'o_wsale_products_opt_has_cta '
|
||||
'o_wsale_products_opt_has_wishlist '
|
||||
'o_wsale_products_opt_has_comparison '
|
||||
'o_wsale_products_opt_actions_inline '
|
||||
'o_wsale_products_opt_wishlist_inline '
|
||||
'o_wsale_products_opt_actions_promote '
|
||||
'o_wsale_products_opt_cc o_wsale_products_opt_cc1 '
|
||||
'o_wsale_products_opt_rounded_4 '
|
||||
'o_wsale_products_opt_layout_catalog '
|
||||
'o_wsale_products_opt_design_chips',
|
||||
},
|
||||
'category_fields': {
|
||||
'show_category_title': True,
|
||||
'show_category_description': True,
|
||||
'align_category_content': True,
|
||||
},
|
||||
'scss_customization_params': {
|
||||
'header-links-style': 'default',
|
||||
'header-template': 'sales_one',
|
||||
},
|
||||
},
|
||||
'condensed_list': {
|
||||
'title': _lt("Condensed List"),
|
||||
'img_src': '/website_sale/static/src/img/configurator/shop/condensed_list.jpg',
|
||||
'views': {
|
||||
'enable': [
|
||||
'website.template_header_hamburger', # Header
|
||||
'website.no_autohide_menu', # Header
|
||||
'website_sale.filmstrip_categories_images', # Category style
|
||||
'website_sale.template_footer_website_sale', # Footer
|
||||
],
|
||||
'disable': [],
|
||||
},
|
||||
'website_fields': {
|
||||
'shop_gap': '16px',
|
||||
'shop_opt_products_design_classes': 'o_wsale_products_opt_name_color_regular '
|
||||
'o_wsale_products_opt_thumb_cover '
|
||||
'o_wsale_products_opt_has_cta '
|
||||
'o_wsale_products_opt_has_wishlist '
|
||||
'o_wsale_products_opt_actions_inline '
|
||||
'o_wsale_products_opt_cc o_wsale_products_opt_cc1 '
|
||||
'o_wsale_products_opt_rounded_2 '
|
||||
'o_wsale_products_opt_img_secondary_show '
|
||||
'o_wsale_products_opt_actions_promote '
|
||||
'o_wsale_products_opt_layout_list '
|
||||
'o_wsale_products_opt_design_thumbs',
|
||||
},
|
||||
'category_fields': {
|
||||
'show_category_title': True,
|
||||
'show_category_description': True,
|
||||
'align_category_content': False,
|
||||
},
|
||||
'scss_customization_params': {
|
||||
'header-links-style': 'default',
|
||||
'header-template': 'hamburger',
|
||||
},
|
||||
},
|
||||
'cards': {
|
||||
'title': _lt("Cards"),
|
||||
'img_src': '/website_sale/static/src/img/configurator/shop/cards.jpg',
|
||||
'views': {
|
||||
'enable': [
|
||||
'website_sale.products_mobile_cols_single', # Mobile cols single
|
||||
'website_sale.filmstrip_categories_large_images', # Category style
|
||||
'website_sale.products_attributes_top', # Filters
|
||||
'website_sale.template_footer_website_sale', # Footer
|
||||
],
|
||||
'disable': [
|
||||
'website_sale.products_attributes', # Filters
|
||||
],
|
||||
},
|
||||
'website_fields': {
|
||||
'shop_ppr': 4,
|
||||
'shop_gap': '8px',
|
||||
'shop_opt_products_design_classes': 'o_wsale_products_opt_name_color_regular '
|
||||
'o_wsale_products_opt_thumb_cover '
|
||||
'o_wsale_products_opt_img_secondary_show '
|
||||
'o_wsale_products_opt_img_hover_zoom_out_light '
|
||||
'o_wsale_products_opt_has_cta '
|
||||
'o_wsale_products_opt_has_wishlist '
|
||||
'o_wsale_products_opt_actions_onhover '
|
||||
'o_wsale_products_opt_wishlist_fixed '
|
||||
'o_wsale_products_opt_actions_subtle '
|
||||
'o_wsale_products_opt_rounded_2 '
|
||||
'o_wsale_products_opt_layout_catalog '
|
||||
'o_wsale_products_opt_design_cards '
|
||||
'o_wsale_products_opt_thumb_4_5 '
|
||||
'o_wsale_products_opt_has_comparison',
|
||||
},
|
||||
'category_fields': {
|
||||
'show_category_title': False,
|
||||
'show_category_description': True,
|
||||
'align_category_content': False,
|
||||
},
|
||||
},
|
||||
}
|
||||
PRODUCT_PAGE_STYLE_MAPPING = {
|
||||
'classic': {
|
||||
'title': _lt("Classic"),
|
||||
'img_src': '/website_sale/static/src/img/configurator/product/classic.jpg',
|
||||
'views': {
|
||||
'enable': [],
|
||||
'disable': [],
|
||||
},
|
||||
'website_fields': {
|
||||
'product_page_image_roundness': 'medium',
|
||||
},
|
||||
},
|
||||
'image_grid': {
|
||||
'title': _lt("Image Grid"),
|
||||
'img_src': '/website_sale/static/src/img/configurator/product/image_grid.jpg',
|
||||
'views': {
|
||||
'enable': [],
|
||||
'disable': [],
|
||||
},
|
||||
'website_fields': {
|
||||
'product_page_image_width': '66_pc',
|
||||
'product_page_cols_order': 'inverse',
|
||||
'product_page_image_layout': 'grid',
|
||||
'product_page_image_spacing': 'medium',
|
||||
'product_page_image_roundness': 'medium',
|
||||
'product_page_image_ratio': '2_3',
|
||||
},
|
||||
},
|
||||
'focused': {
|
||||
'title': _lt("Focused"),
|
||||
'img_src': '/website_sale/static/src/img/configurator/product/focused.jpg',
|
||||
'views': {
|
||||
'enable': [
|
||||
# Purchase style
|
||||
'website_sale.cta_wrapper_large',
|
||||
'website_sale.product_buy_now_large',
|
||||
'website_sale.product_quantity_large',
|
||||
],
|
||||
'disable': [
|
||||
'website_sale.cta_wrapper_boxed', # Purchase style
|
||||
],
|
||||
},
|
||||
'website_fields': {
|
||||
'product_page_image_width': '66_pc',
|
||||
'product_page_image_layout': 'grid',
|
||||
'product_page_grid_columns': 1,
|
||||
'product_page_image_spacing': 'small',
|
||||
'product_page_image_roundness': 'small',
|
||||
},
|
||||
},
|
||||
'large_image': {
|
||||
'title': _lt("Large Image"),
|
||||
'img_src': '/website_sale/static/src/img/configurator/product/large_image.jpg',
|
||||
'views': {
|
||||
'enable': [
|
||||
'website_sale.carousel_product_indicators_bottom', # Thumbnail position
|
||||
'website_sale.cta_wrapper_boxed', # Purchase style
|
||||
],
|
||||
'disable': [
|
||||
'website_sale.carousel_product_indicators_left', # Thumbnail position
|
||||
'website_sale.cta_separator', # Separator
|
||||
# Purchase style
|
||||
'website_sale.cta_wrapper_large',
|
||||
'website_sale.product_buy_now_large',
|
||||
'website_sale.product_quantity_large',
|
||||
],
|
||||
},
|
||||
'website_fields': {
|
||||
'product_page_image_width': '100_pc',
|
||||
'product_page_image_ratio': '21_9',
|
||||
},
|
||||
},
|
||||
'functional': {
|
||||
'title': _lt("Functional"),
|
||||
'img_src': '/website_sale/static/src/img/configurator/product/functional.jpg',
|
||||
'views': {
|
||||
'enable': [
|
||||
'website_sale.carousel_product_indicators_bottom', # Thumbnail position
|
||||
'website_sale.cta_wrapper_boxed', # Purchase style
|
||||
],
|
||||
'disable': [
|
||||
'website_sale.carousel_product_indicators_left', # Thumbnail position
|
||||
# Purchase style
|
||||
'website_sale.cta_wrapper_large',
|
||||
'website_sale.product_buy_now_large',
|
||||
'website_sale.product_quantity_large',
|
||||
],
|
||||
},
|
||||
'website_fields': {
|
||||
'product_page_image_width': '33_pc',
|
||||
'product_page_image_roundness': 'small',
|
||||
|
||||
},
|
||||
},
|
||||
'large_grid': {
|
||||
'title': _lt("Large Grid"),
|
||||
'img_src': '/website_sale/static/src/img/configurator/product/large_grid.jpg',
|
||||
'views': {
|
||||
'enable': [
|
||||
# Purchase style
|
||||
'website_sale.cta_wrapper_large',
|
||||
'website_sale.product_buy_now_large',
|
||||
'website_sale.product_quantity_large',
|
||||
],
|
||||
'disable': [
|
||||
'website_sale.cta_separator', # Separator
|
||||
'website_sale.cta_wrapper_boxed', # Purchase style
|
||||
],
|
||||
},
|
||||
'website_fields': {
|
||||
'product_page_image_width': '100_pc',
|
||||
'product_page_image_layout': 'grid',
|
||||
'product_page_image_spacing': 'big',
|
||||
'product_page_image_roundness': 'big',
|
||||
'product_page_image_ratio': '16_9',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
SNIPPET_DEFAULTS = {
|
||||
'website_sale.s_dynamic_snippet_products': {
|
||||
'filter_xmlid': 'website_sale.dynamic_filter_newest_products',
|
||||
'template_key': 'website_sale.dynamic_filter_template_product_product_products_item',
|
||||
'data_attributes': {
|
||||
'snippet': 's_dynamic_snippet_products',
|
||||
'carousel-interval': '5000',
|
||||
'product-category-id': 'all',
|
||||
'number-of-elements': '4',
|
||||
'number-of-elements-small-devices': '2',
|
||||
'show-variants': 'true',
|
||||
},
|
||||
'add_classes': [
|
||||
'o_wsale_products_opt_design_cards',
|
||||
'o_wsale_products_opt_has_comparison',
|
||||
{
|
||||
's_dynamic_snippet_title': 's_dynamic_snippet_title_aside col-lg-3 flex-lg-column justify-content-lg-start',
|
||||
},
|
||||
],
|
||||
'remove_classes': [
|
||||
'o_wsale_products_opt_design_thumbs',
|
||||
'o_wsale_products_opt_has_description',
|
||||
],
|
||||
},
|
||||
'website_sale.s_dynamic_snippet_category_list': {
|
||||
'filter_xmlid': 'website_sale.dynamic_filter_category_list',
|
||||
'template_key': (
|
||||
'website_sale.dynamic_filter_template_product_public_category_clickable_items'
|
||||
),
|
||||
'data_attributes': {
|
||||
'snippet': 's_dynamic_snippet_category_list',
|
||||
'show-parent': 'true',
|
||||
'columns': '4',
|
||||
'rounded': '2',
|
||||
'gap': '2',
|
||||
'size': 'medium',
|
||||
'alignment': 'center',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
PRODUCT_FEED_SOFT_LIMIT = 5000
|
||||
PRODUCT_FEED_HARD_LIMIT = 6000
|
||||
|
||||
# Google Merchant Center
|
||||
GMC_SUPPORTED_UOM = {
|
||||
'oz',
|
||||
'lb',
|
||||
'mg',
|
||||
'g',
|
||||
'kg',
|
||||
'floz',
|
||||
'pt',
|
||||
'ct',
|
||||
'qt',
|
||||
'gal',
|
||||
'ml',
|
||||
'cl',
|
||||
'l',
|
||||
'cbm',
|
||||
'in',
|
||||
'ft',
|
||||
'yd',
|
||||
'cm',
|
||||
'm',
|
||||
'sqft',
|
||||
'sqm',
|
||||
}
|
||||
GMC_BASE_MEASURE = re.compile(r'(?P<base_count>\d+)?\s*(?P<base_unit>[a-z]+)')
|
||||
|
||||
SHOP_PATH = '/shop'
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import backend
|
||||
from . import cart
|
||||
from . import combo_configurator
|
||||
from . import delivery
|
||||
from . import main
|
||||
from . import payment
|
||||
from . import product_configurator
|
||||
from . import product_feed
|
||||
from . import reorder
|
||||
from . import sale
|
||||
from . import variant
|
||||
from . import website
|
||||
|
|
|
|||
|
|
@ -1,183 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import babel.dates
|
||||
|
||||
from datetime import datetime, timedelta, time
|
||||
|
||||
from odoo import fields, http, _
|
||||
from odoo.addons.website.controllers.backend import WebsiteBackend
|
||||
from odoo.http import request
|
||||
from odoo.tools.misc import get_lang
|
||||
|
||||
|
||||
class WebsiteSaleBackend(WebsiteBackend):
|
||||
|
||||
@http.route()
|
||||
def fetch_dashboard_data(self, website_id, date_from, date_to):
|
||||
Website = request.env['website']
|
||||
current_website = website_id and Website.browse(website_id) or Website.get_current_website()
|
||||
|
||||
results = super(WebsiteSaleBackend, self).fetch_dashboard_data(website_id, date_from, date_to)
|
||||
|
||||
date_date_from = fields.Date.from_string(date_from)
|
||||
date_date_to = fields.Date.from_string(date_to)
|
||||
date_diff_days = (date_date_to - date_date_from).days
|
||||
datetime_from = datetime.combine(date_date_from, time.min)
|
||||
datetime_to = datetime.combine(date_date_to, time.max)
|
||||
|
||||
sales_values = dict(
|
||||
graph=[],
|
||||
best_sellers=[],
|
||||
summary=dict(
|
||||
order_count=0, order_carts_count=0, order_unpaid_count=0,
|
||||
order_to_invoice_count=0, order_carts_abandoned_count=0,
|
||||
payment_to_capture_count=0, total_sold=0,
|
||||
order_per_day_ratio=0, order_sold_ratio=0, order_convertion_pctg=0,
|
||||
)
|
||||
)
|
||||
|
||||
results['dashboards']['sales'] = sales_values
|
||||
|
||||
results['groups']['sale_salesman'] = request.env['res.users'].has_group('sales_team.group_sale_salesman')
|
||||
|
||||
if not results['groups']['sale_salesman']:
|
||||
return results
|
||||
|
||||
results['dashboards']['sales']['utm_graph'] = self.fetch_utm_data(datetime_from, datetime_to)
|
||||
# Product-based computation
|
||||
sale_report_domain = [
|
||||
('website_id', '=', current_website.id),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('date', '>=', datetime_from),
|
||||
('date', '<=', fields.Datetime.now())
|
||||
]
|
||||
report_product_lines = request.env['sale.report'].read_group(
|
||||
domain=sale_report_domain,
|
||||
fields=['product_tmpl_id', 'product_uom_qty', 'price_subtotal'],
|
||||
groupby='product_tmpl_id', orderby='product_uom_qty desc', limit=5)
|
||||
for product_line in report_product_lines:
|
||||
product_tmpl_id = request.env['product.template'].browse(product_line['product_tmpl_id'][0])
|
||||
sales_values['best_sellers'].append({
|
||||
'id': product_tmpl_id.id,
|
||||
'name': product_tmpl_id.name,
|
||||
'qty': product_line['product_uom_qty'],
|
||||
'sales': product_line['price_subtotal'],
|
||||
})
|
||||
|
||||
# Sale-based results computation
|
||||
sale_order_domain = [
|
||||
('website_id', '=', current_website.id),
|
||||
('date_order', '>=', fields.Datetime.to_string(datetime_from)),
|
||||
('date_order', '<=', fields.Datetime.to_string(datetime_to))]
|
||||
so_group_data = request.env['sale.order'].read_group(sale_order_domain, fields=['state'], groupby='state')
|
||||
for res in so_group_data:
|
||||
if res.get('state') == 'sent':
|
||||
sales_values['summary']['order_unpaid_count'] += res['state_count']
|
||||
elif res.get('state') in ['sale', 'done']:
|
||||
sales_values['summary']['order_count'] += res['state_count']
|
||||
sales_values['summary']['order_carts_count'] += res['state_count']
|
||||
|
||||
report_price_lines = request.env['sale.report'].read_group(
|
||||
domain=[
|
||||
('website_id', '=', current_website.id),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('date', '>=', datetime_from),
|
||||
('date', '<=', datetime_to)],
|
||||
fields=['team_id', 'price_subtotal'],
|
||||
groupby=['team_id'],
|
||||
)
|
||||
sales_values['summary'].update(
|
||||
order_to_invoice_count=request.env['sale.order'].search_count(sale_order_domain + [
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('order_line', '!=', False),
|
||||
('partner_id', '!=', request.env.ref('base.public_partner').id),
|
||||
('invoice_status', '=', 'to invoice'),
|
||||
]),
|
||||
order_carts_abandoned_count=request.env['sale.order'].search_count(sale_order_domain + [
|
||||
('is_abandoned_cart', '=', True),
|
||||
('cart_recovery_email_sent', '=', False)
|
||||
]),
|
||||
payment_to_capture_count=request.env['payment.transaction'].search_count([
|
||||
('state', '=', 'authorized'),
|
||||
# that part perform a search on sale.order in order to comply with access rights as tx do not have any
|
||||
('sale_order_ids', 'in', request.env['sale.order'].search(sale_order_domain + [('state', '!=', 'cancel')]).ids),
|
||||
]),
|
||||
total_sold=sum(price_line['price_subtotal'] for price_line in report_price_lines)
|
||||
)
|
||||
|
||||
# Ratio computation
|
||||
sales_values['summary']['order_per_day_ratio'] = round(float(sales_values['summary']['order_count']) / date_diff_days, 2)
|
||||
sales_values['summary']['order_sold_ratio'] = round(float(sales_values['summary']['total_sold']) / sales_values['summary']['order_count'], 2) if sales_values['summary']['order_count'] else 0
|
||||
sales_values['summary']['order_convertion_pctg'] = 100.0 * sales_values['summary']['order_count'] / sales_values['summary']['order_carts_count'] if sales_values['summary']['order_carts_count'] else 0
|
||||
|
||||
# Graphes computation
|
||||
if date_diff_days == 7:
|
||||
previous_sale_label = _('Previous Week')
|
||||
elif date_diff_days > 7 and date_diff_days <= 31:
|
||||
previous_sale_label = _('Previous Month')
|
||||
else:
|
||||
previous_sale_label = _('Previous Year')
|
||||
|
||||
sales_values['graph'] += [{
|
||||
'values': self._compute_sale_graph(date_date_from, date_date_to, sale_report_domain),
|
||||
'key': 'Untaxed Total',
|
||||
}, {
|
||||
'values': self._compute_sale_graph(date_date_from - timedelta(days=date_diff_days), date_date_from, sale_report_domain, previous=True),
|
||||
'key': previous_sale_label,
|
||||
}]
|
||||
|
||||
return results
|
||||
|
||||
def fetch_utm_data(self, date_from, date_to):
|
||||
sale_utm_domain = [
|
||||
('website_id', '!=', False),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('date_order', '>=', date_from),
|
||||
('date_order', '<=', date_to)
|
||||
]
|
||||
|
||||
orders_data_groupby_campaign_id = request.env['sale.order']._read_group(
|
||||
domain=sale_utm_domain + [('campaign_id', '!=', False)],
|
||||
fields=['amount_total', 'id', 'campaign_id'],
|
||||
groupby='campaign_id')
|
||||
|
||||
orders_data_groupby_medium_id = request.env['sale.order']._read_group(
|
||||
domain=sale_utm_domain + [('medium_id', '!=', False)],
|
||||
fields=['amount_total', 'id', 'medium_id'],
|
||||
groupby='medium_id')
|
||||
|
||||
orders_data_groupby_source_id = request.env['sale.order']._read_group(
|
||||
domain=sale_utm_domain + [('source_id', '!=', False)],
|
||||
fields=['amount_total', 'id', 'source_id'],
|
||||
groupby='source_id')
|
||||
|
||||
return {
|
||||
'campaign_id': self.compute_utm_graph_data('campaign_id', orders_data_groupby_campaign_id),
|
||||
'medium_id': self.compute_utm_graph_data('medium_id', orders_data_groupby_medium_id),
|
||||
'source_id': self.compute_utm_graph_data('source_id', orders_data_groupby_source_id),
|
||||
}
|
||||
|
||||
def compute_utm_graph_data(self, utm_type, utm_graph_data):
|
||||
return [{
|
||||
'utm_type': data[utm_type][1],
|
||||
'amount_total': data['amount_total']
|
||||
} for data in utm_graph_data]
|
||||
|
||||
def _compute_sale_graph(self, date_from, date_to, sales_domain, previous=False):
|
||||
days_between = (date_to - date_from).days
|
||||
date_list = [(date_from + timedelta(days=x)) for x in range(0, days_between + 1)]
|
||||
|
||||
daily_sales = request.env['sale.report'].read_group(
|
||||
domain=sales_domain,
|
||||
fields=['date', 'price_subtotal'],
|
||||
groupby='date:day')
|
||||
|
||||
daily_sales_dict = {p['date:day']: p['price_subtotal'] for p in daily_sales}
|
||||
|
||||
sales_graph = [{
|
||||
'0': fields.Date.to_string(d) if not previous else fields.Date.to_string(d + timedelta(days=days_between)),
|
||||
# Respect read_group format in models.py
|
||||
'1': daily_sales_dict.get(babel.dates.format_date(d, format='dd MMM yyyy', locale=get_lang(request.env).code), 0)
|
||||
} for d in date_list]
|
||||
|
||||
return sales_graph
|
||||
|
|
@ -0,0 +1,539 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from odoo import fields
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.http import request, route
|
||||
from odoo.tools import consteq
|
||||
from odoo.tools.image import image_data_uri
|
||||
from odoo.tools.translate import _
|
||||
|
||||
from odoo.addons.payment import utils as payment_utils
|
||||
from odoo.addons.payment.controllers.portal import PaymentPortal
|
||||
from odoo.addons.sale.controllers.portal import CustomerPortal
|
||||
from odoo.addons.website_sale.controllers.main import WebsiteSale
|
||||
|
||||
|
||||
class Cart(PaymentPortal):
|
||||
|
||||
@route(route='/shop/cart', type='http', auth='public', website=True, sitemap=False)
|
||||
def cart(self, id=None, access_token=None, revive_method='', **post):
|
||||
""" Display the cart page.
|
||||
|
||||
This route is responsible for the main cart management and abandoned cart revival logic.
|
||||
|
||||
:param str id: The abandoned cart's id.
|
||||
:param str access_token: The abandoned cart's access token.
|
||||
:param str revive_method: The revival method for abandoned carts. Can be 'merge' or 'squash'.
|
||||
:return: The rendered cart page.
|
||||
:rtype: str
|
||||
"""
|
||||
if not request.website.has_ecommerce_access():
|
||||
return request.redirect('/web/login')
|
||||
|
||||
order_sudo = request.cart
|
||||
|
||||
values = {}
|
||||
if id and access_token:
|
||||
abandoned_order = request.env['sale.order'].sudo().browse(int(id)).exists()
|
||||
if not abandoned_order or not consteq(abandoned_order.access_token, access_token): # wrong token (or SO has been deleted)
|
||||
raise NotFound()
|
||||
if abandoned_order.state != 'draft': # abandoned cart already finished
|
||||
values.update({'abandoned_proceed': True})
|
||||
elif revive_method == 'squash' or (revive_method == 'merge' and not request.session.get('sale_order_id')): # restore old cart or merge with unexistant
|
||||
request.session['sale_order_id'] = abandoned_order.id
|
||||
return request.redirect('/shop/cart')
|
||||
elif revive_method == 'merge':
|
||||
abandoned_order.order_line.write({'order_id': request.session['sale_order_id']})
|
||||
abandoned_order.action_cancel()
|
||||
elif abandoned_order.id != request.session.get('sale_order_id'): # abandoned cart found, user have to choose what to do
|
||||
values.update({'id': abandoned_order.id, 'access_token': abandoned_order.access_token})
|
||||
|
||||
values.update({
|
||||
'website_sale_order': order_sudo,
|
||||
'date': fields.Date.today(),
|
||||
'suggested_products': [],
|
||||
})
|
||||
if order_sudo:
|
||||
order_sudo.order_line.filtered(lambda sol: sol.product_id and not sol.product_id.active).unlink()
|
||||
values['suggested_products'] = order_sudo._cart_accessories()
|
||||
values.update(self._get_express_shop_payment_values(order_sudo))
|
||||
|
||||
values.update(request.website._get_checkout_step_values())
|
||||
values.update(self._cart_values(**post))
|
||||
values.update(self._prepare_order_history())
|
||||
return request.render('website_sale.cart', values)
|
||||
|
||||
def _cart_values(self, **post):
|
||||
"""
|
||||
This method is a hook to pass additional values when rendering the 'website_sale.cart' template (e.g. add
|
||||
a flag to trigger a style variation)
|
||||
"""
|
||||
return {}
|
||||
|
||||
@route(
|
||||
route='/shop/cart/add',
|
||||
type='jsonrpc',
|
||||
auth='public',
|
||||
methods=['POST'],
|
||||
website=True,
|
||||
sitemap=False
|
||||
)
|
||||
def add_to_cart(
|
||||
self,
|
||||
product_template_id,
|
||||
product_id,
|
||||
quantity=1.0,
|
||||
uom_id=None,
|
||||
product_custom_attribute_values=None,
|
||||
no_variant_attribute_value_ids=None,
|
||||
linked_products=None,
|
||||
**kwargs
|
||||
):
|
||||
""" Adds a product to the shopping cart.
|
||||
|
||||
:param int product_template_id: The product to add to cart, as a
|
||||
`product.template` id.
|
||||
:param int product_id: The product to add to cart, as a
|
||||
`product.product` id.
|
||||
:param int quantity: The quantity to add to the cart.
|
||||
:param list[dict] product_custom_attribute_values: A list of objects representing custom
|
||||
attribute values for the product. Each object contains:
|
||||
- `custom_product_template_attribute_value_id`: The custom attribute's id;
|
||||
- `custom_value`: The custom attribute's value.
|
||||
:param dict no_variant_attribute_value_ids: The selected non-stored attribute(s), as a list
|
||||
of `product.template.attribute.value` ids.
|
||||
:param list linked_products: A list of objects representing additional products linked to
|
||||
the product added to the cart. Can be combo item or optional products.
|
||||
:param dict kwargs: Optional data. This parameter is not used here.
|
||||
:return: The values
|
||||
:rtype: dict
|
||||
"""
|
||||
order_sudo = request.cart or request.website._create_cart()
|
||||
quantity = int(quantity) # Do not allow float values in ecommerce by default
|
||||
|
||||
product = request.env['product.product'].browse(product_id).exists()
|
||||
if not product or not product._is_add_to_cart_allowed():
|
||||
raise UserError(_(
|
||||
"The given product does not exist therefore it cannot be added to cart."
|
||||
))
|
||||
|
||||
added_qty_per_line = {}
|
||||
values = order_sudo.with_context(skip_cart_verification=True)._cart_add(
|
||||
product_id=product_id,
|
||||
quantity=quantity,
|
||||
uom_id=uom_id,
|
||||
product_custom_attribute_values=product_custom_attribute_values,
|
||||
no_variant_attribute_value_ids=no_variant_attribute_value_ids,
|
||||
**kwargs,
|
||||
)
|
||||
line_ids = {product_template_id: values['line_id']}
|
||||
added_qty_per_line[values['line_id']] = values['added_qty']
|
||||
is_combo = product.type == 'combo'
|
||||
updated_line = (
|
||||
values['line_id']
|
||||
and order_sudo.order_line.filtered(lambda line: line.id == values['line_id'])
|
||||
) or order_sudo.env['sale.order.line']
|
||||
|
||||
if linked_products and values['line_id']:
|
||||
for product_data in linked_products:
|
||||
product_sudo = request.env['product.product'].sudo().browse(
|
||||
product_data['product_id']
|
||||
).exists()
|
||||
if product_data['quantity'] and (
|
||||
not product_sudo
|
||||
or (
|
||||
not product_sudo._is_add_to_cart_allowed()
|
||||
# For combos, the validity of the given product will be checked
|
||||
# through the SOline constraints (_check_combo_item_id)
|
||||
and not product_data.get('combo_item_id')
|
||||
)
|
||||
):
|
||||
raise UserError(_(
|
||||
"The given product does not exist therefore it cannot be added to cart."
|
||||
))
|
||||
|
||||
product_values = order_sudo.with_context(skip_cart_verification=True)._cart_add(
|
||||
product_id=product_data['product_id'],
|
||||
quantity=product_data['quantity'],
|
||||
uom_id=product_data.get('uom_id'),
|
||||
product_custom_attribute_values=product_data['product_custom_attribute_values'],
|
||||
no_variant_attribute_value_ids=[
|
||||
int(value_id) for value_id in product_data['no_variant_attribute_value_ids']
|
||||
],
|
||||
# Using `line_ids[...]` instead of `line_ids.get(...)` ensures that this throws
|
||||
# if an optional product contains bad data.
|
||||
linked_line_id=line_ids[product_data['parent_product_template_id']],
|
||||
**self._get_additional_cart_update_values(product_data),
|
||||
**kwargs,
|
||||
)
|
||||
if is_combo and not product_values.get('quantity'):
|
||||
# Early return when one of the combo products if fully unavailable
|
||||
# Delete main combo line (and existing children in cascade)
|
||||
updated_line.unlink()
|
||||
# Return empty notification since cart update is considered as failed
|
||||
return {
|
||||
'cart_quantity': order_sudo.cart_quantity,
|
||||
'notification_info': {
|
||||
'warning': product_values.get('warning', ''),
|
||||
},
|
||||
'quantity': 0,
|
||||
'tracking_info': [],
|
||||
}
|
||||
|
||||
line_ids[product_data['product_template_id']] = product_values['line_id']
|
||||
added_qty_per_line[product_values['line_id']] = product_values['added_qty']
|
||||
|
||||
warning = values.pop('warning', '')
|
||||
if is_combo and order_sudo._check_combo_quantities(updated_line):
|
||||
# If quantities were modified through `_check_combo_quantities`, the added qty per line
|
||||
# must be adapted accordingly, and the returned warning should be the final one saved
|
||||
# on the combo line.
|
||||
added_qty_per_line = {
|
||||
line.id: updated_line.product_uom_qty
|
||||
for line in (updated_line + updated_line.linked_line_ids)
|
||||
}
|
||||
warning = updated_line.shop_warning
|
||||
values['quantity'] = updated_line.product_uom_qty
|
||||
|
||||
# Recompute delivery prices & other cart stuff (loyalty rewards)
|
||||
order_sudo._verify_cart_after_update()
|
||||
|
||||
# The validity of a combo product line can only be checked after creating all of its combo
|
||||
# item lines.
|
||||
main_product_line = request.env['sale.order.line'].browse(values['line_id'])
|
||||
if main_product_line.product_type == 'combo':
|
||||
main_product_line._check_validity()
|
||||
|
||||
positive_added_qty_per_line = {
|
||||
line_id: qty for line_id, qty in added_qty_per_line.items() if qty > 0
|
||||
}
|
||||
|
||||
return {
|
||||
'cart_quantity': order_sudo.cart_quantity,
|
||||
'notification_info': {
|
||||
**self._get_cart_notification_information(
|
||||
order_sudo, positive_added_qty_per_line
|
||||
),
|
||||
'warning': warning,
|
||||
},
|
||||
'quantity': values.pop('quantity', 0),
|
||||
'tracking_info': self._get_tracking_information(order_sudo, line_ids.values()),
|
||||
}
|
||||
|
||||
@route(
|
||||
route='/shop/cart/quick_add', type='jsonrpc', auth='user', methods=['POST'], website=True
|
||||
)
|
||||
def quick_add(self, product_template_id, product_id, quantity=1.0, **kwargs):
|
||||
values = self.add_to_cart(product_template_id, product_id, quantity=quantity, **kwargs)
|
||||
|
||||
IrUiView = request.env['ir.ui.view']
|
||||
order_sudo = request.cart
|
||||
values['website_sale.cart_lines'] = IrUiView._render_template(
|
||||
'website_sale.cart_lines', {
|
||||
'website_sale_order': order_sudo,
|
||||
'date': fields.Date.today(),
|
||||
'suggested_products': order_sudo._cart_accessories(),
|
||||
}
|
||||
)
|
||||
values['website_sale.shorter_cart_summary'] = IrUiView._render_template(
|
||||
'website_sale.shorter_cart_summary', {
|
||||
'website_sale_order': order_sudo,
|
||||
'show_shorter_cart_summary': True,
|
||||
**self._get_express_shop_payment_values(order_sudo),
|
||||
**request.website._get_checkout_step_values(),
|
||||
}
|
||||
)
|
||||
values['website_sale.quick_reorder_history'] = IrUiView._render_template(
|
||||
'website_sale.quick_reorder_history', {
|
||||
'website_sale_order': order_sudo,
|
||||
**self._prepare_order_history(),
|
||||
}
|
||||
)
|
||||
values['cart_ready'] = order_sudo._is_cart_ready()
|
||||
return values
|
||||
|
||||
def _get_express_shop_payment_values(self, order, **kwargs):
|
||||
payment_form_values = CustomerPortal._get_payment_values(
|
||||
self, order, website_id=request.website.id, is_express_checkout=True
|
||||
)
|
||||
payment_form_values.update({
|
||||
'payment_access_token': payment_form_values.pop('access_token'), # Rename the key.
|
||||
# Do not include delivery related lines
|
||||
'minor_amount': payment_utils.to_minor_currency_units(
|
||||
order._get_amount_total_excluding_delivery(), order.currency_id
|
||||
),
|
||||
'merchant_name': request.website.name,
|
||||
'transaction_route': f'/shop/payment/transaction/{order.id}',
|
||||
'express_checkout_route': WebsiteSale._express_checkout_route,
|
||||
'landing_route': '/shop/payment/validate',
|
||||
'payment_method_unknown_id': request.env.ref('payment.payment_method_unknown').id,
|
||||
'shipping_info_required': order._has_deliverable_products(),
|
||||
# Todo: remove in master
|
||||
'delivery_amount': payment_utils.to_minor_currency_units(
|
||||
order.amount_total - order._compute_amount_total_without_delivery(),
|
||||
order.currency_id,
|
||||
),
|
||||
'shipping_address_update_route': WebsiteSale._express_checkout_delivery_route,
|
||||
})
|
||||
if request.website.is_public_user():
|
||||
payment_form_values['partner_id'] = -1
|
||||
return payment_form_values
|
||||
|
||||
@route(
|
||||
route='/shop/cart/update',
|
||||
type='jsonrpc',
|
||||
auth='public',
|
||||
methods=['POST'],
|
||||
website=True,
|
||||
sitemap=False
|
||||
)
|
||||
def update_cart(self, line_id, quantity, product_id=None, **kwargs):
|
||||
"""Update the quantity of a specific line of the current cart.
|
||||
|
||||
:param int line_id: line to update, as a `sale.order.line` id.
|
||||
:param float quantity: new line quantity.
|
||||
0 or negative numbers will only delete the line, the ecommerce
|
||||
doesn't work with negative numbers.
|
||||
:param int|None product_id: product_id of the edited line, only used when line_id
|
||||
is falsy
|
||||
:params dict kwargs: additional parameters given to _cart_update_line_quantity calls.
|
||||
"""
|
||||
order_sudo = request.cart
|
||||
quantity = int(quantity) # Do not allow float values in ecommerce by default
|
||||
IrUiView = request.env['ir.ui.view']
|
||||
|
||||
# This method must be only called from the cart page BUT in some advanced logic
|
||||
# eg. website_sale_loyalty, a cart line could be a temporary record without id.
|
||||
# In this case, the line_id must be found out through the given product id.
|
||||
if not line_id:
|
||||
line_id = order_sudo.order_line.filtered(
|
||||
lambda sol: sol.product_id.id == product_id
|
||||
)[:1].id
|
||||
|
||||
values = order_sudo._cart_update_line_quantity(line_id, quantity, **kwargs)
|
||||
|
||||
values['cart_quantity'] = order_sudo.cart_quantity
|
||||
values['cart_ready'] = order_sudo._is_cart_ready()
|
||||
values['amount'] = order_sudo.amount_total
|
||||
values['minor_amount'] = (
|
||||
order_sudo and payment_utils.to_minor_currency_units(
|
||||
order_sudo.amount_total, order_sudo.currency_id
|
||||
)
|
||||
) or 0.0
|
||||
values['website_sale.cart_lines'] = IrUiView._render_template(
|
||||
'website_sale.cart_lines', {
|
||||
'website_sale_order': order_sudo,
|
||||
'date': fields.Date.today(),
|
||||
'suggested_products': order_sudo._cart_accessories()
|
||||
}
|
||||
)
|
||||
values['website_sale.total'] = IrUiView._render_template(
|
||||
'website_sale.total', {
|
||||
'website_sale_order': order_sudo,
|
||||
}
|
||||
)
|
||||
values['website_sale.quick_reorder_history'] = IrUiView._render_template(
|
||||
'website_sale.quick_reorder_history', {
|
||||
'website_sale_order': order_sudo,
|
||||
**self._prepare_order_history(),
|
||||
}
|
||||
)
|
||||
return values
|
||||
|
||||
def _prepare_order_history(self):
|
||||
"""Prepare the order history of the current user.
|
||||
|
||||
The valid order lines of the last 10 confirmed orders are considered and grouped by date. An
|
||||
order line is not valid if:
|
||||
|
||||
- Its product is already in the cart.
|
||||
- It's a combo parent line.
|
||||
- It has an unsellable product.
|
||||
- It has a zero-priced product (if the website blocks them).
|
||||
- It has an already seen product (duplicate or identical combo).
|
||||
|
||||
The dates are represented by labels like "Today", "Yesterday", or "X days ago".
|
||||
|
||||
:return: The order history, in the format
|
||||
{'order_history': [{'label': str, 'lines': SaleOrderLine}, ...]}.
|
||||
:rtype: dict
|
||||
"""
|
||||
def is_same_combo(line1_, line2_):
|
||||
"""Check if two combo lines have the same linked product combination."""
|
||||
return line1_.linked_line_ids.product_id.ids == line2_.linked_line_ids.product_id.ids
|
||||
|
||||
# Get the last 10 confirmed orders from the current website user.
|
||||
previous_orders_lines_sudo = request.env['sale.order'].sudo().search(
|
||||
[
|
||||
('partner_id', '=', request.env.user.partner_id.id),
|
||||
('state', '=', 'sale'),
|
||||
('website_id', '=', request.website.id),
|
||||
],
|
||||
order='date_order desc',
|
||||
limit=10,
|
||||
).order_line
|
||||
|
||||
# Prepare the order history.
|
||||
SaleOrderLineSudo = request.env['sale.order.line'].sudo()
|
||||
cart_lines_sudo = request.cart.order_line if request.cart else SaleOrderLineSudo
|
||||
seen_lines_sudo = SaleOrderLineSudo
|
||||
lines_per_order_date = {}
|
||||
for line_sudo in previous_orders_lines_sudo:
|
||||
# Ignore lines that are combo parents, unsellable, or zero-priced.
|
||||
product_id = line_sudo.product_id.id
|
||||
if (
|
||||
line_sudo.linked_line_id.product_type == 'combo'
|
||||
or not line_sudo._is_sellable()
|
||||
or (
|
||||
request.website.prevent_zero_price_sale
|
||||
and line_sudo.product_id._get_combination_info_variant()['price'] == 0
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
# Ignore lines that are already in the cart or have already been seen.
|
||||
is_combo = line_sudo.product_type == 'combo'
|
||||
if any(
|
||||
l.product_id.id == product_id and (not is_combo or is_same_combo(line_sudo, l))
|
||||
for l in cart_lines_sudo + seen_lines_sudo
|
||||
):
|
||||
continue
|
||||
seen_lines_sudo |= line_sudo
|
||||
|
||||
# Group lines by date.
|
||||
days_ago = (fields.Date.today() - line_sudo.order_id.date_order.date()).days
|
||||
if days_ago == 0:
|
||||
line_group_label = self.env._("Today")
|
||||
elif days_ago == 1:
|
||||
line_group_label = self.env._("Yesterday")
|
||||
else:
|
||||
line_group_label = self.env._("%s days ago", days_ago)
|
||||
lines_per_order_date.setdefault(line_group_label, SaleOrderLineSudo)
|
||||
lines_per_order_date[line_group_label] |= line_sudo
|
||||
|
||||
# Flatten the line groups to get the final order history.
|
||||
return {
|
||||
'order_history': [
|
||||
{'label': label, 'lines': lines} for label, lines in lines_per_order_date.items()
|
||||
]
|
||||
}
|
||||
|
||||
@route(
|
||||
route='/shop/cart/quantity',
|
||||
type='jsonrpc',
|
||||
auth='public',
|
||||
methods=['POST'],
|
||||
website=True
|
||||
)
|
||||
def cart_quantity(self):
|
||||
if 'website_sale_cart_quantity' not in request.session:
|
||||
return request.cart.cart_quantity
|
||||
return request.session['website_sale_cart_quantity']
|
||||
|
||||
@route(
|
||||
route='/shop/cart/clear',
|
||||
type='jsonrpc',
|
||||
auth='public',
|
||||
website=True
|
||||
)
|
||||
def clear_cart(self):
|
||||
request.cart.order_line.unlink()
|
||||
|
||||
def _get_cart_notification_information(self, order, added_qty_per_line):
|
||||
""" Get the information about the sales order lines to show in the notification.
|
||||
|
||||
:param sale.order order: The sales order.
|
||||
:param dict added_qty_per_line: The added qty per order line.
|
||||
:rtype: dict
|
||||
:return: A dict with the following structure:
|
||||
{
|
||||
'currency_id': int
|
||||
'lines': [{
|
||||
'id': int
|
||||
'image_url': int
|
||||
'quantity': float
|
||||
'name': str
|
||||
'description': str
|
||||
'added_qty_price_total': float
|
||||
}],
|
||||
}
|
||||
"""
|
||||
lines = order.order_line.filtered(lambda line: line.id in set(added_qty_per_line))
|
||||
if not lines:
|
||||
return {}
|
||||
|
||||
show_tax = order.website_id.show_line_subtotals_tax_selection == 'tax_included'
|
||||
return {
|
||||
'currency_id': order.currency_id.id,
|
||||
'lines': [
|
||||
{ # For the cart_notification
|
||||
'id': line.id,
|
||||
'image_url': order.website_id.image_url(line.product_id, 'image_128'),
|
||||
'quantity': added_qty_per_line[line.id],
|
||||
'name': line._get_line_header(),
|
||||
'combination_name': line._get_combination_name(),
|
||||
'description': line._get_sale_order_line_multiline_description_variants(),
|
||||
'price_total': (
|
||||
line.price_reduce_taxinc
|
||||
if show_tax else line.price_reduce_taxexcl
|
||||
) * added_qty_per_line[line.id],
|
||||
**self._get_additional_cart_notification_information(line),
|
||||
} for line in lines
|
||||
],
|
||||
}
|
||||
|
||||
def _get_tracking_information(self, order_sudo, line_ids):
|
||||
""" Get the tracking information about the sales order lines.
|
||||
|
||||
:param sale.order order: The sales order.
|
||||
:param list[int] line_ids: The ids of the lines to track.
|
||||
:rtype: dict
|
||||
:return: The tracking information.
|
||||
"""
|
||||
lines = order_sudo.order_line.filtered(
|
||||
lambda line: line.id in line_ids
|
||||
).with_context(display_default_code=False)
|
||||
return [
|
||||
{
|
||||
'item_id': line.product_id.barcode or line.product_id.id,
|
||||
'item_name': line.product_id.display_name,
|
||||
'item_category': line.product_id.categ_id.name,
|
||||
'currency': line.currency_id.name,
|
||||
'price': line.price_reduce_taxexcl,
|
||||
'discount': line.price_unit - line.price_reduce_taxexcl,
|
||||
'quantity': line.product_uom_qty,
|
||||
} for line in lines
|
||||
]
|
||||
|
||||
def _get_additional_cart_update_values(self, data):
|
||||
""" Look for extra information in a given dictionary to be included in a `_cart_add` call.
|
||||
|
||||
:param dict data: A dictionary in which to look up for extra information.
|
||||
:return: addition values to be passed to `_cart_add`.
|
||||
:rtype: dict
|
||||
"""
|
||||
if data.get('combo_item_id'):
|
||||
return {'combo_item_id': data['combo_item_id']}
|
||||
return {}
|
||||
|
||||
def _get_additional_cart_notification_information(self, line):
|
||||
infos = {}
|
||||
# Only set the linked line id for combo items, not for optional products.
|
||||
if combo_item := line.combo_item_id:
|
||||
infos['linked_line_id'] = line.linked_line_id.id
|
||||
# To sell a product type 'combo', one doesn't need to publish all combo choices. This
|
||||
# causes an issue when public users access the image of each choice via the /web/image
|
||||
# route. To bypass this access check, we send the raw image URL if the product is
|
||||
# inaccessible to the current user.
|
||||
if (
|
||||
not combo_item.product_id.sudo(False).has_access('read')
|
||||
and combo_item.product_id.image_128
|
||||
):
|
||||
infos['image_url'] = image_data_uri(combo_item.product_id.image_128)
|
||||
|
||||
if line.product_template_id._has_multiple_uoms():
|
||||
infos['uom_name'] = line.product_uom_id.name
|
||||
|
||||
return infos
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.http import request, route
|
||||
from odoo.tools.image import image_data_uri
|
||||
|
||||
from odoo.addons.sale.controllers.combo_configurator import SaleComboConfiguratorController
|
||||
from odoo.addons.website_sale.controllers.main import WebsiteSale
|
||||
|
||||
|
||||
class WebsiteSaleComboConfiguratorController(SaleComboConfiguratorController, WebsiteSale):
|
||||
|
||||
@route(
|
||||
route='/website_sale/combo_configurator/get_data',
|
||||
type='jsonrpc',
|
||||
auth='public',
|
||||
website=True,
|
||||
readonly=True,
|
||||
)
|
||||
def website_sale_combo_configurator_get_data(self, *args, **kwargs):
|
||||
self._populate_currency_and_pricelist(kwargs)
|
||||
request.update_context(display_default_code=False) # Hide internal product reference
|
||||
return super().sale_combo_configurator_get_data(*args, **kwargs)
|
||||
|
||||
@route(
|
||||
route='/website_sale/combo_configurator/get_price',
|
||||
type='jsonrpc',
|
||||
auth='public',
|
||||
website=True,
|
||||
readonly=True,
|
||||
)
|
||||
def website_sale_combo_configurator_get_price(self, *args, **kwargs):
|
||||
self._populate_currency_and_pricelist(kwargs)
|
||||
return super().sale_combo_configurator_get_price(*args, **kwargs)
|
||||
|
||||
def _get_combo_item_data(
|
||||
self, combo, combo_item, selected_combo_item, date, currency, pricelist, **kwargs
|
||||
):
|
||||
data = super()._get_combo_item_data(
|
||||
combo, combo_item, selected_combo_item, date, currency, pricelist, **kwargs
|
||||
)
|
||||
# To sell a product type 'combo', one doesn't need to publish all combo choices. This causes
|
||||
# an issue when public users access the image of each choice via the /web/image route. To
|
||||
# bypass this access check, we send the raw image URL if the product is inaccessible to the
|
||||
# current user.
|
||||
if (
|
||||
not combo_item.product_id.sudo(False).has_access('read')
|
||||
and (combo_item_image := combo_item.product_id.image_256)
|
||||
):
|
||||
data['product']['image_src'] = image_data_uri(combo_item_image)
|
||||
return data
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.http import request, route
|
||||
|
||||
from odoo.addons.payment import utils as payment_utils
|
||||
from odoo.addons.website_sale.controllers.main import WebsiteSale
|
||||
|
||||
|
||||
class Delivery(WebsiteSale):
|
||||
_express_checkout_delivery_route = '/shop/express/shipping_address_change'
|
||||
|
||||
@route('/shop/delivery_methods', type='jsonrpc', auth='public', website=True)
|
||||
def shop_delivery_methods(self):
|
||||
""" Fetch available delivery methods and render them in the delivery form.
|
||||
|
||||
:return: The rendered delivery form.
|
||||
:rtype: str
|
||||
"""
|
||||
order_sudo = request.cart
|
||||
values = {
|
||||
'delivery_methods': order_sudo._get_delivery_methods(),
|
||||
'selected_dm_id': order_sudo.carrier_id.id,
|
||||
'order': order_sudo, # Needed for accessing default values for pickup points.
|
||||
}
|
||||
values |= self._get_additional_delivery_context()
|
||||
return request.env['ir.ui.view']._render_template('website_sale.delivery_form', values)
|
||||
|
||||
def _get_additional_delivery_context(self):
|
||||
""" Hook to update values used for rendering the website_sale.delivery_form template. """
|
||||
return {}
|
||||
|
||||
@route('/shop/set_delivery_method', type='jsonrpc', auth='public', website=True)
|
||||
def shop_set_delivery_method(self, dm_id=None, **kwargs):
|
||||
""" Set the delivery method on the current order and return the order summary values.
|
||||
|
||||
If the delivery method is already set, the order summary values are returned immediately.
|
||||
|
||||
:param str dm_id: The delivery method to set, as a `delivery.carrier` id.
|
||||
:param dict kwargs: The keyword arguments forwarded to `_order_summary_values`.
|
||||
:return: The order summary values, if any.
|
||||
:rtype: dict
|
||||
"""
|
||||
if not (order_sudo := request.cart):
|
||||
return {}
|
||||
|
||||
dm_id = int(dm_id)
|
||||
if dm_id in order_sudo._get_delivery_methods().ids and dm_id != order_sudo.carrier_id.id:
|
||||
for tx_sudo in order_sudo.transaction_ids:
|
||||
if tx_sudo.state not in ('draft', 'cancel', 'error'):
|
||||
raise UserError(_(
|
||||
"It seems that there is already a transaction for your order; you can't"
|
||||
" change the delivery method anymore."
|
||||
))
|
||||
|
||||
delivery_method_sudo = request.env['delivery.carrier'].sudo().browse(dm_id).exists()
|
||||
order_sudo._set_delivery_method(delivery_method_sudo)
|
||||
return self._order_summary_values(order_sudo, **kwargs)
|
||||
|
||||
def _order_summary_values(self, order, **kwargs):
|
||||
""" Return the summary values of the order.
|
||||
|
||||
:param sale.order order: The sales order whose summary values to return.
|
||||
:param dict kwargs: The keyword arguments. This parameter is not used here.
|
||||
:return: The order summary values.
|
||||
:rtype: dict
|
||||
"""
|
||||
Monetary = request.env['ir.qweb.field.monetary']
|
||||
currency = order.currency_id
|
||||
return {
|
||||
'success': True,
|
||||
'is_free_delivery': not bool(order.amount_delivery),
|
||||
'compute_price_after_delivery': order.carrier_id.invoice_policy == 'real',
|
||||
'amount_delivery': Monetary.value_to_html(
|
||||
order.amount_delivery, {'display_currency': currency}
|
||||
),
|
||||
'amount_untaxed': Monetary.value_to_html(
|
||||
order.amount_untaxed, {'display_currency': currency}
|
||||
),
|
||||
'amount_tax': Monetary.value_to_html(
|
||||
order.amount_tax, {'display_currency': currency}
|
||||
),
|
||||
'amount_total': Monetary.value_to_html(
|
||||
order.amount_total, {'display_currency': currency}
|
||||
),
|
||||
}
|
||||
|
||||
@route('/shop/get_delivery_rate', type='jsonrpc', auth='public', methods=['POST'], website=True)
|
||||
def shop_get_delivery_rate(self, dm_id):
|
||||
""" Return the delivery rate data for the given delivery method.
|
||||
|
||||
:param str dm_id: The delivery method whose rate to get, as a `delivery.carrier` id.
|
||||
:return: The delivery rate data.
|
||||
:rtype: dict
|
||||
"""
|
||||
if not (order_sudo := request.cart):
|
||||
raise ValidationError(_("Your cart is empty."))
|
||||
|
||||
if int(dm_id) not in order_sudo._get_delivery_methods().ids:
|
||||
raise UserError(_(
|
||||
"It seems that a delivery method is not compatible with your address. Please"
|
||||
" refresh the page and try again."
|
||||
))
|
||||
|
||||
Monetary = request.env['ir.qweb.field.monetary']
|
||||
delivery_method = request.env['delivery.carrier'].sudo().browse(int(dm_id)).exists()
|
||||
rate = Delivery._get_rate(delivery_method, order_sudo)
|
||||
if rate['success']:
|
||||
rate['amount_delivery'] = Monetary.value_to_html(
|
||||
rate['price'], {'display_currency': order_sudo.currency_id}
|
||||
)
|
||||
rate['is_free_delivery'] = not bool(rate['price'])
|
||||
rate['compute_price_after_delivery'] = delivery_method.invoice_policy == 'real'
|
||||
else:
|
||||
rate['amount_delivery'] = Monetary.value_to_html(
|
||||
0.0, {'display_currency': order_sudo.currency_id}
|
||||
)
|
||||
return rate
|
||||
|
||||
@route('/website_sale/set_pickup_location', type='jsonrpc', auth='public', website=True)
|
||||
def website_sale_set_pickup_location(self, pickup_location_data):
|
||||
""" Fetch the order from the request and set the pickup location on the current order.
|
||||
|
||||
:param str pickup_location_data: The JSON-formatted pickup location address.
|
||||
:return: None
|
||||
"""
|
||||
order_sudo = request.cart
|
||||
order_sudo._set_pickup_location(pickup_location_data)
|
||||
|
||||
@route('/website_sale/get_pickup_locations', type='jsonrpc', auth='public', website=True)
|
||||
def website_sale_get_pickup_locations(self, zip_code=None, **kwargs):
|
||||
""" Fetch the order from the request and return the pickup locations close to the zip code.
|
||||
|
||||
Determine the country based on GeoIP or fallback on the order's delivery address' country.
|
||||
|
||||
:param int zip_code: The zip code to look up to.
|
||||
:return: The close pickup locations data.
|
||||
:rtype: dict
|
||||
"""
|
||||
order_sudo = request.cart
|
||||
country = order_sudo.partner_shipping_id.country_id
|
||||
return order_sudo._get_pickup_locations(zip_code, country, **kwargs)
|
||||
|
||||
@route(_express_checkout_delivery_route, type='jsonrpc', auth='public', website=True)
|
||||
def express_checkout_process_delivery_address(self, partial_delivery_address):
|
||||
""" Process the shipping address and return the available delivery methods.
|
||||
|
||||
Depending on whether the partner is registered and logged in, a new partner is created or we
|
||||
use an existing partner that matches the partial delivery address received.
|
||||
|
||||
:param dict partial_delivery_address: The delivery information sent by the express payment
|
||||
provider.
|
||||
:return: The available delivery methods, sorted by lowest price.
|
||||
:rtype: dict
|
||||
"""
|
||||
if not (order_sudo := request.cart):
|
||||
return []
|
||||
|
||||
self._include_country_and_state_in_address(partial_delivery_address)
|
||||
partial_delivery_address, _side_values = self._parse_form_data(partial_delivery_address)
|
||||
if order_sudo._is_anonymous_cart():
|
||||
# The partner_shipping_id and partner_invoice_id will be automatically computed when
|
||||
# changing the partner_id of the SO. This allows website_sale to avoid creating
|
||||
# duplicates.
|
||||
partial_delivery_address['name'] = _(
|
||||
'Anonymous express checkout partner for order %s',
|
||||
order_sudo.name,
|
||||
)
|
||||
new_partner_sudo = self._create_new_address(
|
||||
address_values=partial_delivery_address,
|
||||
address_type='delivery',
|
||||
use_delivery_as_billing=False,
|
||||
order_sudo=order_sudo,
|
||||
)
|
||||
# Pricelists are recomputed every time the partner is changed. We don't want to
|
||||
# recompute the price with another pricelist at this state since the customer has
|
||||
# already accepted the amount and validated the payment.
|
||||
with request.env.protecting([order_sudo._fields['pricelist_id']], order_sudo):
|
||||
order_sudo.partner_id = new_partner_sudo
|
||||
elif order_sudo.name in order_sudo.partner_shipping_id.name:
|
||||
order_sudo.partner_shipping_id.write(partial_delivery_address)
|
||||
# TODO VFE TODO VCR do we want to trigger cart recomputation here ?
|
||||
# order_sudo._update_address(
|
||||
# order_sudo.partner_shipping_id.id, ['partner_shipping_id']
|
||||
# )
|
||||
elif not self._are_same_addresses(
|
||||
partial_delivery_address,
|
||||
order_sudo.partner_shipping_id,
|
||||
):
|
||||
# Check if a child partner doesn't already exist with the same information. The phone
|
||||
# isn't always checked because it isn't sent in delivery information with Google Pay.
|
||||
child_partner_id = self._find_child_partner(
|
||||
order_sudo.partner_id.commercial_partner_id.id, partial_delivery_address
|
||||
)
|
||||
partial_delivery_address['name'] = _(
|
||||
'Anonymous express checkout partner for order %s',
|
||||
order_sudo.name,
|
||||
)
|
||||
order_sudo.partner_shipping_id = child_partner_id or self._create_new_address(
|
||||
address_values=partial_delivery_address,
|
||||
address_type='delivery',
|
||||
use_delivery_as_billing=False,
|
||||
order_sudo=order_sudo,
|
||||
)
|
||||
|
||||
sorted_delivery_methods = sorted([{
|
||||
'id': dm.id,
|
||||
'name': dm.name,
|
||||
'description': dm.website_description,
|
||||
'minorAmount': payment_utils.to_minor_currency_units(price, order_sudo.currency_id),
|
||||
} for dm, price in self._get_delivery_methods_express_checkout(order_sudo).items()
|
||||
], key=lambda dm: dm['minorAmount'])
|
||||
|
||||
# Preselect the cheapest method imitating the behavior of the express checkout form.
|
||||
if (
|
||||
sorted_delivery_methods
|
||||
and order_sudo.carrier_id.id != sorted_delivery_methods[0]['id']
|
||||
and (cheapest_dm := next((
|
||||
dm for dm in order_sudo._get_delivery_methods()
|
||||
if dm.id == sorted_delivery_methods[0]['id']), None
|
||||
))
|
||||
):
|
||||
order_sudo._set_delivery_method(cheapest_dm)
|
||||
|
||||
# Return the list of delivery methods available for the sales order.
|
||||
return {'delivery_methods': sorted_delivery_methods}
|
||||
|
||||
@classmethod
|
||||
def _get_delivery_methods_express_checkout(cls, order_sudo):
|
||||
""" Return available delivery methods and their prices for the given order.
|
||||
|
||||
:param sale.order order_sudo: The sudoed sales order.
|
||||
:rtype: dict
|
||||
:return: A dict with a `delivery.carrier` recordset as key, and a rate shipment price as
|
||||
value.
|
||||
"""
|
||||
res = {}
|
||||
for dm in order_sudo._get_delivery_methods():
|
||||
rate = Delivery._get_rate(dm, order_sudo, is_express_checkout_flow=True)
|
||||
if rate['success']:
|
||||
fname = f'{dm.delivery_type}_use_locations'
|
||||
if hasattr(dm, fname) and getattr(dm, fname):
|
||||
continue # Express checkout doesn't allow selecting locations.
|
||||
res[dm] = rate['price']
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def _get_rate(delivery_method, order, is_express_checkout_flow=False):
|
||||
""" Compute the delivery rate and apply the taxes if relevant.
|
||||
|
||||
:param delivery.carrier delivery_method: The delivery method for which the rate must be
|
||||
computed.
|
||||
:param sale.order order: The current sales order.
|
||||
:param boolean is_express_checkout_flow: Whether the flow is express checkout.
|
||||
:return: The delivery rate data.
|
||||
:rtype: dict
|
||||
"""
|
||||
# Some delivery methods check if all the required fields are available before computing the
|
||||
# rate, even if those fields aren't required for the computation (although they are for
|
||||
# delivering the goods). If we only have partial information about the delivery address, but
|
||||
# still want to compute the rate, this context key will ensure that we only check the
|
||||
# required fields for a partial delivery address (city, zip, country_code, state_code).
|
||||
rate = delivery_method.rate_shipment(order.with_context(
|
||||
express_checkout_partial_delivery_address=is_express_checkout_flow
|
||||
))
|
||||
if rate.get('success'):
|
||||
tax_ids = delivery_method.product_id.taxes_id.filtered(
|
||||
lambda t: t.company_id == order.company_id
|
||||
)
|
||||
if tax_ids:
|
||||
fpos = order.fiscal_position_id
|
||||
tax_ids = fpos.map_tax(tax_ids)
|
||||
taxes = tax_ids.compute_all(
|
||||
rate['price'],
|
||||
currency=order.currency_id,
|
||||
quantity=1.0,
|
||||
product=delivery_method.product_id,
|
||||
partner=order.partner_shipping_id,
|
||||
)
|
||||
if (
|
||||
not is_express_checkout_flow
|
||||
and request.website.show_line_subtotals_tax_selection == 'tax_excluded'
|
||||
):
|
||||
rate['price'] = taxes['total_excluded']
|
||||
else:
|
||||
rate['price'] = taxes['total_included']
|
||||
return rate
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,86 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from psycopg2.errors import LockNotAvailable
|
||||
|
||||
from odoo import _
|
||||
from odoo.exceptions import AccessError, MissingError, UserError, ValidationError
|
||||
from odoo.fields import Command
|
||||
from odoo.http import request, route
|
||||
from odoo.tools import SQL
|
||||
|
||||
from odoo.addons.payment.controllers import portal as payment_portal
|
||||
|
||||
|
||||
# TODO ANVFE part of payment routes ? /shop/payment ? express_checkout ?
|
||||
|
||||
class PaymentPortal(payment_portal.PaymentPortal):
|
||||
|
||||
def _validate_transaction_for_order(self, transaction, sale_order):
|
||||
"""
|
||||
Perform final checks against the transaction & sale_order.
|
||||
Override me to apply payment unrelated checks & processing
|
||||
"""
|
||||
return
|
||||
|
||||
@route('/shop/payment/transaction/<int:order_id>', type='jsonrpc', auth='public', website=True)
|
||||
def shop_payment_transaction(self, order_id, access_token, **kwargs):
|
||||
""" Create a draft transaction and return its processing values.
|
||||
|
||||
:param int order_id: The sales order to pay, as a `sale.order` id
|
||||
:param str access_token: The access token used to authenticate the request
|
||||
:param dict kwargs: Locally unused data passed to `_create_transaction`
|
||||
:return: The mandatory values for the processing of the transaction
|
||||
:rtype: dict
|
||||
:raise: UserError if the order has already been paid or has an ongoing transaction
|
||||
:raise: ValidationError if the access token is invalid or the order is not in the expected
|
||||
state/configuration.
|
||||
"""
|
||||
# Check the order id and the access token
|
||||
# Then lock it during the transaction to prevent concurrent payments
|
||||
try:
|
||||
order_sudo = self._document_check_access('sale.order', order_id, access_token)
|
||||
request.env.cr.execute(
|
||||
SQL('SELECT 1 FROM sale_order WHERE id = %s FOR NO KEY UPDATE NOWAIT', order_id)
|
||||
)
|
||||
except MissingError:
|
||||
raise
|
||||
except AccessError as e:
|
||||
raise ValidationError(_("The access token is invalid.")) from e
|
||||
except LockNotAvailable:
|
||||
raise UserError(_("Payment is already being processed."))
|
||||
|
||||
if order_sudo.state == "cancel":
|
||||
raise ValidationError(_("The order has been cancelled."))
|
||||
|
||||
order_sudo._check_cart_is_ready_to_be_paid()
|
||||
|
||||
self._validate_transaction_kwargs(kwargs)
|
||||
kwargs.update({
|
||||
'partner_id': order_sudo.partner_invoice_id.id,
|
||||
'currency_id': order_sudo.currency_id.id,
|
||||
'sale_order_id': order_id, # Include the SO to allow Subscriptions to tokenize the tx
|
||||
})
|
||||
if not kwargs.get('amount'):
|
||||
kwargs['amount'] = order_sudo.amount_total
|
||||
|
||||
compare_amounts = order_sudo.currency_id.compare_amounts
|
||||
if compare_amounts(kwargs['amount'], order_sudo.amount_total):
|
||||
raise ValidationError(_("The cart has been updated. Please refresh the page."))
|
||||
if compare_amounts(order_sudo.amount_paid, order_sudo.amount_total) == 0:
|
||||
raise UserError(_("The cart has already been paid. Please refresh the page."))
|
||||
|
||||
if delay_token_charge := kwargs.get('flow') == 'token':
|
||||
request.update_context(delay_token_charge=True) # wait until after tx validation
|
||||
tx_sudo = self._create_transaction(
|
||||
custom_create_values={'sale_order_ids': [Command.set([order_id])]}, **kwargs,
|
||||
)
|
||||
|
||||
# Store the new transaction into the transaction list and if there's an old one, we remove
|
||||
# it until the day the ecommerce supports multiple orders at the same time.
|
||||
request.session['__website_sale_last_tx_id'] = tx_sudo.id
|
||||
|
||||
self._validate_transaction_for_order(tx_sudo, order_sudo)
|
||||
if delay_token_charge:
|
||||
tx_sudo._charge_with_token()
|
||||
|
||||
return tx_sudo._get_processing_values()
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.http import request, route
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
from odoo.addons.sale.controllers.product_configurator import SaleProductConfiguratorController
|
||||
from odoo.addons.website_sale.controllers.main import WebsiteSale
|
||||
|
||||
|
||||
class WebsiteSaleProductConfiguratorController(SaleProductConfiguratorController, WebsiteSale):
|
||||
|
||||
@route(
|
||||
route='/website_sale/should_show_product_configurator',
|
||||
type='jsonrpc',
|
||||
auth='public',
|
||||
website=True,
|
||||
readonly=True,
|
||||
)
|
||||
def website_sale_should_show_product_configurator(
|
||||
self, product_template_id, ptav_ids, is_product_configured
|
||||
):
|
||||
""" Return whether the product configurator dialog should be shown.
|
||||
|
||||
:param int product_template_id: The product being checked, as a `product.template` id.
|
||||
:param list(int) ptav_ids: The combination of the product, as a list of
|
||||
`product.template.attribute.value` ids.
|
||||
:param bool is_product_configured: Whether the product is already configured.
|
||||
:rtype: bool
|
||||
:return: Whether the product configurator dialog should be shown.
|
||||
"""
|
||||
product_template = request.env['product.template'].browse(product_template_id)
|
||||
combination = request.env['product.template.attribute.value'].browse(ptav_ids)
|
||||
single_product_variant = product_template.get_single_product_variant()
|
||||
# We can't use `single_product_variant.get('has_optional_products')` as it doesn't take
|
||||
# `combination` into account.
|
||||
has_optional_products = bool(product_template.optional_product_ids.filtered(
|
||||
lambda op: self._should_show_product(op, combination)
|
||||
))
|
||||
return (
|
||||
has_optional_products
|
||||
or not (single_product_variant.get('product_id') or is_product_configured)
|
||||
)
|
||||
|
||||
def _get_product_template(self, product_template_id):
|
||||
if request.is_frontend:
|
||||
combo_item = request.env['product.combo.item'].sudo().search([
|
||||
('product_id.product_tmpl_id.id', '=', product_template_id),
|
||||
])
|
||||
if combo_item and request.env['product.template'].sudo().search_count([
|
||||
('combo_ids', 'in', combo_item.mapped('combo_id.id')),
|
||||
('website_published', '=', True),
|
||||
]):
|
||||
return request.env['product.template'].sudo().browse(product_template_id)
|
||||
return super()._get_product_template(product_template_id)
|
||||
|
||||
@route(
|
||||
route='/website_sale/product_configurator/get_values',
|
||||
type='jsonrpc',
|
||||
auth='public',
|
||||
website=True,
|
||||
readonly=True,
|
||||
)
|
||||
def website_sale_product_configurator_get_values(self, *args, **kwargs):
|
||||
self._populate_currency_and_pricelist(kwargs)
|
||||
return super().sale_product_configurator_get_values(*args, **kwargs)
|
||||
|
||||
@route(
|
||||
route='/website_sale/product_configurator/create_product',
|
||||
type='jsonrpc',
|
||||
auth='public',
|
||||
methods=['POST'],
|
||||
website=True,
|
||||
)
|
||||
def website_sale_product_configurator_create_product(self, *args, **kwargs):
|
||||
return super().sale_product_configurator_create_product(*args, **kwargs)
|
||||
|
||||
@route(
|
||||
route='/website_sale/product_configurator/update_combination',
|
||||
type='jsonrpc',
|
||||
auth='public',
|
||||
methods=['POST'],
|
||||
website=True,
|
||||
readonly=True,
|
||||
)
|
||||
def website_sale_product_configurator_update_combination(self, *args, **kwargs):
|
||||
self._populate_currency_and_pricelist(kwargs)
|
||||
return super().sale_product_configurator_update_combination(*args, **kwargs)
|
||||
|
||||
@route(
|
||||
route='/website_sale/product_configurator/get_optional_products',
|
||||
type='jsonrpc',
|
||||
auth='public',
|
||||
website=True,
|
||||
readonly=True,
|
||||
)
|
||||
def website_sale_product_configurator_get_optional_products(self, *args, **kwargs):
|
||||
self._populate_currency_and_pricelist(kwargs)
|
||||
return super().sale_product_configurator_get_optional_products(*args, **kwargs)
|
||||
|
||||
def _get_basic_product_information(
|
||||
self, product_or_template, pricelist, combination, currency=None, date=None, **kwargs
|
||||
):
|
||||
""" Override of `sale` to append website data and apply taxes.
|
||||
|
||||
:param product.product|product.template product_or_template: The product for which to seek
|
||||
information.
|
||||
:param product.pricelist pricelist: The pricelist to use.
|
||||
:param product.template.attribute.value combination: The combination of the product.
|
||||
:param res.currency|None currency: The currency of the transaction.
|
||||
:param datetime|None date: The date of the `sale.order`, to compute the price at the right
|
||||
rate.
|
||||
:param dict kwargs: Locally unused data passed to `super`.
|
||||
:rtype: dict
|
||||
:return: A dict with the following structure:
|
||||
{
|
||||
... # fields from `super`.
|
||||
'price': float,
|
||||
'can_be_sold': bool,
|
||||
'category_name': str,
|
||||
'currency_name': str,
|
||||
'strikethrough_price': float, # if there's a strikethrough_price to display.
|
||||
}
|
||||
"""
|
||||
basic_product_information = super()._get_basic_product_information(
|
||||
product_or_template.with_context(display_default_code=not request.is_frontend),
|
||||
pricelist,
|
||||
combination,
|
||||
currency=currency,
|
||||
date=date,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if request.is_frontend:
|
||||
has_zero_price = float_is_zero(
|
||||
basic_product_information['price'], precision_rounding=currency.rounding
|
||||
)
|
||||
basic_product_information['can_be_sold'] = not (
|
||||
request.website.prevent_zero_price_sale and has_zero_price
|
||||
)
|
||||
# Don't compute the strikethrough price if there's a custom price (i.e. if `price_info`
|
||||
# is populated).
|
||||
strikethrough_price = self._get_strikethrough_price(
|
||||
product_or_template.with_context(
|
||||
**product_or_template._get_product_price_context(combination)
|
||||
),
|
||||
currency,
|
||||
date,
|
||||
basic_product_information['price'],
|
||||
basic_product_information['pricelist_rule_id'],
|
||||
) if 'price_info' not in basic_product_information else None
|
||||
if strikethrough_price:
|
||||
basic_product_information['strikethrough_price'] = strikethrough_price
|
||||
return basic_product_information
|
||||
|
||||
def _get_ptav_price_extra(self, ptav, currency, date, product_or_template):
|
||||
""" Override of `sale` to apply taxes.
|
||||
|
||||
:param product.template.attribute.value ptav: The product template attribute value for which
|
||||
to compute the extra price.
|
||||
:param res.currency currency: The currency to compute the extra price in.
|
||||
:param datetime date: The date to compute the extra price at.
|
||||
:param product.product|product.template product_or_template: The product on which the
|
||||
product template attribute value applies.
|
||||
:rtype: float
|
||||
:return: The extra price for the product template attribute value.
|
||||
"""
|
||||
price_extra = super()._get_ptav_price_extra(ptav, currency, date, product_or_template)
|
||||
if request.is_frontend:
|
||||
return self._apply_taxes_to_price(price_extra, product_or_template, currency)
|
||||
return price_extra
|
||||
|
||||
def _get_strikethrough_price(self, product_or_template, currency, date, price, pricelist_rule_id=None):
|
||||
""" Return the strikethrough price of the product, if there is one.
|
||||
|
||||
:param product.product|product.template product_or_template: The product for which to
|
||||
compute the strikethrough price.
|
||||
:param res.currency currency: The currency to compute the strikethrough price in.
|
||||
:param datetime date: The date to compute the strikethrough price at.
|
||||
:param float price: The actual price of the product.
|
||||
:rtype: float|None
|
||||
:return: The strikethrough price of the product, if there is one.
|
||||
"""
|
||||
pricelist_rule = request.env['product.pricelist.item'].browse(pricelist_rule_id)
|
||||
|
||||
# First, try to use the base price as the strikethrough price.
|
||||
# Apply taxes before comparing it to the actual price.
|
||||
if pricelist_rule._show_discount_on_shop():
|
||||
pricelist_base_price = self._apply_taxes_to_price(
|
||||
pricelist_rule._compute_price_before_discount(
|
||||
product=product_or_template,
|
||||
quantity=1.0,
|
||||
uom=product_or_template.uom_id,
|
||||
date=date,
|
||||
currency=currency,
|
||||
),
|
||||
product_or_template,
|
||||
currency,
|
||||
)
|
||||
# Only show the base price if it's greater than the actual price.
|
||||
if currency.compare_amounts(pricelist_base_price, price) == 1:
|
||||
return pricelist_base_price
|
||||
|
||||
# Second, try to use `compare_list_price` as the strikethrough price.
|
||||
# Don't apply taxes since this price should always be displayed as is.
|
||||
if (
|
||||
request.env['res.groups']._is_feature_enabled('website_sale.group_product_price_comparison')
|
||||
and product_or_template.compare_list_price
|
||||
):
|
||||
compare_list_price = product_or_template.currency_id._convert(
|
||||
from_amount=product_or_template.compare_list_price,
|
||||
to_currency=currency,
|
||||
company=request.env.company,
|
||||
date=date,
|
||||
round=False,
|
||||
)
|
||||
# Only show `compare_list_price` if it's greater than the actual price.
|
||||
if currency.compare_amounts(compare_list_price, price) == 1:
|
||||
return compare_list_price
|
||||
return None
|
||||
|
||||
def _should_show_product(self, product_template, parent_combination):
|
||||
""" Override of `sale` to only show products that can be added to the cart.
|
||||
|
||||
:param product.template product_template: The product being checked.
|
||||
:param product.template.attribute.value parent_combination: The combination of the parent
|
||||
product.
|
||||
:rtype: bool
|
||||
:return: Whether the product should be shown in the configurator.
|
||||
"""
|
||||
should_show_product = super()._should_show_product(product_template, parent_combination)
|
||||
if request.is_frontend:
|
||||
return (
|
||||
should_show_product
|
||||
and product_template._is_add_to_cart_possible(parent_combination)
|
||||
and product_template.filtered_domain(request.website.website_domain())
|
||||
)
|
||||
return should_show_product
|
||||
|
||||
@staticmethod
|
||||
def _apply_taxes_to_price(price, product_or_template, currency):
|
||||
product_taxes = product_or_template.sudo().taxes_id._filter_taxes_by_company(
|
||||
request.env.company
|
||||
)
|
||||
if product_taxes:
|
||||
taxes = request.fiscal_position.map_tax(product_taxes)
|
||||
return request.env['product.template']._apply_taxes_to_price(
|
||||
price, currency, product_taxes, taxes, product_or_template, website=request.website
|
||||
)
|
||||
return price
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from werkzeug.exceptions import BadRequest, Forbidden, NotFound
|
||||
|
||||
from odoo.http import Controller, request, route
|
||||
from odoo.tools import consteq
|
||||
|
||||
|
||||
class ProductFeed(Controller):
|
||||
|
||||
@route(
|
||||
'/gmc.xml',
|
||||
type='http',
|
||||
auth='public',
|
||||
website=True,
|
||||
sitemap=False,
|
||||
)
|
||||
def gmc_feed(self, feed_id='', access_token=''):
|
||||
"""Serve a dynamic XML feed to synchronize the eCommerce products with Google Merchant
|
||||
Center (GMC).
|
||||
|
||||
This method generates an XML feed containing information about eCommerce products.
|
||||
The feed is configured via the `product.feed` model, allowing customization such as:
|
||||
- Localization by specifying a language or pricelist (currency).
|
||||
- Filtering products by category or categories.
|
||||
|
||||
Notes:
|
||||
- The feed is only accessible through a valid `access_token`.
|
||||
- A feed will contain at most 6000 products. If there are more than 6000 products,
|
||||
only the first 6000 will be included in the feed. This is a technical limit, but a soft
|
||||
limit of 5000 products is also enforced on the `product.feed` record.
|
||||
|
||||
See also https://support.google.com/merchants/answer/7052112 for the XML format.
|
||||
|
||||
:return: The XML feed compressed using GZIP.
|
||||
:rtype: bytes
|
||||
"""
|
||||
if not request.website.enabled_gmc_src:
|
||||
raise NotFound()
|
||||
|
||||
feed_sudo = self._find_and_check_feed_access(feed_id, access_token)
|
||||
|
||||
if feed_sudo.website_id != request.website:
|
||||
raise BadRequest("Website does not match.")
|
||||
|
||||
compressed_gmc_xml = feed_sudo._render_and_cache_compressed_gmc_feed()
|
||||
|
||||
return request.make_response(compressed_gmc_xml, [
|
||||
('Content-Type', 'application/xml; charset=utf-8'),
|
||||
('Content-Encoding', 'gzip'),
|
||||
])
|
||||
|
||||
def _find_and_check_feed_access(self, feed_id, access_token):
|
||||
"""Find the feed by its ID and validate its access token.
|
||||
|
||||
:param str feed_id: The ID of the feed to validate.
|
||||
:param str access_token: The access token associated with the feed.
|
||||
:raises BadRequest: If the feed ID cannot be converted to an integer.
|
||||
:raises NotFound: If the feed ID does not match any existing feed.
|
||||
:raises Forbidden: If the provided access token does not match the feed's access token.
|
||||
:return: The feed record if access is successfully validated, in sudo mode.
|
||||
:rtype: product.feed
|
||||
"""
|
||||
try:
|
||||
feed_id = int(feed_id)
|
||||
except ValueError:
|
||||
raise BadRequest()
|
||||
feed_sudo = request.env['product.feed'].sudo().browse(feed_id).exists()
|
||||
if not feed_sudo:
|
||||
raise NotFound()
|
||||
|
||||
if not consteq(feed_sudo.access_token, access_token):
|
||||
raise Forbidden()
|
||||
|
||||
return feed_sudo
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.exceptions import AccessError, MissingError, ValidationError
|
||||
from odoo.http import request, route
|
||||
|
||||
from odoo.addons.sale.controllers import portal as sale_portal
|
||||
from odoo.addons.website_sale.controllers.cart import Cart
|
||||
|
||||
|
||||
class CustomerPortal(sale_portal.CustomerPortal):
|
||||
|
||||
@route(
|
||||
'/my/orders/reorder',
|
||||
type='jsonrpc',
|
||||
auth='public',
|
||||
website=True,
|
||||
)
|
||||
def my_orders_reorder(self, order_id, access_token=None):
|
||||
""" Retrieve reorder content and automatically add products to the cart.
|
||||
|
||||
param int order_id: The ID of the sale order to reorder.
|
||||
param str access_token: The access token for the sale order.
|
||||
return: Details of the added products.
|
||||
rtype: dict
|
||||
"""
|
||||
try:
|
||||
sale_order = self._document_check_access('sale.order', order_id, access_token=access_token)
|
||||
except (AccessError, MissingError):
|
||||
return request.redirect('/my')
|
||||
|
||||
lines_to_reorder = sale_order.order_line.filtered(
|
||||
# Skip section headers, deliveries, event tickets, ...
|
||||
lambda line: line.with_user(request.env.user).sudo()._is_reorder_allowed()
|
||||
)
|
||||
|
||||
if not lines_to_reorder:
|
||||
raise ValidationError(request.env._("Nothing can be reordered in this order"))
|
||||
|
||||
Cart_controller = Cart()
|
||||
order_sudo = request.cart or request.website._create_cart()
|
||||
warnings_to_aggregate = set()
|
||||
values = {
|
||||
'tracking_info': [],
|
||||
}
|
||||
for line in lines_to_reorder:
|
||||
|
||||
linked_products = []
|
||||
if line.product_id.type == 'combo':
|
||||
for linked_line in line.linked_line_ids.filtered('combo_item_id'):
|
||||
combination = (
|
||||
linked_line.product_id.product_template_attribute_value_ids
|
||||
| linked_line.product_no_variant_attribute_value_ids
|
||||
)
|
||||
linked_products.append({
|
||||
'product_template_id': linked_line.product_id.product_tmpl_id.id,
|
||||
'product_id': linked_line.product_id.id,
|
||||
'combination': combination.ids,
|
||||
'no_variant_attribute_value_ids': linked_line.product_no_variant_attribute_value_ids.ids,
|
||||
'product_custom_attribute_values': [{
|
||||
'custom_product_template_attribute_value_id': pcav.custom_product_template_attribute_value_id.id,
|
||||
'custom_value': pcav.custom_value,
|
||||
} for pcav in linked_line.product_custom_attribute_value_ids],
|
||||
'quantity': linked_line.product_uom_qty,
|
||||
'combo_item_id': linked_line.combo_item_id.id,
|
||||
'parent_product_template_id': line.product_id.product_tmpl_id.id,
|
||||
})
|
||||
|
||||
cart_values = Cart_controller.add_to_cart(
|
||||
product_id=line.product_id.id,
|
||||
product_template_id=line.product_id.product_tmpl_id.id,
|
||||
quantity=line.product_uom_qty,
|
||||
product_custom_attribute_values=[{
|
||||
'custom_product_template_attribute_value_id': pcav.custom_product_template_attribute_value_id.id,
|
||||
'custom_value': pcav.custom_value,
|
||||
} for pcav in line.product_custom_attribute_value_ids],
|
||||
no_variant_attribute_value_ids=line.product_no_variant_attribute_value_ids.ids,
|
||||
linked_products=linked_products,
|
||||
)
|
||||
if not cart_values['quantity']:
|
||||
# Only aggregate order warnings
|
||||
warnings_to_aggregate.add(order_sudo.shop_warning)
|
||||
|
||||
values['tracking_info'].extend(cart_values['tracking_info'])
|
||||
|
||||
if warnings_to_aggregate:
|
||||
order_sudo.shop_warning = '\n'.join(warnings_to_aggregate)
|
||||
|
||||
values['cart_quantity'] = order_sudo.cart_quantity
|
||||
return values
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.sale.controllers import portal as sale_portal
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class CustomerPortal(sale_portal.CustomerPortal):
|
||||
|
||||
def _get_payment_values(self, order_sudo, website_id=None, **kwargs):
|
||||
""" Override of `sale` to inject the `website_id` into the kwargs.
|
||||
|
||||
:param sale.order order_sudo: The sales order being paid.
|
||||
:param int website_id: The website on which the order was made, if any, as a `website` id.
|
||||
:param dict kwargs: Locally unused keywords arguments.
|
||||
:return: The payment-specific values.
|
||||
:rtype: dict
|
||||
"""
|
||||
if not website_id:
|
||||
if order_sudo.website_id:
|
||||
website_id = order_sudo.website_id.id
|
||||
elif request.website:
|
||||
website_id = request.website.id
|
||||
|
||||
return super()._get_payment_values(order_sudo, website_id=website_id, **kwargs)
|
||||
|
|
@ -1,33 +1,72 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details
|
||||
|
||||
from odoo.addons.sale.controllers.variant import VariantController
|
||||
from odoo.http import Controller, request, route
|
||||
|
||||
|
||||
class WebsiteSaleVariantController(VariantController):
|
||||
@http.route(['/sale/get_combination_info_website'], type='json', auth="public", methods=['POST'], website=True)
|
||||
def get_combination_info_website(self, product_template_id, product_id, combination, add_qty, **kw):
|
||||
"""Special route to use website logic in get_combination_info override.
|
||||
This route is called in JS by appending _website to the base route.
|
||||
"""
|
||||
kw.pop('pricelist_id')
|
||||
combination = self.get_combination_info(product_template_id, product_id, combination, add_qty, request.website.get_current_pricelist(), **kw)
|
||||
class WebsiteSaleVariantController(Controller):
|
||||
|
||||
if request.website.google_analytics_key:
|
||||
combination['product_tracking_info'] = request.env['product.template'].get_google_analytics_data(combination)
|
||||
@route(
|
||||
'/website_sale/get_combination_info',
|
||||
type='jsonrpc',
|
||||
auth='public',
|
||||
methods=['POST'],
|
||||
website=True,
|
||||
readonly=True,
|
||||
)
|
||||
def get_combination_info_website(
|
||||
self, product_template_id, product_id, combination, add_qty, uom_id=None, **kwargs
|
||||
):
|
||||
product_template_id = product_template_id and int(product_template_id)
|
||||
product_id = product_id and int(product_id)
|
||||
add_qty = (add_qty and float(add_qty)) or 1.0
|
||||
|
||||
product_template = request.env['product.template'].browse(product_template_id)
|
||||
|
||||
combination_info = product_template._get_combination_info(
|
||||
combination=request.env['product.template.attribute.value'].browse(combination),
|
||||
product_id=product_id,
|
||||
add_qty=add_qty,
|
||||
uom_id=uom_id,
|
||||
)
|
||||
combination_info['currency_precision'] = combination_info['currency'].decimal_places
|
||||
|
||||
for key in (
|
||||
# Only provided to ease server-side computations.
|
||||
'product_taxes', 'taxes', 'currency', 'date', 'combination',
|
||||
# Only used in Google Merchant Center logic, not client-side.
|
||||
'discount_start_date', 'discount_end_date'
|
||||
):
|
||||
combination_info.pop(key)
|
||||
|
||||
product = request.env['product.product'].browse(combination_info['product_id'])
|
||||
if product and product.id == product_id:
|
||||
combination_info['no_product_change'] = True
|
||||
return combination_info
|
||||
|
||||
if request.website.product_page_image_width != 'none' and not request.env.context.get('website_sale_no_images', False):
|
||||
carousel_view = request.env['ir.ui.view']._render_template('website_sale.shop_product_images', values={
|
||||
'product': request.env['product.template'].browse(combination['product_template_id']),
|
||||
'product_variant': request.env['product.product'].browse(combination['product_id']),
|
||||
'website': request.env['website'].get_current_website(),
|
||||
})
|
||||
combination['carousel'] = carousel_view
|
||||
return combination
|
||||
product_or_template = product or product_template
|
||||
combination_info['display_image'] = bool(product_or_template.image_128)
|
||||
combination_info['carousel'] = request.env['ir.ui.view']._render_template(
|
||||
'website_sale.shop_product_images',
|
||||
values={
|
||||
'product': product_template,
|
||||
'product_variant': product,
|
||||
'website': request.website,
|
||||
},
|
||||
)
|
||||
|
||||
@http.route(auth="public")
|
||||
if request.website.is_view_active('website_sale.product_tags'):
|
||||
all_tags = product.all_product_tag_ids if product else product_template.product_tag_ids
|
||||
combination_info['product_tags'] = request.env['ir.ui.view']._render_template(
|
||||
'website_sale.product_tags', values={
|
||||
'all_product_tags': all_tags.filtered('visible_to_customers'),
|
||||
}
|
||||
)
|
||||
return combination_info
|
||||
|
||||
@route('/sale/create_product_variant', type='jsonrpc', auth='public', methods=['POST'])
|
||||
def create_product_variant(self, product_template_id, product_template_attribute_value_ids, **kwargs):
|
||||
"""Override because on the website the public user must access it."""
|
||||
return super(WebsiteSaleVariantController, self).create_product_variant(product_template_id, product_template_attribute_value_ids, **kwargs)
|
||||
"""Old product configurator logic, only used by frontend configurator, will be deprecated soon"""
|
||||
return request.env['product.template'].browse(
|
||||
int(product_template_id)
|
||||
).create_product_variant(product_template_attribute_value_ids)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.http import request, route
|
||||
|
||||
from odoo.addons.base.models.ir_qweb_fields import nl2br_enclose
|
||||
from odoo.addons.website.controllers import main
|
||||
from odoo.addons.website.controllers.form import WebsiteForm
|
||||
from odoo.addons.website_sale.models.website import (
|
||||
FISCAL_POSITION_SESSION_CACHE_KEY,
|
||||
PRICELIST_SESSION_CACHE_KEY,
|
||||
PRICELIST_SELECTED_SESSION_CACHE_KEY
|
||||
)
|
||||
|
||||
|
||||
class WebsiteSaleForm(WebsiteForm):
|
||||
|
||||
@route('/website/form/shop.sale.order', type='http', auth="public", methods=['POST'], website=True)
|
||||
def website_form_saleorder(self, **kwargs):
|
||||
model_record = request.env.ref('sale.model_sale_order').sudo()
|
||||
try:
|
||||
data = self.extract_data(model_record, kwargs)
|
||||
except ValidationError as e:
|
||||
return json.dumps({'error_fields': e.args[0]})
|
||||
|
||||
if not (order_sudo := request.cart):
|
||||
return json.dumps({'error': "No order found; please add a product to your cart."})
|
||||
|
||||
if data['record']:
|
||||
order_sudo.write(data['record'])
|
||||
|
||||
if data['custom']:
|
||||
order_sudo._message_log(
|
||||
body=nl2br_enclose(data['custom'], 'p'),
|
||||
message_type='comment',
|
||||
)
|
||||
|
||||
if data['attachments']:
|
||||
self.insert_attachment(model_record, order_sudo.id, data['attachments'])
|
||||
|
||||
return json.dumps({'id': order_sudo.id})
|
||||
|
||||
|
||||
class Website(main.Website):
|
||||
|
||||
def _login_redirect(self, uid, redirect=None):
|
||||
# If we are logging in, clear the current pricelist to be able to find
|
||||
# the pricelist that corresponds to the user afterwards.
|
||||
request.session.pop(PRICELIST_SESSION_CACHE_KEY, None)
|
||||
request.session.pop(FISCAL_POSITION_SESSION_CACHE_KEY, None)
|
||||
request.session.pop(PRICELIST_SELECTED_SESSION_CACHE_KEY, None)
|
||||
return super()._login_redirect(uid, redirect=redirect)
|
||||
|
||||
@route()
|
||||
def autocomplete(self, search_type=None, term=None, order=None, limit=5, max_nb_chars=999, options=None):
|
||||
options = options or {}
|
||||
if 'display_currency' not in options:
|
||||
options['display_currency'] = request.website.currency_id
|
||||
return super().autocomplete(search_type, term, order, limit, max_nb_chars, options)
|
||||
|
||||
@route()
|
||||
def theme_customize_data(self, is_view_data, enable=None, disable=None, reset_view_arch=False):
|
||||
super().theme_customize_data(is_view_data, enable, disable, reset_view_arch)
|
||||
if any(key in enable or key in disable for key in ['website_sale.products_list_view', 'website_sale.add_grid_or_list_option']):
|
||||
request.session.pop('website_sale_shop_layout_mode', None)
|
||||
|
||||
@route()
|
||||
def get_current_currency(self, **kwargs):
|
||||
return {
|
||||
'id': request.website.currency_id.id,
|
||||
'symbol': request.website.currency_id.symbol,
|
||||
'position': request.website.currency_id.position,
|
||||
}
|
||||
|
||||
@route()
|
||||
def change_lang(self, lang, **kwargs):
|
||||
if cart := request.cart:
|
||||
request.env.add_to_compute(
|
||||
cart.order_line._fields['name'],
|
||||
cart.order_line.with_context(lang=lang),
|
||||
)
|
||||
return super().change_lang(lang, **kwargs)
|
||||
|
|
@ -18,37 +18,36 @@
|
|||
<field name="state">open</field>
|
||||
</record>
|
||||
|
||||
<record id="product_attribute_brand" model="product.attribute">
|
||||
<field name="name">Brand</field>
|
||||
<field name="sequence">0</field>
|
||||
</record>
|
||||
|
||||
<record id="website_sale.sale_ribbon" model="product.ribbon">
|
||||
<field name="html">Sale</field>
|
||||
<field name="html_class">o_ribbon_left</field>
|
||||
<field name="bg_color">rgb(40, 167, 69)</field>
|
||||
<field name="text_color">white</field>
|
||||
<field name="name">Sale</field>
|
||||
<field name="position">left</field>
|
||||
<field name="text_color">#FFFFFF</field>
|
||||
<field name="bg_color">#0CA725</field>
|
||||
<field name="sequence">3</field>
|
||||
</record>
|
||||
|
||||
<record id="website_sale.sold_out_ribbon" model="product.ribbon">
|
||||
<field name="html">Sold out</field>
|
||||
<field name="html_class">o_ribbon_left</field>
|
||||
<field name="bg_color">rgb(220, 53, 69)</field>
|
||||
<field name="text_color">white</field>
|
||||
<field name="name">Sold out</field>
|
||||
<field name="position">left</field>
|
||||
<field name="text_color">#FFFFFF</field>
|
||||
<field name="bg_color">#d9534f</field>
|
||||
<field name="sequence">1</field>
|
||||
</record>
|
||||
|
||||
<record id="website_sale.out_of_stock_ribbon" model="product.ribbon">
|
||||
<field name="html">Out of stock</field>
|
||||
<field name="html_class">o_ribbon_left</field>
|
||||
<field name="bg_color">rgb(255, 193, 7)</field>
|
||||
<field name="text_color">black</field>
|
||||
<field name="name">Out of stock</field>
|
||||
<field name="position">left</field>
|
||||
<field name="text_color">#FFFFFF</field>
|
||||
<field name="bg_color">#ffc107</field>
|
||||
<field name="sequence">2</field>
|
||||
</record>
|
||||
|
||||
<record id="website_sale.new_ribbon" model="product.ribbon">
|
||||
<field name="html">New!</field>
|
||||
<field name="html_class">o_ribbon_left</field>
|
||||
<field name="bg_color">rgb(0, 123, 255)</field>
|
||||
<field name="text_color">white</field>
|
||||
<field name="name">New!</field>
|
||||
<field name="position">left</field>
|
||||
<field name="text_color">#FFFFFF</field>
|
||||
<field name="bg_color">#0275d8</field>
|
||||
<field name="sequence">4</field>
|
||||
</record>
|
||||
|
||||
<record id="sales_team.salesteam_website_sales" model="crm.team">
|
||||
|
|
@ -57,11 +56,42 @@
|
|||
|
||||
<record model="website" id="website.default_website">
|
||||
<field name="salesteam_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="salesperson_id" ref="base.user_admin"/>
|
||||
</record>
|
||||
|
||||
<record model="product.pricelist" id="product.list0">
|
||||
<field name="selectable" eval="True" />
|
||||
<field name="website_id" eval="False"/>
|
||||
<record id="delivery.free_delivery_carrier" model="delivery.carrier" forcecreate="False">
|
||||
<field name="is_published" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- Generic steps used to generate new specific steps -->
|
||||
<record id="website_sale.checkout_step_cart" model="website.checkout.step">
|
||||
<field name="name">Order</field>
|
||||
<field name="sequence">0</field>
|
||||
<field name="step_href">/shop/cart</field>
|
||||
<field name="back_button_label">Back to cart</field>
|
||||
</record>
|
||||
|
||||
<record id="website_sale.checkout_step_delivery" model="website.checkout.step">
|
||||
<field name="name">Address</field>
|
||||
<field name="sequence">250</field>
|
||||
<field name="step_href">/shop/checkout</field>
|
||||
<field name="main_button_label">Checkout</field>
|
||||
<field name="back_button_label">Back to address</field>
|
||||
</record>
|
||||
|
||||
<record id="website_sale.checkout_step_extra" model="website.checkout.step">
|
||||
<field name="name">Extra Info</field>
|
||||
<field name="sequence">500</field>
|
||||
<field name="step_href">/shop/extra_info</field>
|
||||
<field name="main_button_label">Confirm</field>
|
||||
<field name="back_button_label">Back to extra info</field>
|
||||
</record>
|
||||
|
||||
<record id="website_sale.checkout_step_payment" model="website.checkout.step">
|
||||
<field name="name">Payment</field>
|
||||
<field name="sequence">999</field>
|
||||
<field name="step_href">/shop/payment</field>
|
||||
<field name="main_button_label">Confirm</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
|
@ -70,7 +100,7 @@
|
|||
<record id="dynamic_snippet_newest_products_filter" model="ir.filters">
|
||||
<field name="name">Newest Products</field>
|
||||
<field name="model_id">product.product</field>
|
||||
<field name="user_id" eval="False" />
|
||||
<field name="user_ids" eval="False" />
|
||||
<field name="domain">[('website_published', '=', True)]</field>
|
||||
<field name="context">{'display_default_code': False, 'add2cart_rerender': False}</field>
|
||||
<field name="sort">["create_date desc"]</field>
|
||||
|
|
@ -83,16 +113,16 @@
|
|||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
DynamicFilter = model.env['website.snippet.filter']
|
||||
response = DynamicFilter._get_products('latest_sold', model.env.context)
|
||||
response = DynamicFilter._get_products('latest_sold')
|
||||
</field>
|
||||
</record>
|
||||
<record id="dynamic_snippet_latest_viewed_products_action" model="ir.actions.server">
|
||||
<field name="name">Recently Viewed Products</field>
|
||||
<field name="name">Recently Viewed Products (per user)</field>
|
||||
<field name="model_id" ref="model_product_product"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
DynamicFilter = model.env['website.snippet.filter']
|
||||
res_products = DynamicFilter._get_products('latest_viewed', model.env.context)
|
||||
res_products = DynamicFilter._get_products('latest_viewed')
|
||||
for data in res_products:
|
||||
data['_latest_viewed'] = True
|
||||
response = res_products
|
||||
|
|
@ -104,8 +134,7 @@ response = res_products
|
|||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
DynamicFilter = model.env['website.snippet.filter']
|
||||
model.env.context['product_template_id'] = request.params.get('productTemplateId')
|
||||
response = DynamicFilter._get_products('accessories', model.env.context)
|
||||
response = DynamicFilter._get_products('accessories', product_template_id=request.params.get('productTemplateId'))
|
||||
</field>
|
||||
</record>
|
||||
<record id="dynamic_snippet_recently_sold_with_action" model="ir.actions.server">
|
||||
|
|
@ -114,8 +143,7 @@ response = DynamicFilter._get_products('accessories', model.env.context)
|
|||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
DynamicFilter = model.env['website.snippet.filter']
|
||||
model.env.context['product_template_id'] = request.params.get('productTemplateId')
|
||||
response = DynamicFilter._get_products('recently_sold_with', model.env.context)
|
||||
response = DynamicFilter._get_products('recently_sold_with', product_template_id=request.params.get('productTemplateId'))
|
||||
</field>
|
||||
</record>
|
||||
<record id="dynamic_snippet_alternative_products" model="ir.actions.server">
|
||||
|
|
@ -124,8 +152,16 @@ response = DynamicFilter._get_products('recently_sold_with', model.env.context)
|
|||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
DynamicFilter = model.env['website.snippet.filter']
|
||||
model.env.context['product_template_id'] = request.params.get('productTemplateId')
|
||||
response = DynamicFilter._get_products('alternative_products', model.env.context)
|
||||
response = DynamicFilter._get_products('alternative_products', product_template_id=request.params.get('productTemplateId'))
|
||||
</field>
|
||||
</record>
|
||||
<record id="dynamic_snippet_category_list" model="ir.actions.server">
|
||||
<field name="name">Category List</field>
|
||||
<field name="model_id" ref="model_product_public_category"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
DynamicFilter = model.env['website.snippet.filter']
|
||||
response = DynamicFilter._prepare_category_list_data(parent_id=request.params.get('parentId'))
|
||||
</field>
|
||||
</record>
|
||||
<!-- Dynamic Filter -->
|
||||
|
|
@ -145,7 +181,8 @@ response = DynamicFilter._get_products('alternative_products', model.env.context
|
|||
<field name="action_server_id" ref="website_sale.dynamic_snippet_latest_viewed_products_action"/>
|
||||
<field name="field_names">display_name,description_sale,image_512</field>
|
||||
<field name="limit" eval="16"/>
|
||||
<field name="name">Recently Viewed Products</field>
|
||||
<field name="name">Recently Viewed Products (per user)</field>
|
||||
<field name="help">The building block will remain empty until the user visits a product page.</field>
|
||||
</record>
|
||||
<record id="dynamic_filter_cross_selling_accessories" model="website.snippet.filter">
|
||||
<field name="action_server_id" ref="website_sale.dynamic_snippet_accessories_action"/>
|
||||
|
|
@ -168,6 +205,12 @@ response = DynamicFilter._get_products('alternative_products', model.env.context
|
|||
<field name="name">Alternative Products</field>
|
||||
<field name="product_cross_selling">True</field>
|
||||
</record>
|
||||
<record id="dynamic_filter_category_list" model="website.snippet.filter">
|
||||
<field name="action_server_id" ref="website_sale.dynamic_snippet_category_list"/>
|
||||
<field name="field_names">id,name,cover_image</field>
|
||||
<field name="name">Category List</field>
|
||||
<field name="limit" eval="10"/>
|
||||
</record>
|
||||
|
||||
<function model="ir.model.fields" name="formbuilder_whitelist">
|
||||
<value>sale.order</value>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,23 @@
|
|||
<field name="salesteam_id" ref="sales_team.salesteam_website_sales"/>
|
||||
</record>
|
||||
|
||||
<record id="product.product_attribute_2" model="product.attribute">
|
||||
<record id="product.pa_color" model="product.attribute">
|
||||
<field name="preview_variants">hover</field>
|
||||
</record>
|
||||
<record id="product.pa_legs" model="product.attribute">
|
||||
<field name="preview_variants">visible</field>
|
||||
</record>
|
||||
<record id="product.pa_options" model="product.attribute">
|
||||
<field name="visibility">hidden</field>
|
||||
</record>
|
||||
<record id="product.pa_size" model="product.attribute">
|
||||
<field name="preview_variants">visible</field>
|
||||
</record>
|
||||
<record id="product.pa_fabric" model="product.attribute">
|
||||
<field name="preview_variants">visible</field>
|
||||
<field name="is_thumbnail_visible" eval="True"/>
|
||||
</record>
|
||||
<record id="product.pa_shoe_size" model="product.attribute">
|
||||
<field name="visibility">hidden</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -57,36 +73,39 @@
|
|||
<record id="product.product_order_01" model="product.product">
|
||||
<field name="is_published" eval="True"/>
|
||||
</record>
|
||||
<record id="product.office_combo" model="product.product">
|
||||
<field name="is_published" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_4" model="product.product">
|
||||
<field name="is_published" eval="True"/>
|
||||
<field name="website_sequence">9950</field>
|
||||
<field name="website_description" type="html">
|
||||
<section class="s_text_image pt32 pb32 o_colored_level o_cc o_cc1" data-snippet="s_text_image" data-name="Text - Image">
|
||||
<section class="s_text_image pt80 pb80 o_colored_level o_cc o_cc1" data-snippet="s_text_image" data-name="Text - Image">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="pt16 pb16 col-lg-6">
|
||||
<h2>Ergonomic</h2>
|
||||
<h2 class="h3-fs">Ergonomic</h2>
|
||||
<p>Press a button and watch your desk glide effortlessly from sitting to standing height in seconds.</p>
|
||||
<p>The minimum height is 65 cm, and for standing work the maximum height position is 125 cm.</p>
|
||||
</div>
|
||||
<div class="pt16 pb16 col-lg-6">
|
||||
<img src="/website/static/src/img/snippets_demo/s_text_image.jpg" class="img img-fluid mx-auto" alt=""/>
|
||||
<img src="/website/static/src/img/snippets_demo/s_text_image.webp" class="img img-fluid mx-auto rounded" alt=""/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="s_text_image pt32 pb32 o_colored_level o_cc o_cc1" data-snippet="s_image_text" data-name="Image - Text">
|
||||
<section class="s_text_image pt80 pb80 o_colored_level o_cc o_cc1" data-snippet="s_image_text" data-name="Image - Text">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="pt16 pb16 col-lg-6">
|
||||
<img src="/website_sale/static/src/img/carpentry.jpg" class="img img-fluid mx-auto" alt=""/>
|
||||
<img src="/website_sale/static/src/img/carpentry.jpg" class="img img-fluid mx-auto rounded" alt=""/>
|
||||
</div>
|
||||
<div class="pt16 pb16 col-lg-6">
|
||||
<h2>Locally handmade</h2>
|
||||
<h2 class="h3-fs">Locally handmade</h2>
|
||||
<p>We pay special attention to detail, which is why our desks are of a superior quality.</p>
|
||||
<p>Looking for a custom bamboo stain to match existing furniture? Contact us for a quote.</p>
|
||||
<p><a href="/contactus" class="mb-2 btn btn-primary">Contact Us</a></p>
|
||||
<p><a href="/contactus" class="mb-2 btn btn-primary o_translate_inline">Contact Us</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -115,70 +134,285 @@
|
|||
<field name="accessory_product_ids" eval="[(6, 0, [ref('product.product_product_7')])]"/>
|
||||
</record>
|
||||
|
||||
<record id="item1" model="product.pricelist.item">
|
||||
<field name="base">list_price</field>
|
||||
<field name="applied_on">1_product</field>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="product_tmpl_id" ref="product.product_product_4_product_template"/>
|
||||
<field name="price_discount">20</field>
|
||||
<field name="min_quantity">2</field>
|
||||
<field name="compute_price">formula</field>
|
||||
</record>
|
||||
|
||||
<!-- product.public.category -->
|
||||
|
||||
<record id="public_category_desks" model="product.public.category">
|
||||
<field name="name">Desks</field>
|
||||
<field name="sequence">15</field>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/categories/desks.jpg"/>
|
||||
<field name="cover_image" type="base64" file="website_sale/static/src/img/categories/desks.jpg"/>
|
||||
</record>
|
||||
<record id="public_category_furnitures" model="product.public.category">
|
||||
<field name="name">Furnitures</field>
|
||||
<field name="sequence">17</field>
|
||||
<field name="sequence">22</field>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/categories/furnitures.jpg"/>
|
||||
<field name="cover_image" type="base64" file="website_sale/static/src/img/categories/furnitures.jpg"/>
|
||||
</record>
|
||||
<record id="public_category_boxes" model="product.public.category">
|
||||
<field name="name">Boxes</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="sequence">29</field>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/categories/boxes.jpg"/>
|
||||
<field name="cover_image" type="base64" file="website_sale/static/src/img/categories/boxes.jpg"/>
|
||||
</record>
|
||||
<record id="public_category_drawers" model="product.public.category">
|
||||
<field name="name">Drawers</field>
|
||||
<field name="sequence">21</field>
|
||||
<field name="sequence">35</field>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/categories/drawers.jpg"/>
|
||||
<field name="cover_image" type="base64" file="website_sale/static/src/img/categories/drawers.jpg"/>
|
||||
</record>
|
||||
<record id="public_category_cabinets" model="product.public.category">
|
||||
<field name="name">Cabinets</field>
|
||||
<field name="sequence">22</field>
|
||||
<field name="sequence">40</field>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/categories/cabinets.jpg"/>
|
||||
<field name="cover_image" type="base64" file="website_sale/static/src/img/categories/cabinets.jpg"/>
|
||||
</record>
|
||||
<record id="public_category_bins" model="product.public.category">
|
||||
<field name="name">Bins</field>
|
||||
<field name="sequence">23</field>
|
||||
<field name="sequence">45</field>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/categories/bins.jpg"/>
|
||||
<field name="cover_image" type="base64" file="website_sale/static/src/img/categories/bins.jpg"/>
|
||||
</record>
|
||||
<record id="public_category_lamps" model="product.public.category">
|
||||
<field name="name">Lamps</field>
|
||||
<field name="sequence">24</field>
|
||||
<field name="sequence">49</field>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/categories/lamps.jpg"/>
|
||||
<field name="cover_image" type="base64" file="website_sale/static/src/img/categories/lamps.jpg"/>
|
||||
</record>
|
||||
<record id="services" model="product.public.category">
|
||||
<record id="public_category_services" model="product.public.category">
|
||||
<field name="name">Services</field>
|
||||
<field name="sequence">25</field>
|
||||
<field name="sequence">55</field>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/warranty.jpg"/>
|
||||
<field name="cover_image" type="base64" file="website_sale/static/src/img/warranty.jpg"/>
|
||||
</record>
|
||||
<record id="public_category_multimedia" model="product.public.category">
|
||||
<field name="name">Multimedia</field>
|
||||
<field name="sequence">26</field>
|
||||
<field name="sequence">59</field>
|
||||
<field name="image_1920" type="base64" file="product/static/img/product_product_43-image.jpg"/>
|
||||
<field name="cover_image" type="base64" file="product/static/img/product_product_43-image.jpg"/>
|
||||
</record>
|
||||
|
||||
<!-- subcategories -->
|
||||
|
||||
<!-- subcategories for desks -->
|
||||
<record id="public_category_desks_components" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_desks')"/>
|
||||
<field name="name">Components</field>
|
||||
<field name="sequence">16</field>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/categories/desk_components.jpg"/>
|
||||
<field name="cover_image" type="base64" file="website_sale/static/src/img/categories/desk_components.jpg"/>
|
||||
</record>
|
||||
<record id="public_category_desks_office" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_desks')"/>
|
||||
<field name="name">Office Desks</field>
|
||||
<field name="sequence">17</field>
|
||||
</record>
|
||||
<record id="public_category_desks_gaming" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_desks')"/>
|
||||
<field name="name">Gaming Desks</field>
|
||||
<field name="sequence">18</field>
|
||||
</record>
|
||||
<record id="public_category_desks_glass" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_desks')"/>
|
||||
<field name="name">Glass Desks</field>
|
||||
<field name="sequence">19</field>
|
||||
</record>
|
||||
<record id="public_category_desks_standing" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_desks')"/>
|
||||
<field name="name">Standing Desks</field>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
<record id="public_category_desks_foldable" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_desks')"/>
|
||||
<field name="name">Foldable Desks</field>
|
||||
<field name="sequence">21</field>
|
||||
</record>
|
||||
|
||||
<!-- subcategories for furnitures -->
|
||||
<record id="public_category_furnitures_sofas" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_furnitures')"/>
|
||||
<field name="name">Sofas</field>
|
||||
<field name="sequence">23</field>
|
||||
</record>
|
||||
<record id="public_category_furnitures_chairs" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_furnitures')"/>
|
||||
<field name="name">Chairs</field>
|
||||
<field name="sequence">24</field>
|
||||
</record>
|
||||
<record id="public_category_furnitures_couches" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_furnitures')"/>
|
||||
<field name="name">Couches</field>
|
||||
<field name="sequence">25</field>
|
||||
</record>
|
||||
<record id="public_category_furnitures_recliners" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_furnitures')"/>
|
||||
<field name="name">Recliners</field>
|
||||
<field name="sequence">26</field>
|
||||
</record>
|
||||
<record id="public_category_furnitures_beds" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_furnitures')"/>
|
||||
<field name="name">Beds</field>
|
||||
<field name="sequence">27</field>
|
||||
</record>
|
||||
<record id="public_category_furnitures_wardrobes" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_furnitures')"/>
|
||||
<field name="name">Wardrobes</field>
|
||||
<field name="sequence">28</field>
|
||||
</record>
|
||||
|
||||
<!-- subcategories for boxes -->
|
||||
<record id="public_category_boxes_vintage" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_boxes')"/>
|
||||
<field name="name">Vintage Boxes</field>
|
||||
<field name="sequence">30</field>
|
||||
</record>
|
||||
<record id="public_category_boxes_rustic" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_boxes')"/>
|
||||
<field name="name">Rustic Boxes</field>
|
||||
<field name="sequence">31</field>
|
||||
</record>
|
||||
<record id="public_category_boxes_luxury" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_boxes')"/>
|
||||
<field name="name">Luxury Boxes</field>
|
||||
<field name="sequence">32</field>
|
||||
</record>
|
||||
<record id="public_category_boxes_stackable" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_boxes')"/>
|
||||
<field name="name">Stackable Boxes</field>
|
||||
<field name="sequence">33</field>
|
||||
</record>
|
||||
<record id="public_category_boxes_collapsible" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_boxes')"/>
|
||||
<field name="name">Collapsible Boxes</field>
|
||||
<field name="sequence">34</field>
|
||||
</record>
|
||||
|
||||
<!-- subcategories for drawers -->
|
||||
<record id="public_category_drawers_nightstand" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_drawers')"/>
|
||||
<field name="name">Nightstand Drawers</field>
|
||||
<field name="sequence">36</field>
|
||||
</record>
|
||||
<record id="public_category_drawers_underbed" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_drawers')"/>
|
||||
<field name="name">Under-bed Drawers</field>
|
||||
<field name="sequence">37</field>
|
||||
</record>
|
||||
<record id="public_category_drawers_file" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_drawers')"/>
|
||||
<field name="name">File Drawers</field>
|
||||
<field name="sequence">38</field>
|
||||
</record>
|
||||
<record id="public_category_drawers_kitchen" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_drawers')"/>
|
||||
<field name="name">Kitchen Drawer Units</field>
|
||||
<field name="sequence">39</field>
|
||||
</record>
|
||||
|
||||
<!-- subcategories for cabinets -->
|
||||
<record id="public_category_cabinets_kitchen" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_cabinets')"/>
|
||||
<field name="name">Kitchen Cabinets</field>
|
||||
<field name="sequence">41</field>
|
||||
</record>
|
||||
<record id="public_category_cabinets_bathroom" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_cabinets')"/>
|
||||
<field name="name">Bathroom Cabinets</field>
|
||||
<field name="sequence">42</field>
|
||||
</record>
|
||||
<record id="public_category_cabinets_storage" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_cabinets')"/>
|
||||
<field name="name">Storage Cabinets</field>
|
||||
<field name="sequence">43</field>
|
||||
</record>
|
||||
<record id="public_category_cabinets_medicine" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_cabinets')"/>
|
||||
<field name="name">Medicine Cabinets</field>
|
||||
<field name="sequence">44</field>
|
||||
</record>
|
||||
|
||||
<!-- subcategories for bins -->
|
||||
<record id="public_category_bins_laundry" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_bins')"/>
|
||||
<field name="name">Laundry Bins</field>
|
||||
<field name="sequence">46</field>
|
||||
</record>
|
||||
<record id="public_category_bins_toy" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_bins')"/>
|
||||
<field name="name">Toy Bins</field>
|
||||
<field name="sequence">47</field>
|
||||
</record>
|
||||
<record id="public_category_bins_storage" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_bins')"/>
|
||||
<field name="name">Food Storage Bins</field>
|
||||
<field name="sequence">48</field>
|
||||
</record>
|
||||
|
||||
<!-- subcategories for lamps -->
|
||||
<record id="public_category_lamps_desk" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_lamps')"/>
|
||||
<field name="name">Desk Lamps</field>
|
||||
<field name="sequence">50</field>
|
||||
</record>
|
||||
<record id="public_category_lamps_ceiling" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_lamps')"/>
|
||||
<field name="name">Ceiling Lamps</field>
|
||||
<field name="sequence">51</field>
|
||||
</record>
|
||||
<record id="public_category_lamps_chandelier" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_lamps')"/>
|
||||
<field name="name">Chandeliers</field>
|
||||
<field name="sequence">52</field>
|
||||
</record>
|
||||
<record id="public_category_lamps_touch" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_lamps')"/>
|
||||
<field name="name">Touch Lamps</field>
|
||||
<field name="sequence">53</field>
|
||||
</record>
|
||||
|
||||
<!-- subcategories for services -->
|
||||
<record id="public_category_services_design_and_planning" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_services')"/>
|
||||
<field name="name">Design and Planning</field>
|
||||
<field name="sequence">55</field>
|
||||
</record>
|
||||
<record id="public_category_services_delivery_and_installation" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_services')"/>
|
||||
<field name="name">Delivery and Installation</field>
|
||||
<field name="sequence">56</field>
|
||||
</record>
|
||||
<record id="public_category_services_repair_and_maintenance" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_services')"/>
|
||||
<field name="name">Repair and Maintenance</field>
|
||||
<field name="sequence">57</field>
|
||||
</record>
|
||||
<record id="public_category_services_relocation_and_moving" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_services')"/>
|
||||
<field name="name">Relocation and Moving</field>
|
||||
<field name="sequence">58</field>
|
||||
</record>
|
||||
|
||||
<!-- subcategories for lamps -->
|
||||
<record id="public_category_multimedia_virtual_design" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_multimedia')"/>
|
||||
<field name="name">Virtual Design Tools</field>
|
||||
<field name="sequence">60</field>
|
||||
</record>
|
||||
<record id="public_category_multimedia_augmented_reality" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_multimedia')"/>
|
||||
<field name="name">Augmented Reality Tools</field>
|
||||
<field name="sequence">61</field>
|
||||
</record>
|
||||
<record id="public_category_multimedia_education" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_multimedia')"/>
|
||||
<field name="name">Education Tools</field>
|
||||
<field name="sequence">62</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_1_product_template" model="product.template">
|
||||
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_services')])]"/>
|
||||
</record>
|
||||
<record id="product.product_product_2_product_template" model="product.template">
|
||||
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_services')])]"/>
|
||||
</record>
|
||||
<record id="public_category_furnitures_chairs" model="product.public.category">
|
||||
<field name="parent_id" eval="ref('public_category_furnitures')"/>
|
||||
|
|
@ -192,10 +426,10 @@
|
|||
</record>
|
||||
|
||||
<record id="product.product_product_1_product_template" model="product.template">
|
||||
<field name="public_categ_ids" eval="[(6,0,[ref('services')])]"/>
|
||||
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_services')])]"/>
|
||||
</record>
|
||||
<record id="product.product_product_2_product_template" model="product.template">
|
||||
<field name="public_categ_ids" eval="[(6,0,[ref('services')])]"/>
|
||||
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_services')])]"/>
|
||||
</record>
|
||||
<record id="product.product_product_3_product_template" model="product.template">
|
||||
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_desks_components')])]"/>
|
||||
|
|
@ -243,7 +477,7 @@
|
|||
<record id="product.product_product_22_product_template" model="product.template">
|
||||
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_desks_components')])]"/>
|
||||
</record>
|
||||
<record id="product.product_product_25_product_template" model="product.template">
|
||||
<record id="product.product_template_acoustic_bloc_screens" model="product.template">
|
||||
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_desks_components')])]"/>
|
||||
</record>
|
||||
<record id="product.product_product_27_product_template" model="product.template">
|
||||
|
|
@ -268,17 +502,17 @@
|
|||
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_lamps')])]"/>
|
||||
</record>
|
||||
|
||||
<record model="product.pricelist" id="product.list0">
|
||||
<field name="selectable" eval="True" />
|
||||
<field name="sequence">3</field>
|
||||
</record>
|
||||
|
||||
<record id="benelux" model="res.country.group">
|
||||
<field name="name">BeNeLux</field>
|
||||
<field name="country_ids" eval="[(6,0,[
|
||||
ref('base.be'),ref('base.lu'),ref('base.nl')])]"/>
|
||||
</record>
|
||||
|
||||
<!-- Since we are adding pricelists, we activate the feature -->
|
||||
<record id="base.group_user" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('product.group_product_pricelist'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="list_christmas" model="product.pricelist">
|
||||
<field name="name">Christmas</field>
|
||||
<field name="selectable" eval="False" />
|
||||
|
|
@ -323,12 +557,6 @@
|
|||
<field name="base">list_price</field>
|
||||
</record>
|
||||
|
||||
<record id="item_us" model="product.pricelist.item">
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="compute_price">formula</field>
|
||||
<field name="base">list_price</field>
|
||||
</record>
|
||||
|
||||
<!-- Add demo-data for pretty website sales graph (for the sales dashboard) -->
|
||||
<record id="website_sale_order_1" model="sale.order">
|
||||
<field name="create_date" eval="datetime.now() - timedelta(days=8)"/>
|
||||
|
|
@ -336,7 +564,6 @@
|
|||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="state">sale</field>
|
||||
|
|
@ -344,10 +571,7 @@
|
|||
|
||||
<record id="website_sale_order_line_1" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_1"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_6').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_6"/>
|
||||
<field name="product_uom_qty">1</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">599.0</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -357,7 +581,6 @@
|
|||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=6)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="state">sale</field>
|
||||
|
|
@ -365,10 +588,7 @@
|
|||
|
||||
<record id="website_sale_order_line_2" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_2"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_4').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_4"/>
|
||||
<field name="product_uom_qty">1</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">900</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -378,7 +598,6 @@
|
|||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor2'))]"/>
|
||||
|
|
@ -387,10 +606,7 @@
|
|||
|
||||
<record id="website_sale_order_line_3" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_3"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_4').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_4"/>
|
||||
<field name="product_uom_qty">1</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">750</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -400,7 +616,6 @@
|
|||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=4)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="state">sale</field>
|
||||
|
|
@ -408,10 +623,7 @@
|
|||
|
||||
<record id="website_sale_order_line_4" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_4"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_8').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_8"/>
|
||||
<field name="product_uom_qty">1</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">1199.0</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -421,7 +633,6 @@
|
|||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="state">sale</field>
|
||||
|
|
@ -429,10 +640,8 @@
|
|||
|
||||
<record id="website_sale_order_line_5" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_5"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_4').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_4"/>
|
||||
<field name="product_uom_qty">3</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">349.0</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -442,7 +651,6 @@
|
|||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="state">sale</field>
|
||||
|
|
@ -450,10 +658,7 @@
|
|||
|
||||
<record id="website_sale_order_line_6" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_6"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_8').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_8"/>
|
||||
<field name="product_uom_qty">1</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">1599.00</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -464,7 +669,6 @@
|
|||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="website_id" ref="website.default_website"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="state">sale</field>
|
||||
|
|
@ -472,10 +676,7 @@
|
|||
|
||||
<record id="website_sale_order_line_7" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_7"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_8').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_8"/>
|
||||
<field name="product_uom_qty">1</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">1349.00</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -485,7 +686,6 @@
|
|||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="website_id" ref="website.default_website"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="date_order" eval="datetime.now()"/>
|
||||
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor1'))]"/>
|
||||
|
|
@ -494,10 +694,7 @@
|
|||
|
||||
<record id="website_sale_order_line_8" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_8"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_8').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_8"/>
|
||||
<field name="product_uom_qty">1</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">1799.00</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -507,26 +704,19 @@
|
|||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="website_id" ref="website.default_website"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(hours=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
</record>
|
||||
|
||||
<record id="website_sale_order_line_9" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_9"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_25').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_25"/>
|
||||
<field name="product_uom_qty">1</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">295.00</field>
|
||||
</record>
|
||||
|
||||
<record id="website_sale_order_line_10" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_9"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_12').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_12"/>
|
||||
<field name="product_uom_qty">1</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">120.50</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -537,7 +727,6 @@
|
|||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="website_id" ref="website.default_website"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="date_order" eval="datetime.now()"/>
|
||||
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor5'))]"/>
|
||||
|
|
@ -545,10 +734,8 @@
|
|||
|
||||
<record id="website_sale_order_line_11" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_10"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_11').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_11"/>
|
||||
<field name="product_uom_qty">2</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">33</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -559,17 +746,13 @@
|
|||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="website_id" ref="website.default_website"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="date_order" eval="(datetime.now()-timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
</record>
|
||||
|
||||
<record id="website_sale_order_line_12" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_11"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_9').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_9"/>
|
||||
<field name="product_uom_qty">1</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">47.0</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -580,7 +763,6 @@
|
|||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="website_id" ref="website.default_website"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="payment_term_id" ref="account.account_payment_term_immediate"/>
|
||||
<field name="date_order" eval="(datetime.now()-timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
|
|
@ -589,10 +771,7 @@
|
|||
|
||||
<record id="website_sale_order_line_14" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_13"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_8').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_8"/>
|
||||
<field name="product_uom_qty">1</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">1799.0</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -603,16 +782,12 @@
|
|||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="website_id" ref="website.default_website"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
</record>
|
||||
|
||||
<record id="website_sale_order_line_15" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_14"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_16').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_16"/>
|
||||
<field name="product_uom_qty">1</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">25.0</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -623,7 +798,6 @@
|
|||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="website_id" ref="website.default_website"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="date_order" eval="datetime.now()-relativedelta(months=1)"/>
|
||||
<field name="state">sale</field>
|
||||
|
|
@ -631,10 +805,8 @@
|
|||
|
||||
<record id="website_sale_order_line_16" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_16"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_8').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_8"/>
|
||||
<field name="product_uom_qty">2</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">1799.0</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -645,17 +817,14 @@
|
|||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="website_id" ref="website.default_website"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="date_order" eval="datetime.now()-relativedelta(months=1, days=2)"/>
|
||||
</record>
|
||||
|
||||
<record id="website_sale_order_line_17" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_17"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_9').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_9"/>
|
||||
<field name="product_uom_qty">7</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">47.0</field>
|
||||
<field name="invoice_status">to invoice</field>
|
||||
</record>
|
||||
|
|
@ -667,17 +836,14 @@
|
|||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="website_id" ref="website.default_website"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="date_order" eval="datetime.now()-relativedelta(months=2)"/>
|
||||
</record>
|
||||
|
||||
<record id="website_sale_order_line_18" model="sale.order.line">
|
||||
<field name="order_id" ref="website_sale_order_18"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_9').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_9"/>
|
||||
<field name="product_uom_qty">3</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">47.0</field>
|
||||
<field name="invoice_status">to invoice</field>
|
||||
</record>
|
||||
|
|
@ -693,18 +859,23 @@
|
|||
<field name="is_published" eval="True"/>
|
||||
<field name="type">service</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="description_sale">Warranty, issued to the purchaser of an article by its manufacturer, promising to repair or replace it if necessary within a specified period of time.</field>
|
||||
<field name="categ_id" ref="product.product_category_3"/>
|
||||
<field name="categ_id" ref="product.product_category_services"/>
|
||||
<field name="invoice_policy">delivery</field>
|
||||
<field name="public_categ_ids" eval="[(6, 0, [ref('website_sale.services')])]"/>
|
||||
<field name="public_categ_ids" eval="[(6, 0, [ref('website_sale.public_category_services')])]"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/warranty.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="product_1_attribute_3_product_template_attribute_line" model="product.template.attribute.line">
|
||||
<field name="product_tmpl_id" ref="website_sale.product_product_1_product_template"/>
|
||||
<field name="attribute_id" ref="product.product_attribute_3"/>
|
||||
<field name="value_ids" eval="[(6,0,[ref('product.product_attribute_value_5'), ref('product.product_attribute_value_6')])]"/>
|
||||
<field name="attribute_id" ref="product.pa_duration"/>
|
||||
<field
|
||||
name="value_ids"
|
||||
eval="[Command.set([
|
||||
ref('product.pav_duration_year_1'),
|
||||
ref('product.pav_duration_year_2'),
|
||||
ref('product.pav_duration_year_3'),
|
||||
])]"/>
|
||||
</record>
|
||||
|
||||
<!-- Handle automatically created product.template.attribute.value -->
|
||||
|
|
@ -717,7 +888,13 @@
|
|||
'xml_id': 'website_sale.product_1_attribute_3_value_2',
|
||||
'record': obj().env.ref('website_sale.product_1_attribute_3_product_template_attribute_line').product_template_value_ids[1],
|
||||
'noupdate': True,
|
||||
}]"/>
|
||||
},
|
||||
{
|
||||
'xml_id': 'website_sale.product_1_attribute_3_value_3',
|
||||
'record': obj().env.ref('website_sale.product_1_attribute_3_product_template_attribute_line').product_template_value_ids[2],
|
||||
'noupdate': True,
|
||||
},
|
||||
]"/>
|
||||
</function>
|
||||
|
||||
<function model="ir.model.data" name="_update_xmlids">
|
||||
|
|
@ -729,7 +906,13 @@
|
|||
'xml_id': 'website_sale.product_product_1b',
|
||||
'record': obj().env.ref('website_sale.product_product_1_product_template')._get_variant_for_combination(obj().env.ref('website_sale.product_1_attribute_3_value_2')),
|
||||
'noupdate': True,
|
||||
},]"/>
|
||||
},
|
||||
{
|
||||
'xml_id': 'website_sale.product_product_1c',
|
||||
'record': obj().env.ref('website_sale.product_product_1_product_template')._get_variant_for_combination(obj().env.ref('website_sale.product_1_attribute_3_value_3')),
|
||||
'noupdate': True,
|
||||
},
|
||||
]"/>
|
||||
</function>
|
||||
|
||||
<record id="product_product_1" model="product.product">
|
||||
|
|
@ -743,11 +926,16 @@
|
|||
<field name="price_extra">18.00</field>
|
||||
</record>
|
||||
|
||||
<record id="delivery.delivery_carrier" model="delivery.carrier">
|
||||
<field name="is_published" eval="False" />
|
||||
</record>
|
||||
|
||||
<record id="website_sale_activity_1" model="mail.activity">
|
||||
<field name="res_id" ref="website_sale.website_sale_order_3"/>
|
||||
<field name="res_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="activity_type_id" ref="mail.mail_activity_data_call"/>
|
||||
<field name="date_deadline" eval="(DateTime.today() + relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')" />
|
||||
<field name="summary">Schedule phone meeting with client</field>
|
||||
<field name="create_uid" ref="base.user_demo"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
</record>
|
||||
|
|
@ -779,4 +967,162 @@
|
|||
<field name="user_id" ref="base.user_demo"/>
|
||||
</record>
|
||||
|
||||
<record id="sale.product_product_1_product_template" model="product.template">
|
||||
<field name="website_sequence">9985</field>
|
||||
<field name="is_published" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="product.desk_organizer_product_template" model="product.template">
|
||||
<field name="is_published" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_4_product_template" model="product.template">
|
||||
<field name="optional_product_ids" eval="[Command.set([
|
||||
ref('product.product_product_11_product_template'),
|
||||
ref('website_sale.product_product_1_product_template'),
|
||||
])]"
|
||||
/>
|
||||
</record>
|
||||
|
||||
<!-- Products' secondary images -->
|
||||
<record id="product_delivery_01_extra_image" model="product.image">
|
||||
<field name="name">Office Chair - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.product_delivery_01"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/table02_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="product_delivery_01_extra_image_2" model="product.image">
|
||||
<field name="name">Office Chair - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.product_delivery_01"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/product_product_3-image_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_16_extra_image" model="product.image">
|
||||
<field name="name">Drawer Black - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.product_product_16"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/product_product_16-image_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="product_delivery_02_extra_image" model="product.image">
|
||||
<field name="name">Office Lamp - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.product_delivery_02"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/product_lamp_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_4_extra_image" model="product.image">
|
||||
<field name="name">Customizable Desk Steel - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.product_product_4"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/table02_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_1_product_template_extra_image" model="product.image">
|
||||
<field name="name">Chair floor protection - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_tmpl_id" ref="sale.product_product_1_product_template"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/floor_protection-image_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="consu_delivery_03_extra_image" model="product.image">
|
||||
<field name="name">Four Person Desk - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.consu_delivery_03"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/product_product_d03_image_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="desk_organizer_extra_image" model="product.image">
|
||||
<field name="name">Desk Organizer - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.desk_organizer"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/desk_organizer_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_24_extra_image" model="product.image">
|
||||
<field name="name">Individual Workplace - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.product_product_24"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/product_product_24-image_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_12_extra_image" model="product.image">
|
||||
<field name="name">Office Chair Black - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.product_product_12"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/product_product_12-image_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_27_extra_image" model="product.image">
|
||||
<field name="name">Drawer - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.product_product_27"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/product_product_27-image_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="consu_delivery_02_extra_image" model="product.image">
|
||||
<field name="name">Large Meeting Table - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.consu_delivery_02"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/product_product_46-image_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_10_extra_image" model="product.image">
|
||||
<field name="name">Cabinet Width Doors - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.product_product_10"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/product_product_10-image_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_7_extra_image" model="product.image">
|
||||
<field name="name">Storage Box - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.product_product_7"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/product_product_7-image_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_9_extra_image" model="product.image">
|
||||
<field name="name">Pedal Bin - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.product_product_9"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/product_product_9-image_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_6_extra_image" model="product.image">
|
||||
<field name="name">Large Cabinet - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.product_product_6"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/product_product_6-image_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_5_extra_image" model="product.image">
|
||||
<field name="name">Corner Desk Right Sit - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.product_product_5"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/product_product_5-image_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="consu_delivery_01_extra_image" model="product.image">
|
||||
<field name="name">Two-Seat Sofa - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.consu_delivery_01"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/product_product_d01_image_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_3_extra_image" model="product.image">
|
||||
<field name="name">Desk Combination - Context View</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="product_variant_id" ref="product.product_product_3"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/product_product_3-image_02.jpg"/>
|
||||
</record>
|
||||
|
||||
<!-- Products' tertiary images -->
|
||||
<record id="product_product_4_extra_image_1" model="product.image">
|
||||
<field name="name">Customizable Desk Steel - Detail View</field>
|
||||
<field name="sequence">11</field>
|
||||
<field name="product_variant_id" ref="product.product_product_4"/>
|
||||
<field name="image_1920" type="base64" file="website_sale/static/src/img/products_demo/table02_03.webp"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@
|
|||
<field name="name">eCommerce: send email to customers about their abandoned cart</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">hours</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<field name="model_id" ref="model_website"/>
|
||||
<field name="code">model._send_abandoned_cart_email()</field>
|
||||
<field name="state">code</field>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="subject">You left items in your cart!</field>
|
||||
<field name="email_from">{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted or '') }}</field>
|
||||
<field name="partner_to">{{ object.partner_id.id }}</field>
|
||||
<field name="partner_to" eval="False"/>
|
||||
<field name="use_default_to" eval="True"/>
|
||||
<field name="description">If the setting is set, sent to authenticated visitors who abandoned their cart</field>
|
||||
<field name="body_html" type="html">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 0px; background-color: white; color: #454748; border-collapse:separate;">
|
||||
|
|
@ -30,7 +31,7 @@
|
|||
<strong t-out="line.product_id.display_name or ''">[FURN_7800] Desk Combination</strong><br/><t t-out="line.name or ''">[FURN_7800] Desk Combination Desk combination, black-brown: chair + desk + drawer.</t>
|
||||
</td>
|
||||
<td width="100px" align="right">
|
||||
<t t-out="int(line.product_uom_qty) or ''">10000</t> <t t-out="line.product_uom.name or ''">Units</t>
|
||||
<t t-out="int(line.product_uom_qty) or ''">10000</t> <t t-out="line.product_uom_id.name or ''">Units</t>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -38,9 +39,9 @@
|
|||
<hr/>
|
||||
</t>
|
||||
<div style="text-align: center; padding: 16px 0px 16px 0px; font-size: 14px;">
|
||||
<a t-attf-href="{{ object.get_base_url() }}/shop/cart?access_token={{ object.access_token }}"
|
||||
<a t-attf-href="{{ object.get_base_url() }}/shop/cart?id={{ object.id }}&access_token={{ object.access_token }}"
|
||||
target="_blank"
|
||||
style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
|
||||
t-attf-style="background-color: {{object.user_id.company_id.email_secondary_color or '#875A7B'}}; padding: 8px 16px 8px 16px; text-decoration: none; color: {{object.user_id.company_id.email_primary_color or '#FFFFFF'}}; border-radius: 5px; font-size:13px;">
|
||||
Resume order
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -53,7 +54,6 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -1,302 +1,119 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Templates for Dynamic Snippet -->
|
||||
<template id="dynamic_filter_template_product_product_add_to_cart" name="Classic Card">
|
||||
<t t-foreach="records" t-as="data" data-thumb="/website_sale/static/src/img/snippets_options/product_add_to_cart.svg">
|
||||
|
||||
<!-- Template for Dynamic Snippet -->
|
||||
|
||||
<template
|
||||
id="dynamic_filter_template_product_product_products_item"
|
||||
name="Generic Product Template (customizable)"
|
||||
>
|
||||
<t
|
||||
t-foreach="records"
|
||||
t-as="data"
|
||||
data-number-of-elements="4"
|
||||
data-number-of-elements-sm="2"
|
||||
>
|
||||
|
||||
<t t-set="record" t-value="data['_record']"/>
|
||||
<div class="o_carousel_product_card card h-100 w-100" t-att-data-add2cart-rerender="data.get('_add2cart_rerender')">
|
||||
<div t-if="is_sample" class="h5 o_ribbon_right bg-primary text-uppercase">Sample</div>
|
||||
<t t-set="product" t-value="record"/>
|
||||
<t t-set="product_template_id" t-value="record.id if not record.is_product_variant else record.product_tmpl_id.id"/>
|
||||
<t t-set="product_id" t-value="record.id if record.is_product_variant else record.product_variant_id.id"/>
|
||||
|
||||
<div
|
||||
class="oe_product_cart o_carousel_product_card d-flex"
|
||||
role="article"
|
||||
t-att-aria-label="product.display_name"
|
||||
t-att-data-add2cart-rerender="data.get('_add2cart_rerender')"
|
||||
>
|
||||
<input type="hidden" name="product-id" t-att-data-product-id="record.id"/>
|
||||
<a class="o_carousel_product_img_link o_dynamic_product_hovered overflow-hidden" t-att-href="record.website_url">
|
||||
<img class="card-img-top o_img_product_square o_img_product_cover h-auto" loading="lazy" t-att-src="data['image_512']"
|
||||
t-att-alt="record.display_name"/>
|
||||
</a>
|
||||
<i t-if="data.get('_latest_viewed')" class="fa fa-trash o_carousel_product_remove js_remove"/>
|
||||
<div class="o_carousel_product_card_body card-body d-flex flex-wrap">
|
||||
<a t-att-href="record.website_url" class="text-decoration-none d-block w-100">
|
||||
<div class="h6 card-title mb-0" t-field="record.display_name"/>
|
||||
|
||||
<!-- Image -->
|
||||
<div t-attf-class="oe_product_image position-relative flex-grow-0 overflow-hidden">
|
||||
<a
|
||||
t-att-href="product.website_url"
|
||||
t-attf-class="oe_product_image_link position-relative"
|
||||
contenteditable="false"
|
||||
t-att-title="product.display_name"
|
||||
>
|
||||
<span
|
||||
role="img"
|
||||
t-attf-alt="{{product.display_name}} image"
|
||||
class="oe_product_image_img_wrapper d-flex h-100 justify-content-center align-items-center"
|
||||
>
|
||||
<span
|
||||
class="oe_product_image_img h-100 w-100"
|
||||
t-attf-style="background-image: url({{data['image_512']}});"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<div class="mt-2">
|
||||
<t t-if="is_view_active('website_sale.product_comment')" t-call="portal_rating.rating_widget_stars_static">
|
||||
<t t-set="rating_avg" t-value="record.rating_avg"/>
|
||||
<t t-set="rating_count" t-value="record.rating_count"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="w-100 d-flex flex-wrap flex-md-column flex-lg-row align-items-center align-self-end justify-content-between mt-3">
|
||||
<div class="py-2">
|
||||
<t t-call="website_sale.price_dynamic_filter_template_product_product"/>
|
||||
</div>
|
||||
<div class="o_dynamic_snippet_btn_wrapper" t-if="record._website_show_quick_add()">
|
||||
<button type="button" role="button" class="btn btn-primary js_add_cart ms-auto" title="Add to Cart">
|
||||
<i class="fa fa-fw fa-shopping-cart"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="is_sample" class="o_ribbon_right small text-bg-primary">Sample</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="dynamic_filter_template_product_product_view_detail" name="Classic Card - Detailed">
|
||||
<t t-foreach="records" t-as="data" data-number-of-elements="3" data-thumb="/website_sale/static/src/img/snippets_options/product_view_detail.svg">
|
||||
<t t-set="record" t-value="data['_record']" data-arrow-position="bottom"/>
|
||||
<div class="o_carousel_product_card card h-100 w-100" t-att-data-add2cart-rerender="data.get('_add2cart_rerender')">
|
||||
<div t-if="is_sample" class="h5 o_ribbon_right bg-primary text-uppercase">Sample</div>
|
||||
<a class="o_carousel_product_img_link o_dynamic_product_hovered overflow-hidden" t-att-href="record.website_url">
|
||||
<img class="card-img-top o_img_product_square o_img_product_cover h-auto" loading="lazy" t-att-src="data['image_512']" t-att-alt="record.display_name"/>
|
||||
</a>
|
||||
<div class="o_carousel_product_card_body card-body d-flex flex-column justify-content-between">
|
||||
<div class="card-title h5" t-field="record.display_name"/>
|
||||
<div class="card-text flex-grow-1 text-muted h6" t-field="record.description_sale"/>
|
||||
<div class="mt-2">
|
||||
<t t-if="is_view_active('website_sale.product_comment')" t-call="portal_rating.rating_widget_stars_static">
|
||||
<t t-set="rating_avg" t-value="record.rating_avg"/>
|
||||
<t t-set="rating_count" t-value="record.rating_count"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-column flex-lg-row align-items-center align-self-end w-100 mt-2 pt-3 border-top">
|
||||
<div class="pb-2">
|
||||
<t t-call="website_sale.price_dynamic_filter_template_product_product"/>
|
||||
</div>
|
||||
<a class="btn btn-primary" t-att-href="record.website_url">
|
||||
View product
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="dynamic_filter_template_product_product_mini_image" name="Image only">
|
||||
<t t-foreach="records" t-as="data" data-number-of-elements="4" data-number-of-elements-sm="1" data-thumb="/website_sale/static/src/img/snippets_options/product_image_only.svg">
|
||||
<t t-set="record" t-value="data['_record']"/>
|
||||
<div class="card h-100 border-0 w-100 rounded-0 bg-transparent" t-att-data-url="record.website_url">
|
||||
<div t-if="is_sample" class="h5 o_ribbon_right bg-primary text-uppercase">Sample</div>
|
||||
<a class="o_carousel_product_img_link o_dynamic_product_hovered overflow-hidden" t-att-href="record.website_url">
|
||||
<img class="card-img-top h-auto o_img_product_square o_img_product_cover rounded" loading="lazy" t-att-src="data['image_512']" t-att-alt="record.display_name"/>
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="dynamic_filter_template_product_product_mini_price" name="Image with price">
|
||||
<t t-foreach="records" t-as="data" data-thumb="/website_sale/static/src/img/snippets_options/product_image_with_price.svg">
|
||||
<t t-set="record" t-value="data['_record']"/>
|
||||
<div class="card h-100 border-0 w-100 rounded-0 bg-transparent o_dynamic_product_hovered" t-att-data-url="record.website_url">
|
||||
<div t-if="is_sample" class="h5 o_ribbon_right bg-primary text-uppercase">Sample</div>
|
||||
<a class="o_carousel_product_img_link o_dynamic_product_hovered overflow-hidden" t-att-href="record.website_url">
|
||||
<img class="card-img-top h-auto o_img_product_square o_img_product_cover rounded" loading="lazy" t-att-src="data['image_512']" t-att-alt="record.display_name"/>
|
||||
</a>
|
||||
<div class="o_carousel_product_card_body mt-2 d-flex justify-content-between">
|
||||
<t t-if="is_view_active('website_sale.product_comment')" t-call="portal_rating.rating_widget_stars_static">
|
||||
<t t-set="rating_style_compressed" t-value="true"/>
|
||||
<t t-set="rating_avg" t-value="record.rating_avg"/>
|
||||
<t t-set="rating_count" t-value="record.rating_count"/>
|
||||
</t>
|
||||
<div class="ms-auto">
|
||||
<t t-call="website_sale.price_dynamic_filter_template_product_product"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="dynamic_filter_template_product_product_mini_name" name="Image with name">
|
||||
<t t-foreach="records" t-as="data" data-thumb="/website_sale/static/src/img/snippets_options/product_image_with_name.svg">
|
||||
<t t-set="record" t-value="data['_record']"/>
|
||||
<div class="card h-100 border-0 w-100 rounded-0 bg-transparent o_dynamic_product_hovered" t-att-data-url="record.website_url">
|
||||
<div t-if="is_sample" class="h5 o_ribbon_right bg-primary text-uppercase">Sample</div>
|
||||
<a class="o_carousel_product_img_link overflow-hidden" t-att-href="record.website_url">
|
||||
<img class="card-img-top h-auto o_img_product_square o_img_product_cover rounded" loading="lazy" t-att-src="data['image_512']" t-att-alt="record.display_name"/>
|
||||
</a>
|
||||
<div class="h6 text-center mt-2 p-2" t-field="record.display_name"/>
|
||||
<div class="text-center">
|
||||
<t t-if="is_view_active('website_sale.product_comment')" t-call="portal_rating.rating_widget_stars_static">
|
||||
<t t-set="rating_avg" t-value="record.rating_avg"/>
|
||||
<t t-set="rating_count" t-value="record.rating_count"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="dynamic_filter_template_product_product_centered" name="Centered Product">
|
||||
<t t-foreach="records" t-as="data" data-arrow-position="bottom" data-thumb="/website_sale/static/src/img/snippets_options/product_centered.svg">
|
||||
<t t-set="record" t-value="data['_record']"/>
|
||||
<div class="o_carousel_product_card card w-100" t-att-data-add2cart-rerender="data.get('_add2cart_rerender')">
|
||||
<div t-if="is_sample" class="h5 o_ribbon_right bg-primary text-uppercase">Sample</div>
|
||||
<input type="hidden" name="product-id" t-att-data-product-id="record.id"/>
|
||||
<a class="o_carousel_product_img_link position-absolute mx-auto" t-att-href="record.website_url">
|
||||
<img class="card-img-top" loading="lazy" t-att-src="data['image_512']" t-att-alt="record.display_name"/>
|
||||
</a>
|
||||
<div class="o_carousel_product_card_body card-body d-flex flex-column justify-content-between">
|
||||
<div class="card-title h5 text-center" t-field="record.display_name"/>
|
||||
<div class="text-center">
|
||||
<div class="h5">
|
||||
<t t-call="website_sale.price_dynamic_filter_template_product_product"/>
|
||||
</div>
|
||||
<div class="h6 mb-0">
|
||||
<t t-if="is_view_active('website_sale.product_comment')">
|
||||
<button t-if="data.get('_latest_viewed')"
|
||||
class="btn o_carousel_product_remove js_remove rounded-pill lh-1"
|
||||
t-att-data-product-id="product_id"
|
||||
t-att-data-product-selected="record.is_product_variant"
|
||||
t-att-data-product-template-id="product_template_id"
|
||||
title="Remove from recently viewed"
|
||||
>
|
||||
<i class="oi oi-close" role="presentation"/>
|
||||
</button>
|
||||
<div class="o_wsale_product_information flex-grow-1 flex-shrink-1">
|
||||
<div class="o_wsale_product_info_attributes_wrapper">
|
||||
<div class="o_wsale_product_information_text">
|
||||
<!-- Product Name -->
|
||||
<h2 class="o_wsale_products_item_title h6 text-break">
|
||||
<a
|
||||
class="text-decoration-none"
|
||||
t-att-href="product.website_url"
|
||||
t-att-content="data.get('display_name')"
|
||||
t-att-title="data.get('display_name')"
|
||||
t-out="data.get('display_name')"
|
||||
/>
|
||||
</h2>
|
||||
<!-- Description -->
|
||||
<div class="oe_subdescription_wrapper">
|
||||
<div
|
||||
class="oe_subdescription text-muted small"
|
||||
contenteditable="false"
|
||||
>
|
||||
<div t-field="product.description_sale"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Rating -->
|
||||
<div
|
||||
t-if="is_view_active('website_sale.product_comment')"
|
||||
class="o_wsale_product_rating_wrapper"
|
||||
>
|
||||
<t t-call="portal_rating.rating_widget_stars_static">
|
||||
<t t-set="rating_avg" t-value="record.rating_avg"/>
|
||||
<t t-set="rating_count" t-value="record.rating_count"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_carousel_product_card_footer d-flex align-items-center justify-content-center pb-4">
|
||||
<a class="btn btn-primary d-block" t-att-href="record.website_url">
|
||||
View Product
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="dynamic_filter_template_product_product_borderless_1" name="Borderless Product n°1">
|
||||
<t t-foreach="records" t-as="data" data-thumb="/website_sale/static/src/img/snippets_options/product_borderless_1.svg">
|
||||
<t t-set="record" t-value="data['_record']"/>
|
||||
<div class="o_carousel_product_card bg-transparent w-100 card border-0">
|
||||
<div t-if="is_sample" class="h5 o_ribbon_right bg-primary text-uppercase">Sample</div>
|
||||
<input type="hidden" name="product-id" t-att-data-product-id="record.id"/>
|
||||
<a class="o_carousel_product_img_link o_dynamic_product_hovered stretched-link" t-att-href="record.website_url">
|
||||
<div class="overflow-hidden rounded">
|
||||
<img class="card-img-top o_img_product_square o_img_product_cover h-auto" loading="lazy" t-att-src="data['image_512']"
|
||||
t-att-alt="record.display_name"/>
|
||||
</div>
|
||||
</a>
|
||||
<div class="o_carousel_product_card_body d-flex flex-wrap flex-column justify-content-between h-100 p-3">
|
||||
<div class="h6 card-title" t-field="record.display_name"/>
|
||||
<div>
|
||||
<t t-if="is_view_active('website_sale.product_comment')" t-call="portal_rating.rating_widget_stars_static">
|
||||
<t t-set="rating_avg" t-value="record.rating_avg"/>
|
||||
<t t-set="rating_count" t-value="record.rating_count"/>
|
||||
</t>
|
||||
<div class="mt-2">
|
||||
<t t-call="website_sale.price_dynamic_filter_template_product_product"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="dynamic_filter_template_product_product_borderless_2" name="Borderless Product n°2">
|
||||
<t t-foreach="records" t-as="data" data-thumb="/website_sale/static/src/img/snippets_options/product_borderless_2.svg">
|
||||
<t t-set="record" t-value="data['_record']"/>
|
||||
<div class="o_carousel_product_card card w-100 border-0 bg-transparent" t-att-data-add2cart-rerender="data.get('_add2cart_rerender')">
|
||||
<div t-if="is_sample" class="h5 o_ribbon_right bg-primary text-uppercase">Sample</div>
|
||||
<input type="hidden" name="product-id" t-att-data-product-id="record.id"/>
|
||||
<a class="o_carousel_product_img_link o_dynamic_product_hovered" t-att-href="record.website_url">
|
||||
<div class="overflow-hidden rounded">
|
||||
<img class="card-img-top o_img_product_square o_img_product_cover h-auto" loading="lazy" t-att-src="data['image_512']"
|
||||
t-att-alt="record.display_name"/>
|
||||
</div>
|
||||
</a>
|
||||
<div class="o_carousel_product_card_body h-100 p-3 d-flex flex-column justify-content-between">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-2">
|
||||
<div class="h5 mb-0 me-4">
|
||||
<t t-call="website_sale.price_dynamic_filter_template_product_product"/>
|
||||
</div>
|
||||
<div class="h6 mb-0">
|
||||
<t t-if="is_view_active('website_sale.product_comment')">
|
||||
<t t-call="portal_rating.rating_widget_stars_static">
|
||||
<t t-set="rating_style_compressed" t-value="true"/>
|
||||
<t t-set="rating_avg" t-value="record.rating_avg"/>
|
||||
<t t-set="rating_count" t-value="record.rating_count"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-title h6 flex-grow-1 w-100 mt-2 mb-3" t-field="record.display_name"/>
|
||||
<div class="text-end o_dynamic_snippet_btn_wrapper" t-if="record._website_show_quick_add()">
|
||||
<button type="button" role="button" class="btn btn-primary js_add_cart w-100" title="Add to Cart">
|
||||
Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="dynamic_filter_template_product_product_banner" name="Large Banner">
|
||||
<t t-foreach="records" t-as="data" data-number-of-elements="1" data-number-of-elements-sm="1" data-thumb="/website_sale/static/src/img/snippets_options/product_banner.svg">
|
||||
<t t-set="record" t-value="data['_record']"/>
|
||||
<div class="o_carousel_product_card card w-100" t-att-data-add2cart-rerender="data.get('_add2cart_rerender')">
|
||||
<div t-if="is_sample" class="h5 o_ribbon_right bg-primary text-uppercase">Sample</div>
|
||||
<input type="hidden" name="product-id" t-att-data-product-id="record.id"/>
|
||||
<div class="row flex-row-reverse">
|
||||
<div class="col-lg-6 d-flex align-items-center justify-content-center justify-content-lg-end o_wrap_product_img position-relative">
|
||||
<img class="img img-fluid position-absolute o_img_product_cover w-100 h-100" loading="lazy" t-att-src="data['image_512']" t-att-alt="record.display_name"/>
|
||||
</div>
|
||||
<div class="col-lg-6 px-5 d-flex align-items-center">
|
||||
<div class="o_carousel_product_card_body card-body p-5">
|
||||
<div class="card-title h1" t-field="record.display_name"/>
|
||||
<div class="d-flex align-items-center my-4">
|
||||
<div class="h4 mb-0 me-3">
|
||||
<t t-call="website_sale.price_dynamic_filter_template_product_product"/>
|
||||
</div>
|
||||
<t t-if="is_view_active('website_sale.product_comment')" t-call="portal_rating.rating_widget_stars_static">
|
||||
<t t-set="rating_avg" t-value="record.rating_avg"/>
|
||||
<t t-set="rating_count" t-value="record.rating_count"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="card-text text-muted" t-field="record.description_sale"/>
|
||||
<div class="mt-4">
|
||||
<button t-if="record._website_show_quick_add()" type="button" role="button" class="btn btn-primary js_add_cart mt-1" title="Add to Cart">
|
||||
Add to Cart
|
||||
</button>
|
||||
<a class="btn btn-link me-1 mt-1" t-att-href="record.website_url">
|
||||
View Product
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="dynamic_filter_template_product_product_horizontal_card" name="Horizontal Card">
|
||||
<t t-foreach="records" t-as="data"
|
||||
data-number-of-elements="3"
|
||||
data-number-of-elements-sm="1"
|
||||
data-row-per-slide="2"
|
||||
data-arrow-position="bottom"
|
||||
data-extra-classes="o_carousel_multiple_rows"
|
||||
data-thumb="/website_sale/static/src/img/snippets_options/product_horizontal_card.svg">
|
||||
<t t-set="record" t-value="data['_record']"/>
|
||||
<div class="o_carousel_product_card card w-100 border-0 bg-light p-3" t-att-data-add2cart-rerender="data.get('_add2cart_rerender')">
|
||||
<div t-if="is_sample" class="h5 o_ribbon_right bg-primary text-uppercase">Sample</div>
|
||||
<input type="hidden" name="product-id" t-att-data-product-id="record.id"/>
|
||||
<div class="row h-100 p-0">
|
||||
<div class="col-lg-4 position-static">
|
||||
<a class="stretched-link o_dynamic_product_hovered" t-att-href="record.website_url">
|
||||
<img class="img img-fluid mx-auto o_img_product_square" loading="lazy" t-att-src="data['image_512']" t-att-alt="record.display_name"/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="o_carousel_product_card_body col-lg-8 d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<div class="card-title h6" t-field="record.display_name"/>
|
||||
<div class="o_wsale_product_sub justify-content-between gap-2">
|
||||
<!-- Price(s) -->
|
||||
<div class="product_price" aria-label="Price information">
|
||||
<t t-call="website_sale.price_dynamic_filter_template_product_product"></t>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1">
|
||||
<t t-if="is_view_active('website_sale.product_comment')" t-call="portal_rating.rating_widget_stars_static">
|
||||
<t t-set="rating_avg" t-value="record.rating_avg"/>
|
||||
<t t-set="rating_count" t-value="record.rating_count"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="d-flex align-items-center flex-wrap">
|
||||
<div class="my-2">
|
||||
<t t-call="website_sale.price_dynamic_filter_template_product_product"/>
|
||||
</div>
|
||||
<div t-if="record._website_show_quick_add()" class="o_dynamic_snippet_btn_wrapper ms-auto">
|
||||
<button type="button" role="button" class="btn btn-primary js_add_cart" title="Add to Cart">
|
||||
<i class="fa fa-fw fa-shopping-cart"/>
|
||||
<!-- Actions -->
|
||||
<div class="o_wsale_product_btn">
|
||||
<div class="o_wsale_product_action_row">
|
||||
<t t-if="product._website_show_quick_add()">
|
||||
<button
|
||||
type="button"
|
||||
class="o_wsale_product_btn_primary btn js_add_cart"
|
||||
title="Add to Cart"
|
||||
t-att-data-product-id="product.id if product.is_product_variant else product.product_variant_id.id"
|
||||
t-att-data-product-selected="product.is_product_variant"
|
||||
t-att-data-product-template-id="product.id if not product.is_product_variant else product.product_tmpl_id.id"
|
||||
t-att-data-product-type="product.type"
|
||||
t-att-data-show-quantity="is_view_active('website_sale.product_quantity')"
|
||||
>
|
||||
<!-- Avoid whitespaces undesired gaps -->
|
||||
<i class="fa fa-shopping-cart fa-fw" role="presentation"/><span class="o_label small ms-1">Add to Cart</span>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -304,103 +121,31 @@
|
|||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="dynamic_filter_template_product_product_horizontal_card_2" name="Horizontal Card width covered image">
|
||||
<t t-foreach="records" t-as="data"
|
||||
data-row-per-slide="2"
|
||||
data-arrow-position="bottom"
|
||||
data-number-of-elements="2"
|
||||
data-number-of-elements-sm="1"
|
||||
data-extra-classes="o_carousel_multiple_rows"
|
||||
data-thumb="/website_sale/static/src/img/snippets_options/product_horizontal_card_2.svg">
|
||||
<t t-set="record" t-value="data['_record']"/>
|
||||
<div class="o_carousel_product_card card w-100 border-0 o_dynamic_product_hovered o_cc o_cc5">
|
||||
<div t-if="is_sample" class="h5 o_ribbon_right bg-primary text-uppercase">Sample</div>
|
||||
<input type="hidden" name="product-id" t-att-data-product-id="record.id"/>
|
||||
<a class="stretched-link" t-att-href="record.website_url">
|
||||
<img class="img img-fluid position-absolute w-100 h-100 o_img_product_cover" loading="lazy" t-att-src="data['image_512']" t-att-alt="record.display_name"/>
|
||||
</a>
|
||||
<div class="o_carousel_product_card_body d-flex flex-column justify-content-between h-100 bg-black-50 p-3 position-relative">
|
||||
<div class="mb-3">
|
||||
<div class="card-title h5" t-field="record.display_name"/>
|
||||
<t t-if="is_view_active('website_sale.product_comment')" t-call="portal_rating.rating_widget_stars_static">
|
||||
<t t-set="rating_avg" t-value="record.rating_avg"/>
|
||||
<t t-set="rating_count" t-value="record.rating_count"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="card-text h6 flex-grow-1" t-field="record.description_sale"/>
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mt-3">
|
||||
<div class="h5 mb-0 me-2">
|
||||
<t t-call="website_sale.price_dynamic_filter_template_product_product"/>
|
||||
</div>
|
||||
<div t-if="record._website_show_quick_add()" class="o_dynamic_snippet_btn_wrapper">
|
||||
<button type="button" role="button" class="btn btn-primary js_add_cart" title="Add to Cart">
|
||||
Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="dynamic_filter_template_product_product_card_group" name="Card group">
|
||||
<t t-foreach="records" t-as="data"
|
||||
data-row-per-slide="2"
|
||||
data-number-of-elements="2"
|
||||
data-number-of-elements-sm="1"
|
||||
data-arrow-position="bottom"
|
||||
data-extra-classes="o_card_group rounded"
|
||||
data-thumb="/website_sale/static/src/img/snippets_options/product_card_group.svg">
|
||||
<t t-set="record" t-value="data['_record']"/>
|
||||
<div class="o_carousel_product_card card w-100 rounded-0 border-top-0 border-start-0" t-att-data-url="record.website_url">
|
||||
<div t-if="is_sample" class="h5 o_ribbon_right bg-primary text-uppercase">Sample</div>
|
||||
<input type="hidden" name="product-id" t-att-data-product-id="record.id"/>
|
||||
<div class="o_carousel_product_card_body card-body justify-content-between h-100 p-3">
|
||||
<div class="row h-100">
|
||||
<div class="col-8 d-flex flex-column">
|
||||
<div class="card-title h5" t-field="record.display_name"/>
|
||||
<div class="card-text h6 text-muted" t-field="record.description_sale"/>
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap w-100 mt-auto">
|
||||
<div class="h5 text-primary mb-0 me-2">
|
||||
<t t-call="website_sale.price_dynamic_filter_template_product_product"/>
|
||||
</div>
|
||||
<t t-if="is_view_active('website_sale.product_comment')" t-call="portal_rating.rating_widget_stars_static">
|
||||
<t t-set="rating_avg" t-value="record.rating_avg"/>
|
||||
<t t-set="rating_count" t-value="record.rating_count"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 position-static">
|
||||
<div class="overflow-hidden position-static">
|
||||
<a class="stretched-link o_dynamic_product_hovered" t-att-href="record.website_url">
|
||||
<img class="img img-fluid o_img_product_square o_img_product_cover h-auto" loading="lazy" t-att-src="data['image_512']" t-att-alt="record.display_name"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="price_dynamic_filter_template_product_product" name="Dynamic Product Filter Price">
|
||||
<t t-set="record_price" t-value="record._get_contextual_price_tax_selection()"/>
|
||||
<t t-if="not website.prevent_zero_price_sale or record_price">
|
||||
<span t-esc="record_price" class="fw-bold"
|
||||
<t t-if="not data.get('prevent_zero_price_sale') and not data.get('is_sample')">
|
||||
<span t-out="data['price']"
|
||||
class="mb-0 fw-bold"
|
||||
name="product_price"
|
||||
t-options="{'widget': 'monetary', 'display_currency': website.currency_id}"/>
|
||||
<del t-if="data.get('has_discounted_price')" class="text-danger ms-1 h6" style="white-space: nowrap;"
|
||||
t-esc="data['list_price']"
|
||||
t-options="{'widget': 'monetary', 'display_currency': website.currency_id}"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-field="website.prevent_zero_price_sale_text"/>
|
||||
<del
|
||||
t-if="data.get('has_discounted_price')"
|
||||
aria-label="Original price"
|
||||
t-attf-class="text-muted me-1 mb-0"
|
||||
style="white-space: nowrap;"
|
||||
>
|
||||
<small
|
||||
t-out="data['list_price']"
|
||||
t-options="{'widget': 'monetary', 'display_currency': website.currency_id}"
|
||||
/>
|
||||
</del>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Assets -->
|
||||
<record id="website_sale.s_dynamic_snippet_products_000_scss" model="ir.asset">
|
||||
<field name="name">Dynamic snippet products 000 SCSS</field>
|
||||
<field name="bundle">web.assets_frontend</field>
|
||||
<field name="path">website_sale/static/src/snippets/s_dynamic_snippet_products/000.scss</field>
|
||||
</record>
|
||||
</data>
|
||||
<asset id="website_sale.s_dynamic_snippet_products_000_scss" name="Dynamic snippet products 000 SCSS">
|
||||
<bundle>web.assets_frontend</bundle>
|
||||
<path>website_sale/static/src/snippets/s_dynamic_snippet_products/000.scss</path>
|
||||
</asset>
|
||||
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="dynamic_filter_template_product_public_category_default">
|
||||
<t t-foreach="records" t-as="category">
|
||||
<t t-set="is_parent" t-value="category['id'] == parentCategoryId"/>
|
||||
<div
|
||||
t-if="not is_parent or includeParent"
|
||||
name="category_item"
|
||||
class="s_dynamic_category_item position-relative bg-black overflow-hidden oe_unremovable oe_unmovable"
|
||||
t-att-data-category-id="category['id']"
|
||||
t-attf-style="grid-row: span {{ 4 if is_parent else size }}; grid-column: span {{ 2 if is_parent and colSpanTwo else 1 }}"
|
||||
role="article"
|
||||
>
|
||||
<div class="overflow-hidden">
|
||||
<img
|
||||
name="category_image"
|
||||
loading="lazy"
|
||||
t-att-src="category['cover_image']"
|
||||
alt="Category Image"
|
||||
class="o_category_image img-fluid position-absolute w-100 h-100 object-fit-cover"
|
||||
/>
|
||||
<div t-if="category.get('unpublished')" class="o_ribbon_right small text-bg-light">Unpublished</div>
|
||||
</div>
|
||||
<div
|
||||
name="category_overlay"
|
||||
t-attf-class="p-3 bg-black-25 position-relative w-100 h-100 d-flex flex-column justify-content-between {{alignmentClass}}"
|
||||
>
|
||||
<h3 class="h4" t-out="category['name']"/>
|
||||
<a
|
||||
name="category_button"
|
||||
role="button"
|
||||
t-attf-href="/shop/category/{{category['id']}}"
|
||||
class="s_dynamic_category_button btn btn-primary align-self-start oe_unremovable"
|
||||
t-out="buttonText"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template
|
||||
id="dynamic_filter_template_product_public_category_clickable_items"
|
||||
inherit_id="website_sale.dynamic_filter_template_product_public_category_default"
|
||||
primary="True"
|
||||
>
|
||||
<div name="category_item" position="attributes">
|
||||
<attribute name="class" add="opacity-trigger-hover" separator=" "/>
|
||||
</div>
|
||||
<div name="category_overlay" position="attributes">
|
||||
<attribute name="t-attf-class" add="z-0" separator=" "/>
|
||||
</div>
|
||||
<h3 class="h4" position="before">
|
||||
<span
|
||||
class="o_category_filter position-absolute start-0 top-0 end-0 bottom-0 bg-black-25 transition-base z-n1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</h3>
|
||||
<h3 class="h4" position="attributes">
|
||||
<attribute name="class" add="mb-0" separator=" "/>
|
||||
</h3>
|
||||
<img name="category_image" position="attributes">
|
||||
<attribute name="class" add="transition-base" separator=" "/>
|
||||
</img>
|
||||
<a name="category_button" position="attributes">
|
||||
<attribute
|
||||
name="class"
|
||||
add="stretched-link h-0 p-0 opacity-0 o_not_editable o_not-animable"
|
||||
separator=" "
|
||||
/>
|
||||
</a>
|
||||
<a name="category_button" position="after">
|
||||
<p class="s_dynamic_category_arrow position-absolute end-0 bottom-0 mb-3 me-3">
|
||||
<i class="oi oi-arrow-right fa-lg"/>
|
||||
</p>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="test_01_admin_shop_tour" model="web_tour.tour">
|
||||
<field name="name">test_01_admin_shop_tour</field>
|
||||
<field name="sequence">130</field>
|
||||
</record>
|
||||
</odoo>
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
6740
odoo-bringout-oca-ocb-website_sale/website_sale/i18n/es_419.po
Normal file
6740
odoo-bringout-oca-ocb-website_sale/website_sale/i18n/es_419.po
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,19 +0,0 @@
|
|||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.5\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-26 08:17+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 08:17+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: es_BO\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: website_sale
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale.address_b2b
|
||||
msgid "TIN / VAT"
|
||||
msgstr "NIT"
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
6289
odoo-bringout-oca-ocb-website_sale/website_sale/i18n/ku.po
Normal file
6289
odoo-bringout-oca-ocb-website_sale/website_sale/i18n/ku.po
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
6290
odoo-bringout-oca-ocb-website_sale/website_sale/i18n/my.po
Normal file
6290
odoo-bringout-oca-ocb-website_sale/website_sale/i18n/my.po
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue