# -*- coding: utf-8 -*- # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. import json import logging from werkzeug.exceptions import Forbidden from flectra import http, tools, _ from flectra.http import request from flectra.addons.base.ir.ir_qweb.fields import nl2br from flectra.addons.http_routing.models.ir_http import slug from flectra.addons.website.controllers.main import QueryURL from flectra.exceptions import ValidationError from flectra.addons.website_form.controllers.main import WebsiteForm _logger = logging.getLogger(__name__) PPG = 20 # Products Per Page PPR = 4 # Products Per Row class TableCompute(object): def __init__(self): self.table = {} def _check_place(self, posx, posy, sizex, sizey): 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=PPG): # 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): 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 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 WebsiteProductLimit(http.Controller): @http.route(['/shop/product_limit'], type='json', auth="public") def change_limit(self, value): global PPG PPG = int(value) return True 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'].sudo().create(values) if data['attachments']: self.insert_attachment(model_record, order.id, data['attachments']) return json.dumps({'id': order.id}) class WebsiteSale(http.Controller): def _get_compute_currency_and_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']) from_currency = request.env.user.company_id.currency_id to_currency = pricelist.currency_id compute_currency = lambda price: from_currency.compute(price, to_currency) return compute_currency, pricelist_context, pricelist def get_attribute_value_ids(self, product): """ list of selectable attributes of a product :return: list of product variant description as requested website (variant id, [visible attribute ids], variant price, variant sale price) """ # product attributes with at least two choices quantity = product._context.get('quantity') or 1 product = product.with_context(quantity=quantity) visible_attrs_ids = product.attribute_line_ids.filtered(lambda l: len(l.value_ids) > 1).mapped('attribute_id').ids to_currency = request.website.get_current_pricelist().currency_id attribute_value_ids = [] for variant in product.product_variant_ids: if to_currency != product.currency_id: price = variant.currency_id.compute(variant.website_public_price, to_currency) / quantity else: price = variant.website_public_price / quantity visible_attribute_ids = [v.id for v in variant.attribute_value_ids if v.attribute_id.id in visible_attrs_ids] attribute_value_ids.append([variant.id, visible_attribute_ids, variant.website_price, price]) return attribute_value_ids 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 return 'website_published desc,%s , id desc' % post.get('order', 'website_sequence desc') def _get_search_domain(self, search, category, attrib_values, tag_values, brand_values): domain = request.website.sale_product_domain() if search: for srch in search.split(" "): domain += [ '|', '|', '|', ('name', 'ilike', srch), ('description', 'ilike', srch), ('description_sale', 'ilike', srch), ('product_variant_ids.default_code', 'ilike', srch)] if category: domain += [('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: domain += [('attribute_line_ids.value_ids', 'in', ids)] attrib = value[0] ids = [value[1]] if attrib: domain += [('attribute_line_ids.value_ids', 'in', ids)] if not request.env.user.has_group('website.group_website_publisher'): domain += ['|', ('website_ids', '=', False), ('website_ids', 'in', request.website.id)] if tag_values: domain += [('tag_ids', 'in', tag_values)] if brand_values: domain += [('brand_id', 'in', brand_values)] return domain @http.route([ '/shop', '/shop/page/', '/shop/category/', '/shop/category//page/' ], type='http', auth="public", website=True) def shop(self, page=0, category=None, search='', ppg=False, **post): if ppg: try: ppg = int(ppg) except ValueError: ppg = PPG post["ppg"] = ppg else: ppg = PPG 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} # For Tags tag_list = request.httprequest.args.getlist('tags') tag_values = [list(map(str, v)) for v in tag_list if v] tag_set = set([int(v[0]) for v in tag_values]) # For Brands brand_list = request.httprequest.args.getlist('brands') brand_values = [list(map(str, v)) for v in brand_list if v] brand_set = set([int(v[0]) for v in brand_values]) domain = self._get_search_domain(search, category, attrib_values, list(tag_set), list(brand_set)) keep = QueryURL('/shop', category=category and int(category), search=search, attrib=attrib_list, order=post.get('order')) compute_currency, pricelist_context, pricelist = self._get_compute_currency_and_context() request.context = dict(request.context, pricelist=pricelist.id, partner=request.env.user.partner_id) url = "/shop" if search: post["search"] = search if category: category = request.env['product.public.category'].browse(int(category)) website = [] for web in category.website_ids: website.append(web.id) if request.website.id not in website: return request.render('website.404') url = "/shop/category/%s" % slug(category) if attrib_list: post['attrib'] = attrib_list categs = request.env['product.public.category'].search([('parent_id', '=', False), '|', ('website_ids', '=', False), ('website_ids', 'in', request.website.id)]) Product = request.env['product.template'] parent_category_ids = [] if category: parent_category_ids = [category.id] current_category = category while current_category.parent_id: parent_category_ids.append(current_category.parent_id.id) current_category = current_category.parent_id product_count = Product.search_count(domain) pager = request.website.pager(url=url, total=product_count, page=page, step=ppg, scope=7, url_args=post) products = Product.search(domain, limit=ppg, offset=pager['offset'], order=self._get_search_order(post)) ProductAttribute = request.env['product.attribute'] ProductBrand = request.env['product.brand'] ProductTag = request.env['product.tags'] if products: attributes = ProductAttribute.search([('attribute_line_ids.product_tmpl_id', 'in', products.ids)]) prod_brands = [] prod_tags = [] for product in products: if product.brand_id: prod_brands.append(product.brand_id.id) if product.tag_ids: for tag_id in product.tag_ids.ids: prod_tags.append(tag_id) brands = ProductBrand.browse(list(set(prod_brands))) tags = ProductTag.browse(list(set(prod_tags))) else: attributes = ProductAttribute.browse(attributes_ids) brands = ProductBrand.browse(brand_set) tags = ProductTag.browse(tag_set) limits = request.env['product.view.limit'].search([]) values = { 'search': search, 'category': category, 'attrib_values': attrib_values, 'attrib_set': attrib_set, 'pager': pager, 'pricelist': pricelist, 'products': products, 'tag_set': tag_set, 'brand_set': brand_set, 'search_count': product_count, # common for all searchbox 'bins': TableCompute().process(products, ppg), 'rows': PPR, 'categories': categs, 'attributes': attributes, 'compute_currency': compute_currency, 'keep': keep, 'limits': limits, 'parent_category_ids': parent_category_ids, 'get_attribute_value_ids': self.get_attribute_value_ids, 'tags': tags, 'brands': brands, } if category: values['main_object'] = category return request.render("website_sale.products", values) @http.route(['/shop/product/'], type='http', auth="public", website=True) def product(self, product, category='', search='', **kwargs): product_context = dict(request.env.context, 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() from_currency = request.env.user.company_id.currency_id to_currency = pricelist.currency_id compute_currency = lambda price: from_currency.compute(price, to_currency) if not product_context.get('pricelist'): product_context['pricelist'] = pricelist.id product = product.with_context(product_context) values = { 'search': search, 'category': category, 'pricelist': pricelist, 'attrib_values': attrib_values, 'compute_currency': compute_currency, 'attrib_set': attrib_set, 'keep': keep, 'categories': categs, 'main_object': product, 'product': product, 'get_attribute_value_ids': self.get_attribute_value_ids, } return request.render("website_sale.product", values) @http.route(['/shop/change_pricelist/'], type='http', auth="public", website=True) def pricelist_change(self, pl_id, **post): if (pl_id.website_id 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) def pricelist(self, promo, **post): redirect = post.get('r', '/shop/cart') 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) 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() 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) return request.render('website.404') if abandoned_order.state != 'draft': # abandoned cart already finished values.update({'abandoned_proceed': True}) elif revive == 'squash' or (revive == 'merge' and not request.session['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['sale_order_id']: # abandoned cart found, user have to choose what to do values.update({'access_token': abandoned_order.access_token}) if order: from_currency = order.company_id.currency_id to_currency = order.pricelist_id.currency_id compute_currency = lambda price: from_currency.compute(price, to_currency) else: compute_currency = lambda price: price values.update({ 'website_sale_order': order, 'compute_currency': compute_currency, 'suggested_products': [], }) if order: _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, csrf=False) def cart_update(self, product_id, add_qty=1, set_qty=0, **kw): request.website.sale_get_order(force_create=1)._cart_update( product_id=int(product_id), add_qty=add_qty, set_qty=set_qty, attributes=self._filter_attributes(**kw), ) return request.redirect("/shop/cart") def _filter_attributes(self, **kw): return {k: v for k, v in kw.items() if "attribute" in k} @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): 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 from_currency = order.company_id.currency_id to_currency = order.pricelist_id.currency_id 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, 'compute_currency': lambda price: from_currency.compute(price, to_currency), 'suggested_products': order._cart_accessories() }) return value # ------------------------------------------------------ # 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 'state_code' in country.get_address_fields() and country.state_ids: required_fields += ['state_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 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.items() 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().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): return values 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['customer'] = True new_values['team_id'] = request.website.salesteam_id and request.website.salesteam_id.id lang = request.lang if request.lang 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) 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) 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') 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') 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.onchange_partner_id() elif mode[1] == 'shipping': order.partner_shipping_id = partner_id order.message_partner_ids = [(4, partner_id), (3, request.website.partner_id.id)] if not errors: return request.redirect(kw.get('callback') or '/shop/checkout') 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, 'country': country, 'countries': country.get_website_sale_countries(mode=mode[1]), "states": country.get_website_sale_states(mode=mode[1]), 'error': errors, 'callback': kw.get('callback'), } return request.render("website_sale.address", render_values) @http.route(['/shop/checkout'], type='http', auth="public", website=True) 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) 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) 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.env.ref('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) def extra_info(self, **post): # Check that this option is activated extra_step = request.env.ref('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): shipping_partner_id = False if order: shipping_partner_id = order.partner_shipping_id.id or order.partner_invoice_id.id 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 ) acquirers = request.env['payment.acquirer'].search( [('website_published', '=', True), ('company_id', '=', order.company_id.id)] ) values['access_token'] = order.access_token values['form_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 'form' and acq.view_template_id] values['s2s_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 's2s' and acq.registration_view_template_id] values['tokens'] = request.env['payment.token'].search( [('partner_id', '=', order.partner_id.id), ('acquirer_id', 'in', [acq.id for acq in values['s2s_acquirers']])]) for acq in values['form_acquirers']: acq.form = acq.with_context(submit_class='btn btn-primary', submit_txt=_('Pay Now')).sudo().render( '/', order.amount_total, order.pricelist_id.currency_id.id, values={ 'return_url': '/shop/payment/validate', 'partner_id': shipping_partner_id, 'billing_partner_id': order.partner_invoice_id.id, } ) return values @http.route(['/shop/payment'], type='http', auth="public", website=True) 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) 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/', '/shop/payment/transaction//'], 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 """ tx_type = 'form' if save_token: tx_type = 'form_save' # In case the route is called directly from the JS (as done in Stripe payment method) if so_id and access_token: order = request.env['sale.order'].sudo().search([('id', '=', so_id), ('access_token', '=', access_token)]) elif so_id: order = request.env['sale.order'].search([('id', '=', so_id)]) else: order = request.website.sale_get_order() if not order or not order.order_line or acquirer_id is None: return False assert order.partner_id.id != request.website.partner_id.id # find or create transaction tx = request.website.sale_get_transaction() or request.env['payment.transaction'].sudo() acquirer = request.env['payment.acquirer'].browse(int(acquirer_id)) payment_token = request.env['payment.token'].sudo().browse(int(token)) if token else None tx = tx._check_or_create_sale_tx(order, acquirer, payment_token=payment_token, tx_type=tx_type) request.session['sale_transaction_id'] = tx.id return tx.render_sale_button(order, '/shop/payment/validate') @http.route('/shop/payment/token', type='http', auth='public', website=True) 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 token = request.env['payment.token'].sudo().browse(pm_id) if not token: return request.redirect('/shop/?error=token_not_found') # we retrieve an existing transaction (if it exists obviously) tx = request.website.sale_get_transaction() or request.env['payment.transaction'].sudo() # we check if the transaction is Ok, if not then we create it tx = tx._check_or_create_sale_tx(order, token.acquirer_id, payment_token=token, tx_type='server2server') # we set the transaction id into the session (so `sale_get_transaction` can retrieve it ) request.session['sale_transaction_id'] = tx.id # we proceed the s2s payment res = tx.confirm_sale_token() # we then redirect to the page that validates the payment by giving it error if there's one if res is not True: return request.redirect('/shop/payment/validate?success=False&error=%s' % res) return request.redirect('/shop/payment/validate?success=True') @http.route('/shop/payment/get_status/', 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() assert order.id == request.session.get('sale_last_order_id') return { 'recall': order.payment_tx_id.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) 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 transaction_id is None: tx = request.website.sale_get_transaction() else: tx = request.env['payment.transaction'].browse(transaction_id) 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 not order or (order.amount_total and not tx): return request.redirect('/shop') if (not order.amount_total and not tx) or tx.state in ['pending', 'done', 'authorized']: if (not order.amount_total and not tx): # Orders are confirmed by payment transactions, but there is none for free orders, # (e.g. free events), so confirm immediately order.with_context(send_email=True).action_confirm() elif tx and tx.state == 'cancel': # cancel the quotation order.action_cancel() # clean context and session, then redirect to the confirmation page request.website.sale_reset() if tx and tx.state == 'draft': return request.redirect('/shop') return request.redirect('/shop/confirmation') @http.route(['/shop/terms'], type='http', auth="public", website=True) def terms(self, **kw): return request.render("website_sale.terms") @http.route(['/shop/confirmation'], type='http', auth="public", website=True) 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) def print_saleorder(self): 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 @http.route(['/shop/get_unit_price'], type='json', auth="public", methods=['POST'], website=True) def get_unit_price(self, product_ids, add_qty, **kw): products = request.env['product.product'].with_context({'quantity': add_qty}).browse(product_ids) return {product.id: product.website_price / add_qty for product in products} # ------------------------------------------------------ # Edit # ------------------------------------------------------ @http.route(['/shop/add_product'], type='json', auth="user", methods=['POST'], website=True) def add_product(self, name=None, category=0, **post): product = request.env['product.product'].create({ 'name': name or _("New Product"), 'public_categ_ids': category }) return "/shop/product/%s?enable_editor=1" % slug(product.product_tmpl_id) @http.route(['/shop/change_sequence'], type='json', auth="public") 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="public") 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}) 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/'], 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 )