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