[MERGE] forward port of branch 8.0 up to e883193
[odoo/odoo.git] / addons / website_sale / controllers / main.py
1 # -*- coding: utf-8 -*-
2 import werkzeug
3
4 from openerp import SUPERUSER_ID
5 from openerp import http
6 from openerp.http import request
7 from openerp.tools.translate import _
8 from openerp.addons.website.models.website import slug
9
10 PPG = 20 # Products Per Page
11 PPR = 4  # Products Per Row
12
13 class table_compute(object):
14     def __init__(self):
15         self.table = {}
16
17     def _check_place(self, posx, posy, sizex, sizey):
18         res = True
19         for y in range(sizey):
20             for x in range(sizex):
21                 if posx+x>=PPR:
22                     res = False
23                     break
24                 row = self.table.setdefault(posy+y, {})
25                 if row.setdefault(posx+x) is not None:
26                     res = False
27                     break
28             for x in range(PPR):
29                 self.table[posy+y].setdefault(x, None)
30         return res
31
32     def process(self, products):
33         # Compute products positions on the grid
34         minpos = 0
35         index = 0
36         maxy = 0
37         for p in products:
38             x = min(max(p.website_size_x, 1), PPR)
39             y = min(max(p.website_size_y, 1), PPR)
40             if index>PPG:
41                 x = y = 1
42
43             pos = minpos
44             while not self._check_place(pos%PPR, pos/PPR, x, y):
45                 pos += 1
46
47             if index>PPG and (pos/PPR)>maxy:
48                 break
49
50             if x==1 and y==1:   # simple heuristic for CPU optimization
51                 minpos = pos/PPR
52
53             for y2 in range(y):
54                 for x2 in range(x):
55                     self.table[(pos/PPR)+y2][(pos%PPR)+x2] = False
56             self.table[pos/PPR][pos%PPR] = {
57                 'product': p, 'x':x, 'y': y,
58                 'class': " ".join(map(lambda x: x.html_class or '', p.website_style_ids))
59             }
60             if index<=PPG:
61                 maxy=max(maxy,y+(pos/PPR))
62             index += 1
63
64         # Format table according to HTML needs
65         rows = self.table.items()
66         rows.sort()
67         rows = map(lambda x: x[1], rows)
68         for col in range(len(rows)):
69             cols = rows[col].items()
70             cols.sort()
71             x += len(cols)
72             rows[col] = [c for c in map(lambda x: x[1], cols) if c != False]
73
74         return rows
75
76         # TODO keep with input type hidden
77
78
79 class QueryURL(object):
80     def __init__(self, path='', **args):
81         self.path = path
82         self.args = args
83
84     def __call__(self, path=None, **kw):
85         if not path:
86             path = self.path
87         for k,v in self.args.items():
88             kw.setdefault(k,v)
89         l = []
90         for k,v in kw.items():
91             if v:
92                 if isinstance(v, list) or isinstance(v, set):
93                     l.append(werkzeug.url_encode([(k,i) for i in v]))
94                 else:
95                     l.append(werkzeug.url_encode([(k,v)]))
96         if l:
97             path += '?' + '&'.join(l)
98         return path
99
100
101 class website_sale(http.Controller):
102
103     def get_pricelist(self):
104         cr, uid, context, pool = request.cr, request.uid, request.context, request.registry
105         sale_order = context.get('sale_order')
106         if sale_order:
107             pricelist = sale_order.pricelist_id
108         else:
109             partner = pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id
110             pricelist = partner.property_product_pricelist
111         return pricelist
112
113     def get_attribute_value_ids(self, product):
114         cr, uid, context, pool = request.cr, request.uid, request.context, request.registry
115         currency_obj = pool['res.currency']
116         attribute_value_ids = []
117         if request.website.pricelist_id.id != context['pricelist']:
118             website_currency_id = request.website.currency_id.id
119             currency_id = self.get_pricelist().currency_id.id
120             for p in product.product_variant_ids:
121                 price = currency_obj.compute(cr, uid, website_currency_id, currency_id, p.lst_price)
122                 attribute_value_ids.append([p.id, map(int, p.attribute_value_ids), p.price, price])
123         else:
124             attribute_value_ids = [[p.id, map(int, p.attribute_value_ids), p.price, p.lst_price] for p in product.product_variant_ids]
125
126         return attribute_value_ids
127
128     @http.route(['/shop',
129         '/shop/page/<int:page>',
130         '/shop/category/<model("product.public.category"):category>',
131         '/shop/category/<model("product.public.category"):category>/page/<int:page>'
132     ], type='http', auth="public", website=True)
133     def shop(self, page=0, category=None, search='', **post):
134         cr, uid, context, pool = request.cr, request.uid, request.context, request.registry
135
136         domain = request.website.sale_product_domain()
137         if search:
138             domain += ['|', '|', '|', ('name', 'ilike', search), ('description', 'ilike', search),
139                 ('description_sale', 'ilike', search), ('product_variant_ids.default_code', 'ilike', search)]
140         if category:
141             domain += [('product_variant_ids.public_categ_ids', 'child_of', int(category))]
142
143         attrib_list = request.httprequest.args.getlist('attrib')
144         attrib_values = [map(int,v.split("-")) for v in attrib_list if v]
145         attrib_set = set([v[1] for v in attrib_values])
146
147         if attrib_values:
148             attrib = None
149             ids = []
150             for value in attrib_values:
151                 if not attrib:
152                     attrib = value[0]
153                     ids.append(value[1])
154                 elif value[0] == attrib:
155                     ids.append(value[1])
156                 else:
157                     domain += [('attribute_line_ids.value_ids', 'in', ids)]
158                     attrib = value[0]
159                     ids = [value[1]]
160             if attrib:
161                 domain += [('attribute_line_ids.value_ids', 'in', ids)]
162
163         keep = QueryURL('/shop', category=category and int(category), search=search, attrib=attrib_list)
164
165         if not context.get('pricelist'):
166             pricelist = self.get_pricelist()
167             context['pricelist'] = int(pricelist)
168         else:
169             pricelist = pool.get('product.pricelist').browse(cr, uid, context['pricelist'], context)
170
171         url = "/shop"
172         if search:
173             post["search"] = search
174         if category:
175             url = "/shop/category/%s" % slug(category)
176
177         style_obj = pool['product.style']
178         style_ids = style_obj.search(cr, uid, [], context=context)
179         styles = style_obj.browse(cr, uid, style_ids, context=context)
180
181         category_obj = pool['product.public.category']
182         category_ids = category_obj.search(cr, uid, [], context=context)
183         categories = category_obj.browse(cr, uid, category_ids, context=context)
184         categs = filter(lambda x: not x.parent_id, categories)
185
186         domain += [('public_categ_ids', 'in', category_ids)]
187         product_obj = pool.get('product.template')
188
189         product_count = product_obj.search_count(cr, uid, domain, context=context)
190         pager = request.website.pager(url=url, total=product_count, page=page, step=PPG, scope=7, url_args=post)
191         product_ids = product_obj.search(cr, uid, domain, limit=PPG+10, offset=pager['offset'], order='website_published desc, website_sequence desc', context=context)
192         products = product_obj.browse(cr, uid, product_ids, context=context)
193
194         attributes_obj = request.registry['product.attribute']
195         attributes_ids = attributes_obj.search(cr, uid, [], context=context)
196         attributes = attributes_obj.browse(cr, uid, attributes_ids, context=context)
197
198         from_currency = pool.get('product.price.type')._get_field_currency(cr, uid, 'list_price', context)
199         to_currency = pricelist.currency_id
200         compute_currency = lambda price: pool['res.currency']._compute(cr, uid, from_currency, to_currency, price, context=context)
201
202         values = {
203             'search': search,
204             'category': category,
205             'attrib_values': attrib_values,
206             'attrib_set': attrib_set,
207             'pager': pager,
208             'pricelist': pricelist,
209             'products': products,
210             'bins': table_compute().process(products),
211             'rows': PPR,
212             'styles': styles,
213             'categories': categs,
214             'attributes': attributes,
215             'compute_currency': compute_currency,
216             'keep': keep,
217             'style_in_product': lambda style, product: style.id in [s.id for s in product.website_style_ids],
218             'attrib_encode': lambda attribs: werkzeug.url_encode([('attrib',i) for i in attribs]),
219         }
220         return request.website.render("website_sale.products", values)
221
222     @http.route(['/shop/product/<model("product.template"):product>'], type='http', auth="public", website=True)
223     def product(self, product, category='', search='', **kwargs):
224         cr, uid, context, pool = request.cr, request.uid, request.context, request.registry
225         category_obj = pool['product.public.category']
226         template_obj = pool['product.template']
227
228         context.update(active_id=product.id)
229
230         if category:
231             category = category_obj.browse(cr, uid, int(category), context=context)
232
233         attrib_list = request.httprequest.args.getlist('attrib')
234         attrib_values = [map(int,v.split("-")) for v in attrib_list if v]
235         attrib_set = set([v[1] for v in attrib_values])
236
237         keep = QueryURL('/shop', category=category and category.id, search=search, attrib=attrib_list)
238
239         category_ids = category_obj.search(cr, uid, [], context=context)
240         category_list = category_obj.name_get(cr, uid, category_ids, context=context)
241         category_list = sorted(category_list, key=lambda category: category[1])
242
243         pricelist = self.get_pricelist()
244
245         from_currency = pool.get('product.price.type')._get_field_currency(cr, uid, 'list_price', context)
246         to_currency = pricelist.currency_id
247         compute_currency = lambda price: pool['res.currency']._compute(cr, uid, from_currency, to_currency, price, context=context)
248
249         if not context.get('pricelist'):
250             context['pricelist'] = int(self.get_pricelist())
251             product = template_obj.browse(cr, uid, int(product), context=context)
252
253         values = {
254             'search': search,
255             'category': category,
256             'pricelist': pricelist,
257             'attrib_values': attrib_values,
258             'compute_currency': compute_currency,
259             'attrib_set': attrib_set,
260             'keep': keep,
261             'category_list': category_list,
262             'main_object': product,
263             'product': product,
264             'get_attribute_value_ids': self.get_attribute_value_ids
265         }
266         return request.website.render("website_sale.product", values)
267
268     @http.route(['/shop/product/comment/<int:product_template_id>'], type='http', auth="public", methods=['POST'], website=True)
269     def product_comment(self, product_template_id, **post):
270         cr, uid, context = request.cr, request.uid, request.context
271         if post.get('comment'):
272             request.registry['product.template'].message_post(
273                 cr, uid, product_template_id,
274                 body=post.get('comment'),
275                 type='comment',
276                 subtype='mt_comment',
277                 context=dict(context, mail_create_nosubcribe=True))
278         return werkzeug.utils.redirect(request.httprequest.referrer + "#comments")
279
280     @http.route(['/shop/pricelist'], type='http', auth="public", website=True)
281     def pricelist(self, promo, **post):
282         cr, uid, context = request.cr, request.uid, request.context
283         request.website.sale_get_order(code=promo, context=context)
284         return request.redirect("/shop/cart")
285
286     @http.route(['/shop/cart'], type='http', auth="public", website=True)
287     def cart(self, **post):
288         cr, uid, context, pool = request.cr, request.uid, request.context, request.registry
289         order = request.website.sale_get_order()
290         if order:
291             from_currency = pool.get('product.price.type')._get_field_currency(cr, uid, 'list_price', context)
292             to_currency = order.pricelist_id.currency_id
293             compute_currency = lambda price: pool['res.currency']._compute(cr, uid, from_currency, to_currency, price, context=context)
294         else:
295             compute_currency = lambda price: price
296
297         values = {
298             'order': order,
299             'compute_currency': compute_currency,
300             'suggested_products': [],
301         }
302         if order:
303             _order = order
304             if not context.get('pricelist'):
305                 _order = order.with_context(pricelist=order.pricelist_id.id)
306             values['suggested_products'] = _order._cart_accessories()
307
308         return request.website.render("website_sale.cart", values)
309
310     @http.route(['/shop/cart/update'], type='http', auth="public", methods=['POST'], website=True)
311     def cart_update(self, product_id, add_qty=1, set_qty=0, **kw):
312         cr, uid, context = request.cr, request.uid, request.context
313         request.website.sale_get_order(force_create=1)._cart_update(product_id=int(product_id), add_qty=float(add_qty), set_qty=float(set_qty))
314         return request.redirect("/shop/cart")
315
316     @http.route(['/shop/cart/update_json'], type='json', auth="public", methods=['POST'], website=True)
317     def cart_update_json(self, product_id, line_id, add_qty=None, set_qty=None, display=True):
318         order = request.website.sale_get_order(force_create=1)
319         value = order._cart_update(product_id=product_id, line_id=line_id, add_qty=add_qty, set_qty=set_qty)
320         if not display:
321             return None
322         value['cart_quantity'] = order.cart_quantity
323         value['website_sale.total'] = request.website._render("website_sale.total", {
324                 'website_sale_order': request.website.sale_get_order()
325             })
326         return value
327
328     #------------------------------------------------------
329     # Checkout
330     #------------------------------------------------------
331
332     def checkout_redirection(self, order):
333         cr, uid, context, registry = request.cr, request.uid, request.context, request.registry
334
335         # must have a draft sale order with lines at this point, otherwise reset
336         if not order or order.state != 'draft':
337             request.session['sale_order_id'] = None
338             request.session['sale_transaction_id'] = None
339             return request.redirect('/shop')
340
341         # if transaction pending / done: redirect to confirmation
342         tx = context.get('website_sale_transaction')
343         if tx and tx.state != 'draft':
344             return request.redirect('/shop/payment/confirmation/%s' % order.id)
345
346     def checkout_values(self, data=None):
347         cr, uid, context, registry = request.cr, request.uid, request.context, request.registry
348         orm_partner = registry.get('res.partner')
349         orm_user = registry.get('res.users')
350         orm_country = registry.get('res.country')
351         state_orm = registry.get('res.country.state')
352
353         country_ids = orm_country.search(cr, SUPERUSER_ID, [], context=context)
354         countries = orm_country.browse(cr, SUPERUSER_ID, country_ids, context)
355         states_ids = state_orm.search(cr, SUPERUSER_ID, [], context=context)
356         states = state_orm.browse(cr, SUPERUSER_ID, states_ids, context)
357         partner = orm_user.browse(cr, SUPERUSER_ID, request.uid, context).partner_id
358
359         order = None
360
361         shipping_id = None
362         shipping_ids = []
363         checkout = {}
364         if not data:
365             print request.uid, request.website.user_id.id
366             if request.uid != request.website.user_id.id:
367                 checkout.update( self.checkout_parse("billing", partner) )
368                 shipping_ids = orm_partner.search(cr, SUPERUSER_ID, [("parent_id", "=", partner.id), ('type', "=", 'delivery')], context=context)
369             else:
370                 order = request.website.sale_get_order(force_create=1, context=context)
371                 if order.partner_id:
372                     domain = [("partner_id", "=", order.partner_id.id)]
373                     user_ids = request.registry['res.users'].search(cr, SUPERUSER_ID, domain, context=dict(context or {}, active_test=False))
374                     if not user_ids or request.website.user_id.id not in user_ids:
375                         checkout.update( self.checkout_parse("billing", order.partner_id) )
376         else:
377             checkout = self.checkout_parse('billing', data)
378             try: 
379                 shipping_id = int(data["shipping_id"])
380             except ValueError:
381                 pass
382             if shipping_id == -1:
383                 checkout.update(self.checkout_parse('shipping', data))
384
385         if shipping_id is None:
386             if not order:
387                 order = request.website.sale_get_order(context=context)
388             if order and order.partner_shipping_id:
389                 shipping_id = order.partner_shipping_id.id
390
391         shipping_ids = list(set(shipping_ids) - set([partner.id]))
392
393         if shipping_id == partner.id:
394             shipping_id = 0
395         elif shipping_id > 0 and shipping_id not in shipping_ids:
396             shipping_ids.append(shipping_id)
397         elif shipping_id is None and shipping_ids:
398             shipping_id = shipping_ids[0]
399
400         ctx = dict(context, show_address=1)
401         shippings = []
402         if shipping_ids:
403             shippings = shipping_ids and orm_partner.browse(cr, SUPERUSER_ID, list(shipping_ids), ctx) or []
404         if shipping_id > 0:
405             shipping = orm_partner.browse(cr, SUPERUSER_ID, shipping_id, ctx)
406             checkout.update( self.checkout_parse("shipping", shipping) )
407
408         checkout['shipping_id'] = shipping_id
409
410         # Default search by user country
411         if not checkout.get('country_id'):
412             country_code = request.session['geoip'].get('country_code')
413             if country_code:
414                 country_ids = request.registry.get('res.country').search(cr, uid, [('code', '=', country_code)], context=context)
415                 if country_ids:
416                     checkout['country_id'] = country_ids[0]
417
418         values = {
419             'countries': countries,
420             'states': states,
421             'checkout': checkout,
422             'shipping_id': partner.id != shipping_id and shipping_id or 0,
423             'shippings': shippings,
424             'error': {},
425             'has_check_vat': hasattr(registry['res.partner'], 'check_vat')
426         }
427
428         return values
429
430     mandatory_billing_fields = ["name", "phone", "email", "street", "city", "country_id", "zip"]
431     optional_billing_fields = ["street2", "state_id", "vat"]
432     mandatory_shipping_fields = ["name", "phone", "street", "city", "country_id", "zip"]
433     optional_shipping_fields = ["state_id"]
434
435     def checkout_parse(self, address_type, data, remove_prefix=False):
436         """ data is a dict OR a partner browse record
437         """
438         # set mandatory and optional fields
439         assert address_type in ('billing', 'shipping')
440         if address_type == 'billing':
441             all_fields = self.mandatory_billing_fields + self.optional_billing_fields
442             prefix = ''
443         else:
444             all_fields = self.mandatory_shipping_fields + self.optional_shipping_fields
445             prefix = 'shipping_'
446
447         # set data
448         if isinstance(data, dict):
449             query = dict((prefix + field_name, data[prefix + field_name])
450                 for field_name in all_fields if data.get(prefix + field_name))
451         else:
452             query = dict((prefix + field_name, getattr(data, field_name))
453                 for field_name in all_fields if field_name != "street2" and getattr(data, field_name))
454             if data.parent_id:
455                 query[prefix + 'street2'] = data.parent_id.name
456
457         if query.get(prefix + 'state_id'):
458             query[prefix + 'state_id'] = int(query[prefix + 'state_id'])
459         if query.get(prefix + 'country_id'):
460             query[prefix + 'country_id'] = int(query[prefix + 'country_id'])
461
462         if not remove_prefix:
463             return query
464
465         return dict((field_name, data[prefix + field_name]) for field_name in all_fields if data.get(prefix + field_name))
466
467     def checkout_form_validate(self, data):
468         cr, uid, context, registry = request.cr, request.uid, request.context, request.registry
469
470         # Validation
471         error = dict()
472         for field_name in self.mandatory_billing_fields:
473             if not data.get(field_name):
474                 error[field_name] = 'missing'
475
476         if data.get("vat") and hasattr(registry["res.partner"], "check_vat"):
477             if request.website.company_id.vat_check_vies:
478                 # force full VIES online check
479                 check_func = registry["res.partner"].vies_vat_check
480             else:
481                 # quick and partial off-line checksum validation
482                 check_func = registry["res.partner"].simple_vat_check
483             vat_country, vat_number = registry["res.partner"]._split_vat(data.get("vat"))
484             if not check_func(cr, uid, vat_country, vat_number, context=None): # simple_vat_check
485                 error["vat"] = 'error'
486
487         if data.get("shipping_id") == -1:
488             for field_name in self.mandatory_shipping_fields:
489                 field_name = 'shipping_' + field_name
490                 if not data.get(field_name):
491                     error[field_name] = 'missing'
492
493         return error
494
495     def checkout_form_save(self, checkout):
496         cr, uid, context, registry = request.cr, request.uid, request.context, request.registry
497
498         order = request.website.sale_get_order(force_create=1, context=context)
499
500         orm_partner = registry.get('res.partner')
501         orm_user = registry.get('res.users')
502         order_obj = request.registry.get('sale.order')
503
504         billing_info = self.checkout_parse('billing', checkout, True)
505
506         # set partner_id
507         partner_id = None
508         if request.uid != request.website.user_id.id:
509             partner_id = orm_user.browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
510         elif order.partner_id:
511             user_ids = request.registry['res.users'].search(cr, SUPERUSER_ID,
512                 [("partner_id", "=", order.partner_id.id)], context=dict(context or {}, active_test=False))
513             if not user_ids or request.website.user_id.id not in user_ids:
514                 partner_id = order.partner_id.id
515
516         # save partner informations
517         if partner_id and request.website.partner_id.id != partner_id:
518             orm_partner.write(cr, SUPERUSER_ID, [partner_id], billing_info, context=context)
519         else:
520             # create partner
521             partner_id = orm_partner.create(cr, SUPERUSER_ID, billing_info, context=context)
522
523         # create a new shipping partner
524         if checkout.get('shipping_id') == -1:
525             shipping_info = self.checkout_parse('shipping', checkout, True)
526             shipping_info['type'] = 'delivery'
527             shipping_info['parent_id'] = partner_id
528             checkout['shipping_id'] = orm_partner.create(cr, SUPERUSER_ID, shipping_info, context)
529
530         order_info = {
531             'partner_id': partner_id,
532             'message_follower_ids': [(4, partner_id), (3, request.website.partner_id.id)],
533             'partner_invoice_id': partner_id,
534         }
535         order_info.update(order_obj.onchange_partner_id(cr, SUPERUSER_ID, [], partner_id, context=context)['value'])
536         order_info.update(order_obj.onchange_delivery_id(cr, SUPERUSER_ID, [], order.company_id.id, partner_id, checkout.get('shipping_id'), None, context=context)['value'])
537
538         order_info.pop('user_id')
539         order_info.update(partner_shipping_id=checkout.get('shipping_id') or partner_id)
540
541         order_obj.write(cr, SUPERUSER_ID, [order.id], order_info, context=context)
542
543     @http.route(['/shop/checkout'], type='http', auth="public", website=True)
544     def checkout(self, **post):
545         cr, uid, context = request.cr, request.uid, request.context
546
547         order = request.website.sale_get_order(force_create=1, context=context)
548
549         redirection = self.checkout_redirection(order)
550         if redirection:
551             return redirection
552
553         values = self.checkout_values()
554
555         return request.website.render("website_sale.checkout", values)
556
557     @http.route(['/shop/confirm_order'], type='http', auth="public", website=True)
558     def confirm_order(self, **post):
559         cr, uid, context, registry = request.cr, request.uid, request.context, request.registry
560
561         order = request.website.sale_get_order(context=context)
562         if not order:
563             return request.redirect("/shop")
564
565         redirection = self.checkout_redirection(order)
566         if redirection:
567             return redirection
568
569         values = self.checkout_values(post)
570
571         values["error"] = self.checkout_form_validate(values["checkout"])
572         if values["error"]:
573             return request.website.render("website_sale.checkout", values)
574
575         self.checkout_form_save(values["checkout"])
576
577         request.session['sale_last_order_id'] = order.id
578
579         request.website.sale_get_order(update_pricelist=True, context=context)
580
581         return request.redirect("/shop/payment")
582
583     #------------------------------------------------------
584     # Payment
585     #------------------------------------------------------
586
587     @http.route(['/shop/payment'], type='http', auth="public", website=True)
588     def payment(self, **post):
589         """ Payment step. This page proposes several payment means based on available
590         payment.acquirer. State at this point :
591
592          - a draft sale order with lines; otherwise, clean context / session and
593            back to the shop
594          - no transaction in context / session, or only a draft one, if the customer
595            did go to a payment.acquirer website but closed the tab without
596            paying / canceling
597         """
598         cr, uid, context = request.cr, request.uid, request.context
599         payment_obj = request.registry.get('payment.acquirer')
600
601         order = request.website.sale_get_order(context=context)
602
603         redirection = self.checkout_redirection(order)
604         if redirection:
605             return redirection
606
607         shipping_partner_id = False
608         if order:
609             if order.partner_shipping_id.id:
610                 shipping_partner_id = order.partner_shipping_id.id
611             else:
612                 shipping_partner_id = order.partner_invoice_id.id
613
614         values = {
615             'order': request.registry['sale.order'].browse(cr, SUPERUSER_ID, order.id, context=context)
616         }
617         values.update(request.registry.get('sale.order')._get_website_data(cr, uid, order, context))
618
619         # fetch all registered payment means
620         # if tx:
621         #     acquirer_ids = [tx.acquirer_id.id]
622         # else:
623         acquirer_ids = payment_obj.search(cr, SUPERUSER_ID, [('website_published', '=', True), ('company_id', '=', order.company_id.id)], context=context)
624         values['acquirers'] = list(payment_obj.browse(cr, uid, acquirer_ids, context=context))
625         render_ctx = dict(context, submit_class='btn btn-primary', submit_txt=_('Pay Now'))
626         for acquirer in values['acquirers']:
627             acquirer.button = payment_obj.render(
628                 cr, SUPERUSER_ID, acquirer.id,
629                 order.name,
630                 order.amount_total,
631                 order.pricelist_id.currency_id.id,
632                 partner_id=shipping_partner_id,
633                 tx_values={
634                     'return_url': '/shop/payment/validate',
635                 },
636                 context=render_ctx)
637
638         return request.website.render("website_sale.payment", values)
639
640     @http.route(['/shop/payment/transaction/<int:acquirer_id>'], type='json', auth="public", website=True)
641     def payment_transaction(self, acquirer_id):
642         """ Json method that creates a payment.transaction, used to create a
643         transaction when the user clicks on 'pay now' button. After having
644         created the transaction, the event continues and the user is redirected
645         to the acquirer website.
646
647         :param int acquirer_id: id of a payment.acquirer record. If not set the
648                                 user is redirected to the checkout page
649         """
650         cr, uid, context = request.cr, request.uid, request.context
651         transaction_obj = request.registry.get('payment.transaction')
652         order = request.website.sale_get_order(context=context)
653
654         if not order or not order.order_line or acquirer_id is None:
655             return request.redirect("/shop/checkout")
656
657         assert order.partner_id.id != request.website.partner_id.id
658
659         # find an already existing transaction
660         tx = request.website.sale_get_transaction()
661         if tx:
662             if tx.state == 'draft':  # button cliked but no more info -> rewrite on tx or create a new one ?
663                 tx.write({
664                     'acquirer_id': acquirer_id,
665                 })
666             tx_id = tx.id
667         else:
668             tx_id = transaction_obj.create(cr, SUPERUSER_ID, {
669                 'acquirer_id': acquirer_id,
670                 'type': 'form',
671                 'amount': order.amount_total,
672                 'currency_id': order.pricelist_id.currency_id.id,
673                 'partner_id': order.partner_id.id,
674                 'partner_country_id': order.partner_id.country_id.id,
675                 'reference': order.name,
676                 'sale_order_id': order.id,
677             }, context=context)
678             request.session['sale_transaction_id'] = tx_id
679
680         # update quotation
681         request.registry['sale.order'].write(
682             cr, SUPERUSER_ID, [order.id], {
683                 'payment_acquirer_id': acquirer_id,
684                 'payment_tx_id': request.session['sale_transaction_id']
685             }, context=context)
686
687         return tx_id
688
689     @http.route('/shop/payment/get_status/<int:sale_order_id>', type='json', auth="public", website=True)
690     def payment_get_status(self, sale_order_id, **post):
691         cr, uid, context = request.cr, request.uid, request.context
692
693         order = request.registry['sale.order'].browse(cr, SUPERUSER_ID, sale_order_id, context=context)
694         assert order.id == request.session.get('sale_last_order_id')
695
696         if not order:
697             return {
698                 'state': 'error',
699                 'message': '<p>%s</p>' % _('There seems to be an error with your request.'),
700             }
701
702         tx_ids = request.registry['payment.transaction'].search(
703             cr, SUPERUSER_ID, [
704                 '|', ('sale_order_id', '=', order.id), ('reference', '=', order.name)
705             ], context=context)
706
707         if not tx_ids:
708             if order.amount_total:
709                 return {
710                     'state': 'error',
711                     'message': '<p>%s</p>' % _('There seems to be an error with your request.'),
712                 }
713             else:
714                 state = 'done'
715                 message = ""
716                 validation = None
717         else:
718             tx = request.registry['payment.transaction'].browse(cr, SUPERUSER_ID, tx_ids[0], context=context)
719             state = tx.state
720             if state == 'done':
721                 message = '<p>%s</p>' % _('Your payment has been received.')
722             elif state == 'cancel':
723                 message = '<p>%s</p>' % _('The payment seems to have been canceled.')
724             elif state == 'pending' and tx.acquirer_id.validation == 'manual':
725                 message = '<p>%s</p>' % _('Your transaction is waiting confirmation.')
726                 if tx.acquirer_id.post_msg:
727                     message += tx.acquirer_id.post_msg
728             else:
729                 message = '<p>%s</p>' % _('Your transaction is waiting confirmation.')
730             validation = tx.acquirer_id.validation
731
732         return {
733             'state': state,
734             'message': message,
735             'validation': validation
736         }
737
738     @http.route('/shop/payment/validate', type='http', auth="public", website=True)
739     def payment_validate(self, transaction_id=None, sale_order_id=None, **post):
740         """ Method that should be called by the server when receiving an update
741         for a transaction. State at this point :
742
743          - UDPATE ME
744         """
745         cr, uid, context = request.cr, request.uid, request.context
746         email_act = None
747         sale_order_obj = request.registry['sale.order']
748
749         if transaction_id is None:
750             tx = request.website.sale_get_transaction()
751         else:
752             tx = request.registry['payment.transaction'].browse(cr, uid, transaction_id, context=context)
753
754         if sale_order_id is None:
755             order = request.website.sale_get_order(context=context)
756         else:
757             order = request.registry['sale.order'].browse(cr, SUPERUSER_ID, sale_order_id, context=context)
758             assert order.id == request.session.get('sale_last_order_id')
759
760         if not order or (order.amount_total and not tx):
761             return request.redirect('/shop')
762
763         if (not order.amount_total and not tx) or tx.state in ['pending', 'done']:
764             if (not order.amount_total and not tx):
765                 # Orders are confirmed by payment transactions, but there is none for free orders,
766                 # (e.g. free events), so confirm immediately
767                 order.action_button_confirm()
768             # send by email
769             email_act = sale_order_obj.action_quotation_send(cr, SUPERUSER_ID, [order.id], context=request.context)
770         elif tx and tx.state == 'cancel':
771             # cancel the quotation
772             sale_order_obj.action_cancel(cr, SUPERUSER_ID, [order.id], context=request.context)
773
774         # send the email
775         if email_act and email_act.get('context'):
776             composer_values = {}
777             email_ctx = email_act['context']
778             public_id = request.website.user_id.id
779             if uid == public_id:
780                 composer_values['email_from'] = request.website.user_id.company_id.email
781             composer_id = request.registry['mail.compose.message'].create(cr, SUPERUSER_ID, composer_values, context=email_ctx)
782             request.registry['mail.compose.message'].send_mail(cr, SUPERUSER_ID, [composer_id], context=email_ctx)
783
784         # clean context and session, then redirect to the confirmation page
785         request.website.sale_reset(context=context)
786
787         return request.redirect('/shop/confirmation')
788
789     @http.route(['/shop/confirmation'], type='http', auth="public", website=True)
790     def payment_confirmation(self, **post):
791         """ End of checkout process controller. Confirmation is basically seing
792         the status of a sale.order. State at this point :
793
794          - should not have any context / session info: clean them
795          - take a sale.order id, because we request a sale.order and are not
796            session dependant anymore
797         """
798         cr, uid, context = request.cr, request.uid, request.context
799
800         sale_order_id = request.session.get('sale_last_order_id')
801         if sale_order_id:
802             order = request.registry['sale.order'].browse(cr, SUPERUSER_ID, sale_order_id, context=context)
803         else:
804             return request.redirect('/shop')
805
806         return request.website.render("website_sale.confirmation", {'order': order})
807
808     #------------------------------------------------------
809     # Edit
810     #------------------------------------------------------
811
812     @http.route(['/shop/add_product'], type='http', auth="user", methods=['POST'], website=True)
813     def add_product(self, name=None, category=0, **post):
814         cr, uid, context, pool = request.cr, request.uid, request.context, request.registry
815         if not name:
816             name = _("New Product")
817         product_obj = request.registry.get('product.product')
818         product_id = product_obj.create(cr, uid, { 'name': name, 'public_categ_ids': category }, context=context)
819         product = product_obj.browse(cr, uid, product_id, context=context)
820
821         return request.redirect("/shop/product/%s?enable_editor=1" % slug(product.product_tmpl_id))
822
823     @http.route(['/shop/change_styles'], type='json', auth="public")
824     def change_styles(self, id, style_id):
825         product_obj = request.registry.get('product.template')
826         product = product_obj.browse(request.cr, request.uid, id, context=request.context)
827
828         remove = []
829         active = False
830         for style in product.website_style_ids:
831             if style.id == style_id:
832                 remove.append(style.id)
833                 active = True
834                 break
835
836         style = request.registry.get('product.style').browse(request.cr, request.uid, style_id, context=request.context)
837
838         if remove:
839             product.write({'website_style_ids': [(3, rid) for rid in remove]})
840         if not active:
841             product.write({'website_style_ids': [(4, style.id)]})
842
843         return not active
844
845     @http.route(['/shop/change_sequence'], type='json', auth="public")
846     def change_sequence(self, id, sequence):
847         product_obj = request.registry.get('product.template')
848         if sequence == "top":
849             product_obj.set_sequence_top(request.cr, request.uid, [id], context=request.context)
850         elif sequence == "bottom":
851             product_obj.set_sequence_bottom(request.cr, request.uid, [id], context=request.context)
852         elif sequence == "up":
853             product_obj.set_sequence_up(request.cr, request.uid, [id], context=request.context)
854         elif sequence == "down":
855             product_obj.set_sequence_down(request.cr, request.uid, [id], context=request.context)
856
857     @http.route(['/shop/change_size'], type='json', auth="public")
858     def change_size(self, id, x, y):
859         product_obj = request.registry.get('product.template')
860         product = product_obj.browse(request.cr, request.uid, id, context=request.context)
861         return product.write({'website_size_x': x, 'website_size_y': y})
862
863     def order_lines_2_google_api(self, order_lines):
864         """ Transforms a list of order lines into a dict for google analytics """
865         ret = []
866         for line in order_lines:
867             ret.append({
868                 'id': line.order_id and line.order_id.id,
869                 'name': line.product_id.categ_id and line.product_id.categ_id.name or '-',
870                 'sku': line.product_id.id,
871                 'quantity': line.product_uom_qty,
872                 'price': line.price_unit,
873             })
874         return ret
875
876     @http.route(['/shop/tracking_last_order'], type='json', auth="public")
877     def tracking_cart(self, **post):
878         """ return data about order in JSON needed for google analytics"""
879         cr, uid, context = request.cr, request.uid, request.context
880         ret = {}
881         sale_order_id = request.session.get('sale_last_order_id')
882         if sale_order_id:
883             order = request.registry['sale.order'].browse(cr, SUPERUSER_ID, sale_order_id, context=context)
884             ret['transaction'] = {
885                 'id': sale_order_id,
886                 'affiliation': order.company_id.name,
887                 'revenue': order.amount_total,
888                 'currency': order.currency_id.name
889             }
890             ret['lines'] = self.order_lines_2_google_api(order.order_line)
891         return ret
892
893 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: