summaryrefslogtreecommitdiff
path: root/addons/website_sale/controllers/main.py
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/website_sale/controllers/main.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/website_sale/controllers/main.py')
-rw-r--r--addons/website_sale/controllers/main.py1319
1 files changed, 1319 insertions, 0 deletions
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
new file mode 100644
index 00000000..d8fef59c
--- /dev/null
+++ b/addons/website_sale/controllers/main.py
@@ -0,0 +1,1319 @@
+# -*- 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.main import Website
+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(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):
+ if not product.can_access_from_current_website():
+ raise NotFound()
+
+ 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_check_address(self, order):
+ billing_fields_required = self._get_mandatory_fields_billing(order.partner_id.country_id.id)
+ if not all(order.partner_id.read(billing_fields_required)[0].values()):
+ return request.redirect('/shop/address?partner_id=%d' % order.partner_id.id)
+
+ shipping_fields_required = self._get_mandatory_fields_shipping(order.partner_shipping_id.country_id.id)
+ if not all(order.partner_shipping_id.read(shipping_fields_required)[0].values()):
+ return request.redirect('/shop/address?partner_id=%d' % order.partner_shipping_id.id)
+
+ 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
+
+ values = {
+ 'order': order,
+ 'shippings': shippings,
+ 'only_services': order and order.only_services or False
+ }
+ return values
+
+ def _get_mandatory_billing_fields(self):
+ # deprecated for _get_mandatory_fields_billing which handle zip/state required
+ return ["name", "email", "street", "city", "country_id"]
+
+ def _get_mandatory_shipping_fields(self):
+ # deprecated for _get_mandatory_fields_shipping which handle zip/state required
+ return ["name", "street", "city", "country_id"]
+
+ def _get_mandatory_fields_billing(self, country_id=False):
+ req = self._get_mandatory_billing_fields()
+ if country_id:
+ country = request.env['res.country'].browse(country_id)
+ if country.state_required:
+ req += ['state_id']
+ if country.zip_required:
+ req += ['zip']
+ return req
+
+ def _get_mandatory_fields_shipping(self, country_id=False):
+ req = self._get_mandatory_shipping_fields()
+ if country_id:
+ country = request.env['res.country'].browse(country_id)
+ if country.state_required:
+ req += ['state_id']
+ if country.zip_required:
+ req += ['zip']
+ return req
+
+ 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
+ country_id = int(data.get('country_id', False))
+ required_fields += mode[1] == 'shipping' and self._get_mandatory_fields_shipping(country_id) or self._get_mandatory_fields_billing(country_id)
+
+ # 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 country_id:
+ data["vat"] = Partner.fix_eu_vat_number(country_id, data.get("vat"))
+ partner_dummy = Partner.new(self._get_vat_validation_fields(data))
+ try:
+ partner_dummy.check_vat()
+ except ValidationError as exception:
+ error["vat"] = 'error'
+ error_message.append(exception.args[0])
+
+ if [err for err in error.values() if err == 'missing']:
+ error_message.append(_('Some required fields are empty.'))
+
+ return error, error_message
+
+ def _get_vat_validation_fields(self, data):
+ return {
+ 'vat': data['vat'],
+ 'country_id': int(data['country_id']) if data.get('country_id') else False,
+ }
+
+ 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
+ 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
+ # 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 order.partner_id.commercial_partner_id.id == partner_id:
+ mode = ('new', 'shipping')
+ partner_id = -1
+ elif partner_id in shippings.mapped('id'):
+ mode = ('edit', 'shipping')
+ else:
+ return Forbidden()
+ if mode and partner_id != -1:
+ 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')
+
+ render_values = {
+ 'website_sale_order': order,
+ 'partner_id': partner_id,
+ 'mode': mode,
+ 'checkout': values,
+ 'can_edit_vat': can_edit_vat,
+ 'error': errors,
+ 'callback': kw.get('callback'),
+ 'only_services': order and order.only_services,
+ }
+ render_values.update(self._get_country_related_render_values(kw, render_values))
+ return request.render("website_sale.address", render_values)
+
+ def _get_country_related_render_values(self, kw, render_values):
+ '''
+ This method provides fields related to the country to render the website sale form
+ '''
+ values = render_values['checkout']
+ mode = render_values['mode']
+ order = render_values['website_sale_order']
+
+ def_country_id = order.partner_id.country_id
+ # IF PUBLIC ORDER
+ if order.partner_id.id == request.website.user_id.sudo().partner_id.id:
+ 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
+
+ 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
+
+ res = {
+ 'country': country,
+ 'country_states': country.get_website_sale_states(mode=mode[1]),
+ 'countries': country.get_website_sale_countries(mode=mode[1]),
+ }
+ return res
+
+ @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')
+
+ redirection = self.checkout_check_address(order)
+ if redirection:
+ return redirection
+
+ 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) or self.checkout_check_address(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) or self.checkout_check_address(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/terms'], type='http', auth="public", website=True, sitemap=True)
+ def terms(self, **kw):
+ return request.render("website_sale.terms")
+
+ @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()
+
+ # --------------------------------------------------------------------------
+ # Website Snippet Filters
+ # --------------------------------------------------------------------------
+
+ @http.route('/website_sale/snippet/options_filters', type='json', auth='user', website=True)
+ def get_dynamic_snippet_filters(self):
+ domain = expression.AND([
+ request.website.website_domain(),
+ ['|', ('filter_id.model_id', '=', 'product.product'), ('action_server_id.model_id.model', '=', 'product.product')]
+ ])
+ filters = request.env['website.snippet.filter'].sudo().search_read(
+ domain, ['id']
+ )
+ return filters