Files
design-themes/theme_prime/controllers/main.py
T

1052 lines
61 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (c) 2019-Present Droggol Infotech Private Limited. (<https://www.droggol.com/>)
import base64
import io
import json
import re
import string
from collections import defaultdict
import datetime
try:
from werkzeug.utils import send_file
except ImportError:
from odoo.tools._vendor.send_file import send_file
from odoo import _, http
from odoo.addons.website.controllers.main import Website
from odoo.addons.website_sale.controllers.main import WebsiteSale
from odoo.addons.website_sale_wishlist.controllers.main import WebsiteSaleWishlist
from odoo.http import request
from odoo.osv import expression
from odoo.tools import html_escape, file_path, file_open
from odoo.tools.mimetypes import guess_mimetype
class ThemePrimeWebsiteSale(WebsiteSale):
def _get_search_domain(self, search, category, attrib_values, search_in_description=True, search_rating=True):
""" Overrided method used to apply extra filters.
Extra parameters are added to skip some filters (Used for attribute count)
:param search_rating: if false rating domain will not be added
"""
domains = super()._get_search_domain(search, category, attrib_values, search_in_description)
# Hide out of stock
if request.httprequest.args.get('hide_out_of_stock'):
domains = expression.AND([domains, ['|', '|', ('is_storable', '=', False), ('allow_out_of_stock_order', '=', True), '&', ('dr_free_qty', '>', 0), ('allow_out_of_stock_order', '=', False)]])
# Rating
ratings = request.httprequest.args.getlist('rating')
if ratings and search_rating:
result = request.env['rating.rating'].sudo().read_group([('res_model', '=', 'product.template')], ['rating:avg'], groupby=['res_id'], lazy=False)
rating_product_ids = []
for rating in ratings:
rating_product_ids.extend([item['res_id'] for item in result if item['rating'] >= int(rating)])
if rating_product_ids:
domains = expression.AND([domains, [('id', 'in', rating_product_ids)]])
else:
domains = expression.AND([domains, [('id', 'in', [])]])
return domains
def _get_search_options(self, category=None, attrib_values=None, tags=None, pricelist=None, min_price=0.0, max_price=0.0, conversion_rate=1, **post):
res = super()._get_search_options(category=category, attrib_values=attrib_values, tags=tags, pricelist=pricelist, min_price=min_price, max_price=max_price, conversion_rate=conversion_rate, **post)
if request.httprequest.args.get('hide_out_of_stock'):
res['hide_out_of_stock'] = request.httprequest.args.get('hide_out_of_stock')
if post.get('rating') != 'skip' and request.httprequest.args.getlist('rating'):
res['rating'] = request.httprequest.args.getlist('rating')
return res
def _shop_get_query_url_kwargs(self, category, search, min_price, max_price, attrib=None, order=None, tags=None, **post):
result = super()._shop_get_query_url_kwargs(category, search, min_price, max_price, attrib=attrib, order=order, tags=tags, **post)
result.update(
view_mode=post.get('view_mode'),
hide_out_of_stock=post.get('hide_out_of_stock'),
rating=post.get('rating'),
)
return result
@http.route()
def shop(self, page=0, category=None, search='', min_price=0.0, max_price=0.0, ppg=False, **post):
response = super().shop(page=page, category=category, search=search, min_price=min_price, max_price=max_price, ppg=ppg, **post)
try:
min_price = float(min_price)
except ValueError:
min_price = 0
try:
max_price = float(max_price)
except ValueError:
max_price = 0
theme_id = request.website.sudo().theme_id
if theme_id and theme_id.name.startswith('theme_prime'):
ProductTemplate = request.env['product.template']
request_args = request.httprequest.args
attrib_list = request_args.getlist('attrib')
attrib_values = [[int(x) for x in v.split('-')] for v in attrib_list if v]
attributes_ids = [v[0] for v in attrib_values]
post['tags'] = response.qcontext.get('tags')
fuzzy_search_term = response.qcontext.get('search') or search
pricelist, category = response.qcontext.get('pricelist'), response.qcontext.get('category')
if request.website._get_dr_theme_config('json_shop_filters')['show_category_count']:
# Categories
domain = self._prepare_filters_domain(search=fuzzy_search_term, attrib_values=attrib_values, pricelist=pricelist, min_price=min_price, max_price=max_price, **post)
response.qcontext.update(get_category_count=ProductTemplate._get_product_category_count(domain=domain))
if request.website._get_dr_theme_config('json_shop_filters')['show_attribute_count'] or request.website._get_dr_theme_config('json_shop_filters')['hide_extra_attrib_value']:
# Attributes
domain = self._prepare_filters_domain(search=fuzzy_search_term, category=category, pricelist=pricelist, min_price=min_price, max_price=max_price, **post)
response.qcontext.update(get_attrib_count=ProductTemplate._get_product_attrib_count(attrib_values, domain=domain))
# Rating
domain = self._prepare_filters_domain(search=fuzzy_search_term, attrib_values=attrib_values, category=category, pricelist=pricelist, min_price=min_price, max_price=max_price, **{**post, 'rating': 'skip'})
response.qcontext.update(get_ratings_count=ProductTemplate._get_product_rating_count(domain=domain))
selected_ratings = [int(x) for x in request_args.getlist('rating')]
selected_hide_out_of_stock = request_args.get('hide_out_of_stock')
_config_shop_layout = request.website._get_dr_theme_config('json_shop_layout')
if _config_shop_layout.get('layout') == 'prime':
response.template = 'theme_prime.shop_layout'
response.qcontext.update(
_config_shop_layout=_config_shop_layout,
_config_product_item=request.website._get_dr_theme_config('json_shop_product_item'),
_config_shop_filters=request.website._get_dr_theme_config('json_shop_filters'),
_config_category_pills=request.website._get_dr_theme_config('json_shop_category_pills'),
view_mode=request_args.get('view_mode', _config_shop_layout.get('default_view_mode')),
page=page,
attributes_ids=attributes_ids,
selected_ratings=selected_ratings,
selected_hide_out_of_stock=selected_hide_out_of_stock,
selected_attributes=response.qcontext.get('attrib_set') or request_args.get('min_price') or request_args.get('max_price') or response.qcontext.get('tags') or selected_ratings or selected_hide_out_of_stock
)
return response
def _prepare_filters_domain(self, search=None, pricelist=None, category=None, attrib_values=None, min_price=None, max_price=None, **post):
options = self._get_search_options(category=category, attrib_values=attrib_values, pricelist=pricelist, min_price=min_price, max_price=max_price, **post)
search_detail = request.website._search_get_details('products_only', None, options)[0]
search_fields = search_detail['search_fields']
base_domain = search_detail['base_domain']
return request.env['product.template']._search_build_domain(base_domain, search, search_fields, search_detail.get('search_extra'))
def _prepare_product_values(self, product, category, search, **kwargs):
res = super()._prepare_product_values(product, category, search, **kwargs)
if request.website._get_dr_theme_config('bool_show_products_nav'):
ProductTemplate = request.env['product.template']
res['prev_product_id'] = ProductTemplate.search([('website_sequence', '<', product.website_sequence)] + request.website.website_domain(), limit=1, order='website_sequence desc')
res['next_product_id'] = ProductTemplate.search([('website_sequence', '>', product.website_sequence)] + request.website.website_domain(), limit=1, order='website_sequence')
return res
@http.route()
def cart(self, access_token=None, revive='', **post):
res = super().cart(access_token=access_token, revive=revive, **post)
if post.get('type') == 'tp_cart_sidebar_request':
order = request.website.sale_get_order()
if order and order.state != 'draft':
request.session['sale_order_id'] = None
return request.render('theme_prime.cart_sidebar', {'order': order}, headers={'Cache-Control': 'no-cache'})
return res
@http.route('/theme_prime/get_search_sidebar', type='http', auth='public', website=True, sitemap=False)
def search_sidebar(self, **post):
return request.render('theme_prime.search_sidebar')
@http.route('/theme_prime/get_categories_list', type='json', auth='public', website=True, sitemap=False)
def _get_categories_list(self, **post):
return {
'categories': request.env['product.public.category'].search_read(request.website.website_domain(), ['name', 'parent_id', 'child_id']),
'category_count': request.env['product.template']._get_product_category_count(domain=request.website.website_domain()) if request.website._get_dr_theme_config('json_sidebar_config').get('category_sidebar_show_count') else {},
}
@http.route('/theme_prime/get_quick_view_html', type='json', auth='public', website=True)
def get_quick_view_html(self, options, **kwargs):
IrUiView = request.env['ir.ui.view']
productID = options.get('productID')
variantID = options.get('variantID')
if variantID:
productID = request.env['product.product'].browse(variantID).product_tmpl_id.id
domain = expression.AND([request.website.sale_product_domain(), [('id', '=', productID)]])
product = request.env['product.template'].search(domain, limit=1)
# If moved to another website or delete
if not product:
return False
values = self._prepare_product_values(product, category='', search='', **kwargs)
result = request.website.get_theme_prime_shop_config()
values.update(result)
values['d_url_root'] = request.httprequest.url_root[:-1]
if options.get('variant_selector'):
values['auto_add_product'] = product.product_variant_count == 1
return IrUiView._render_template('theme_prime.product_variant_selector_dialog', values=values)
if options.get('right_panel'):
return IrUiView._render_template('theme_prime.tp_product_right_panel', values=values)
return IrUiView._render_template('theme_prime.tp_product_quick_view', values=values)
@http.route()
def cart_update_json(
self, product_id, line_id=None, add_qty=None, set_qty=None, display=True,
product_custom_attribute_values=None, no_variant_attribute_values=None, **kw
):
response = super().cart_update_json(
product_id, line_id=line_id, add_qty=add_qty, set_qty=set_qty, display=display,
product_custom_attribute_values=product_custom_attribute_values, no_variant_attribute_values=no_variant_attribute_values, **kw)
if kw.get('dr_cart_flow') and response:
sale_order = request.website.sale_get_order(force_create=True)
product = request.env['product.product'].browse(int(product_id))
response.update({
'cart_quantity': sale_order.cart_quantity,
'product_name': product.name,
'product_id': int(product_id),
'accessory_product_ids': product.accessory_product_ids and product.accessory_product_ids.mapped('product_tmpl_id').ids or []
})
return response
@http.route()
def clear_cart(self):
# JAT: Return formatted order amount on clear cart(in sidebar) to properly update cart total in headers.
super().clear_cart()
order = request.website.sale_get_order()
return {
'notification_info': {
'order_amount_html': request.env['ir.qweb.field.monetary'].value_to_html(order.amount_total, {'display_currency': order.pricelist_id.currency_id or request.website.company_id.currency_id})
}
}
def _get_cart_notification_information(self, order, line_ids):
values = super()._get_cart_notification_information(order, line_ids)
values['order_amount_html'] = request.env['ir.qweb.field.monetary'].value_to_html(order.amount_total, {'display_currency': order.pricelist_id.currency_id or request.website.company_id.currency_id})
return values
class DroggolWishlist(WebsiteSaleWishlist):
@http.route('/theme_prime/wishlist_general', auth="public", type='json', website=True)
def wishlist_general(self, product_id=False, **post):
res = {}
if product_id:
res['wishlist_id'] = self.add_to_wishlist(product_id).id
res.update({
'products': request.env['product.wishlist'].with_context(display_default_code=False).current().mapped('product_id').ids,
'name': request.env['product.product'].browse(product_id).name
})
return res
class ThemePrimeMainClass(http.Controller):
# ----------------------------------------------------------
# Helper methods
# ----------------------------------------------------------
# This method is needs to be clean in next version we will do refactor
# Currently our rendering process is weird.
def _get_products(self, domain=None, fields=[], limit=25, order=None, options={}):
pricelist = request.website.pricelist_id
website_sale_domain = request.website.sale_product_domain()
final_domain = expression.AND([website_sale_domain, domain])
product_tmpl_ids = []
temp_order = order
if order == 'last_viewed':
product_tmpl_ids = self._get_last_viewed_products(domain, limit)
final_domain = [('id', 'in', product_tmpl_ids)]
order = None
# bestseller is not a real field
bestseller_ids, old_limit = [], limit
if order == 'bestseller':
bestseller_ids, limit, order = self._get_bestseller_products(old_limit)
products = request.env['product.template'].with_context(pricelist=pricelist.id).search(final_domain, limit=limit, order=order)
if temp_order == 'last_viewed' and len(product_tmpl_ids):
products = products.sorted(lambda p: product_tmpl_ids.index(p.id))
default_fields = ['id', 'name', 'website_url', 'default_code']
fields = set(default_fields + fields)
if bestseller_ids:
bestseller_ids_filtered = set(bestseller_ids) & set(products.ids)
bestseller_sorted_ids = [p_id for p_id in bestseller_ids if p_id in bestseller_ids_filtered]
remain_products = set(products.ids) - set(bestseller_sorted_ids)
final_product_ids = bestseller_sorted_ids + list(remain_products)
products = request.env['product.template'].with_context(pricelist=pricelist.id).browse(final_product_ids[:old_limit])
return self._prepare_product_data(products, fields)
def _get_last_viewed_products(self, domain, limit):
domain = expression.AND([[('website_published', '=', True)], request.website.get_current_website().website_domain(), domain])
products = request.env['website.snippet.filter']._get_products_latest_viewed(request.website, limit, domain, request.env.context)
return products.mapped("product_tmpl_id").ids if len(products) else []
def _prepare_product_data(self, products, fields, options=None):
options = options or {}
pricelist = request.website.pricelist_id
price_public_visibility = request.website._dr_has_b2b_access()
visibility_label = False
showStockLabel = False
if not price_public_visibility:
visibility_label = self._get_tp_view_template('theme_prime.tp_b2b_price_label')
extra_data = {'rating', 'offer_data', 'dr_stock_label', 'colors'} & set(fields)
fields = list(set(fields) - extra_data)
if 'dr_stock_label' in extra_data:
showStockLabel = request.website._get_dr_theme_config('json_shop_product_item').get('show_stock_label')
currency_id = pricelist.currency_id or request.website.company_id.currency_id
result = products.read(fields)
for res_product, product in zip(result, products):
combination_info = product._get_combination_info(only_template=True)
res_product.update(combination_info)
price_info = self._get_computed_product_price(product, res_product, price_public_visibility, visibility_label, currency_id)
res_product.update(price_info)
res_product['product_variant_id'] = product._get_first_possible_variant_id()
# Images
res_product['img_small'] = request.website.image_url(product, 'image_256')
res_product['img_medium'] = request.website.image_url(product, 'image_512')
res_product['img_large'] = request.website.image_url(product, 'image_1024')
# short Description
if 'description_sale' in fields:
description = res_product.get('description_sale')
res_product['short_description'] = description[:125] + '...' if description and len(description) > 125 else description or False
# label and color
if 'colors' in extra_data:
res_product['colors'] = self._get_tp_view_template('theme_prime.product_preview_swatches', {'product': product, '_limit': 4, 'parent_selector': '.card', 'img_selector': '.d-product-img'})
# label and color
if 'dr_label_id' in fields and product.dr_label_id:
res_product['label'] = product.dr_label_id
res_product['label_id'] = product.dr_label_id.id
res_product['label_template'] = self._get_tp_view_template('theme_prime.product_label', {'label': product.dr_label_id})
if 'dr_stock_label' in extra_data and showStockLabel and product.dr_show_out_of_stock:
res_product['dr_stock_label'] = self._get_tp_view_template('theme_prime.product_label', {'product': product, 'stock': True})
res_product['dr_show_out_of_stock'] = product.dr_show_out_of_stock
# rating
if 'offer_data' in extra_data:
offer = product._get_product_pricelist_offer()
if offer:
rule = offer.get('rule')
res_product['offer_data'] = {
'date_end': offer.get('date_end'),
'offer_msg': rule.dr_offer_msg,
'offer_finish_msg': rule.dr_offer_finish_msg
}
if 'rating' in extra_data:
res_product['rating'] = self._get_rating_template(product.rating_avg)
res_product['rating_avg'] = product.rating_avg
# images
if 'product_variant_ids' in fields:
res_product['images'] = product.product_variant_ids.ids
# website_category
if 'public_categ_ids' in fields and product.public_categ_ids:
first_category = product.public_categ_ids[0]
res_product['category_info'] = {
'name': first_category.name,
'id': first_category.id,
'website_url': '/shop/category/' + str(first_category.id),
}
# brand
if 'dr_brand_value_id' in fields:
res_product['brand_info'] = False
if product.dr_brand_value_id:
res_product['brand_info'] = {
'name': product.dr_brand_value_id.name,
'id': product.dr_brand_value_id.id,
}
return result
def _get_computed_product_price(self, product, product_data, price_public_visibility, visibility_label, currency_id):
FieldMonetary = request.env['ir.qweb.field.monetary']
monetary_options = {'display_currency': currency_id}
return {
'visibility': price_public_visibility,
'price_raw': product_data['price'] if price_public_visibility else visibility_label,
'list_price_raw': product_data['list_price'] if price_public_visibility else ' ',
'price': FieldMonetary.value_to_html(product_data['price'], monetary_options) if price_public_visibility else visibility_label,
'list_price': FieldMonetary.value_to_html(product_data['list_price'], monetary_options) if price_public_visibility else ' '
}
def _get_tp_view_template(self, tmpl, values={}):
IrUiView = request.env['ir.ui.view']
return IrUiView._render_template(tmpl, values=values)
def _get_bestseller_products(self, old_limit):
past_date = datetime.datetime.now() - datetime.timedelta(days=30)
result = request.env['sale.report'].sudo().read_group([('date', '>', past_date), ('website_id', '=', request.website.id), ('state', 'in', ['sale', 'done'])], ['product_tmpl_id', 'product_uom_qty:sum'], ['product_tmpl_id'], orderby="product_uom_qty desc")
return [product_line['product_tmpl_id'][0] for product_line in result], None if len(result) else old_limit, None
def _get_shop_related_data(self, options):
shop_data = {}
if (options.get('shop_config_params')):
shop_data['shop_config_params'] = request.website.get_theme_prime_shop_config()
if (options.get('wishlist_enabled')) and shop_data.get('shop_config_params', {}).get('is_wishlist_active'):
shop_data['wishlist_products'] = request.env['product.wishlist'].with_context(display_default_code=False).current().mapped('product_id').ids
return shop_data
def _get_rating_template(self, rating_avg, rating_count=False):
return request.website._get_theme_prime_rating_template(rating_avg, rating_count)
def _get_categories(self, domain=[], fields=['name', 'display_name', 'id'], limit=20, order=None, extras={}):
final_domain = expression.AND([request.website.website_domain(), domain])
final_list = False
# Pure Shit
if extras.get('count', False):
count_dict = {k: v for k, v in sorted(self.get_categories_count().items(), key=lambda item: item[1], reverse=True)}
if order == 'count':
order = 'sequence asc'
category_ids = count_dict.keys()
final_list = [i for i in category_ids if i not in extras.get('ids', [])][:limit]
final_domain = expression.AND([[['id', 'in', final_list]], final_domain])
result = request.env['product.public.category'].search_read(final_domain, fields=fields + ['display_name'], limit=limit, order=order)
for res in result:
if extras.get('count', False):
res.update({'count': count_dict.get(res.get('id'), 0)})
if final_list:
final_result = []
for res_id in final_list:
for res in result:
if res.get('id') == res_id:
final_result.append(res)
return final_result
return result
def _get_products_for_top_categories(self, params):
result = {}
categoryIDs = params.get('categoryIDs')
order = params.get('sortBy')
operator = '='
if params.get('includesChild'):
operator = 'child_of'
initial_domain = expression.AND([request.website.website_domain(), [('website_published', '=', True)]])
for id in categoryIDs:
domain = expression.AND([initial_domain, [['public_categ_ids', operator, id]]])
products = self._get_products(domain, ['id'], 4, order)
result[id] = [product['id'] for product in products]
return result
@http.route('/theme_prime/tp_search_read', type='json', auth='user', website=True, sitemap=False)
def _tp_search_read(self, model, domain=None, fields=[], order=None, limit=20, extras={}, **post):
if model == 'product.template':
if extras.get('show_suggestion', False):
order = 'bestseller'
return self._get_products(domain, fields, limit, order)
elif model == 'product.public.category':
extras.update({'count': True})
if extras.get('show_suggestion', False):
order = 'count'
return self._get_categories(domain, fields, limit, order, extras)
elif model == 'product.attribute.value':
if extras.get('brands'):
brand_attributes = request.website._get_brand_attributes()
domain = expression.AND([domain, [('attribute_id', 'in', brand_attributes.ids)]])
return request.env[model].with_context(show_attribute=False).search_read(domain, fields=fields, limit=limit, order=order)
return request.env[model].search_read(domain, fields=fields, limit=limit, order=order)
elif model in ['dr.product.label', 'product.tag']:
return request.env[model].search_read(domain, fields=fields, limit=limit, order=order)
elif model == 'ir.ui.view' and extras.get('templateIDs'):
templateIDs = extras.get('templateIDs')
views = request.env['ir.ui.view'].sudo().search([('key', 'in', templateIDs)])
Qweb = request.env['ir.qweb'].sudo()
viewData = []
for tmpl in templateIDs:
view = views.filtered(lambda x: x.key == tmpl)
if view:
view = view[0]
view_keys = view.key.split('.')
imgSrc = view_keys[1]
snippetName = view.name
if extras.get('registryToUse') == 'theme_prime_snippet_registry' and imgSrc == 's_mega_menu_category_tabs_snippet':
imgSrc = 's_tp_categories_menu'
snippetName = 'Categories Menu'
viewData.append({
'id': view_keys[1],
'html': Qweb._render(view.id, {"dr_uid": datetime.datetime.now().microsecond}, minimal_qcontext=True),
'subtitle': snippetName,
'imgSrc': '/theme_prime/static/src/img/dynamic_snippet/%s.jpg' % imgSrc,
'icon': 'theme_prime.icon_grid_1_2',
'title': 'Choose a Snippet'
})
return viewData
@http.route('/theme_prime/get_similar_products_sidebar', type='http', auth='public', website=True, sitemap=False)
def _get_similar_products_sidebar(self, productID, **post):
product_tmpl_id = request.env['product.template'].browse(int(productID))
return request.render('theme_prime.similar_products_sidebar', {'products': product_tmpl_id.alternative_product_ids}, headers={'Cache-Control': 'no-cache'})
@http.route('/theme_prime/get_tab_listing_products', type='json', auth='public', website=True)
def get_tab_listing_products(self, domain=None, fields=[], options={}, limit=25, order=None, **kwargs):
result = {}
if options.get('categoryID', False):
category_domain = [('id', '=', options.get('categoryID'))]
category = self._get_categories(category_domain)
if category:
result['listing_category'] = category
domain = expression.AND([domain, [('public_categ_ids', 'child_of', options.get('categoryID'))]])
result['products'] = self._get_products(domain, fields, limit, order)
result.update(self._get_shop_related_data(options))
return result
@http.route('/theme_prime/get_brands_category_data', type='json', auth='public', website=True)
def get_brands_category_data(self, domain=None, fields=[], options={}):
if options.get('model') in ['product.attribute.value', 'product.public.category']:
if options.get('model') == 'product.attribute.value':
fields += ['attribute_id']
result = self._tp_search_read(options.get('model'), domain, fields)
if options.get('model') == 'product.attribute.value':
get_brand_count = request.env['product.template']._get_product_attrib_count(attrib_values=[], domain=request.env['website'].sale_product_domain())
for res in result:
res['count'] = get_brand_count.get(res.get('id'))
return result
@http.route('/theme_prime/get_listing_products', type='json', auth='public', website=True)
def get_listing_products(self, domain=None, fields=[], options={}, limit=5, **kwargs):
result = {}
# [TO-DO] even snippet don't allow manual selection it will set the attrs
domain = None if options.get('mode') == 'manual' else domain
if options.get('bestseller'):
result['bestseller'] = self._get_products(domain, fields, limit, 'bestseller')
if options.get('newArrived'):
result['newArrived'] = self._get_products(domain, fields, limit, 'create_date desc')
if options.get('discount'):
if domain:
domain = expression.AND([[("dr_has_discount", "!=", False)], domain])
else:
domain = [["dr_has_discount", "!=", False]]
result['discount'] = self._get_products(domain, fields, limit)
final = {}
final['products'] = result
final.update(self._get_shop_related_data(options))
return final
@http.route('/theme_prime/get_products_data', type='json', auth='public', website=True)
def get_products_data(self, domain=None, fields=[], options={}, limit=25, order=None, **kwargs):
result = {
'products': self._get_products(domain, fields, limit, order),
}
result.update(self._get_shop_related_data(options))
return result
@http.route('/theme_prime/get_products_by_category', type='json', auth='public', website=True)
def get_products_by_category(self, domain, fields=[], options={}, **kwargs):
final_domain = expression.AND([[('website_published', '=', True)], domain])
result = {
'products': self._get_products(domain=final_domain, fields=fields, order=options.get('order', False), limit=options.get('limit', False)),
}
result.update(self._get_shop_related_data(options))
if (options.get('get_categories')):
# get category names for snippet
domain = [('id', 'in', options.get('categoryIDs'))]
result['categories'] = self._get_categories(domain)
if (options.get('get_brands')):
# get category names for snippet
domain = [('id', 'in', options.get('categoryIDs'))]
result['categories'] = request.website._get_brands(domain).read(['name', 'id'])
return result
@http.route('/theme_prime/get_top_categories', type='json', auth='public', website=True)
def get_top_categories(self, options={}):
params = options.get('params')
result = []
website_sale_domain = request.website.sale_product_domain()
if params:
categoryIDs = params.get('categoryIDs')
if categoryIDs:
domain = [('id', 'in', categoryIDs)]
category_names = {i['id']: i['name'] for i in self._get_categories(domain)}
# Update categoryIDs if already set category moved to other website
categoryIDs = category_names.keys()
params['categoryIDs'] = categoryIDs
categories = self._get_products_for_top_categories(params)
price_public_visibility = request.website._dr_has_b2b_access()
for category_id in categoryIDs:
category_data = {}
product_ids = categories.get(category_id)
category_data['name'] = category_names.get(category_id)
category_data['id'] = category_id
category_data['website_url'] = '/shop/category/' + str(category_id)
category_data['productIDs'] = product_ids
final_domain = expression.AND([website_sale_domain, [('public_categ_ids', 'child_of', category_id)]])
products = self._get_products(domain=final_domain, fields=[], limit=1, order="list_price asc")
if len(products):
category_data['min_price'] = products[0].get('price')
category_data['price_public_visibility'] = price_public_visibility
result.append(category_data)
return result
@http.route(['/theme_prime/get_dialog_content'], type='json', website=True, auth="public")
def get_dialog_content(self, res_id, res_model, fields, **post):
return request.env[res_model].sudo().search_read([('id', '=', res_id)], fields=fields)
@http.route('/theme_prime/get_categories_count', type='json', auth='public', website=True)
def get_categories_count(self):
return request.env['product.template']._get_product_category_count(domain=request.env['website'].sale_product_domain())
@http.route('/theme_prime/get_categories_info', type='json', auth='public', website=True)
def get_categories_info(self, fields=[], options={}, **kwargs):
categoryIDs = options.get('categoryIDs', [])
fields = ['name', 'display_name', 'id'] + fields
domain = expression.AND([request.website.website_domain(), [('id', 'in', categoryIDs)]])
categories = request.env['product.public.category'].search(domain)
result = categories.read(fields)
if options.get('getCount', False):
get_category_count = self.get_categories_count()
for res_category, category in zip(result, categories):
if 'dr_category_label_id' in fields and category.dr_category_label_id:
category_label = category.dr_category_label_id
res_category['category_lable_info'] = {
'id': category_label.id,
'name': category_label.name,
'background_color': category_label.background_color,
'text_color': category_label.text_color,
}
if options.get('getCount', False):
res_category['count'] = get_category_count.get(category.id, 0)
res_category['website_url'] = '/shop/category/' + str(category.id)
res_category['image_url'] = '/web/image?model=product.public.category&id=%d&field=image_512' % (category.id)
res_category['cover_image'] = '/web/image?model=product.public.category&id=%d&field=dr_category_cover_image' % (category.id)
return result
@http.route('/theme_prime/get_brands', type='json', auth='public', website=True)
def get_brands(self, fields=['id', 'name', 'attribute_id'], options={}):
if options.get('categories'):
domain = expression.AND([request.website.sale_product_domain(), [('public_categ_ids', 'child_of', options.get('categories'))]])
brands = request.env['product.template'].search(domain).mapped('dr_brand_value_id')[:options.get('limit', 12)]
if options.get('recordsIDs'):
brands = request.website._get_brands([('id', 'in', options.get('recordsIDs'))], options.get('limit', 12))
domain = request.env['website'].sale_product_domain()
get_attrib_count = request.env['product.template']._get_product_attrib_count(attrib_values=[], domain=domain)
return [{**brand_data, 'product_count': get_attrib_count.get(brand_data['id'], 0)} for brand_data in brands.read(fields)]
@http.route('/theme_prime/get_preview_body', type='http', auth="user", website=True, sitemap=False, multilang=False)
def get_preview_body(self, **kw):
return request.env['ir.ui.view']._render_template('theme_prime.tp_preview_body', {'mobile': kw.get('mobile')})
@http.route('/theme_prime/get_megamenu_categories', type='json', auth='public', website=True)
def get_megamenu_categories(self, options={}, limit=5, fields=['name', 'id'], order='count', **kwargs):
category_ids = request.env['product.public.category'].browse(options.get('categoryIDs', [])).exists().ids
final_domain = expression.AND([request.website.website_domain(), [('parent_id', 'child_of', category_ids)]])
categories = request.env['product.public.category'].search(final_domain, limit=None)
all_categories = []
all_category_count = request.env['product.template']._get_product_category_count(request.env['website'].sale_product_domain())
for category in categories:
all_categories.append({
'id': category.id, 'name': category.name, 'parent_id': category.parent_id.id,
'count': all_category_count.get(category['id'], 0),
'website_url': '/shop/category/' + str(category.id),
'image_url': '/web/image?model=product.public.category&id=%d&field=image_512' % (category.id),
'cover_image': '/web/image?model=product.public.category&id=%d&field=dr_category_cover_image' % (category.id),
'dr_category_icon': '/web/image?model=product.public.category&id=%d&field=dr_category_icon' % (category.id),
'category_label_info': category.dr_category_label_id and {f: category.dr_category_label_id[f] for f in ['name', 'background_color', 'text_color']} or False,
})
parent_categories = filter(lambda category: category.get('id') in category_ids, all_categories)
return [self._get_megamenu_child_categories(category_id, limit, all_categories, order, options) for category_id in parent_categories]
def _get_megamenu_child_categories(self, parent_category, limit, all_categories, order, options):
child_categories = [categ for categ in all_categories if categ.get('parent_id') == parent_category.get('id')]
if not child_categories:
return {'category': parent_category, 'child': []}
if order == 'count' or not order:
child_categories = sorted(child_categories, key=lambda category: category.get('count', 0), reverse=True)
child_categories = child_categories[:limit]
remain_limit = limit - len(child_categories)
if remain_limit <= 0 or options.get('onlyDirectChild', False):
return {'category': parent_category, 'child': child_categories}
for child_category in child_categories[:]:
new_born_child = self._get_megamenu_child_categories(child_category, remain_limit, all_categories, order, options).get('child')
child_categories.extend(new_born_child)
remain_limit = limit - len(child_categories)
if remain_limit <= 0:
break
return {'category': parent_category, 'child': child_categories}
class ThemeWebsite(Website):
@http.route('/website/dr_search', type='json', auth="public", website=True, sitemap=False)
def dr_search(self, term, max_nb_chars, options, **kw):
fuzzy_term, global_match = False, False
search_config = request.website._get_dr_theme_config('json_product_search')
has_formulate = self._dr_has_formulate(search_config)
fuzzy_enabled = search_config.get('search_fuzzy')
limit = max(min(search_config.get('search_limit'), 10), 5)
search_types = ['products', 'categories', 'autocomplete', 'suggestions']
results = {search_type: {'results': [], 'results_count': 0, 'parts': {}} for search_type in search_types}
product_limit = max(min(search_config.get('search_max_product'), 5), 0)
options = {'allowFuzzy': fuzzy_enabled, 'displayDescription': False, 'displayDetail': True, 'displayExtraLink': True, 'displayImage': True}
if product_limit:
results['products'] = self.autocomplete(search_type='products_only', term=term, order='name asc', limit=product_limit, options=options)
product_fuzzy_term = results['products'].get('fuzzy_search')
if search_config.get('search_category') and not has_formulate:
results['categories'] = self.autocomplete(search_type='product_categories_only', term=term, order='sequence, name, id', limit=5, options=options)
category_fuzzy_term = results['categories'].get('fuzzy_search')
if fuzzy_enabled:
empty_search = {'results': [], 'results_count': 0, 'parts': {}}
if category_fuzzy_term == product_fuzzy_term:
fuzzy_term = product_fuzzy_term
elif not category_fuzzy_term and results['categories'].get('results_count'):
results['products'], fuzzy_term = empty_search, False
elif not product_fuzzy_term and results['products'].get('results_count'):
results['categories'], fuzzy_term = empty_search, False
elif product_fuzzy_term and not category_fuzzy_term: # category will be always empty based on above conditions
fuzzy_term = product_fuzzy_term
elif category_fuzzy_term and not product_fuzzy_term: # products will be always empty based on above conditions
fuzzy_term = category_fuzzy_term
else: # super rare case
all_results = self.autocomplete(search_type='products', term=term, order='sequence, name, id', limit=limit, options=options)
products_result = [res for res in all_results['results'] if res.get('_fa') == 'fa-shopping-cart']
category_result = [res for res in all_results['results'] if res.get('_fa') == 'fa-folder-o']
fuzzy_term = all_results.get('fuzzy_search')
results = {'products': {'results': products_result, 'results_count': len(products_result), 'parts': {}}, 'categories': {'results': category_result, 'results_count': len(category_result), 'parts': {}}}
# Search from category synonyms if no category found
if results.get('categories') and not results.get('categories').get('results_count'):
category_result = self.autocomplete(search_type='category_synonyms', term=term, order='name asc', limit=5, options=options)
results['categories'] = {'results': category_result['results'], 'results_count': len(category_result['results']), 'parts': {}}
# suggestion search
if search_config.get('search_attribute') or search_config.get('search_suggestion'):
remain_limit = limit - min(product_limit, results['products'].get('results_count', 0)) # Odoo results_count returns count for full result (without limit)
words = [i for i in term.split(' ') if i] # split and filter spaces
matchs, matched_dicts = False, {}
for word in words:
if matchs:
for match in matchs:
match_dict = matched_dicts[match]
if match_dict['remaining_words']:
match_dict['remaining_words'].append(word)
else:
unmatched_record_name = match_dict['unmatched_record_name']
regex_match = re.search(re.escape(word), unmatched_record_name, re.IGNORECASE)
if regex_match:
match_dict['matched_words'].append(word)
match_dict['unmatched_record_name'] = re.sub(re.escape(word), ' ', unmatched_record_name, flags=re.I)
else:
match_dict['remaining_words'].append(word)
else:
matchs = self._match_attr_or_category(word)
if matchs:
for match in matchs:
matched_dicts[match] = match_dict = {'match': match, 'matched_words': [word], 'remaining_words': []}
match_dict['unmatched_record_name'] = re.sub(re.escape(match_dict['matched_words'][0]), ' ', match.ds_name, flags=re.I)
match_list_raw = list(matched_dicts.values())
match_list_raw.sort(key=lambda m: len(m['matched_words']), reverse=True)
# filter matched result and remove the duplicate matches from the synonyms
match_list, matched_ids = [], []
for matched_dict in match_list_raw:
matched_record = matched_dict['match']
if matched_record.id not in matched_ids:
matched_ids.append(matched_record.id)
match_list.append(matched_dict)
autocomplete_result = []
for match_dict in match_list:
autocomplete_data = []
if match_dict['remaining_words']:
autocomplete_data = self._get_autocomplete_data(match_dict, remain_limit, search_config)
elif not search_config.get('search_category') and match_dict['match']._name == 'product.public.category':
autocomplete_result.append(self.generate_result_dict(match_dict['match'], False, match_dict['matched_words'], ''))
remain_limit -= 1
if not remain_limit:
break
if not match_dict['remaining_words']:
# if no remaining_words that means full data matched with record so suggestions become autocomplete
autocomplete_data += self._get_suggestions_data(match_dict, autocomplete_result, remain_limit, search_config, ignore_config=True)
remain_limit -= len(autocomplete_data)
autocomplete_result.extend(autocomplete_data)
if not remain_limit:
break
suggestions_result = []
for match_dict in match_list:
suggestions_data = self._get_suggestions_data(match_dict, autocomplete_result, min(remain_limit, 5), search_config)
remain_limit -= len(suggestions_data)
suggestions_result.extend(suggestions_data)
if not remain_limit:
break
results['autocomplete'] = {'results': autocomplete_result, 'results_count': len(autocomplete_result), "parts": {"name": True, "website_url": True}}
results['suggestions'] = {'results': suggestions_result, 'results_count': len(suggestions_result), "parts": {"name": True, "website_url": True}}
global_match = False
if matchs and len(matchs) == 1 and (results['autocomplete'].get('results_count') or results['suggestions'].get('results_count')):
if matchs[0]._name == 'product.public.category':
fixed_str = _('View all products with category')
global_match = {'name': f'{fixed_str} <b class="text-primary">{matchs[0].ds_name}</b>', 'website_url': f'/shop?category={matchs[0].id}'}
else:
fixed_str = _('View all products with')
global_match = {'name': f'{fixed_str} {matchs.attribute_id.name.lower()} <b class="text-primary">{matchs.ds_name}</b>', 'website_url': f'/shop?&attrib={matchs.attribute_id.id}-{matchs.id}'}
# fill in the extra products is keyword does not match with normal products
if search_config.get('search_fill_products') and results['products']['results_count'] == 0 and results['autocomplete']['results_count'] > 0:
domain_all = []
for autocomplete_dict in results['autocomplete']['results'][:1]:
domain, catgory_id, attribute_value_od = [], autocomplete_dict.get('category'), autocomplete_dict.get('attribute')
if catgory_id:
domain.append(('public_categ_ids', 'child_of', [catgory_id]))
if attribute_value_od:
domain.append(('attribute_line_ids.value_ids', 'in', [attribute_value_od]))
domain_all.append(domain)
# TODO: Improve this. Here is the simple hack to trick autocomplete and ignore search term use our domain, This will save tons of extra code
options['dr_search_domain'] = expression.OR(domain_all)
results['products'] = self.autocomplete(search_type='products_only', term=term, order='name asc', limit=product_limit, options=options)
search_result = {**results, 'fuzzy_search': fuzzy_term, 'results': [], 'global_match': global_match, 'result_length': sum([results.get(r_type, {}).get('results_count', 0) for r_type in search_types])}
if search_config.get('search_report') and search_result["result_length"] == 0:
report_data = {
'search_term': term,
'device_type': kw.get('device_type')
}
request.env['dr.search.report']._add_report_entry(report_data)
return search_result
def _get_autocomplete_data(self, match_dict, remain_limit, search_config):
match, remaining_words, matched_words = match_dict['match'], match_dict['remaining_words'], match_dict['matched_words']
results = []
if search_config.get('search_attribute') and remaining_words and match:
for related_match, word in self.match_remaining_words(match, remaining_words):
results.append(self.generate_result_dict(match, related_match, matched_words, word))
matched_words.append(word)
if len(results) >= remain_limit:
break
return results
def _get_suggestions_data(self, match_dict, autocomplete_data, remain_limit, search_config, ignore_config=False):
results = []
match, matched_words = match_dict['match'], match_dict['matched_words']
if (search_config.get('search_suggestion') or ignore_config) and remain_limit > 0:
if match._name == 'product.public.category':
for related_match in self._category_counterpart_iterator(match, search_type=['auto_suggestion']):
term = self.generate_result_dict(match, related_match, matched_words)
if not self.is_search_added(autocomplete_data, results, term):
results.append(term)
if len(results) >= remain_limit:
break
else:
domain = request.website.sale_product_domain() + [('attribute_line_ids.value_ids', 'in', match.ids)]
all_related_records = request.env['product.template'].with_context(bin_size=True).search(domain).mapped('public_categ_ids')
for related_match in all_related_records:
term = self.generate_result_dict(match, related_match, matched_words)
if not self.is_search_added(autocomplete_data, results, term):
results.append(term)
if len(results) >= remain_limit:
break
return results
def _match_attr_or_category(self, term, return_on_match=True):
result = self._match_category(term=term)
if not result:
result = self._match_attr(term)
return result
def _match_category(self, term=False, categories_ids=[]):
CSD = request.env['product.public.category']._get_search_db(request.website)
return CSD.search(term=term, categories_ids=categories_ids)
def _match_attr(self, term):
all_active_attributes = self._website_active_attributes()
matched_values = request.env['product.attribute.value']
if all_active_attributes:
matched_values = matched_values.search([('ds_name', 'ilike', term), ('attribute_id', 'in', all_active_attributes.ids)])
return matched_values
def _website_active_attributes(self):
all_products = request.env['product.template'].with_context(bin_size=True).search(request.website.sale_product_domain())
return request.env['product.attribute'].search([
('product_tmpl_ids', 'in', all_products.ids), ('visibility', '=', 'visible'), ('dr_search_suggestion', '!=', False)
])
def match_remaining_words(self, match, remaining_words):
if match._name == 'product.public.category':
for word in remaining_words:
for attribute_value in self._category_counterpart_iterator(match):
regex_match = re.search(re.escape(word), attribute_value.ds_name, re.IGNORECASE)
if regex_match:
yield attribute_value, word
if match._name == 'product.attribute.value':
domain = request.website.sale_product_domain() + [('attribute_line_ids.value_ids', 'in', match.ids)]
related_categories = request.env['product.template'].with_context(bin_size=True).search(domain).mapped('public_categ_ids')
categories = self._match_category(categories_ids=related_categories.ids)
for word in remaining_words:
for category in categories:
regex_match = re.search(re.escape(word), category.ds_name, re.IGNORECASE)
if regex_match:
yield category, word
def _category_counterpart_iterator(self, category_dst, search_type=['auto_suggestion', 'auto']):
category = request.env['product.public.category'].browse(category_dst.id)
attribute_values = category.mapped('product_tmpl_ids.attribute_line_ids').filtered(lambda line: line.attribute_id.dr_search_suggestion in search_type).mapped('value_ids')
for value in attribute_values:
yield value
# Child category
child_categories = request.env['product.public.category'].search([('parent_id', 'child_of', category.id)]) - category
child_attribute_values = child_categories.mapped('product_tmpl_ids.attribute_line_ids').filtered(lambda line: line.attribute_id.dr_search_suggestion in search_type).mapped('value_ids') # plus to maintain order
for value in child_attribute_values:
if value not in attribute_values:
yield value
def generate_result_dict(self, primary_match, secondary_match, matched_words, word=False):
category, attribute = (primary_match, secondary_match) if primary_match._name == 'product.public.category' else (secondary_match, primary_match)
attribute_str = f"&attrib={attribute.attribute_id.id}-{attribute.id}" if attribute else '' # just for category
return {
'name': self.format_result(matched_words + (word and [word] or []), f"{primary_match.ds_name} {secondary_match and secondary_match.ds_name or ''}"),
'website_url': f"/shop?category={category.id}{attribute_str}",
'category': category and category.id,
'attribute': attribute and attribute.id
}
def format_result(self, matched_words, value):
pattern = '|'.join(map(re.escape, matched_words))
parts = re.split(f'({pattern})', value, flags=re.IGNORECASE)
if len(parts) > 1:
value = request.env['ir.ui.view'].sudo()._render_template("website.search_text_with_highlight", {'parts': parts})
html_val = request.env[('ir.qweb.field.html')].value_to_html(value, {'template_options': {}})
return html_escape(html_val)
return False
def is_search_added(self, autocomplete_result, suggestions_results, new_term):
auto_found = len([term for term in autocomplete_result if new_term['website_url'] == term['website_url']])
sugg_found = len([term for term in suggestions_results if new_term['website_url'] == term['website_url']])
return auto_found + sugg_found
def _dr_has_formulate(self, search_config): # for performance
if search_config.get('search_attribute') or search_config.get('search_suggestion'):
formulate_category = request.env['product.public.category'].search(([('dr_search_formulate', '=', True)] + request.website.website_domain()), limit=1)
if formulate_category:
request.update_context(dr_formulate=True)
return len(formulate_category)
return False
@http.route('/website/dr_search/add_report', type='json', auth="public", website=True, sitemap=False)
def dr_search_report(self, **kw):
request.env['dr.search.report']._add_report_entry(kw)
return {}
class ThemePrimeBrand(http.Controller):
@http.route('/shop/all-brands', type='http', auth='public', website=True, sitemap=True)
def brands(self, search='', **args):
is_disable_grouping = request.website._get_dr_theme_config('json_brands_page')['disable_brands_grouping']
brands = request.website._get_brands([('name', 'ilike', search)] if search else [])
grouped_brands = defaultdict(list)
if is_disable_grouping:
grouped_brands = {'All Brands': brands}
else:
alphabet_range = string.ascii_uppercase
grouped_brands.update((alphabet, []) for alphabet in alphabet_range)
for brand in brands:
first_char = str.upper(brand.name[:1])
grouped_brands[first_char].append(brand)
get_brand_count = request.env['product.template']._get_product_attrib_count(attrib_values=[], domain=request.env['website'].sale_product_domain())
return request.render('theme_prime.all_brands', {'is_disable_grouping': is_disable_grouping, 'brands': brands, 'grouped_brands': grouped_brands, 'search': search, 'get_brand_count': get_brand_count})
class ThemePrimePWA(http.Controller):
@http.route('/pwa/<int:website_id>/manifest.json', type='http', auth='public', website=True)
def get_pwa_manifest(self, website_id, **kargs):
manifest_data = {"fake": 1}
website = request.website
if website and website.id == website_id and website.dr_pwa_activated:
manifest_data = {
"name": website.dr_pwa_name,
"short_name": website.dr_pwa_short_name,
"display": "standalone",
"background_color": website.dr_pwa_background_color,
"theme_color": website.dr_pwa_theme_color,
"id": website.dr_pwa_start_url,
"start_url": website.dr_pwa_start_url,
"scope": "/",
"icons": [{
"src": "/web/image/website/%s/dr_pwa_icon_192/192x192" % website.id,
"sizes": "192x192",
"type": "image/png",
}, {
"src": "/web/image/website/%s/dr_pwa_icon_512/512x512" % website.id,
"sizes": "512x512",
"type": "image/png",
}]
}
if website.dr_pwa_screenshots:
manifest_data['screenshots'] = [{
"src": "/web/image/dr.pwa.screenshots/%s/image" % screenshot.id,
"type": "image/jpg",
"sizes": screenshot.sizes,
"form_factor": screenshot.form_factor,
} for screenshot in website.dr_pwa_screenshots]
if website.dr_pwa_shortcuts:
manifest_data['shortcuts'] = [{
"name": shortcut.name,
"short_name": shortcut.short_name or '',
"description": shortcut.description or '',
"url": shortcut.url,
"icons": [{"src": "/web/image/dr.pwa.shortcuts/%s/icon/192x192" % shortcut.id, "sizes": "192x192"}]
} for shortcut in website.dr_pwa_shortcuts]
return request.make_response(
data=json.dumps(manifest_data),
headers=[('Content-Type', 'application/json')]
)
@http.route('/service_worker.js', type='http', auth='public', website=True, sitemap=False)
def get_pwa_service_worker(self, **kargs):
website = request.website
offline_bool = 'true' if website.dr_pwa_offline_page else 'false'
data = file_open('theme_prime/static/src/js/service_worker.js', 'rb').read().decode()
data = data.replace('"##1##"', str(website.dr_pwa_version))
data = data.replace('"##2##"', offline_bool)
return request.make_response(
data=data,
headers=[('Content-Type', 'text/javascript')]
)
@http.route('/pwa/offline_page', type='http', auth='public', website=True, cors='*', sitemap=False)
def get_pwa_offline_page(self, **kargs):
return request.render('theme_prime.pwa_offline_page', {})
@http.route('/pwa/logo.png', type='http', auth='public', website=True, cors='*', sitemap=False)
def get_pwa_logo(self, **kargs):
website = request.website
imgname = 'logo'
imgext = '.png'
if not website.logo:
response = http.Stream.from_path(file_path('web/static/img/nologo.png')).get_response()
else:
image_base64 = base64.b64decode(website.logo)
image_data = io.BytesIO(image_base64)
mimetype = guess_mimetype(image_base64, default='image/png')
imgext = '.' + mimetype.split('/')[1]
if imgext == '.svg+xml':
imgext = '.svg'
response = send_file(image_data, request.httprequest.environ, download_name=imgname + imgext, mimetype=mimetype, last_modified=website.write_date)
return response