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.
385 lines
15 KiB
385 lines
15 KiB
# -*- coding: utf-8 -*-
|
|
#############################################################################
|
|
#
|
|
# Cybrosys Technologies Pvt. Ltd.
|
|
#
|
|
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
|
|
# Author: Ayisha Sumayya K (odoo@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/>.
|
|
#
|
|
#############################################################################
|
|
|
|
from collections import defaultdict
|
|
from itertools import product as cartesian_product
|
|
from datetime import datetime
|
|
from werkzeug.exceptions import NotFound
|
|
|
|
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.models.ir_http import sitemap_qs2dom
|
|
|
|
from odoo.osv import expression
|
|
from odoo.tools import lazy
|
|
|
|
|
|
class TableComputeCoffee(object):
|
|
""" Compute data related to coffee shop theme """
|
|
|
|
def __init__(self):
|
|
self.table = {}
|
|
|
|
def _check_place(self, posx, posy, sizex, sizey, ppr):
|
|
res = True
|
|
for yaxix in range(sizey):
|
|
for xaxix in range(sizex):
|
|
if posx + xaxix >= ppr:
|
|
res = False
|
|
break
|
|
row = self.table.setdefault(posy + yaxix, {})
|
|
if row.setdefault(posx + xaxix) is not None:
|
|
res = False
|
|
break
|
|
for x in range(ppr):
|
|
self.table[posy + yaxix].setdefault(xaxix, None)
|
|
return res
|
|
|
|
def process(self, products, ppg=20, ppr=4):
|
|
"""Compute products positions on the grid"""
|
|
minpos = 0
|
|
index = 0
|
|
maxy = 0
|
|
rec = 0
|
|
for pdct in products:
|
|
rec = min(max(pdct.website_size_x, 1), ppr)
|
|
res = min(max(pdct.website_size_y, 1), ppr)
|
|
if index >= ppg:
|
|
rec = res = 1
|
|
|
|
pos = minpos
|
|
while not self._check_place(pos % ppr, pos // ppr, rec, res, ppr):
|
|
pos += 1
|
|
if index >= ppg and ((pos + 1.0) // ppr) > maxy:
|
|
break
|
|
|
|
if rec == 1 and res == 1:
|
|
minpos = pos // ppr
|
|
|
|
for y2 in range(res):
|
|
for x2 in range(rec):
|
|
self.table[(pos // ppr) + y2][(pos % ppr) + x2] = False
|
|
self.table[pos // ppr][pos % ppr] = {
|
|
'product': pdct, 'x': rec, 'y': res,
|
|
'ribbon': pdct._get_website_ribbon(),
|
|
}
|
|
if index <= ppg:
|
|
maxy = max(maxy, res + (pos // ppr))
|
|
index += 1
|
|
|
|
rows = sorted(self.table.items())
|
|
rows = [r[1] for r in rows]
|
|
for col in range(len(rows)):
|
|
cols = sorted(rows[col].items())
|
|
rec += len(cols)
|
|
rows[col] = [r[1] for r in cols if r[1]]
|
|
|
|
return rows
|
|
|
|
|
|
class ThemeCoffeeMenu(http.Controller):
|
|
""" controller for rendering datas to menu page """
|
|
|
|
def _get_search_order(self, post):
|
|
""" OrderBy will be parsed in orm and so no direct sql injection id is
|
|
added to be sure that order is a unique sort key
|
|
"""
|
|
order = post.get('order') or \
|
|
request.env['website'].get_current_website().shop_default_sort
|
|
return 'is_published desc, %s, id desc' % order
|
|
|
|
def _get_search_domain(self, search, category, attrib_values,
|
|
search_in_description=True):
|
|
domains = [request.website.sale_product_domain()]
|
|
if search:
|
|
for srch in search.split(" "):
|
|
subdomains = [
|
|
[('name', 'ilike', srch)],
|
|
[('product_variant_ids.default_code', 'ilike', srch)]
|
|
]
|
|
if search_in_description:
|
|
subdomains.append([('website_description', 'ilike', srch)])
|
|
subdomains.append([('description_sale', 'ilike', srch)])
|
|
domains.append(expression.OR(subdomains))
|
|
|
|
if category:
|
|
domains.append([('public_categ_ids', 'child_of', int(category))])
|
|
|
|
if attrib_values:
|
|
attrib = None
|
|
ids = []
|
|
for value in attrib_values:
|
|
if not attrib:
|
|
attrib = value[0]
|
|
ids.append(value[1])
|
|
elif value[0] == attrib:
|
|
ids.append(value[1])
|
|
else:
|
|
domains.append([('attribute_line_ids.value_ids', 'in', ids)])
|
|
attrib = value[0]
|
|
ids = [value[1]]
|
|
if attrib:
|
|
domains.append([('attribute_line_ids.value_ids', 'in', ids)])
|
|
|
|
return expression.AND(domains)
|
|
|
|
def sitemap_shop(env, rule, qs):
|
|
if not qs or qs.lower() in '/menu':
|
|
yield {'loc': '/menu'}
|
|
|
|
Category = env['product.public.category']
|
|
dom = sitemap_qs2dom(qs, '/menu/category', Category._rec_name)
|
|
dom += env['website'].get_current_website().website_domain()
|
|
for cat in Category.search(dom):
|
|
loc = '/menu/category/%s' % slug(cat)
|
|
if not qs or qs.lower() in loc:
|
|
yield {'loc': loc}
|
|
|
|
def _get_search_options(
|
|
self, category=None, attrib_values=None, pricelist=None, **post):
|
|
return {
|
|
'displayDescription': True,
|
|
'displayDetail': True,
|
|
'displayExtraDetail': True,
|
|
'displayExtraLink': True,
|
|
'displayImage': True,
|
|
'allowFuzzy': not post.get('noFuzzy'),
|
|
'category': str(category.id) if category else None,
|
|
'attrib_values': attrib_values,
|
|
'display_currency': pricelist.currency_id,
|
|
}
|
|
|
|
def _shop_lookup_products(self, attrib_set, options, post, search, website):
|
|
""" No limit because attributes are obtained from complete
|
|
product list"""
|
|
|
|
product_count, details, fuzzy_search_term = website._search_with_fuzzy(
|
|
"products_only", search, limit=None,
|
|
order=self._get_search_order(post), options=options
|
|
)
|
|
search_result = details[0].get(
|
|
'results', request.env['product.template']
|
|
).with_context(bin_size=True)
|
|
if attrib_set:
|
|
attribute_values = request.env['product.attribute.value'].browse(attrib_set)
|
|
values_per_attribute = defaultdict(
|
|
lambda: request.env['product.attribute.value'])
|
|
|
|
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):
|
|
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:
|
|
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_result = search_result.filtered(
|
|
lambda tmpl: filter_template(tmpl, possible_attrib_values_list)
|
|
)
|
|
return fuzzy_search_term, product_count, search_result
|
|
|
|
def _menu_get_query_url_kwargs(self, category, search, attrib=None,
|
|
order=None, **post):
|
|
return {
|
|
'category': category,
|
|
'search': search,
|
|
'attrib': attrib,
|
|
'order': order,
|
|
}
|
|
|
|
def _get_additional_shop_values(self, values):
|
|
""" Hook to update values used for rendering website_sale.products template """
|
|
return {}
|
|
|
|
@http.route([
|
|
'/menu',
|
|
'/menu/page/<int:page>',
|
|
'/menu/category/<model("product.public.category"):category>',
|
|
'/menu/category/<model("product.public.category"):category>/page/<int:page>',
|
|
], type='http', auth="public", website=True, sitemap=sitemap_shop)
|
|
def menu_page(self, page=0, category=None, search='',
|
|
min_price=0.0, max_price=0.0, ppg=False, **post):
|
|
add_qty = int(post.get('add_qty', 1))
|
|
|
|
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
|
|
|
|
website = request.env['website'].get_current_website()
|
|
if ppg:
|
|
try:
|
|
ppg = int(ppg)
|
|
post['ppg'] = ppg
|
|
except ValueError:
|
|
ppg = False
|
|
if not ppg:
|
|
ppg = website.shop_ppg or 20
|
|
|
|
ppr = 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}
|
|
|
|
keep = QueryURL('/menu', **self._menu_get_query_url_kwargs(category
|
|
and int(category), search, min_price, max_price, **post))
|
|
|
|
now = datetime.timestamp(datetime.now())
|
|
pricelist = request.env['product.pricelist'].\
|
|
browse(request.session.get('website_sale_current_pl'))
|
|
if not pricelist or request.session.\
|
|
get('website_sale_pricelist_time', 0) < now - 60*60:
|
|
pricelist = 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)
|
|
|
|
url = "/menu"
|
|
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,
|
|
**post
|
|
)
|
|
fuzzy_search_term, product_count, search_product = \
|
|
self._shop_lookup_products(attrib_set, options, post, search, website)
|
|
|
|
website_domain = 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 = lazy(lambda: Category.search(categs_domain))
|
|
|
|
if category:
|
|
url = "/menu/category/%s" % slug(category)
|
|
|
|
pager = website.pager(url=url, total=product_count, page=page,
|
|
step=ppg, scope=7, url_args=post)
|
|
offset = pager['offset']
|
|
products = search_product[offset:offset + ppg]
|
|
|
|
ProductAttribute = request.env['product.attribute']
|
|
if products:
|
|
attributes = lazy(lambda: ProductAttribute.search([
|
|
('product_tmpl_ids', 'in', search_product.ids),
|
|
('visibility', '=', 'visible'),
|
|
]))
|
|
else:
|
|
attributes = lazy(lambda: ProductAttribute.browse(attributes_ids))
|
|
|
|
layout_mode = request.session.get('website_sale_shop_layout_mode')
|
|
if not layout_mode:
|
|
if website.viewref('website_sale.products_list_view').active:
|
|
layout_mode = 'list'
|
|
else:
|
|
layout_mode = 'grid'
|
|
request.session['website_sale_shop_layout_mode'] = layout_mode
|
|
|
|
products_prices = lazy(lambda: products._get_sales_prices(pricelist))
|
|
|
|
values = {
|
|
'order': post.get('order', ''),
|
|
'category': category,
|
|
'attrib_values': attrib_values,
|
|
'attrib_set': attrib_set,
|
|
'pager': pager,
|
|
'pricelist': pricelist,
|
|
'add_qty': add_qty,
|
|
'products': products,
|
|
'search_product': search_product,
|
|
'search_count': product_count,
|
|
'bins': lazy(lambda: TableComputeCoffee().process(products, ppg, ppr)),
|
|
'ppg': ppg,
|
|
'ppr': ppr,
|
|
'categories': categs,
|
|
'attributes': attributes,
|
|
'keep': keep,
|
|
'search_categories_ids': search_categories.ids,
|
|
'products_prices': products_prices,
|
|
'get_product_prices': lambda product: lazy(
|
|
lambda: products_prices[product.id]
|
|
),
|
|
'float_round': tools.float_round,
|
|
}
|
|
if category:
|
|
values['main_object'] = category
|
|
values.update(self._get_additional_shop_values(values))
|
|
return request.render("theme_coffee_shop.coffee_menu", values)
|
|
|