You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							389 lines
						
					
					
						
							18 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							389 lines
						
					
					
						
							18 KiB
						
					
					
				| # -*- coding: utf-8 -*- | |
| ############################################################################# | |
| # | |
| #    Cybrosys Technologies Pvt. Ltd. | |
| # | |
| #    Copyright (C) 2020-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) | |
| #    Author: Midilaj (<https://www.cybrosys.com>) | |
| # | |
| #    You can modify it under the terms of the GNU LESSER | |
| #    GENERAL PUBLIC LICENSE (LGPL v3), Version 3. | |
| # | |
| #    This program is distributed in the hope that it will be useful, | |
| #    but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | |
| #    GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. | |
| # | |
| #    You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE | |
| #    (LGPL v3) along with this program. | |
| #    If not, see <http://www.gnu.org/licenses/>. | |
| # | |
| ############################################################################# | |
| 
 | |
| import json | |
| import logging | |
| from collections import defaultdict | |
| from datetime import datetime | |
| from werkzeug.exceptions import Forbidden, NotFound | |
| from itertools import product as cartesian_product | |
| 
 | |
| from odoo import fields, http, tools, _ | |
| from odoo.http import request | |
| from odoo.addons.http_routing.models.ir_http import slug | |
| from odoo.addons.website.controllers.main import QueryURL | |
| from odoo.addons.website_sale.controllers.main import WebsiteSale | |
| from odoo.tools import lazy | |
| 
 | |
| _logger = logging.getLogger(__name__) | |
| 
 | |
| 
 | |
| class TableCompute(object): | |
| 
 | |
|     def __init__(self): | |
|         self.table = {} | |
| 
 | |
|     def _check_place(self, posx, posy, sizex, sizey, ppr): | |
|         res = True | |
|         for y in range(sizey): | |
|             for x in range(sizex): | |
|                 if posx + x >= ppr: | |
|                     res = False | |
|                     break | |
|                 row = self.table.setdefault(posy + y, {}) | |
|                 if row.setdefault(posx + x) is not None: | |
|                     res = False | |
|                     break | |
|             for x in range(ppr): | |
|                 self.table[posy + y].setdefault(x, None) | |
|         return res | |
| 
 | |
|     def process(self, products, ppg=20, ppr=4): | |
|         # Compute products positions on the grid | |
|         minpos = 0 | |
|         index = 0 | |
|         maxy = 0 | |
|         x = 0 | |
|         for p in products: | |
|             x = min(max(p.website_size_x, 1), ppr) | |
|             y = min(max(p.website_size_y, 1), ppr) | |
|             if index >= ppg: | |
|                 x = y = 1 | |
| 
 | |
|             pos = minpos | |
|             while not self._check_place(pos % ppr, pos // ppr, x, y, ppr): | |
|                 pos += 1 | |
|             # if 21st products (index 20) and the last line is full (ppr products in it), break | |
|             # (pos + 1.0) / ppr is the line where the product would be inserted | |
|             # maxy is the number of existing lines | |
|             # + 1.0 is because pos begins at 0, thus pos 20 is actually the 21st block | |
|             # and to force python to not round the division operation | |
|             if index >= ppg and ((pos + 1.0) // ppr) > maxy: | |
|                 break | |
| 
 | |
|             if x == 1 and y == 1:  # simple heuristic for CPU optimization | |
|                 minpos = pos // ppr | |
| 
 | |
|             for y2 in range(y): | |
|                 for x2 in range(x): | |
|                     self.table[(pos // ppr) + y2][(pos % ppr) + x2] = False | |
|             self.table[pos // ppr][pos % ppr] = { | |
|                 'product': p, 'x': x, 'y': y, | |
|                 'ribbon': p.website_ribbon_id, | |
|             } | |
|             if index <= ppg: | |
|                 maxy = max(maxy, y + (pos // ppr)) | |
|             index += 1 | |
| 
 | |
|         # Format table according to HTML needs | |
|         rows = sorted(self.table.items()) | |
|         rows = [r[1] for r in rows] | |
|         for col in range(len(rows)): | |
|             cols = sorted(rows[col].items()) | |
|             x += len(cols) | |
|             rows[col] = [r[1] for r in cols if r[1]] | |
| 
 | |
|         return rows | |
| 
 | |
| 
 | |
| class WebsiteSales(WebsiteSale): | |
|     @http.route([ | |
|         '''/shop''', | |
|         '''/shop/page/<int:page>''', | |
|         '''/shop/category/<model("product.public.category"):category>''', | |
|         '''/shop/category/<model("product.public.category"):categorys>/page/<int:page>''', | |
|         '''/shop/brand/<model("product.brand"):brand>''', | |
|     ], type='http', auth="public", website=True) | |
|     def shop(self, page=0, category=None, search='', min_price=0.0, max_price=0.0, ppg=False, brand=None, | |
|              **post, ): | |
|         add_qty = int(post.get('add_qty', 1)) | |
|         try: | |
|             min_price = float(min_price) | |
|         except ValueError: | |
|             min_price = 0 | |
|         try: | |
|             max_price = float(max_price) | |
|         except ValueError: | |
|             max_price = 0 | |
|         compute_brand = brand | |
|         Category = request.env['product.public.category'] | |
|         if category: | |
|             category = Category.search([('id', '=', int(category))], limit=1) | |
|             if not category or not category.can_access_from_current_website(): | |
|                 raise NotFound() | |
|         else: | |
|             category = Category | |
| 
 | |
|         Brand = request.env['product.brand'] | |
|         if not brand: | |
|             brand = Brand | |
| 
 | |
|         if ppg: | |
|             try: | |
|                 ppg = int(ppg) | |
|                 post['ppg'] = ppg | |
|             except ValueError: | |
|                 ppg = False | |
|         if not ppg: | |
|             ppg = request.env['website'].get_current_website().shop_ppg or 20 | |
| 
 | |
|         ppr = request.env['website'].get_current_website().shop_ppr or 4 | |
| 
 | |
|         attrib_list = request.httprequest.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} | |
|         attrib_set = {v[1] for v in attrib_values} | |
| 
 | |
|         domain = self._get_search_domain(search, category, attrib_values) | |
| 
 | |
|         keep = QueryURL('/shop', category=category and int(category), search=search, attrib=attrib_list, | |
|                         order=post.get('order')) | |
|         now = datetime.timestamp(datetime.now()) | |
|         pricelist = request.website.get_current_pricelist() | |
|         if not pricelist or request.session.get('website_sale_pricelist_time', | |
|                                                 0) < now - 60 * 60:  # test: 1 hour in session | |
|             pricelist = request.website.get_current_pricelist() | |
|             request.session['website_sale_pricelist_time'] = now | |
|             request.session['website_sale_current_pl'] = pricelist.id | |
| 
 | |
|         request.update_context(pricelist=pricelist.id, partner=request.env.user.partner_id) | |
| 
 | |
|         filter_by_price_enabled = request.website.is_view_active('website_sale.filter_products_price') | |
|         if filter_by_price_enabled: | |
|             company_currency = request.website.company_id.currency_id | |
|             conversion_rate = request.env['res.currency']._get_conversion_rate( | |
|                 company_currency, pricelist.currency_id, request.website.company_id, fields.Date.today()) | |
|         else: | |
|             conversion_rate = 1 | |
| 
 | |
|         url = "/shop" | |
|         if search: | |
|             post["search"] = search | |
|         if attrib_list: | |
|             post['attrib'] = attrib_list | |
|         options = self._get_search_options( | |
|             category=category, | |
|             attrib_values=attrib_values, | |
|             pricelist=pricelist, | |
|             min_price=min_price, | |
|             max_price=max_price, | |
|             conversion_rate=conversion_rate, | |
|             **post | |
|         ) | |
|         product_count, details, fuzzy_search_term = request.website._search_with_fuzzy("products_only", search, | |
|                                                                                        limit=None, | |
|                                                                                        order=self._get_search_order( | |
|                                                                                            post), | |
|                                                                                        options=options) | |
|         search_product = details[0].get('results', request.env['product.template']).with_context(bin_size=True) | |
|         if attrib_set: | |
|             # Attributes value per attribute | |
|             attribute_values = request.env['product.attribute.value'].browse(attrib_set) | |
|             values_per_attribute = defaultdict(lambda: request.env['product.attribute.value']) | |
|             # In case we have only one value per attribute we can check for a combination using those attributes directly | |
| 
 | |
|             multi_value_attribute = False | |
|             for value in attribute_values: | |
|                 values_per_attribute[value.attribute_id] |= value | |
|                 if len(values_per_attribute[value.attribute_id]) > 1: | |
|                     multi_value_attribute = True | |
| 
 | |
|             def filter_template(template, attribute_values_list): | |
|                 # Transform product.attribute.value to product.template.attribute.value | |
|                 attribute_value_to_ptav = dict() | |
|                 for ptav in template.attribute_line_ids.product_template_value_ids: | |
|                     attribute_value_to_ptav[ptav.product_attribute_value_id] = ptav.id | |
|                 possible_combinations = False | |
|                 for attribute_values in attribute_values_list: | |
|                     ptavs = request.env['product.template.attribute.value'].browse( | |
|                         [attribute_value_to_ptav[val] for val in attribute_values if val in attribute_value_to_ptav] | |
|                     ) | |
|                     if len(ptavs) < len(attribute_values): | |
|                         # In this case the template is not compatible with this specific combination | |
|                         continue | |
|                     if len(ptavs) == len(template.attribute_line_ids): | |
|                         if template._is_combination_possible(ptavs): | |
|                             return True | |
|                     elif len(ptavs) < len(template.attribute_line_ids): | |
|                         if len(attribute_values_list) == 1: | |
|                             if any(template._get_possible_combinations(necessary_values=ptavs)): | |
|                                 return True | |
|                         if not possible_combinations: | |
|                             possible_combinations = template._get_possible_combinations() | |
|                         if any(len(ptavs & combination) == len(ptavs) for combination in possible_combinations): | |
|                             return True | |
|                 return False | |
| 
 | |
|             if not multi_value_attribute: | |
|                 possible_attrib_values_list = [attribute_values] | |
|             else: | |
|                 # Cartesian product from dict keys and values | |
|                 possible_attrib_values_list = [request.env['product.attribute.value'].browse([v.id for v in values]) | |
|                                                for | |
|                                                values in cartesian_product(*values_per_attribute.values())] | |
| 
 | |
|             search_product = search_product.filtered( | |
|                 lambda tmpl: filter_template(tmpl, possible_attrib_values_list)) | |
| 
 | |
|         filter_by_price_enabled = request.website.is_view_active('website_sale.filter_products_price') | |
|         if filter_by_price_enabled: | |
|             # TODO Find an alternative way to obtain the domain through the search metadata. | |
|             Product = request.env['product.template'].with_context(bin_size=True) | |
|             domain = self._get_search_domain(search, category, attrib_values) | |
| 
 | |
|             # This is ~4 times more efficient than a search for the cheapest and most expensive products | |
|             from_clause, where_clause, where_params = Product._where_calc(domain).get_sql() | |
|             query = f""" | |
|                     SELECT COALESCE(MIN(list_price), 0) * {conversion_rate}, COALESCE(MAX(list_price), 0) * {conversion_rate} | |
|                       FROM {from_clause} | |
|                      WHERE {where_clause} | |
|                 """ | |
|             request.env.cr.execute(query, where_params) | |
|             available_min_price, available_max_price = request.env.cr.fetchone() | |
| 
 | |
|             if min_price or max_price: | |
|                 if min_price: | |
|                     min_price = min_price if min_price <= available_max_price else available_min_price | |
|                     post['min_price'] = min_price | |
|                 if max_price: | |
|                     max_price = max_price if max_price >= available_min_price else available_max_price | |
|                     post['max_price'] = max_price | |
| 
 | |
| 
 | |
|         website_domain = request.website.website_domain() | |
|         categs_domain = [('parent_id', '=', False)] + website_domain | |
|         if search: | |
|             search_categories = Category.search( | |
|                 [('product_tmpl_ids', 'in', search_product.ids)] + website_domain).parents_and_self | |
|             categs_domain.append(('id', 'in', search_categories.ids)) | |
|         else: | |
|             search_categories = Category | |
|         categs = Category.search(categs_domain) | |
| 
 | |
|         if category: | |
|             url = "/shop/category/%s" % slug(category) | |
| 
 | |
|         product_count = len(search_product) | |
|         pager = request.website.pager(url=url, total=product_count, page=page, step=ppg, scope=7, url_args=post) | |
|         products = Product.search(domain, limit=ppg, offset=pager['offset'], order=self._get_search_order(post)) | |
| 
 | |
|         ProductAttribute = request.env['product.attribute'] | |
|         if products: | |
|             # get all products without limit | |
|             attributes = ProductAttribute.search([('product_tmpl_ids', 'in', search_product.ids)]) | |
|         else: | |
|             attributes = ProductAttribute.browse(attributes_ids) | |
| 
 | |
|         layout_mode = request.session.get('website_sale_shop_layout_mode') | |
|         if not layout_mode: | |
|             if request.website.viewref('website_sale.products_list_view').active: | |
|                 layout_mode = 'list' | |
|             else: | |
|                 layout_mode = 'grid' | |
|         products_prices = products._get_sales_prices(pricelist) | |
| 
 | |
|         Brand = request.env['product.brand'].search([]) | |
|         if compute_brand: | |
|             products_brand = request.env['product.template'].search( | |
|                 ['&', ('brand_id', '=', brand.id), ('sale_ok', '=', True)]) | |
|             product_brand_count = len(products_brand) | |
|             pager_brand = request.website.pager(url=url, total=product_brand_count, page=page, step=ppg, scope=7, | |
|                                                 url_args=post) | |
|             values = { | |
|                 'search': search, | |
|                 'original_search':fuzzy_search_term and search, | |
|                 'category': category, | |
|                 'brand': brand, | |
|                 'attrib_values': attrib_values, | |
|                 'attrib_set': attrib_set, | |
|                 'pager': pager_brand, | |
|                 'pricelist': pricelist, | |
|                 # 'website_sale_pricelists': pricelist, | |
|                 'add_qty': add_qty, | |
|                 'products': products_brand, | |
|                 'search_count': product_brand_count,  # common for all searchbox | |
|                 'bins': TableCompute().process(products_brand, ppg, ppr), | |
|                 'ppg': ppg, | |
|                 'ppr': ppr, | |
|                 'categories': categs, | |
|                 'attributes': attributes, | |
|                 'keep': keep, | |
|                 'search_categories_ids': search_categories.ids, | |
|                 'layout_mode': layout_mode, | |
|                 'brands': Brand, | |
|                 'products_prices': products_prices, | |
|                 'get_product_prices': lambda product: lazy(lambda: products_prices[product.id]), | |
|                 'float_round': tools.float_round, | |
| 
 | |
|             } | |
|             website = request.env['website'].get_current_website() | |
|             filter_by_price_enabled = website.is_view_active('website_sale.filter_products_price') | |
|             if filter_by_price_enabled: | |
|                 values['min_price'] = min_price or available_min_price | |
|                 values['max_price'] = max_price or available_max_price | |
|                 values['available_min_price'] = tools.float_round(available_min_price, 2) | |
|                 values['available_max_price'] = tools.float_round(available_max_price, 2) | |
|             if category: | |
|                 values['main_object'] = category | |
|             values.update(self._get_additional_shop_values(values)) | |
|             print(values,'if values') | |
|             return request.render("website_sale.products", values) | |
|         else: | |
|             values = { | |
|                 'brand': brand, | |
|                 'search': search, | |
|                 'category': category, | |
|                 'original_search': fuzzy_search_term and search, | |
|                 'order': post.get('order', ''), | |
|                 'attrib_values': attrib_values, | |
|                 'attrib_set': attrib_set, | |
|                 'pager': pager, | |
|                 'pricelist': pricelist, | |
|                 'add_qty': add_qty, | |
|                 'products': products, | |
|                 'search_count': product_count,  # common for all searchbox | |
|                 'bins': TableCompute().process(products, ppg, ppr), | |
|                 'ppg': ppg, | |
|                 'ppr': ppr, | |
|                 'categories': categs, | |
|                 'attributes': attributes, | |
|                 'keep': keep, | |
|                 'search_categories_ids': search_categories.ids, | |
|                 'layout_mode': layout_mode, | |
|                 'brands': Brand, | |
|                 'products_prices': products_prices, | |
|                 'get_product_prices': lambda product: lazy(lambda: products_prices[product.id]), | |
|                 'float_round': tools.float_round, | |
| 
 | |
|             } | |
|             website = request.env['website'].get_current_website() | |
|             filter_by_price_enabled = website.is_view_active('website_sale.filter_products_price') | |
|             if filter_by_price_enabled: | |
|                 values['min_price'] = min_price or available_min_price | |
|                 values['max_price'] = max_price or available_max_price | |
|                 values['available_min_price'] = tools.float_round(available_min_price, 2) | |
|                 values['available_max_price'] = tools.float_round(available_max_price, 2) | |
|             if category: | |
|                 values['main_object'] = category | |
|             values.update(self._get_additional_shop_values(values)) | |
|             print(values,'else values') | |
|             return request.render("website_sale.products", values)
 | |
| 
 |