# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import json import logging from werkzeug.exceptions import Forbidden, NotFound from odoo import http, tools, _ from odoo.http import request from odoo.addons.base.ir.ir_qweb.fields import nl2br from odoo.addons.website.models.website import slug from odoo.addons.website.controllers.main import QueryURL from odoo.exceptions import ValidationError from odoo.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 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 self.table[pos / PPR][pos % PPR] = { 'product': p, 'x': x, 'y': y, 'class': " ".join(map(lambda x: x.html_class or '', p.website_style_ids)) } if index <= ppg: maxy = max(maxy, y + (pos / PPR)) index += 1 # Format table according to HTML needs rows = self.table.items() rows.sort() rows = map(lambda x: x[1], rows) for col in range(len(rows)): cols = rows[col].items() cols.sort() x += len(cols) rows[col] = [c for c in map(lambda x: x[1], cols) if c] return rows # TODO keep with input type hidden 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, 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_attribute_value_ids(self, product): """ list of selectable attributes of a product :return: list of product variant description (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): 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)] 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 if category: category = request.env['product.public.category'].search([('id', '=', int(category))], limit=1) if not category: raise NotFound() attrib_list = request.httprequest.args.getlist('attrib') attrib_values = [map(int, v.split("-")) for v in attrib_list if v] attributes_ids = set([v[0] for v in attrib_values]) attrib_set = 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 = dict(request.env.context) 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']) 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 categs = request.env['product.public.category'].search([('parent_id', '=', False)]) Product = request.env['product.template'] parent_category_ids = [] if category: url = "/shop/category/%s" % slug(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'] if products: # get all products without limit selected_products = Product.search(domain, limit=False) attributes = ProductAttribute.search([('attribute_line_ids.product_tmpl_id', 'in', selected_products.ids)]) else: attributes = ProductAttribute.browse(attributes_ids) from_currency = request.env.user.company_id.currency_id to_currency = pricelist.currency_id compute_currency = lambda price: from_currency.compute(price, to_currency) values = { 'search': search, 'category': category, 'attrib_values': attrib_values, 'attrib_set': attrib_set, 'pager': pager, 'pricelist': pricelist, 'products': products, '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, 'parent_category_ids': parent_category_ids, } 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'] Rating = request.env['rating.rating'] if category: category = ProductCategory.browse(int(category)).exists() attrib_list = request.httprequest.args.getlist('attrib') attrib_values = [map(int, v.split("-")) for v in attrib_list if v] attrib_set = 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) # get the rating attached to a mail.message, and the rating stats of the product ratings = Rating.search([('message_id', 'in', product.website_message_ids.ids)]) rating_message_values = dict([(record.message_id.id, record.rating) for record in ratings]) rating_product = product.rating_get_stats([('website_published', '=', True)]) 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, 'rating_message_values': rating_message_values, 'rating_product': rating_product } 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.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) def pricelist(self, promo, **post): pricelist = request.env['product.pricelist'].sudo().search([('code', '=', promo)], limit=1) if pricelist and not request.website.is_pricelist_available(pricelist.id): return request.redirect("/shop/cart?code_not_available=1") request.website.sale_get_order(code=promo) return request.redirect("/shop/cart") @http.route(['/shop/cart'], type='http', auth="public", website=True) def cart(self, **post): order = request.website.sale_get_order() 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 = { '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'}) if post.get('code_not_available'): values['code_not_available'] = post.get('code_not_available') 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 {} if not display: return None 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 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 sale 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 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 = filter(None, (all_form_values.get('field_required') or '').split(',')) # 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 if data.get('country_id'): country = request.env['res.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")) check_func = request.website.company_id.vat_check_vies and Partner.vies_vat_check or Partner.simple_vat_check vat_country, vat_number = Partner._split_vat(data.get("vat")) if not check_func(vat_country, vat_number): 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().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'].sudo().search([('model', '=', '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 = { '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) # 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"\'") } values.update(request.env['sale.order']._get_website_data(order)) return request.render("website_sale.extra_info", values) # ------------------------------------------------------ # Payment # ------------------------------------------------------ @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 sale 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 """ SaleOrder = request.env['sale.order'] order = request.website.sale_get_order() redirection = self.checkout_redirection(order) if redirection: return redirection shipping_partner_id = False if order: if order.partner_shipping_id.id: shipping_partner_id = order.partner_shipping_id.id else: shipping_partner_id = order.partner_invoice_id.id values = { 'website_sale_order': order } values['errors'] = SaleOrder._get_errors(order) values.update(SaleOrder._get_website_data(order)) if not values['errors']: acquirers = request.env['payment.acquirer'].search( [('website_published', '=', True), ('company_id', '=', order.company_id.id)] ) values['acquirers'] = [] for acquirer in acquirers: acquirer_button = acquirer.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, } ) acquirer.button = acquirer_button values['acquirers'].append(acquirer) values['tokens'] = request.env['payment.token'].search([('partner_id', '=', order.partner_id.id), ('acquirer_id', 'in', acquirers.ids)]) return request.render("website_sale.payment", values) @http.route(['/shop/payment/transaction_token/confirm'], type='json', auth="public", website=True) def payment_transaction_token_confirm(self, tx, **kwargs): tx = request.env['payment.transaction'].sudo().browse(int(tx)) if (tx and request.website.sale_get_transaction() and tx.id == request.website.sale_get_transaction().id and tx.payment_token_id and tx.partner_id == tx.sale_order_id.partner_id): try: s2s_result = tx.s2s_do_transaction() valid_state = 'authorized' if tx.acquirer_id.auto_confirm == 'authorize' else 'done' if not s2s_result or tx.state != valid_state: return dict(success=False, error=_("Payment transaction failed (%s)") % tx.state_message) else: # Auto-confirm SO if necessary tx._confirm_so() return dict(success=True, url='/shop/payment/validate') except Exception, e: _logger.warning(_("Payment transaction (%s) failed : <%s>") % (tx.id, str(e))) return dict(success=False, error=_("Payment transaction failed (Contact Administrator)")) return dict(success=False, error='Tx missmatch') @http.route(['/shop/payment/transaction_token'], type='http', methods=['POST'], auth="public", website=True) def payment_transaction_token(self, tx_id, **kwargs): tx = request.env['payment.transaction'].sudo().browse(int(tx_id)) if (tx and request.website.sale_get_transaction() and tx.id == request.website.sale_get_transaction().id and tx.payment_token_id and tx.partner_id == tx.sale_order_id.partner_id): return request.render("website_sale.payment_token_form_confirm", dict(tx=tx)) else: return request.redirect("/shop/payment?error=no_token_or_missmatch_tx") @http.route(['/shop/payment/transaction/'], type='json', auth="public", website=True) def payment_transaction(self, acquirer_id, tx_type='form', 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 """ Transaction = request.env['payment.transaction'].sudo() # In case the route is called directly from the JS (as done in Stripe payment method) so_id = kwargs.get('so_id') so_token = kwargs.get('so_token') if so_id and so_token: order = request.env['sale.order'].sudo().search([('id', '=', so_id), ('access_token', '=', so_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 request.redirect("/shop/checkout") assert order.partner_id.id != request.website.partner_id.id # find an already existing transaction tx = request.website.sale_get_transaction() if tx: if tx.sale_order_id.id != order.id or tx.state in ['error', 'cancel'] or tx.acquirer_id.id != acquirer_id: tx = False elif token and tx.payment_token_id and token != tx.payment_token_id.id: # new or distinct token tx = False elif tx.state == 'draft': # button cliked but no more info -> rewrite on tx or create a new one ? tx.write(dict(Transaction.on_change_partner_id(order.partner_id.id).get('value', {}), amount=order.amount_total, type=tx_type)) if not tx: tx_values = { 'acquirer_id': acquirer_id, 'type': tx_type, 'amount': order.amount_total, 'currency_id': order.pricelist_id.currency_id.id, 'partner_id': order.partner_id.id, 'partner_country_id': order.partner_id.country_id.id, 'reference': Transaction.get_next_reference(order.name), 'sale_order_id': order.id, } if token and request.env['payment.token'].sudo().browse(int(token)).partner_id == order.partner_id: tx_values['payment_token_id'] = token tx = Transaction.create(tx_values) request.session['sale_transaction_id'] = tx.id # update quotation order.write({ 'payment_acquirer_id': acquirer_id, 'payment_tx_id': request.session['sale_transaction_id'] }) if token: return request.env.ref('website_sale.payment_token_form').render(dict(tx=tx), engine='ir.qweb') return tx.acquirer_id.with_context(submit_class='btn btn-primary', submit_txt=_('Pay Now')).sudo().render( tx.reference, order.amount_total, order.pricelist_id.currency_id.id, values={ 'return_url': '/shop/payment/validate', 'partner_id': order.partner_shipping_id.id or order.partner_invoice_id.id, 'billing_partner_id': order.partner_invoice_id.id, }, ) @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) assert order.id == request.session.get('sale_last_order_id') values = {} flag = False if not order: values.update({'not_order': True, 'state': 'error'}) else: tx = request.env['payment.transaction'].sudo().search( ['|', ('sale_order_id', '=', order.id), ('reference', '=', order.name)], limit=1 ) if not tx: if order.amount_total: values.update({'tx_ids': False, 'state': 'error'}) else: values.update({'tx_ids': False, 'state': 'done', 'validation': None}) else: state = tx.state flag = state == 'pending' values.update({ 'tx_ids': True, 'state': state, 'acquirer_id': tx.acquirer_id, 'validation': tx.acquirer_id.auto_confirm == 'none', 'tx_post_msg': tx.acquirer_id.post_msg or None }) return {'recall': flag, 'message': request.env['ir.ui.view'].render_template("website_sale.order_state_message", values)} @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['report'].sudo().get_pdf([sale_order_id], 'sale.report_saleorder', data=None) pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', 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='http', 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 request.redirect("/shop/product/%s?enable_editor=1" % slug(product.product_tmpl_id)) @http.route(['/shop/change_styles'], type='json', auth="public") def change_styles(self, id, style_id): product = request.env['product.template'].browse(id) remove = [] active = False style_id = int(style_id) for style in product.website_style_ids: if style.id == style_id: remove.append(style.id) active = True break style = request.env['product.style'].browse(style_id) if remove: product.write({'website_style_ids': [(3, rid) for rid in remove]}) if not active: product.write({'website_style_ids': [(4, style.id)]}) return not active @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 )