# -*- coding: utf-8 -*- # Copyright (c) 2019-Present Droggol Infotech Private Limited. () 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, ['|', '|', ('detailed_type', '!=', 'product'), ('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} {matchs[0].ds_name}', '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()} {matchs.ds_name}', '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//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