Before this commit, routes were prevented to be accessed if the route model was not accessible from the current website, eg you couldn't access blog1 which is set to website2 from website 1. That would raise a 404 even from admin/editor. This commit introduce that behavior at a lowel level in a generic way, instead of having to write it on every route. Note that routes without a model converter won't benefit from this. Closes #63499 closes odoo/odoo#64313 Related: odoo/enterprise#15688 Signed-off-by: Jérémy Kersten (jke) <jke@openerp.com>
1253 lines
54 KiB
Python
1253 lines
54 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from werkzeug.exceptions import Forbidden, NotFound
|
|
|
|
from odoo import fields, http, SUPERUSER_ID, tools, _
|
|
from odoo.http import request
|
|
from odoo.addons.base.models.ir_qweb_fields import nl2br
|
|
from odoo.addons.http_routing.models.ir_http import slug
|
|
from odoo.addons.payment.controllers.portal import PaymentProcessing
|
|
from odoo.addons.website.controllers.main import QueryURL
|
|
from odoo.addons.website.models.ir_http import sitemap_qs2dom
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.addons.portal.controllers.portal import _build_url_w_params
|
|
from odoo.addons.website.controllers import main
|
|
from odoo.addons.website_form.controllers.main import WebsiteForm
|
|
from odoo.osv import expression
|
|
_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 WebsiteSaleForm(WebsiteForm):
|
|
|
|
@http.route('/website_form/shop.sale.order', type='http', auth="public", methods=['POST'], website=True)
|
|
def website_form_saleorder(self, **kwargs):
|
|
model_record = request.env.ref('sale.model_sale_order')
|
|
try:
|
|
data = self.extract_data(model_record, kwargs)
|
|
except ValidationError as e:
|
|
return json.dumps({'error_fields': e.args[0]})
|
|
|
|
order = request.website.sale_get_order()
|
|
if data['record']:
|
|
order.write(data['record'])
|
|
|
|
if data['custom']:
|
|
values = {
|
|
'body': nl2br(data['custom']),
|
|
'model': 'sale.order',
|
|
'message_type': 'comment',
|
|
'no_auto_thread': False,
|
|
'res_id': order.id,
|
|
}
|
|
request.env['mail.message'].with_user(SUPERUSER_ID).create(values)
|
|
|
|
if data['attachments']:
|
|
self.insert_attachment(model_record, order.id, data['attachments'])
|
|
|
|
return json.dumps({'id': order.id})
|
|
|
|
|
|
class Website(main.Website):
|
|
@http.route()
|
|
def get_switchable_related_views(self, key):
|
|
views = super(Website, self).get_switchable_related_views(key)
|
|
if key == 'website_sale.product':
|
|
if not request.env.user.has_group('product.group_product_variant'):
|
|
view_product_variants = request.website.viewref('website_sale.product_variants')
|
|
views = [v for v in views if v['id'] != view_product_variants.id]
|
|
return views
|
|
|
|
@http.route()
|
|
def toggle_switchable_view(self, view_key):
|
|
super(Website, self).toggle_switchable_view(view_key)
|
|
if view_key in ('website_sale.products_list_view', 'website_sale.add_grid_or_list_option'):
|
|
request.session.pop('website_sale_shop_layout_mode', None)
|
|
|
|
|
|
class WebsiteSale(http.Controller):
|
|
|
|
def _get_pricelist_context(self):
|
|
pricelist_context = dict(request.env.context)
|
|
pricelist = False
|
|
if not pricelist_context.get('pricelist'):
|
|
pricelist = request.website.get_current_pricelist()
|
|
pricelist_context['pricelist'] = pricelist.id
|
|
else:
|
|
pricelist = request.env['product.pricelist'].browse(pricelist_context['pricelist'])
|
|
|
|
return pricelist_context, pricelist
|
|
|
|
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 'website_sequence ASC'
|
|
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([('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 '/shop':
|
|
yield {'loc': '/shop'}
|
|
|
|
Category = env['product.public.category']
|
|
dom = sitemap_qs2dom(qs, '/shop/category', Category._rec_name)
|
|
dom += env['website'].get_current_website().website_domain()
|
|
for cat in Category.search(dom):
|
|
loc = '/shop/category/%s' % slug(cat)
|
|
if not qs or qs.lower() in loc:
|
|
yield {'loc': loc}
|
|
|
|
@http.route([
|
|
'''/shop''',
|
|
'''/shop/page/<int:page>''',
|
|
'''/shop/category/<model("product.public.category"):category>''',
|
|
'''/shop/category/<model("product.public.category"):category>/page/<int:page>'''
|
|
], type='http', auth="public", website=True, sitemap=sitemap_shop)
|
|
def shop(self, page=0, category=None, search='', 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
|
|
|
|
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'))
|
|
|
|
pricelist_context, pricelist = self._get_pricelist_context()
|
|
|
|
request.context = dict(request.context, pricelist=pricelist.id, partner=request.env.user.partner_id)
|
|
|
|
url = "/shop"
|
|
if search:
|
|
post["search"] = search
|
|
if attrib_list:
|
|
post['attrib'] = attrib_list
|
|
|
|
Product = request.env['product.template'].with_context(bin_size=True)
|
|
|
|
search_product = Product.search(domain, order=self._get_search_order(post))
|
|
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)
|
|
offset = pager['offset']
|
|
products = search_product[offset: offset + ppg]
|
|
|
|
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'
|
|
|
|
values = {
|
|
'search': search,
|
|
'category': category,
|
|
'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,
|
|
}
|
|
if category:
|
|
values['main_object'] = category
|
|
return request.render("website_sale.products", values)
|
|
|
|
@http.route(['/shop/<model("product.template"):product>'], type='http', auth="public", website=True, sitemap=True)
|
|
def product(self, product, category='', search='', **kwargs):
|
|
return request.render("website_sale.product", self._prepare_product_values(product, category, search, **kwargs))
|
|
|
|
@http.route(['/shop/product/<model("product.template"):product>'], type='http', auth="public", website=True, sitemap=False)
|
|
def old_product(self, product, category='', search='', **kwargs):
|
|
# Compatibility pre-v14
|
|
return request.redirect(_build_url_w_params("/shop/%s" % slug(product), request.params), code=301)
|
|
|
|
def _prepare_product_values(self, product, category, search, **kwargs):
|
|
add_qty = int(kwargs.get('add_qty', 1))
|
|
|
|
product_context = dict(request.env.context, quantity=add_qty,
|
|
active_id=product.id,
|
|
partner=request.env.user.partner_id)
|
|
ProductCategory = request.env['product.public.category']
|
|
|
|
if category:
|
|
category = ProductCategory.browse(int(category)).exists()
|
|
|
|
attrib_list = request.httprequest.args.getlist('attrib')
|
|
attrib_values = [[int(x) for x in v.split("-")] for v in attrib_list if v]
|
|
attrib_set = {v[1] for v in attrib_values}
|
|
|
|
keep = QueryURL('/shop', category=category and category.id, search=search, attrib=attrib_list)
|
|
|
|
categs = ProductCategory.search([('parent_id', '=', False)])
|
|
|
|
pricelist = request.website.get_current_pricelist()
|
|
|
|
if not product_context.get('pricelist'):
|
|
product_context['pricelist'] = pricelist.id
|
|
product = product.with_context(product_context)
|
|
|
|
# Needed to trigger the recently viewed product rpc
|
|
view_track = request.website.viewref("website_sale.product").track
|
|
|
|
return {
|
|
'search': search,
|
|
'category': category,
|
|
'pricelist': pricelist,
|
|
'attrib_values': attrib_values,
|
|
'attrib_set': attrib_set,
|
|
'keep': keep,
|
|
'categories': categs,
|
|
'main_object': product,
|
|
'product': product,
|
|
'add_qty': add_qty,
|
|
'view_track': view_track,
|
|
}
|
|
|
|
@http.route(['/shop/change_pricelist/<model("product.pricelist"):pl_id>'], type='http', auth="public", website=True, sitemap=False)
|
|
def pricelist_change(self, pl_id, **post):
|
|
if (pl_id.selectable or pl_id == request.env.user.partner_id.property_product_pricelist) \
|
|
and request.website.is_pricelist_available(pl_id.id):
|
|
request.session['website_sale_current_pl'] = pl_id.id
|
|
request.website.sale_get_order(force_pricelist=pl_id.id)
|
|
return request.redirect(request.httprequest.referrer or '/shop')
|
|
|
|
@http.route(['/shop/pricelist'], type='http', auth="public", website=True, sitemap=False)
|
|
def pricelist(self, promo, **post):
|
|
redirect = post.get('r', '/shop/cart')
|
|
# empty promo code is used to reset/remove pricelist (see `sale_get_order()`)
|
|
if promo:
|
|
pricelist = request.env['product.pricelist'].sudo().search([('code', '=', promo)], limit=1)
|
|
if (not pricelist or (pricelist and not request.website.is_pricelist_available(pricelist.id))):
|
|
return request.redirect("%s?code_not_available=1" % redirect)
|
|
|
|
request.website.sale_get_order(code=promo)
|
|
return request.redirect(redirect)
|
|
|
|
@http.route(['/shop/cart'], type='http', auth="public", website=True, sitemap=False)
|
|
def cart(self, access_token=None, revive='', **post):
|
|
"""
|
|
Main cart management + abandoned cart revival
|
|
access_token: Abandoned cart SO access token
|
|
revive: Revival method when abandoned cart. Can be 'merge' or 'squash'
|
|
"""
|
|
order = request.website.sale_get_order()
|
|
if order and order.state != 'draft':
|
|
request.session['sale_order_id'] = None
|
|
order = request.website.sale_get_order()
|
|
values = {}
|
|
if access_token:
|
|
abandoned_order = request.env['sale.order'].sudo().search([('access_token', '=', access_token)], limit=1)
|
|
if not abandoned_order: # wrong token (or SO has been deleted)
|
|
raise NotFound()
|
|
if abandoned_order.state != 'draft': # abandoned cart already finished
|
|
values.update({'abandoned_proceed': True})
|
|
elif revive == 'squash' or (revive == 'merge' and not request.session.get('sale_order_id')): # restore old cart or merge with unexistant
|
|
request.session['sale_order_id'] = abandoned_order.id
|
|
return request.redirect('/shop/cart')
|
|
elif revive == 'merge':
|
|
abandoned_order.order_line.write({'order_id': request.session['sale_order_id']})
|
|
abandoned_order.action_cancel()
|
|
elif abandoned_order.id != request.session.get('sale_order_id'): # abandoned cart found, user have to choose what to do
|
|
values.update({'access_token': abandoned_order.access_token})
|
|
|
|
values.update({
|
|
'website_sale_order': order,
|
|
'date': fields.Date.today(),
|
|
'suggested_products': [],
|
|
})
|
|
if order:
|
|
order.order_line.filtered(lambda l: not l.product_id.active).unlink()
|
|
_order = order
|
|
if not request.env.context.get('pricelist'):
|
|
_order = order.with_context(pricelist=order.pricelist_id.id)
|
|
values['suggested_products'] = _order._cart_accessories()
|
|
|
|
if post.get('type') == 'popover':
|
|
# force no-cache so IE11 doesn't cache this XHR
|
|
return request.render("website_sale.cart_popover", values, headers={'Cache-Control': 'no-cache'})
|
|
|
|
return request.render("website_sale.cart", values)
|
|
|
|
@http.route(['/shop/cart/update'], type='http', auth="public", methods=['POST'], website=True)
|
|
def cart_update(self, product_id, add_qty=1, set_qty=0, **kw):
|
|
"""This route is called when adding a product to cart (no options)."""
|
|
sale_order = request.website.sale_get_order(force_create=True)
|
|
if sale_order.state != 'draft':
|
|
request.session['sale_order_id'] = None
|
|
sale_order = request.website.sale_get_order(force_create=True)
|
|
|
|
product_custom_attribute_values = None
|
|
if kw.get('product_custom_attribute_values'):
|
|
product_custom_attribute_values = json.loads(kw.get('product_custom_attribute_values'))
|
|
|
|
no_variant_attribute_values = None
|
|
if kw.get('no_variant_attribute_values'):
|
|
no_variant_attribute_values = json.loads(kw.get('no_variant_attribute_values'))
|
|
|
|
sale_order._cart_update(
|
|
product_id=int(product_id),
|
|
add_qty=add_qty,
|
|
set_qty=set_qty,
|
|
product_custom_attribute_values=product_custom_attribute_values,
|
|
no_variant_attribute_values=no_variant_attribute_values
|
|
)
|
|
|
|
if kw.get('express'):
|
|
return request.redirect("/shop/checkout?express=1")
|
|
|
|
return request.redirect("/shop/cart")
|
|
|
|
@http.route(['/shop/cart/update_json'], type='json', auth="public", methods=['POST'], website=True, csrf=False)
|
|
def cart_update_json(self, product_id, line_id=None, add_qty=None, set_qty=None, display=True):
|
|
"""This route is called when changing quantity from the cart or adding
|
|
a product from the wishlist."""
|
|
order = request.website.sale_get_order(force_create=1)
|
|
if order.state != 'draft':
|
|
request.website.sale_reset()
|
|
return {}
|
|
|
|
value = order._cart_update(product_id=product_id, line_id=line_id, add_qty=add_qty, set_qty=set_qty)
|
|
|
|
if not order.cart_quantity:
|
|
request.website.sale_reset()
|
|
return value
|
|
|
|
order = request.website.sale_get_order()
|
|
value['cart_quantity'] = order.cart_quantity
|
|
|
|
if not display:
|
|
return value
|
|
|
|
value['website_sale.cart_lines'] = request.env['ir.ui.view']._render_template("website_sale.cart_lines", {
|
|
'website_sale_order': order,
|
|
'date': fields.Date.today(),
|
|
'suggested_products': order._cart_accessories()
|
|
})
|
|
value['website_sale.short_cart_summary'] = request.env['ir.ui.view']._render_template("website_sale.short_cart_summary", {
|
|
'website_sale_order': order,
|
|
})
|
|
return value
|
|
|
|
@http.route('/shop/save_shop_layout_mode', type='json', auth='public', website=True)
|
|
def save_shop_layout_mode(self, layout_mode):
|
|
assert layout_mode in ('grid', 'list'), "Invalid shop layout mode"
|
|
request.session['website_sale_shop_layout_mode'] = layout_mode
|
|
|
|
# ------------------------------------------------------
|
|
# Checkout
|
|
# ------------------------------------------------------
|
|
|
|
def checkout_redirection(self, order):
|
|
# must have a draft sales order with lines at this point, otherwise reset
|
|
if not order or order.state != 'draft':
|
|
request.session['sale_order_id'] = None
|
|
request.session['sale_transaction_id'] = None
|
|
return request.redirect('/shop')
|
|
|
|
if order and not order.order_line:
|
|
return request.redirect('/shop/cart')
|
|
|
|
# if transaction pending / done: redirect to confirmation
|
|
tx = request.env.context.get('website_sale_transaction')
|
|
if tx and tx.state != 'draft':
|
|
return request.redirect('/shop/payment/confirmation/%s' % order.id)
|
|
|
|
def checkout_values(self, **kw):
|
|
order = request.website.sale_get_order(force_create=1)
|
|
shippings = []
|
|
if order.partner_id != request.website.user_id.sudo().partner_id:
|
|
Partner = order.partner_id.with_context(show_address=1).sudo()
|
|
shippings = Partner.search([
|
|
("id", "child_of", order.partner_id.commercial_partner_id.ids),
|
|
'|', ("type", "in", ["delivery", "other"]), ("id", "=", order.partner_id.commercial_partner_id.id)
|
|
], order='id desc')
|
|
if shippings:
|
|
if kw.get('partner_id') or 'use_billing' in kw:
|
|
if 'use_billing' in kw:
|
|
partner_id = order.partner_id.id
|
|
else:
|
|
partner_id = int(kw.get('partner_id'))
|
|
if partner_id in shippings.mapped('id'):
|
|
order.partner_shipping_id = partner_id
|
|
elif not order.partner_shipping_id:
|
|
last_order = request.env['sale.order'].sudo().search([("partner_id", "=", order.partner_id.id)], order='id desc', limit=1)
|
|
order.partner_shipping_id.id = last_order and last_order.id
|
|
|
|
values = {
|
|
'order': order,
|
|
'shippings': shippings,
|
|
'only_services': order and order.only_services or False
|
|
}
|
|
return values
|
|
|
|
def _get_mandatory_billing_fields(self):
|
|
return ["name", "email", "street", "city", "country_id"]
|
|
|
|
def _get_mandatory_shipping_fields(self):
|
|
return ["name", "street", "city", "country_id"]
|
|
|
|
def checkout_form_validate(self, mode, all_form_values, data):
|
|
# mode: tuple ('new|edit', 'billing|shipping')
|
|
# all_form_values: all values before preprocess
|
|
# data: values after preprocess
|
|
error = dict()
|
|
error_message = []
|
|
|
|
# Required fields from form
|
|
required_fields = [f for f in (all_form_values.get('field_required') or '').split(',') if f]
|
|
# Required fields from mandatory field function
|
|
required_fields += mode[1] == 'shipping' and self._get_mandatory_shipping_fields() or self._get_mandatory_billing_fields()
|
|
# Check if state required
|
|
country = request.env['res.country']
|
|
if data.get('country_id'):
|
|
country = country.browse(int(data.get('country_id')))
|
|
if country.state_required:
|
|
required_fields += ['state_id']
|
|
if country.zip_required:
|
|
required_fields += ['zip']
|
|
|
|
# error message for empty required fields
|
|
for field_name in required_fields:
|
|
if not data.get(field_name):
|
|
error[field_name] = 'missing'
|
|
|
|
# email validation
|
|
if data.get('email') and not tools.single_email_re.match(data.get('email')):
|
|
error["email"] = 'error'
|
|
error_message.append(_('Invalid Email! Please enter a valid email address.'))
|
|
|
|
# vat validation
|
|
Partner = request.env['res.partner']
|
|
if data.get("vat") and hasattr(Partner, "check_vat"):
|
|
if data.get("country_id"):
|
|
data["vat"] = Partner.fix_eu_vat_number(data.get("country_id"), data.get("vat"))
|
|
partner_dummy = Partner.new({
|
|
'vat': data['vat'],
|
|
'country_id': (int(data['country_id'])
|
|
if data.get('country_id') else False),
|
|
})
|
|
try:
|
|
partner_dummy.check_vat()
|
|
except ValidationError:
|
|
error["vat"] = 'error'
|
|
|
|
if [err for err in error.values() if err == 'missing']:
|
|
error_message.append(_('Some required fields are empty.'))
|
|
|
|
return error, error_message
|
|
|
|
def _checkout_form_save(self, mode, checkout, all_values):
|
|
Partner = request.env['res.partner']
|
|
if mode[0] == 'new':
|
|
partner_id = Partner.sudo().with_context(tracking_disable=True).create(checkout).id
|
|
elif mode[0] == 'edit':
|
|
partner_id = int(all_values.get('partner_id', 0))
|
|
if partner_id:
|
|
# double check
|
|
order = request.website.sale_get_order()
|
|
shippings = Partner.sudo().search([("id", "child_of", order.partner_id.commercial_partner_id.ids)])
|
|
if partner_id not in shippings.mapped('id') and partner_id != order.partner_id.id:
|
|
return Forbidden()
|
|
Partner.browse(partner_id).sudo().write(checkout)
|
|
return partner_id
|
|
|
|
def values_preprocess(self, order, mode, values):
|
|
# Convert the values for many2one fields to integer since they are used as IDs
|
|
partner_fields = request.env['res.partner']._fields
|
|
return {
|
|
k: (bool(v) and int(v)) if k in partner_fields and partner_fields[k].type == 'many2one' else v
|
|
for k, v in values.items()
|
|
}
|
|
|
|
def values_postprocess(self, order, mode, values, errors, error_msg):
|
|
new_values = {}
|
|
authorized_fields = request.env['ir.model']._get('res.partner')._get_form_writable_fields()
|
|
for k, v in values.items():
|
|
# don't drop empty value, it could be a field to reset
|
|
if k in authorized_fields and v is not None:
|
|
new_values[k] = v
|
|
else: # DEBUG ONLY
|
|
if k not in ('field_required', 'partner_id', 'callback', 'submitted'): # classic case
|
|
_logger.debug("website_sale postprocess: %s value has been dropped (empty or not writable)" % k)
|
|
|
|
new_values['team_id'] = request.website.salesteam_id and request.website.salesteam_id.id
|
|
new_values['user_id'] = request.website.salesperson_id and request.website.salesperson_id.id
|
|
|
|
if request.website.specific_user_account:
|
|
new_values['website_id'] = request.website.id
|
|
|
|
if mode[0] == 'new':
|
|
new_values['company_id'] = request.website.company_id.id
|
|
|
|
lang = request.lang.code if request.lang.code in request.website.mapped('language_ids.code') else None
|
|
if lang:
|
|
new_values['lang'] = lang
|
|
if mode == ('edit', 'billing') and order.partner_id.type == 'contact':
|
|
new_values['type'] = 'other'
|
|
if mode[1] == 'shipping':
|
|
new_values['parent_id'] = order.partner_id.commercial_partner_id.id
|
|
new_values['type'] = 'delivery'
|
|
|
|
return new_values, errors, error_msg
|
|
|
|
@http.route(['/shop/address'], type='http', methods=['GET', 'POST'], auth="public", website=True, sitemap=False)
|
|
def address(self, **kw):
|
|
Partner = request.env['res.partner'].with_context(show_address=1).sudo()
|
|
order = request.website.sale_get_order()
|
|
|
|
redirection = self.checkout_redirection(order)
|
|
if redirection:
|
|
return redirection
|
|
|
|
mode = (False, False)
|
|
can_edit_vat = False
|
|
def_country_id = order.partner_id.country_id
|
|
values, errors = {}, {}
|
|
|
|
partner_id = int(kw.get('partner_id', -1))
|
|
|
|
# IF PUBLIC ORDER
|
|
if order.partner_id.id == request.website.user_id.sudo().partner_id.id:
|
|
mode = ('new', 'billing')
|
|
can_edit_vat = True
|
|
country_code = request.session['geoip'].get('country_code')
|
|
if country_code:
|
|
def_country_id = request.env['res.country'].search([('code', '=', country_code)], limit=1)
|
|
else:
|
|
def_country_id = request.website.user_id.sudo().country_id
|
|
# IF ORDER LINKED TO A PARTNER
|
|
else:
|
|
if partner_id > 0:
|
|
if partner_id == order.partner_id.id:
|
|
mode = ('edit', 'billing')
|
|
can_edit_vat = order.partner_id.can_edit_vat()
|
|
else:
|
|
shippings = Partner.search([('id', 'child_of', order.partner_id.commercial_partner_id.ids)])
|
|
if partner_id in shippings.mapped('id'):
|
|
mode = ('edit', 'shipping')
|
|
else:
|
|
return Forbidden()
|
|
if mode:
|
|
values = Partner.browse(partner_id)
|
|
elif partner_id == -1:
|
|
mode = ('new', 'shipping')
|
|
else: # no mode - refresh without post?
|
|
return request.redirect('/shop/checkout')
|
|
|
|
# IF POSTED
|
|
if 'submitted' in kw:
|
|
pre_values = self.values_preprocess(order, mode, kw)
|
|
errors, error_msg = self.checkout_form_validate(mode, kw, pre_values)
|
|
post, errors, error_msg = self.values_postprocess(order, mode, pre_values, errors, error_msg)
|
|
|
|
if errors:
|
|
errors['error_message'] = error_msg
|
|
values = kw
|
|
else:
|
|
partner_id = self._checkout_form_save(mode, post, kw)
|
|
if mode[1] == 'billing':
|
|
order.partner_id = partner_id
|
|
order.with_context(not_self_saleperson=True).onchange_partner_id()
|
|
# This is the *only* thing that the front end user will see/edit anyway when choosing billing address
|
|
order.partner_invoice_id = partner_id
|
|
if not kw.get('use_same'):
|
|
kw['callback'] = kw.get('callback') or \
|
|
(not order.only_services and (mode[0] == 'edit' and '/shop/checkout' or '/shop/address'))
|
|
elif mode[1] == 'shipping':
|
|
order.partner_shipping_id = partner_id
|
|
|
|
# TDE FIXME: don't ever do this
|
|
order.message_partner_ids = [(4, partner_id), (3, request.website.partner_id.id)]
|
|
if not errors:
|
|
return request.redirect(kw.get('callback') or '/shop/confirm_order')
|
|
|
|
country = 'country_id' in values and values['country_id'] != '' and request.env['res.country'].browse(int(values['country_id']))
|
|
country = country and country.exists() or def_country_id
|
|
render_values = {
|
|
'website_sale_order': order,
|
|
'partner_id': partner_id,
|
|
'mode': mode,
|
|
'checkout': values,
|
|
'can_edit_vat': can_edit_vat,
|
|
'country': country,
|
|
'country_states': country.get_website_sale_states(mode=mode[1]),
|
|
'countries': country.get_website_sale_countries(mode=mode[1]),
|
|
'error': errors,
|
|
'callback': kw.get('callback'),
|
|
'only_services': order and order.only_services,
|
|
}
|
|
return request.render("website_sale.address", render_values)
|
|
|
|
@http.route(['/shop/checkout'], type='http', auth="public", website=True, sitemap=False)
|
|
def checkout(self, **post):
|
|
order = request.website.sale_get_order()
|
|
|
|
redirection = self.checkout_redirection(order)
|
|
if redirection:
|
|
return redirection
|
|
|
|
if order.partner_id.id == request.website.user_id.sudo().partner_id.id:
|
|
return request.redirect('/shop/address')
|
|
|
|
for f in self._get_mandatory_billing_fields():
|
|
if not order.partner_id[f]:
|
|
return request.redirect('/shop/address?partner_id=%d' % order.partner_id.id)
|
|
|
|
values = self.checkout_values(**post)
|
|
|
|
if post.get('express'):
|
|
return request.redirect('/shop/confirm_order')
|
|
|
|
values.update({'website_sale_order': order})
|
|
|
|
# Avoid useless rendering if called in ajax
|
|
if post.get('xhr'):
|
|
return 'ok'
|
|
return request.render("website_sale.checkout", values)
|
|
|
|
@http.route(['/shop/confirm_order'], type='http', auth="public", website=True, sitemap=False)
|
|
def confirm_order(self, **post):
|
|
order = request.website.sale_get_order()
|
|
|
|
redirection = self.checkout_redirection(order)
|
|
if redirection:
|
|
return redirection
|
|
|
|
order.onchange_partner_shipping_id()
|
|
order.order_line._compute_tax_id()
|
|
request.session['sale_last_order_id'] = order.id
|
|
request.website.sale_get_order(update_pricelist=True)
|
|
extra_step = request.website.viewref('website_sale.extra_info_option')
|
|
if extra_step.active:
|
|
return request.redirect("/shop/extra_info")
|
|
|
|
return request.redirect("/shop/payment")
|
|
|
|
# ------------------------------------------------------
|
|
# Extra step
|
|
# ------------------------------------------------------
|
|
@http.route(['/shop/extra_info'], type='http', auth="public", website=True, sitemap=False)
|
|
def extra_info(self, **post):
|
|
# Check that this option is activated
|
|
extra_step = request.website.viewref('website_sale.extra_info_option')
|
|
if not extra_step.active:
|
|
return request.redirect("/shop/payment")
|
|
|
|
# check that cart is valid
|
|
order = request.website.sale_get_order()
|
|
redirection = self.checkout_redirection(order)
|
|
if redirection:
|
|
return redirection
|
|
|
|
# if form posted
|
|
if 'post_values' in post:
|
|
values = {}
|
|
for field_name, field_value in post.items():
|
|
if field_name in request.env['sale.order']._fields and field_name.startswith('x_'):
|
|
values[field_name] = field_value
|
|
if values:
|
|
order.write(values)
|
|
return request.redirect("/shop/payment")
|
|
|
|
values = {
|
|
'website_sale_order': order,
|
|
'post': post,
|
|
'escape': lambda x: x.replace("'", r"\'"),
|
|
'partner': order.partner_id.id,
|
|
'order': order,
|
|
}
|
|
|
|
return request.render("website_sale.extra_info", values)
|
|
|
|
# ------------------------------------------------------
|
|
# Payment
|
|
# ------------------------------------------------------
|
|
|
|
def _get_shop_payment_values(self, order, **kwargs):
|
|
values = dict(
|
|
website_sale_order=order,
|
|
errors=[],
|
|
partner=order.partner_id.id,
|
|
order=order,
|
|
payment_action_id=request.env.ref('payment.action_payment_acquirer').id,
|
|
return_url= '/shop/payment/validate',
|
|
bootstrap_formatting= True
|
|
)
|
|
|
|
domain = expression.AND([
|
|
['&', ('state', 'in', ['enabled', 'test']), ('company_id', '=', order.company_id.id)],
|
|
['|', ('website_id', '=', False), ('website_id', '=', request.website.id)],
|
|
['|', ('country_ids', '=', False), ('country_ids', 'in', [order.partner_id.country_id.id])]
|
|
])
|
|
acquirers = request.env['payment.acquirer'].search(domain)
|
|
|
|
values['access_token'] = order.access_token
|
|
values['acquirers'] = [acq for acq in acquirers if (acq.payment_flow == 'form' and acq.view_template_id) or
|
|
(acq.payment_flow == 's2s' and acq.registration_view_template_id)]
|
|
values['tokens'] = request.env['payment.token'].search([
|
|
('acquirer_id', 'in', acquirers.ids),
|
|
('partner_id', 'child_of', order.partner_id.commercial_partner_id.id)])
|
|
|
|
if order:
|
|
values['acq_extra_fees'] = acquirers.get_acquirer_extra_fees(order.amount_total, order.currency_id, order.partner_id.country_id.id)
|
|
return values
|
|
|
|
@http.route(['/shop/payment'], type='http', auth="public", website=True, sitemap=False)
|
|
def payment(self, **post):
|
|
""" Payment step. This page proposes several payment means based on available
|
|
payment.acquirer. State at this point :
|
|
|
|
- a draft sales order with lines; otherwise, clean context / session and
|
|
back to the shop
|
|
- no transaction in context / session, or only a draft one, if the customer
|
|
did go to a payment.acquirer website but closed the tab without
|
|
paying / canceling
|
|
"""
|
|
order = request.website.sale_get_order()
|
|
redirection = self.checkout_redirection(order)
|
|
if redirection:
|
|
return redirection
|
|
|
|
render_values = self._get_shop_payment_values(order, **post)
|
|
render_values['only_services'] = order and order.only_services or False
|
|
|
|
if render_values['errors']:
|
|
render_values.pop('acquirers', '')
|
|
render_values.pop('tokens', '')
|
|
|
|
return request.render("website_sale.payment", render_values)
|
|
|
|
@http.route(['/shop/payment/transaction/',
|
|
'/shop/payment/transaction/<int:so_id>',
|
|
'/shop/payment/transaction/<int:so_id>/<string:access_token>'], type='json', auth="public", website=True)
|
|
def payment_transaction(self, acquirer_id, save_token=False, so_id=None, access_token=None, token=None, **kwargs):
|
|
""" Json method that creates a payment.transaction, used to create a
|
|
transaction when the user clicks on 'pay now' button. After having
|
|
created the transaction, the event continues and the user is redirected
|
|
to the acquirer website.
|
|
|
|
:param int acquirer_id: id of a payment.acquirer record. If not set the
|
|
user is redirected to the checkout page
|
|
"""
|
|
# Ensure a payment acquirer is selected
|
|
if not acquirer_id:
|
|
return False
|
|
|
|
try:
|
|
acquirer_id = int(acquirer_id)
|
|
except:
|
|
return False
|
|
|
|
# Retrieve the sale order
|
|
if so_id:
|
|
env = request.env['sale.order']
|
|
domain = [('id', '=', so_id)]
|
|
if access_token:
|
|
env = env.sudo()
|
|
domain.append(('access_token', '=', access_token))
|
|
order = env.search(domain, limit=1)
|
|
else:
|
|
order = request.website.sale_get_order()
|
|
|
|
# Ensure there is something to proceed
|
|
if not order or (order and not order.order_line):
|
|
return False
|
|
|
|
assert order.partner_id.id != request.website.partner_id.id
|
|
|
|
# Create transaction
|
|
vals = {'acquirer_id': acquirer_id,
|
|
'return_url': '/shop/payment/validate'}
|
|
|
|
if save_token:
|
|
vals['type'] = 'form_save'
|
|
if token:
|
|
vals['payment_token_id'] = int(token)
|
|
|
|
transaction = order._create_payment_transaction(vals)
|
|
|
|
# store the new transaction into the transaction list and if there's an old one, we remove it
|
|
# until the day the ecommerce supports multiple orders at the same time
|
|
last_tx_id = request.session.get('__website_sale_last_tx_id')
|
|
last_tx = request.env['payment.transaction'].browse(last_tx_id).sudo().exists()
|
|
if last_tx:
|
|
PaymentProcessing.remove_payment_transaction(last_tx)
|
|
PaymentProcessing.add_payment_transaction(transaction)
|
|
request.session['__website_sale_last_tx_id'] = transaction.id
|
|
return transaction.render_sale_button(order)
|
|
|
|
@http.route('/shop/payment/token', type='http', auth='public', website=True, sitemap=False)
|
|
def payment_token(self, pm_id=None, **kwargs):
|
|
""" Method that handles payment using saved tokens
|
|
|
|
:param int pm_id: id of the payment.token that we want to use to pay.
|
|
"""
|
|
order = request.website.sale_get_order()
|
|
# do not crash if the user has already paid and try to pay again
|
|
if not order:
|
|
return request.redirect('/shop/?error=no_order')
|
|
|
|
assert order.partner_id.id != request.website.partner_id.id
|
|
|
|
try:
|
|
pm_id = int(pm_id)
|
|
except ValueError:
|
|
return request.redirect('/shop/?error=invalid_token_id')
|
|
|
|
# We retrieve the token the user want to use to pay
|
|
if not request.env['payment.token'].sudo().search_count([('id', '=', pm_id)]):
|
|
return request.redirect('/shop/?error=token_not_found')
|
|
|
|
# Create transaction
|
|
vals = {'payment_token_id': pm_id, 'return_url': '/shop/payment/validate'}
|
|
|
|
tx = order._create_payment_transaction(vals)
|
|
PaymentProcessing.add_payment_transaction(tx)
|
|
return request.redirect('/payment/process')
|
|
|
|
@http.route('/shop/payment/get_status/<int:sale_order_id>', type='json', auth="public", website=True)
|
|
def payment_get_status(self, sale_order_id, **post):
|
|
order = request.env['sale.order'].sudo().browse(sale_order_id).exists()
|
|
if order.id != request.session.get('sale_last_order_id'):
|
|
# either something went wrong or the session is unbound
|
|
# prevent recalling every 3rd of a second in the JS widget
|
|
return {}
|
|
|
|
return {
|
|
'recall': order.get_portal_last_transaction().state == 'pending',
|
|
'message': request.env['ir.ui.view']._render_template("website_sale.payment_confirmation_status", {
|
|
'order': order
|
|
})
|
|
}
|
|
|
|
@http.route('/shop/payment/validate', type='http', auth="public", website=True, sitemap=False)
|
|
def payment_validate(self, transaction_id=None, sale_order_id=None, **post):
|
|
""" Method that should be called by the server when receiving an update
|
|
for a transaction. State at this point :
|
|
|
|
- UDPATE ME
|
|
"""
|
|
if sale_order_id is None:
|
|
order = request.website.sale_get_order()
|
|
else:
|
|
order = request.env['sale.order'].sudo().browse(sale_order_id)
|
|
assert order.id == request.session.get('sale_last_order_id')
|
|
|
|
if transaction_id:
|
|
tx = request.env['payment.transaction'].sudo().browse(transaction_id)
|
|
assert tx in order.transaction_ids()
|
|
elif order:
|
|
tx = order.get_portal_last_transaction()
|
|
else:
|
|
tx = None
|
|
|
|
if not order or (order.amount_total and not tx):
|
|
return request.redirect('/shop')
|
|
|
|
if order and not order.amount_total and not tx:
|
|
order.with_context(send_email=True).action_confirm()
|
|
return request.redirect(order.get_portal_url())
|
|
|
|
# clean context and session, then redirect to the confirmation page
|
|
request.website.sale_reset()
|
|
if tx and tx.state == 'draft':
|
|
return request.redirect('/shop')
|
|
|
|
PaymentProcessing.remove_payment_transaction(tx)
|
|
return request.redirect('/shop/confirmation')
|
|
|
|
@http.route(['/shop/confirmation'], type='http', auth="public", website=True, sitemap=False)
|
|
def payment_confirmation(self, **post):
|
|
""" End of checkout process controller. Confirmation is basically seing
|
|
the status of a sale.order. State at this point :
|
|
|
|
- should not have any context / session info: clean them
|
|
- take a sale.order id, because we request a sale.order and are not
|
|
session dependant anymore
|
|
"""
|
|
sale_order_id = request.session.get('sale_last_order_id')
|
|
if sale_order_id:
|
|
order = request.env['sale.order'].sudo().browse(sale_order_id)
|
|
return request.render("website_sale.confirmation", {'order': order})
|
|
else:
|
|
return request.redirect('/shop')
|
|
|
|
@http.route(['/shop/print'], type='http', auth="public", website=True, sitemap=False)
|
|
def print_saleorder(self, **kwargs):
|
|
sale_order_id = request.session.get('sale_last_order_id')
|
|
if sale_order_id:
|
|
pdf, _ = request.env.ref('sale.action_report_saleorder').sudo()._render_qweb_pdf([sale_order_id])
|
|
pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', u'%s' % len(pdf))]
|
|
return request.make_response(pdf, headers=pdfhttpheaders)
|
|
else:
|
|
return request.redirect('/shop')
|
|
|
|
@http.route(['/shop/tracking_last_order'], type='json', auth="public")
|
|
def tracking_cart(self, **post):
|
|
""" return data about order in JSON needed for google analytics"""
|
|
ret = {}
|
|
sale_order_id = request.session.get('sale_last_order_id')
|
|
if sale_order_id:
|
|
order = request.env['sale.order'].sudo().browse(sale_order_id)
|
|
ret = self.order_2_return_dict(order)
|
|
return ret
|
|
|
|
# ------------------------------------------------------
|
|
# Edit
|
|
# ------------------------------------------------------
|
|
|
|
@http.route(['/shop/add_product'], type='json', auth="user", methods=['POST'], website=True)
|
|
def add_product(self, name=None, category=None, **post):
|
|
product = request.env['product.product'].create({
|
|
'name': name or _("New Product"),
|
|
'public_categ_ids': category,
|
|
'website_id': request.website.id,
|
|
})
|
|
return "%s?enable_editor=1" % product.product_tmpl_id.website_url
|
|
|
|
@http.route(['/shop/change_sequence'], type='json', auth='user')
|
|
def change_sequence(self, id, sequence):
|
|
product_tmpl = request.env['product.template'].browse(id)
|
|
if sequence == "top":
|
|
product_tmpl.set_sequence_top()
|
|
elif sequence == "bottom":
|
|
product_tmpl.set_sequence_bottom()
|
|
elif sequence == "up":
|
|
product_tmpl.set_sequence_up()
|
|
elif sequence == "down":
|
|
product_tmpl.set_sequence_down()
|
|
|
|
@http.route(['/shop/change_size'], type='json', auth='user')
|
|
def change_size(self, id, x, y):
|
|
product = request.env['product.template'].browse(id)
|
|
return product.write({'website_size_x': x, 'website_size_y': y})
|
|
|
|
@http.route(['/shop/change_ppg'], type='json', auth='user')
|
|
def change_ppg(self, ppg):
|
|
request.env['website'].get_current_website().shop_ppg = ppg
|
|
|
|
@http.route(['/shop/change_ppr'], type='json', auth='user')
|
|
def change_ppr(self, ppr):
|
|
request.env['website'].get_current_website().shop_ppr = ppr
|
|
|
|
def order_lines_2_google_api(self, order_lines):
|
|
""" Transforms a list of order lines into a dict for google analytics """
|
|
ret = []
|
|
for line in order_lines:
|
|
product = line.product_id
|
|
ret.append({
|
|
'id': line.order_id.id,
|
|
'sku': product.barcode or product.id,
|
|
'name': product.name or '-',
|
|
'category': product.categ_id.name or '-',
|
|
'price': line.price_unit,
|
|
'quantity': line.product_uom_qty,
|
|
})
|
|
return ret
|
|
|
|
def order_2_return_dict(self, order):
|
|
""" Returns the tracking_cart dict of the order for Google analytics basically defined to be inherited """
|
|
return {
|
|
'transaction': {
|
|
'id': order.id,
|
|
'affiliation': order.company_id.name,
|
|
'revenue': order.amount_total,
|
|
'tax': order.amount_tax,
|
|
'currency': order.currency_id.name
|
|
},
|
|
'lines': self.order_lines_2_google_api(order.order_line)
|
|
}
|
|
|
|
@http.route(['/shop/country_infos/<model("res.country"):country>'], type='json', auth="public", methods=['POST'], website=True)
|
|
def country_infos(self, country, mode, **kw):
|
|
return dict(
|
|
fields=country.get_address_fields(),
|
|
states=[(st.id, st.name, st.code) for st in country.get_website_sale_states(mode=mode)],
|
|
phone_code=country.phone_code,
|
|
zip_required=country.zip_required,
|
|
state_required=country.state_required,
|
|
)
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Products Search Bar
|
|
# --------------------------------------------------------------------------
|
|
|
|
@http.route('/shop/products/autocomplete', type='json', auth='public', website=True)
|
|
def products_autocomplete(self, term, options={}, **kwargs):
|
|
"""
|
|
Returns list of products according to the term and product options
|
|
|
|
Params:
|
|
term (str): search term written by the user
|
|
options (dict)
|
|
- 'limit' (int), default to 5: number of products to consider
|
|
- 'display_description' (bool), default to True
|
|
- 'display_price' (bool), default to True
|
|
- 'order' (str)
|
|
- 'max_nb_chars' (int): max number of characters for the
|
|
description if returned
|
|
|
|
Returns:
|
|
dict (or False if no result)
|
|
- 'products' (list): products (only their needed field values)
|
|
note: the prices will be strings properly formatted and
|
|
already containing the currency
|
|
- 'products_count' (int): the number of products in the database
|
|
that matched the search query
|
|
"""
|
|
ProductTemplate = request.env['product.template']
|
|
|
|
display_description = options.get('display_description', True)
|
|
display_price = options.get('display_price', True)
|
|
order = self._get_search_order(options)
|
|
max_nb_chars = options.get('max_nb_chars', 999)
|
|
|
|
category = options.get('category')
|
|
attrib_values = options.get('attrib_values')
|
|
|
|
domain = self._get_search_domain(term, category, attrib_values, display_description)
|
|
products = ProductTemplate.search(
|
|
domain,
|
|
limit=min(20, options.get('limit', 5)),
|
|
order=order
|
|
)
|
|
|
|
fields = ['id', 'name', 'website_url']
|
|
if display_description:
|
|
fields.append('description_sale')
|
|
|
|
res = {
|
|
'products': products.read(fields),
|
|
'products_count': ProductTemplate.search_count(domain),
|
|
}
|
|
|
|
if display_description:
|
|
for res_product in res['products']:
|
|
desc = res_product['description_sale']
|
|
if desc and len(desc) > max_nb_chars:
|
|
res_product['description_sale'] = "%s..." % desc[:(max_nb_chars - 3)]
|
|
|
|
if display_price:
|
|
FieldMonetary = request.env['ir.qweb.field.monetary']
|
|
monetary_options = {
|
|
'display_currency': request.website.get_current_pricelist().currency_id,
|
|
}
|
|
for res_product, product in zip(res['products'], products):
|
|
combination_info = product._get_combination_info(only_template=True)
|
|
res_product.update(combination_info)
|
|
res_product['list_price'] = FieldMonetary.value_to_html(res_product['list_price'], monetary_options)
|
|
res_product['price'] = FieldMonetary.value_to_html(res_product['price'], monetary_options)
|
|
|
|
return res
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Products Recently Viewed
|
|
# --------------------------------------------------------------------------
|
|
@http.route('/shop/products/recently_viewed', type='json', auth='public', website=True)
|
|
def products_recently_viewed(self, **kwargs):
|
|
return self._get_products_recently_viewed()
|
|
|
|
def _get_products_recently_viewed(self):
|
|
"""
|
|
Returns list of recently viewed products according to current user
|
|
"""
|
|
max_number_of_product_for_carousel = 12
|
|
visitor = request.env['website.visitor']._get_visitor_from_request()
|
|
if visitor:
|
|
excluded_products = request.website.sale_get_order().mapped('order_line.product_id.id')
|
|
products = request.env['website.track'].sudo().read_group(
|
|
[('visitor_id', '=', visitor.id), ('product_id', '!=', False), ('product_id.website_published', '=', True), ('product_id', 'not in', excluded_products)],
|
|
['product_id', 'visit_datetime:max'], ['product_id'], limit=max_number_of_product_for_carousel, orderby='visit_datetime DESC')
|
|
products_ids = [product['product_id'][0] for product in products]
|
|
if products_ids:
|
|
viewed_products = request.env['product.product'].with_context(display_default_code=False).browse(products_ids)
|
|
|
|
FieldMonetary = request.env['ir.qweb.field.monetary']
|
|
monetary_options = {
|
|
'display_currency': request.website.get_current_pricelist().currency_id,
|
|
}
|
|
rating = request.website.viewref('website_sale.product_comment').active
|
|
res = {'products': []}
|
|
for product in viewed_products:
|
|
combination_info = product._get_combination_info_variant()
|
|
res_product = product.read(['id', 'name', 'website_url'])[0]
|
|
res_product.update(combination_info)
|
|
res_product['price'] = FieldMonetary.value_to_html(res_product['price'], monetary_options)
|
|
if rating:
|
|
res_product['rating'] = request.env["ir.ui.view"]._render_template('portal_rating.rating_widget_stars_static', values={
|
|
'rating_avg': product.rating_avg,
|
|
'rating_count': product.rating_count,
|
|
})
|
|
res['products'].append(res_product)
|
|
|
|
return res
|
|
return {}
|
|
|
|
@http.route('/shop/products/recently_viewed_update', type='json', auth='public', website=True)
|
|
def products_recently_viewed_update(self, product_id, **kwargs):
|
|
res = {}
|
|
visitor_sudo = request.env['website.visitor']._get_visitor_from_request(force_create=True)
|
|
if visitor_sudo:
|
|
if request.httprequest.cookies.get('visitor_uuid', '') != visitor_sudo.access_token:
|
|
res['visitor_uuid'] = visitor_sudo.access_token
|
|
visitor_sudo._add_viewed_product(product_id)
|
|
return res
|
|
|
|
@http.route('/shop/products/recently_viewed_delete', type='json', auth='public', website=True)
|
|
def products_recently_viewed_delete(self, product_id, **kwargs):
|
|
visitor_sudo = request.env['website.visitor']._get_visitor_from_request()
|
|
if visitor_sudo:
|
|
request.env['website.track'].sudo().search([('visitor_id', '=', visitor_sudo.id), ('product_id', '=', product_id)]).unlink()
|
|
return self._get_products_recently_viewed()
|