Initial commit: Sale packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:49 +02:00
commit 14e3d26998
6469 changed files with 2479670 additions and 0 deletions

View file

@ -0,0 +1,154 @@
Odoo e-Commerce
---------------
### Optimize sales with an awesome online store.
Odoo is an <a href="https://www.odoo.com/app/ecommerce">Open Source eCommerce</a>
unlike anything you have ever seen before. Get an awesome catalog of products
and great product description pages.
It's full-featured, integrated with your management software, fully
customizable and super easy.
Create Awesome Product Pages
----------------------------
Odoo's unique *'edit inline'* and building blocks approach makes product pages
creation surprisingly easy. "Want to change the price of a product? or put it
in bold? Want to add a banner for a specific product?" just click and change.
What you see is what you get. Really.
Drag & Drop well designed *'Building Blocks'* to create beautifull product
pages that your customer will love.
Increase Your Revenue Per Order
-------------------------------
The built-in cross-selling feature helps you offer extra products related to
what the shopper put in his cart. (e.g. accessories)
Odoo's upselling algorythm allows you to show visitors similar but more
expensive products than the one in view, with incentives.
The inline editing feature allows you to easily change a price, launch a
promotion or fine tune the description of a product, in a just a click.
A Clean Google Analytics Integration
------------------------------------
Get a clear visibility of your sales funnel. Odoo's Google Analytics trackers
are configured by default to track all kind of events related to shopping
carts, call-to-actions, etc.
As Odoo marketing tools (mass mailing, campaigns, etc) are also linked with
Google Analytics, you get a complete view of your business.
Target New Markets
------------------
Get your website translated in multiple languages with no effort. Odoo proposes
and propagates translations automatically across pages.
Our translation "on demand" features allows you to benefit from professional
translators to translate all your changes automatically. Just change any part
of your website (a new blog post, a page modification, product descriptions,
...) and the translated versions are updated automatically in around 32 hours.
Fine Tune Your Catalog
----------------------
Get full control on how you display your products in the catalog page:
promotional ribbons, related size of products, discounts, variants, grid/list
view, etc.
Edit any product inline to make your website evolve with your customer need.
Acquire New Customers
---------------------
SEO tools are ready to use, with no configuration required. Odoo suggests
keywords according to Google most searched terms, Google Analytics tracks your
shopping cart events, sitemap are created automatically for Google indexation,
etc.
We even do structured content automatically to promote your product and events
efficiently in Google.
Leverage Social Media
---------------------
Create new landing pages easily with the Odoo inline editing feature. Send
visitors of your different marketing campaigns to specific landing pages to
optimize conversions.
Manage a Reseller Network
-------------------------
Manage a reseller network to target new market, have local presences or broaden
your distribution. Give them access to your reseller portal for an efficient
collaboration.
Promote your resellers online, forward leads to resellers (with built-in
geolocalisation feature), define specific pricelists, launch a loyalty program
(offer specific discounts to your best customers or resellers), etc.
Benefit from the power of Odoo, in your online store: a powerfull tax engine,
flexible pricing structures, a real inventory management solution, a reseller
interface, support for products with different behaviours; physical goods,
events, services, variants and options, etc.
You don't need to interface with your warehouse, sales or accounting software.
Everything is integrated with Odoo. No pain, real time.
A Clean Checkout Process
------------------------
Convert most visitor interests into real orders with a clean checkout process
with a minimal number of steps and a great useability on every page.
Customize your checkout process to fit your business needs: payment modes,
delivery methods, cross-selling, special conditions, etc.
And much more...
----------------
### Online Sales
- Mobile Interface
- Sell products, events or services
- Flexible pricelists
- Product multi-variants
- Multiple stores
- Great checkout process
### Customer Service
- Customer Portal to track orders
- Assisted shopping with website live chats
- Returns management
- Advanced shipping rules
- Coupons or gift certificates
### Order Management
- Advanced warehouse management features
- Invoicing and accounting integration
- Mass mailing and customer segmentations
- Lead automation and marketing campaigns
- Persistent shopping cart
Fully Integrated With Other Apps
--------------------------------
### CMS
Easily create awesome websites with no technical knowledge required.
### Blogs
Write news, attract new visitors, build customer loyalty.
### Online Events
Schedule, organize, promote or sell events online; conferences, webinars, trainings, etc.

View file

@ -0,0 +1,28 @@
# -*- 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, {})
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)
companies = env['res.company'].search([])
for company in companies:
company.terms_type = 'html'
env['website'].search([]).auth_signup_uninvited = 'b2c'
def uninstall_hook(cr, registry):
''' 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']
multi_company_rules += pl_item_rule or env['ir.rule']
multi_company_rules.write({'active': True})

View file

@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'eCommerce',
'category': 'Website/Website',
'sequence': 50,
'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'],
'data': [
'security/ir.model.access.csv',
'security/website_sale.xml',
'data/data.xml',
'data/mail_template_data.xml',
'data/product_snippet_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',
'views/templates.xml',
'views/snippets/snippets.xml',
'views/snippets/s_add_to_cart.xml',
'views/snippets/s_dynamic_snippet_products.xml',
'views/res_config_settings_views.xml',
'views/digest_views.xml',
'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',
],
'demo': [
'data/demo.xml',
],
'installable': True,
'application': True,
'post_init_hook': '_post_init_hook',
'uninstall_hook': 'uninstall_hook',
'assets': {
'web.assets_frontend': [
'website_sale/static/src/js/tours/tour_utils.js',
'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/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/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_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',
],
'web._assets_primary_variables': [
'website_sale/static/src/scss/primary_variables.scss',
],
'web.assets_backend': [
'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.assets_wysiwyg': [
'website_sale/static/src/scss/website_sale.editor.scss',
'website_sale/static/src/snippets/s_dynamic_snippet_products/options.js',
'website_sale/static/src/snippets/s_add_to_cart/options.js',
'website_sale/static/src/js/website_sale.editor.js',
],
'website.assets_editor': [
'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',
],
'web.assets_common': [
'website_sale/static/src/js/tours/tour_utils.js',
],
'web.assets_tests': [
'website_sale/static/tests/**/*',
],
},
'license': 'LGPL-3',
}

View file

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import backend
from . import main
from . import variant

View file

@ -0,0 +1,183 @@
# -*- 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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.http import request
from odoo.addons.sale.controllers.variant import VariantController
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)
if request.website.google_analytics_key:
combination['product_tracking_info'] = request.env['product.template'].get_google_analytics_data(combination)
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
@http.route(auth="public")
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)

View file

@ -0,0 +1,193 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="menu_shop" model="website.menu">
<field name="name">Shop</field>
<field name="url">/shop</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence" type="int">20</field>
</record>
<record id="action_open_website" model="ir.actions.act_url">
<field name="name">Website Shop</field>
<field name="target">self</field>
<field name="url">/shop</field>
</record>
<record id="base.open_menu" model="ir.actions.todo">
<field name="action_id" ref="action_open_website"/>
<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>
</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>
</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>
</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>
</record>
<record id="sales_team.salesteam_website_sales" model="crm.team">
<field name="active" eval="True"/>
</record>
<record model="website" id="website.default_website">
<field name="salesteam_id" ref="sales_team.salesteam_website_sales"/>
</record>
<record model="product.pricelist" id="product.list0">
<field name="selectable" eval="True" />
<field name="website_id" eval="False"/>
</record>
</data>
<data>
<!-- Filters for Dynamic Filter -->
<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="domain">[('website_published', '=', True)]</field>
<field name="context">{'display_default_code': False, 'add2cart_rerender': False}</field>
<field name="sort">["create_date desc"]</field>
<field name="action_id" ref="website.action_website"/>
</record>
<!-- Action Server for Dynamic Filter -->
<record id="dynamic_snippet_latest_sold_products_action" model="ir.actions.server">
<field name="name">Recently Sold Products</field>
<field name="model_id" ref="model_product_product"/>
<field name="state">code</field>
<field name="code">
DynamicFilter = model.env['website.snippet.filter']
response = DynamicFilter._get_products('latest_sold', model.env.context)
</field>
</record>
<record id="dynamic_snippet_latest_viewed_products_action" model="ir.actions.server">
<field name="name">Recently Viewed Products</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)
for data in res_products:
data['_latest_viewed'] = True
response = res_products
</field>
</record>
<record id="dynamic_snippet_accessories_action" model="ir.actions.server">
<field name="name">Product Accessories</field>
<field name="model_id" ref="model_product_product"/>
<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)
</field>
</record>
<record id="dynamic_snippet_recently_sold_with_action" model="ir.actions.server">
<field name="name">Products Recently Sold With</field>
<field name="model_id" ref="model_product_product"/>
<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)
</field>
</record>
<record id="dynamic_snippet_alternative_products" model="ir.actions.server">
<field name="name">Alternative Products</field>
<field name="model_id" ref="model_product_product"/>
<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)
</field>
</record>
<!-- Dynamic Filter -->
<record id="dynamic_filter_newest_products" model="website.snippet.filter">
<field name="filter_id" ref="website_sale.dynamic_snippet_newest_products_filter"/>
<field name="field_names">display_name,description_sale,image_512</field>
<field name="limit" eval="16"/>
<field name="name">Newest Products</field>
</record>
<record id="dynamic_filter_latest_sold_products" model="website.snippet.filter">
<field name="action_server_id" ref="website_sale.dynamic_snippet_latest_sold_products_action"/>
<field name="field_names">display_name,description_sale,image_512</field>
<field name="limit" eval="16"/>
<field name="name">Recently Sold Products</field>
</record>
<record id="dynamic_filter_latest_viewed_products" model="website.snippet.filter">
<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>
</record>
<record id="dynamic_filter_cross_selling_accessories" model="website.snippet.filter">
<field name="action_server_id" ref="website_sale.dynamic_snippet_accessories_action"/>
<field name="field_names">display_name,description_sale,image_512</field>
<field name="limit" eval="16"/>
<field name="name">Accessories for Product</field>
<field name="product_cross_selling">True</field>
</record>
<record id="dynamic_filter_cross_selling_recently_sold_with" model="website.snippet.filter">
<field name="action_server_id" ref="website_sale.dynamic_snippet_recently_sold_with_action"/>
<field name="field_names">display_name,description_sale,image_512</field>
<field name="limit" eval="16"/>
<field name="name">Products Recently Sold With Product</field>
<field name="product_cross_selling">True</field>
</record>
<record id="dynamic_filter_cross_selling_alternative_products" model="website.snippet.filter">
<field name="action_server_id" ref="website_sale.dynamic_snippet_alternative_products"/>
<field name="field_names">display_name,description_sale,image_512</field>
<field name="limit" eval="16"/>
<field name="name">Alternative Products</field>
<field name="product_cross_selling">True</field>
</record>
<function model="ir.model.fields" name="formbuilder_whitelist">
<value>sale.order</value>
<value eval="[
'client_order_ref',
]"/>
</function>
<record id="base.model_res_partner" model="ir.model">
<field name="website_form_key">create_customer</field>
<field name="website_form_access">True</field>
<field name="website_form_label">Create a Customer</field>
</record>
<function model="ir.model.fields" name="formbuilder_whitelist">
<value>res.partner</value>
<value eval="[
'name', 'phone', 'email',
'city', 'zip', 'street', 'street2', 'state_id', 'country_id',
'vat', 'company_name'
]"/>
</function>
</data>
</odoo>

View file

@ -0,0 +1,782 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record model="website" id="website.website2">
<field name="salesteam_id" ref="sales_team.salesteam_website_sales"/>
</record>
<record id="product.product_attribute_2" model="product.attribute">
<field name="visibility">hidden</field>
</record>
<record id="product.product_product_24" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.product_product_5" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.product_product_12" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.product_product_10" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.product_product_13" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.product_product_25" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.consu_delivery_02" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.product_delivery_01" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.product_product_3" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.product_product_22" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.consu_delivery_03" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.product_product_27" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.product_delivery_02" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.product_product_16" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.consu_delivery_01" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.product_order_01" 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">
<div class="container">
<div class="row align-items-center">
<div class="pt16 pb16 col-lg-6">
<h2>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=""/>
</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">
<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=""/>
</div>
<div class="pt16 pb16 col-lg-6">
<h2>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>
</div>
</div>
</div>
</section>
</field>
</record>
<record id="product.product_product_6" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.product_product_7" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.product_product_8" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.product_product_9" model="product.product">
<field name="is_published" eval="True"/>
</record>
<record id="product.product_product_11" model="product.product">
<field name="is_published" eval="True"/>
<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"/>
</record>
<record id="public_category_furnitures" model="product.public.category">
<field name="name">Furnitures</field>
<field name="sequence">17</field>
<field name="image_1920" 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="image_1920" 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="image_1920" 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="image_1920" 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="image_1920" 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="image_1920" type="base64" file="website_sale/static/src/img/categories/lamps.jpg"/>
</record>
<record id="services" model="product.public.category">
<field name="name">Services</field>
<field name="sequence">25</field>
<field name="image_1920" 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="image_1920" type="base64" file="product/static/img/product_product_43-image.jpg"/>
</record>
<!-- subcategories -->
<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"/>
</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">18</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">19</field>
</record>
<record id="product.product_product_1_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('services')])]"/>
</record>
<record id="product.product_product_2_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('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')])]"/>
</record>
<record id="product.consu_delivery_03_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_desks')])]"/>
</record>
<record id="product.product_product_4_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_desks')])]"/>
</record>
<record id="product.product_product_5_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_desks')])]"/>
</record>
<record id="product.product_product_6_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_cabinets')])]"/>
</record>
<record id="product.product_product_7_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_boxes')])]"/>
</record>
<record id="product.product_product_8_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_desks')])]"/>
</record>
<record id="product.product_product_9_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_bins')])]"/>
</record>
<record id="product.product_product_10_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_cabinets')])]"/>
</record>
<record id="product.product_product_11_product_template" model="product.template">
<field name="website_sequence">9990</field>
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_furnitures_chairs')])]"/>
</record>
<record id="product.product_product_12_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_furnitures_chairs')])]"/>
</record>
<record id="product.product_product_13_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_desks')])]"/>
</record>
<record id="product.product_product_16_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_drawers')])]"/>
</record>
<record id="product.product_product_20_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_desks_components')])]"/>
</record>
<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">
<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">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_drawers')])]"/>
</record>
<record id="product.product_order_01_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_multimedia')])]"/>
</record>
<record id="product.consu_delivery_01_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_furnitures_couches')])]"/>
</record>
<record id="product.consu_delivery_02_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_desks')])]"/>
</record>
<record id="product.consu_delivery_03_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_desks')])]"/>
</record>
<record id="product.product_delivery_01_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_furnitures_chairs')])]"/>
</record>
<record id="product.product_delivery_02_product_template" model="product.template">
<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>
<record id="list_christmas" model="product.pricelist">
<field name="name">Christmas</field>
<field name="selectable" eval="False" />
<field name="website_id" ref="website.default_website" />
<field name="country_group_ids" eval="[(6,0,[ref('base.europe')])]" />
<field name="sequence">20</field>
</record>
<record id="item_christmas" model="product.pricelist.item">
<field name="pricelist_id" ref="list_christmas"/>
<field name="compute_price">formula</field>
<field name="base">list_price</field>
<field name="price_discount">20</field>
</record>
<record id="list_benelux" model="product.pricelist">
<field name="name">Benelux</field>
<field name="selectable" eval="False" />
<field name="website_id" ref="website.default_website" />
<field name="country_group_ids" eval="[(6,0,[ref('benelux')])]" />
<field name="sequence">2</field>
</record>
<record id="item_benelux" model="product.pricelist.item">
<field name="pricelist_id" ref="list_benelux"/>
<field name="compute_price">percentage</field>
<field name="base">list_price</field>
<field name="percent_price">10</field>
<field name="currency_id" ref="base.EUR"/>
</record>
<record id="list_europe" model="product.pricelist">
<field name="name">EUR</field>
<field name="selectable" eval="True" />
<field name="website_id" ref="website.default_website" />
<field name="country_group_ids" eval="[(6,0,[ref('base.europe')])]" />
<field name="sequence">3</field>
<field name="currency_id" ref="base.EUR"/>
</record>
<record id="item_europe" model="product.pricelist.item">
<field name="pricelist_id" ref="list_europe"/>
<field name="compute_price">formula</field>
<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)"/>
<field name="partner_id" ref="base.res_partner_3"/>
<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>
</record>
<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>
<record id="website_sale_order_2" model="sale.order">
<field name="create_date" eval="datetime.now() - timedelta(days=8)"/>
<field name="partner_id" ref="base.res_partner_3"/>
<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>
</record>
<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>
<record id="website_sale_order_3" model="sale.order">
<field name="create_date" eval="datetime.now() - timedelta(days=8)"/>
<field name="partner_id" ref="base.res_partner_3"/>
<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'))]"/>
<field name="state">sale</field>
</record>
<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>
<record id="website_sale_order_4" model="sale.order">
<field name="create_date" eval="datetime.now() - timedelta(days=8)"/>
<field name="partner_id" ref="base.res_partner_3"/>
<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>
</record>
<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>
<record id="website_sale_order_5" model="sale.order">
<field name="create_date" eval="datetime.now() - timedelta(days=8)"/>
<field name="partner_id" ref="base.res_partner_3"/>
<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>
</record>
<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>
<record id="website_sale_order_6" model="sale.order">
<field name="create_date" eval="datetime.now() - timedelta(days=8)"/>
<field name="partner_id" ref="base.res_partner_3"/>
<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>
</record>
<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>
<record id="website_sale_order_7" model="sale.order">
<field name="create_date" eval="datetime.now() - timedelta(days=8)"/>
<field name="partner_id" ref="base.res_partner_3"/>
<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="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>
</record>
<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>
<record id="website_sale_order_8" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<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="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'))]"/>
<field name="state">sale</field>
</record>
<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>
<record id="website_sale_order_9" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<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="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>
<!-- Active Carts -->
<record id="website_sale_order_10" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<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="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'))]"/>
</record>
<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>
<!-- Abandoned Carts -->
<record id="website_sale_order_11" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<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="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>
<!-- Payments to Capture -->
<record id="website_sale_order_13" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<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="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')"/>
<field name="state">sent</field>
</record>
<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>
<!-- Order to Invoice -->
<record id="website_sale_order_14" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<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="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>
<record id="website_sale_order_16" model="sale.order">
<field name="create_date" eval="datetime.now() - relativedelta(months=1)"/>
<field name="partner_id" ref="base.res_partner_3"/>
<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="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>
</record>
<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>
<record id="website_sale_order_17" model="sale.order">
<field name="create_date" eval="datetime.now() - relativedelta(months=1, days=2)"/>
<field name="partner_id" ref="base.res_partner_3"/>
<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="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>
<record id="website_sale_order_18" model="sale.order">
<field name="create_date" eval="datetime.now() - relativedelta(months=2)"/>
<field name="partner_id" ref="base.res_partner_3"/>
<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="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>
<!-- action_confirm for confirmation date -->
<function model="sale.order" name="action_confirm" eval="[[ref('website_sale_order_14')]]"/>
<record id="product_product_1_product_template" model="product.template">
<field name="name">Warranty</field>
<field name="list_price">20.0</field>
<field name="website_sequence">9980</field>
<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="invoice_policy">delivery</field>
<field name="public_categ_ids" eval="[(6, 0, [ref('website_sale.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')])]"/>
</record>
<!-- Handle automatically created product.template.attribute.value -->
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'website_sale.product_1_attribute_3_value_1',
'record': obj().env.ref('website_sale.product_1_attribute_3_product_template_attribute_line').product_template_value_ids[0],
'noupdate': True,
}, {
'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,
}]"/>
</function>
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'website_sale.product_product_1',
'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_1')),
'noupdate': True,
}, {
'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,
},]"/>
</function>
<record id="product_product_1" model="product.product">
<field name="default_code">SERV_125889</field>
</record>
<record id="product_product_1b" model="product.product">
<field name="default_code">SERV_125890</field>
</record>
<record id="website_sale.product_1_attribute_3_value_2" model="product.template.attribute.value">
<field name="price_extra">18.00</field>
</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="create_uid" ref="base.user_demo"/>
<field name="user_id" ref="base.user_demo"/>
</record>
<record id="website_sale_activity_2" model="mail.activity">
<field name="res_id" ref="website_sale.website_sale_order_8"/>
<field name="res_model_id" ref="sale.model_sale_order"/>
<field name="activity_type_id" ref="mail.mail_activity_data_todo"/>
<field name="date_deadline" eval="(DateTime.today() + relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')" />
<field name="summary">Follow-up on satisfaction</field>
<field name="create_uid" ref="base.user_demo"/>
<field name="user_id" ref="base.user_demo"/>
</record>
<record id="website_sale_activity_3" model="mail.activity">
<field name="res_id" ref="website_sale.website_sale_order_9"/>
<field name="res_model_id" ref="sale.model_sale_order"/>
<field name="activity_type_id" ref="mail.mail_activity_data_todo"/>
<field name="date_deadline" eval="(DateTime.today() + relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')" />
<field name="summary">Confirm quote</field>
<field name="create_uid" ref="base.user_demo"/>
<field name="user_id" ref="base.user_demo"/>
</record>
<record id="website_sale_activity_5" model="mail.activity">
<field name="res_id" ref="website_sale.website_sale_order_11"/>
<field name="res_model_id" ref="sale.model_sale_order"/>
<field name="activity_type_id" ref="mail.mail_activity_data_email"/>
<field name="date_deadline" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')" />
<field name="summary">Send updated pricelist</field>
<field name="create_uid" ref="base.user_demo"/>
<field name="user_id" ref="base.user_demo"/>
</record>
</odoo>

View file

@ -0,0 +1,8 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<data noupdate="1">
<record id="digest.digest_digest_default" model="digest.digest">
<field name="kpi_website_sale_total">True</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="ir_cron_send_availability_email" model="ir.cron">
<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>
</record>
</odoo>

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="mail_template_sale_cart_recovery" model="mail.template">
<field name="name">Ecommerce: Cart Recovery</field>
<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="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;">
<tbody>
<!-- CONTENT -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 0px 0px 0px; border-collapse:separate;">
<tr><td valign="top" style="font-size: 13px;">
<h1 style="color:#A9A9A9;">THERE'S SOMETHING IN YOUR CART.</h1>
Would you like to complete your purchase?<br/><br/>
<t t-if="object.order_line">
<t t-foreach="object.website_order_line" t-as="line">
<hr/>
<table width="100%">
<tr>
<td style="padding: 10px; width:150px;">
<img t-attf-src="/web/image/product.product/{{ line.product_id.id }}/image_128" style="width: 100px; height: 100px; object-fit: contain;" alt="Product image"></img>
</td>
<td>
<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>
</td>
</tr>
</table>
</t>
<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 }}"
target="_blank"
style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
Resume order
</a>
</div>
<t t-set="company" t-value="object.company_id or object.user_id.company_id or user.company_id"/>
<div style="text-align: center;"><strong>Thank you for shopping with <t t-out="company.name or ''">My Company (San Francisco)</t>!</strong></div>
</td></tr>
</table>
</td>
</tr>
</tbody>
</table>
</field>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="auto_delete" eval="False"/>
</record>
</data>
</odoo>

View file

@ -0,0 +1,406 @@
<?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">
<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>
<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"/>
</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>
</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')">
<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>
<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"/>
</button>
</div>
</div>
</div>
</div>
</div>
</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-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"/>
</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>
</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 one or more lines are too long

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

View file

@ -0,0 +1,19 @@
#
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

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_move
from . import crm_team
from . import digest
from . import ir_http
from . import payment_provider
from . import product_attribute
from . import product_image
from . import product_pricelist
from . import product_product
from . import product_public_category
from . import product_ribbon
from . import product_tag
from . import product_template
from . import res_company
from . import res_config_settings
from . import res_country
from . import res_partner
from . import sale_order
from . import sale_order_line
from . import website
from . import website_base_unit
from . import website_snippet_filter
from . import website_visitor

View file

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.tools.sql import column_exists, create_column
class AccountMove(models.Model):
_inherit = 'account.move'
website_id = fields.Many2one(
'website', compute='_compute_website_id', string='Website',
help='Website through which this invoice was created for eCommerce orders.',
store=True, readonly=True, tracking=True)
def _auto_init(self):
if not column_exists(self.env.cr, "account_move", "website_id"):
# Creating the column via `_auto_init` prevents a MemoryError in databases where many
# invoices exist when `website_sale` is installed, as it skips the computation of the
# `website_id` field.
create_column(self.env.cr, "account_move", "website_id", "int4")
super()._auto_init()
def preview_invoice(self):
action = super().preview_invoice()
if action['url'].startswith('/'):
# URL should always be relative, safety check
action['url'] = f'/@{action["url"]}'
return action
@api.depends('partner_id') # Dummy depends to trigger compute, will be dropped in master
def _compute_website_id(self):
for move in self:
source_websites = move.line_ids.sale_line_ids.order_id.website_id
if len(source_websites) == 1:
move.website_id = source_websites
else:
move.website_id = False

View file

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from dateutil.relativedelta import relativedelta
from odoo import fields,api, models, _
from odoo.exceptions import UserError, ValidationError
class CrmTeam(models.Model):
_inherit = "crm.team"
website_ids = fields.One2many('website', 'salesteam_id', string='Websites')
abandoned_carts_count = fields.Integer(
compute='_compute_abandoned_carts',
string='Number of Abandoned Carts', readonly=True)
abandoned_carts_amount = fields.Integer(
compute='_compute_abandoned_carts',
string='Amount of Abandoned Carts', readonly=True)
def _compute_abandoned_carts(self):
# abandoned carts to recover are draft sales orders that have no order lines,
# a partner other than the public user, and created over an hour ago
# and the recovery mail was not yet sent
counts = {}
amounts = {}
website_teams = self.filtered(lambda team: team.website_ids)
if website_teams:
abandoned_carts_data = self.env['sale.order']._read_group([
('is_abandoned_cart', '=', True),
('cart_recovery_email_sent', '=', False),
('team_id', 'in', website_teams.ids),
], ['amount_total', 'team_id'], ['team_id'])
counts = {data['team_id'][0]: data['team_id_count'] for data in abandoned_carts_data}
amounts = {data['team_id'][0]: data['amount_total'] for data in abandoned_carts_data}
for team in self:
team.abandoned_carts_count = counts.get(team.id, 0)
team.abandoned_carts_amount = amounts.get(team.id, 0)
def get_abandoned_carts(self):
self.ensure_one()
return {
'name': _('Abandoned Carts'),
'type': 'ir.actions.act_window',
'view_mode': 'tree,form',
'domain': [('is_abandoned_cart', '=', True)],
'search_view_id': [self.env.ref('sale.sale_order_view_search_inherit_sale').id],
'context': {
'search_default_team_id': self.id,
'default_team_id': self.id,
'search_default_recovery_email': 1,
'create': False
},
'res_model': 'sale.order',
'help': _('''<p class="o_view_nocontent_smiling_face">
You can find all abandoned carts here, i.e. the carts generated by your website's visitors from over an hour ago that haven't been confirmed yet.</p>
<p>You should send an email to the customers to encourage them!</p>
'''),
}

View file

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
from odoo.exceptions import AccessError
class Digest(models.Model):
_inherit = 'digest.digest'
kpi_website_sale_total = fields.Boolean('eCommerce Sales')
kpi_website_sale_total_value = fields.Monetary(compute='_compute_kpi_website_sale_total_value')
def _compute_kpi_website_sale_total_value(self):
if not self.env.user.has_group('sales_team.group_sale_salesman_all_leads'):
raise AccessError(_("Do not have access, skip this data for user's digest email"))
for record in self:
start, end, company = record._get_kpi_compute_parameters()
confirmed_website_sales = self.env['sale.order'].search([
('date_order', '>=', start),
('date_order', '<', end),
('state', 'not in', ['draft', 'cancel', 'sent']),
('website_id', '!=', False),
('company_id', '=', company.id)
])
record.kpi_website_sale_total_value = sum(
sale.currency_id._convert(sale.amount_total, company.currency_id, company, sale.date_order)
for sale in confirmed_website_sales
)
def _compute_kpis_actions(self, company, user):
res = super(Digest, self)._compute_kpis_actions(company, user)
res['kpi_website_sale_total'] = 'website.backend_dashboard&menu_id=%s' % self.env.ref('website.menu_website_configuration').id
return res

View file

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo.http import request
class IrHttp(models.AbstractModel):
_inherit = 'ir.http'
@classmethod
def _pre_dispatch(cls, rule, args):
super()._pre_dispatch(rule, args)
affiliate_id = request.httprequest.args.get('affiliate_id')
if affiliate_id:
request.session['affiliate_id'] = int(affiliate_id)

View file

@ -0,0 +1,25 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class PaymentProvider(models.Model):
_inherit = 'payment.provider'
@api.model
def _get_compatible_providers(self, *args, website_id=None, **kwargs):
""" Override of payment to only return providers matching website-specific criteria.
In addition to the base criteria, the website must either not be set or be the same as the
one provided in the kwargs.
:param int website_id: The provided website, as a `website` id
:return: The compatible providers
:rtype: recordset of `payment.provider`
"""
providers = super()._get_compatible_providers(*args, website_id=website_id, **kwargs)
if website_id:
providers = providers.filtered(
lambda p: not p.website_id or p.website_id.id == website_id
)
return providers

View file

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import OrderedDict
from odoo import models, fields
class ProductAttribute(models.Model):
_inherit = 'product.attribute'
visibility = fields.Selection([('visible', 'Visible'), ('hidden', 'Hidden')], default='visible')
class ProductTemplateAttributeLine(models.Model):
_inherit = 'product.template.attribute.line'
def _prepare_single_value_for_display(self):
"""On the product page group together the attribute lines that concern
the same attribute and that have only one value each.
Indeed those are considered informative values, they do not generate
choice for the user, so they are displayed below the configurator.
The returned attributes are ordered as they appear in `self`, so based
on the order of the attribute lines.
"""
single_value_lines = self.filtered(lambda ptal: len(ptal.value_ids) == 1)
single_value_attributes = OrderedDict([(pa, self.env['product.template.attribute.line']) for pa in single_value_lines.attribute_id])
for ptal in single_value_lines:
single_value_attributes[ptal.attribute_id] |= ptal
return single_value_attributes

View file

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError
from odoo.addons.web_editor.tools import get_video_embed_code, get_video_thumbnail
class ProductImage(models.Model):
_name = 'product.image'
_description = "Product Image"
_inherit = ['image.mixin']
_order = 'sequence, id'
name = fields.Char("Name", required=True)
sequence = fields.Integer(default=10)
image_1920 = fields.Image()
product_tmpl_id = fields.Many2one('product.template', "Product Template", index=True, ondelete='cascade')
product_variant_id = fields.Many2one('product.product', "Product Variant", index=True, ondelete='cascade')
video_url = fields.Char('Video URL',
help='URL of a video for showcasing your product.')
embed_code = fields.Html(compute="_compute_embed_code", sanitize=False)
can_image_1024_be_zoomed = fields.Boolean("Can Image 1024 be zoomed", compute='_compute_can_image_1024_be_zoomed', store=True)
@api.depends('image_1920', 'image_1024')
def _compute_can_image_1024_be_zoomed(self):
for image in self:
image.can_image_1024_be_zoomed = image.image_1920 and tools.is_image_size_above(image.image_1920, image.image_1024)
@api.onchange('video_url')
def _onchange_video_url(self):
if not self.image_1920:
thumbnail = get_video_thumbnail(self.video_url)
self.image_1920 = thumbnail and base64.b64encode(thumbnail) or False
@api.depends('video_url')
def _compute_embed_code(self):
for image in self:
image.embed_code = get_video_embed_code(image.video_url) or False
@api.constrains('video_url')
def _check_valid_video_url(self):
for image in self:
if image.video_url and not image.embed_code:
raise ValidationError(_("Provided video URL for '%s' is not valid. Please enter a valid video URL.", image.name))
@api.model_create_multi
def create(self, vals_list):
"""
We don't want the default_product_tmpl_id from the context
to be applied if we have a product_variant_id set to avoid
having the variant images to show also as template images.
But we want it if we don't have a product_variant_id set.
"""
context_without_template = self.with_context({k: v for k, v in self.env.context.items() if k != 'default_product_tmpl_id'})
normal_vals = []
variant_vals_list = []
for vals in vals_list:
if vals.get('product_variant_id') and 'default_product_tmpl_id' in self.env.context:
variant_vals_list.append(vals)
else:
normal_vals.append(vals)
return super().create(normal_vals) + super(ProductImage, context_without_template).create(variant_vals_list)

View file

@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError, UserError
from odoo.addons.website.models import ir_http
class ProductPricelist(models.Model):
_inherit = "product.pricelist"
def _default_website(self):
""" Find the first company's website, if there is one. """
company_id = self.env.company.id
if self._context.get('default_company_id'):
company_id = self._context.get('default_company_id')
domain = [('company_id', '=', company_id)]
return self.env['website'].search(domain, limit=1)
website_id = fields.Many2one('website', string="Website", ondelete='restrict', default=_default_website, domain="[('company_id', '=?', company_id)]")
code = fields.Char(string='E-commerce Promotional Code', groups="base.group_user")
selectable = fields.Boolean(help="Allow the end user to choose this price list")
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('company_id') and not vals.get('website_id'):
# l10n modules install will change the company currency, creating a
# pricelist for that currency. Do not use user's company in that
# case as module install are done with OdooBot (company 1)
# YTI FIXME: The fix is not at the correct place
# It be set when we actually create the pricelist
self = self.with_context(default_company_id=vals['company_id'])
pricelists = super().create(vals_list)
pricelists and pricelists.clear_caches()
return pricelists
def write(self, data):
res = super(ProductPricelist, self).write(data)
if data.keys() & {'code', 'active', 'website_id', 'selectable', 'company_id'}:
self._check_website_pricelist()
self and self.clear_caches()
return res
def unlink(self):
res = super(ProductPricelist, self).unlink()
self._check_website_pricelist()
self and self.clear_caches()
return res
def _get_partner_pricelist_multi_search_domain_hook(self, company_id):
domain = super()._get_partner_pricelist_multi_search_domain_hook(company_id)
website = ir_http.get_request_website()
if website:
domain += self._get_website_pricelists_domain(website)
return domain
def _get_partner_pricelist_multi_filter_hook(self):
res = super()._get_partner_pricelist_multi_filter_hook()
website = ir_http.get_request_website()
if website:
res = res.filtered(lambda pl: pl._is_available_on_website(website))
return res
def _check_website_pricelist(self):
for website in self.env['website'].search([]):
# sudo() to be able to read pricelists/website from another company
if not website.sudo().pricelist_ids:
raise UserError(_("With this action, '%s' website would not have any pricelist available.") % (website.name))
def _is_available_on_website(self, website):
""" To be able to be used on a website, a pricelist should either:
- Have its `website_id` set to current website (specific pricelist).
- Have no `website_id` set and should be `selectable` (generic pricelist)
or should have a `code` (generic promotion).
- Have no `company_id` or a `company_id` matching its website one.
Note: A pricelist without a website_id, not selectable and without a
code is a backend pricelist.
Change in this method should be reflected in `_get_website_pricelists_domain`.
"""
self.ensure_one()
if self.company_id and self.company_id != website.company_id:
return False
return self.website_id.id == website.id or (not self.website_id and (self.selectable or self.sudo().code))
def _is_available_in_country(self, country_code):
self.ensure_one()
if not country_code or not self.country_group_ids:
return True
return country_code in self.country_group_ids.country_ids.mapped('code')
def _get_website_pricelists_domain(self, website):
''' Check above `_is_available_on_website` for explanation.
Change in this method should be reflected in `_is_available_on_website`.
'''
return [
('active', '=', True),
('company_id', 'in', [False, website.company_id.id]),
'|', ('website_id', '=', website.id),
'&', ('website_id', '=', False),
'|', ('selectable', '=', True), ('code', '!=', False),
]
@api.constrains('company_id', 'website_id')
def _check_websites_in_company(self):
'''Prevent misconfiguration multi-website/multi-companies.
If the record has a company, the website should be from that company.
'''
for record in self.filtered(lambda pl: pl.website_id and pl.company_id):
if record.website_id.company_id != record.company_id:
raise ValidationError(_("""Only the company's websites are allowed.\nLeave the Company field empty or select a website from that company."""))

View file

@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class Product(models.Model):
_inherit = "product.product"
_mail_post_access = 'read'
website_id = fields.Many2one(related='product_tmpl_id.website_id', readonly=False)
product_variant_image_ids = fields.One2many('product.image', 'product_variant_id', string="Extra Variant Images")
website_url = fields.Char('Website URL', compute='_compute_product_website_url', help='The full URL to access the document through the website.')
base_unit_count = fields.Float('Base Unit Count', required=True, default=1, help="Display base unit price on your eCommerce pages. Set to 0 to hide it for this product.")
base_unit_id = fields.Many2one('website.base.unit', string='Custom Unit of Measure', help="Define a custom unit to display in the price per unit of measure field.")
base_unit_price = fields.Monetary("Price Per Unit", currency_field="currency_id", compute="_compute_base_unit_price")
base_unit_name = fields.Char(compute='_compute_base_unit_name', help='Displays the custom unit for the products if defined or the selected unit of measure otherwise.')
def _get_base_unit_price(self, price):
self.ensure_one()
return self.base_unit_count and price / self.base_unit_count
@api.depends('lst_price', 'base_unit_count')
def _compute_base_unit_price(self):
for product in self:
if not product.id:
product.base_unit_price = 0
else:
product.base_unit_price = product._get_base_unit_price(product.lst_price)
@api.depends('uom_name', 'base_unit_id')
def _compute_base_unit_name(self):
for product in self:
product.base_unit_name = product.base_unit_id.name or product.uom_name
@api.constrains('base_unit_count')
def _check_base_unit_count(self):
if any(product.base_unit_count < 0 for product in self):
raise ValidationError(_('The value of Base Unit Count must be greater than 0. Use 0 to hide the price per unit on this product.'))
@api.depends_context('lang')
@api.depends('product_tmpl_id.website_url', 'product_template_attribute_value_ids')
def _compute_product_website_url(self):
for product in self:
attributes = ','.join(str(x) for x in product.product_template_attribute_value_ids.ids)
product.website_url = "%s#attr=%s" % (product.product_tmpl_id.website_url, attributes)
def _prepare_variant_values(self, combination):
variant_dict = super()._prepare_variant_values(combination)
variant_dict['base_unit_count'] = self.base_unit_count
return variant_dict
def website_publish_button(self):
self.ensure_one()
return self.product_tmpl_id.website_publish_button()
def open_website_url(self):
self.ensure_one()
res = self.product_tmpl_id.open_website_url()
res['url'] = self.website_url
return res
def _get_images(self):
"""Return a list of records implementing `image.mixin` to
display on the carousel on the website for this variant.
This returns a list and not a recordset because the records might be
from different models (template, variant and image).
It contains in this order: the main image of the variant (which will fall back on the main
image of the template, if unset), the Variant Extra Images, and the Template Extra Images.
"""
self.ensure_one()
variant_images = list(self.product_variant_image_ids)
template_images = list(self.product_tmpl_id.product_template_image_ids)
return [self] + variant_images + template_images
def _website_show_quick_add(self):
website = self.env['website'].get_current_website()
return self.sale_ok and (not website.prevent_zero_price_sale or self._get_contextual_price())
def _is_add_to_cart_allowed(self):
self.ensure_one()
return self.user_has_groups('base.group_system') or (self.active and self.sale_ok and self.website_published)
def _get_contextual_price_tax_selection(self):
self.ensure_one()
fpos_id = self.env['website'].sudo()._get_current_fiscal_position_id(self.env.user.partner_id)
fiscal_position_sudo = self.env['account.fiscal.position'].sudo().browse(fpos_id)
product_taxes = self.sudo().taxes_id.filtered(lambda x: x.company_id == self.env.company)
return self.env['product.template']._price_with_tax_computed(
self._get_contextual_price(),
product_taxes,
fiscal_position_sudo.map_tax(product_taxes),
self.env.company.id,
self.env['product.template']._get_contextual_pricelist(),
self,
self.env.user.partner_id,
)

View file

@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.tools.translate import html_translate
class ProductPublicCategory(models.Model):
_name = "product.public.category"
_inherit = [
'website.seo.metadata',
'website.multi.mixin',
'website.searchable.mixin',
'image.mixin',
]
_description = "Website Product Category"
_parent_store = True
_order = "sequence, name, id"
def _default_sequence(self):
cat = self.search([], limit=1, order="sequence DESC")
if cat:
return cat.sequence + 5
return 10000
name = fields.Char(required=True, translate=True)
parent_id = fields.Many2one('product.public.category', string='Parent Category', index=True, ondelete="cascade")
parent_path = fields.Char(index=True, unaccent=False)
child_id = fields.One2many('product.public.category', 'parent_id', string='Children Categories')
parents_and_self = fields.Many2many('product.public.category', compute='_compute_parents_and_self')
sequence = fields.Integer(help="Gives the sequence order when displaying a list of product categories.", index=True, default=_default_sequence)
website_description = fields.Html('Category Description', sanitize_overridable=True, sanitize_attributes=False, translate=html_translate, sanitize_form=False)
product_tmpl_ids = fields.Many2many('product.template', relation='product_public_category_product_template_rel')
@api.constrains('parent_id')
def check_parent_id(self):
if not self._check_recursion():
raise ValueError(_('Error ! You cannot create recursive categories.'))
def name_get(self):
res = []
for category in self:
res.append((category.id, " / ".join(category.parents_and_self.mapped('name'))))
return res
def _compute_parents_and_self(self):
for category in self:
if category.parent_path:
category.parents_and_self = self.env['product.public.category'].browse([int(p) for p in category.parent_path.split('/')[:-1]])
else:
category.parents_and_self = category
@api.model
def _search_get_detail(self, website, order, options):
with_description = options['displayDescription']
search_fields = ['name']
fetch_fields = ['id', 'name']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
'website_url': {'name': 'url', 'type': 'text', 'truncate': False},
}
if with_description:
search_fields.append('website_description')
fetch_fields.append('website_description')
mapping['description'] = {'name': 'website_description', 'type': 'text', 'match': True, 'html': True}
return {
'model': 'product.public.category',
'base_domain': [website.website_domain()],
'search_fields': search_fields,
'fetch_fields': fetch_fields,
'mapping': mapping,
'icon': 'fa-folder-o',
'order': 'name desc, id desc' if 'name desc' in order else 'name asc, id desc',
}
def _search_render_results(self, fetch_fields, mapping, icon, limit):
results_data = super()._search_render_results(fetch_fields, mapping, icon, limit)
for data in results_data:
data['url'] = '/shop/category/%s' % data['id']
return results_data

Some files were not shown because too many files have changed in this diff Show more