[MERGE] forward port of branch 8.0 up to 2b192be
[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", "street2", "city", "country_id", "zip"]
431     optional_billing_fields = ["street", "state_id", "vat", "vat_subjected"]
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 getattr(data, field_name))
454             if data.parent_id:
455                 query[prefix + 'street'] = 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 query.get(prefix + 'vat'):
463             query[prefix + 'vat_subjected'] = True
464
465         if not remove_prefix:
466             return query
467
468         return dict((field_name, data[prefix + field_name]) for field_name in all_fields if data.get(prefix + field_name))
469
470     def checkout_form_validate(self, data):
471         cr, uid, context, registry = request.cr, request.uid, request.context, request.registry
472
473         # Validation
474         error = dict()
475         for field_name in self.mandatory_billing_fields:
476             if not data.get(field_name):
477                 error[field_name] = 'missing'
478
479         if data.get("vat") and hasattr(registry["res.partner"], "check_vat"):
480             if request.website.company_id.vat_check_vies:
481                 # force full VIES online check
482                 check_func = registry["res.partner"].vies_vat_check
483             else:
484                 # quick and partial off-line checksum validation
485                 check_func = registry["res.partner"].simple_vat_check
486             vat_country, vat_number = registry["res.partner"]._split_vat(data.get("vat"))
487             if not check_func(cr, uid, vat_country, vat_number, context=None): # simple_vat_check
488                 error["vat"] = 'error'
489
490         if data.get("shipping_id") == -1:
491             for field_name in self.mandatory_shipping_fields:
492                 field_name = 'shipping_' + field_name
493                 if not data.get(field_name):
494                     error[field_name] = 'missing'
495
496         return error
497
498     def checkout_form_save(self, checkout):
499         cr, uid, context, registry = request.cr, request.uid, request.context, request.registry
500
501         order = request.website.sale_get_order(force_create=1, context=context)
502
503         orm_partner = registry.get('res.partner')
504         orm_user = registry.get('res.users')
505         order_obj = request.registry.get('sale.order')
506
507         billing_info = self.checkout_parse('billing', checkout, True)
508
509         # set partner_id
510         partner_id = None
511         if request.uid != request.website.user_id.id:
512             partner_id = orm_user.browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
513         elif order.partner_id:
514             user_ids = request.registry['res.users'].search(cr, SUPERUSER_ID,
515                 [("partner_id", "=", order.partner_id.id)], context=dict(context or {}, active_test=False))
516             if not user_ids or request.website.user_id.id not in user_ids:
517                 partner_id = order.partner_id.id
518
519         # save partner informations
520         if partner_id and request.website.partner_id.id != partner_id:
521             orm_partner.write(cr, SUPERUSER_ID, [partner_id], billing_info, context=context)
522         else:
523             # create partner
524             partner_id = orm_partner.create(cr, SUPERUSER_ID, billing_info, context=context)
525
526         # create a new shipping partner
527         if checkout.get('shipping_id') == -1:
528             shipping_info = self.checkout_parse('shipping', checkout, True)
529             shipping_info['type'] = 'delivery'
530             shipping_info['parent_id'] = partner_id
531             checkout['shipping_id'] = orm_partner.create(cr, SUPERUSER_ID, shipping_info, context)
532
533         order_info = {
534             'partner_id': partner_id,
535             'message_follower_ids': [(4, partner_id), (3, request.website.partner_id.id)],
536             'partner_invoice_id': partner_id,
537         }
538         order_info.update(order_obj.onchange_partner_id(cr, SUPERUSER_ID, [], partner_id, context=context)['value'])
539         address_change = order_obj.onchange_delivery_id(cr, SUPERUSER_ID, [], order.company_id.id, partner_id,
540                                                         checkout.get('shipping_id'), None, context=context)['value']
541         order_info.update(address_change)
542         if address_change.get('fiscal_position'):
543             fiscal_update = order_obj.onchange_fiscal_position(cr, SUPERUSER_ID, [], address_change['fiscal_position'],
544                                                                [(4, l.id) for l in order.order_line], context=None)['value']
545             order_info.update(fiscal_update)
546
547         order_info.pop('user_id')
548         order_info.update(partner_shipping_id=checkout.get('shipping_id') or partner_id)
549
550         order_obj.write(cr, SUPERUSER_ID, [order.id], order_info, context=context)
551
552     @http.route(['/shop/checkout'], type='http', auth="public", website=True)
553     def checkout(self, **post):
554         cr, uid, context = request.cr, request.uid, request.context
555
556         order = request.website.sale_get_order(force_create=1, context=context)
557
558         redirection = self.checkout_redirection(order)
559         if redirection:
560             return redirection
561
562         values = self.checkout_values()
563
564         return request.website.render("website_sale.checkout", values)
565
566     @http.route(['/shop/confirm_order'], type='http', auth="public", website=True)
567     def confirm_order(self, **post):
568         cr, uid, context, registry = request.cr, request.uid, request.context, request.registry
569
570         order = request.website.sale_get_order(context=context)
571         if not order:
572             return request.redirect("/shop")
573
574         redirection = self.checkout_redirection(order)
575         if redirection:
576             return redirection
577
578         values = self.checkout_values(post)
579
580         values["error"] = self.checkout_form_validate(values["checkout"])
581         if values["error"]:
582             return request.website.render("website_sale.checkout", values)
583
584         self.checkout_form_save(values["checkout"])
585
586         request.session['sale_last_order_id'] = order.id
587
588         request.website.sale_get_order(update_pricelist=True, context=context)
589
590         return request.redirect("/shop/payment")
591
592     #------------------------------------------------------
593     # Payment
594     #------------------------------------------------------
595
596     @http.route(['/shop/payment'], type='http', auth="public", website=True)
597     def payment(self, **post):
598         """ Payment step. This page proposes several payment means based on available
599         payment.acquirer. State at this point :
600
601          - a draft sale order with lines; otherwise, clean context / session and
602            back to the shop
603          - no transaction in context / session, or only a draft one, if the customer
604            did go to a payment.acquirer website but closed the tab without
605            paying / canceling
606         """
607         cr, uid, context = request.cr, request.uid, request.context
608         payment_obj = request.registry.get('payment.acquirer')
609         sale_order_obj = request.registry.get('sale.order')
610
611         order = request.website.sale_get_order(context=context)
612
613         redirection = self.checkout_redirection(order)
614         if redirection:
615             return redirection
616
617         shipping_partner_id = False
618         if order:
619             if order.partner_shipping_id.id:
620                 shipping_partner_id = order.partner_shipping_id.id
621             else:
622                 shipping_partner_id = order.partner_invoice_id.id
623
624         values = {
625             'order': request.registry['sale.order'].browse(cr, SUPERUSER_ID, order.id, context=context)
626         }
627         values['errors'] = sale_order_obj._get_errors(cr, uid, order, context=context)
628         values.update(sale_order_obj._get_website_data(cr, uid, order, context))
629
630         # fetch all registered payment means
631         # if tx:
632         #     acquirer_ids = [tx.acquirer_id.id]
633         # else:
634         if not values['errors']:
635             acquirer_ids = payment_obj.search(cr, SUPERUSER_ID, [('website_published', '=', True), ('company_id', '=', order.company_id.id)], context=context)
636             values['acquirers'] = list(payment_obj.browse(cr, uid, acquirer_ids, context=context))
637             render_ctx = dict(context, submit_class='btn btn-primary', submit_txt=_('Pay Now'))
638             for acquirer in values['acquirers']:
639                 acquirer.button = payment_obj.render(
640                     cr, SUPERUSER_ID, acquirer.id,
641                     order.name,
642                     order.amount_total,
643                     order.pricelist_id.currency_id.id,
644                     partner_id=shipping_partner_id,
645                     tx_values={
646                         'return_url': '/shop/payment/validate',
647                     },
648                     context=render_ctx)
649
650         return request.website.render("website_sale.payment", values)
651
652     @http.route(['/shop/payment/transaction/<int:acquirer_id>'], type='json', auth="public", website=True)
653     def payment_transaction(self, acquirer_id):
654         """ Json method that creates a payment.transaction, used to create a
655         transaction when the user clicks on 'pay now' button. After having
656         created the transaction, the event continues and the user is redirected
657         to the acquirer website.
658
659         :param int acquirer_id: id of a payment.acquirer record. If not set the
660                                 user is redirected to the checkout page
661         """
662         cr, uid, context = request.cr, request.uid, request.context
663         transaction_obj = request.registry.get('payment.transaction')
664         order = request.website.sale_get_order(context=context)
665
666         if not order or not order.order_line or acquirer_id is None:
667             return request.redirect("/shop/checkout")
668
669         assert order.partner_id.id != request.website.partner_id.id
670
671         # find an already existing transaction
672         tx = request.website.sale_get_transaction()
673         if tx:
674             if tx.state == 'draft':  # button cliked but no more info -> rewrite on tx or create a new one ?
675                 tx.write({
676                     'acquirer_id': acquirer_id,
677                 })
678             tx_id = tx.id
679         else:
680             tx_id = transaction_obj.create(cr, SUPERUSER_ID, {
681                 'acquirer_id': acquirer_id,
682                 'type': 'form',
683                 'amount': order.amount_total,
684                 'currency_id': order.pricelist_id.currency_id.id,
685                 'partner_id': order.partner_id.id,
686                 'partner_country_id': order.partner_id.country_id.id,
687                 'reference': order.name,
688                 'sale_order_id': order.id,
689             }, context=context)
690             request.session['sale_transaction_id'] = tx_id
691             tx = transaction_obj.browse(cr, SUPERUSER_ID, tx_id, context=context)
692
693         # update quotation
694         request.registry['sale.order'].write(
695             cr, SUPERUSER_ID, [order.id], {
696                 'payment_acquirer_id': acquirer_id,
697                 'payment_tx_id': request.session['sale_transaction_id']
698             }, context=context)
699
700         # confirm the quotation
701         if tx.acquirer_id.auto_confirm == 'at_pay_now':
702             request.registry['sale.order'].action_button_confirm(cr, SUPERUSER_ID, [order.id], context=request.context)
703
704         return tx_id
705
706     @http.route('/shop/payment/get_status/<int:sale_order_id>', type='json', auth="public", website=True)
707     def payment_get_status(self, sale_order_id, **post):
708         cr, uid, context = request.cr, request.uid, request.context
709
710         order = request.registry['sale.order'].browse(cr, SUPERUSER_ID, sale_order_id, context=context)
711         assert order.id == request.session.get('sale_last_order_id')
712
713         if not order:
714             return {
715                 'state': 'error',
716                 'message': '<p>%s</p>' % _('There seems to be an error with your request.'),
717             }
718
719         tx_ids = request.registry['payment.transaction'].search(
720             cr, SUPERUSER_ID, [
721                 '|', ('sale_order_id', '=', order.id), ('reference', '=', order.name)
722             ], context=context)
723
724         if not tx_ids:
725             if order.amount_total:
726                 return {
727                     'state': 'error',
728                     'message': '<p>%s</p>' % _('There seems to be an error with your request.'),
729                 }
730             else:
731                 state = 'done'
732                 message = ""
733                 validation = None
734         else:
735             tx = request.registry['payment.transaction'].browse(cr, SUPERUSER_ID, tx_ids[0], context=context)
736             state = tx.state
737             if state == 'done':
738                 message = '<p>%s</p>' % _('Your payment has been received.')
739             elif state == 'cancel':
740                 message = '<p>%s</p>' % _('The payment seems to have been canceled.')
741             elif state == 'pending' and tx.acquirer_id.validation == 'manual':
742                 message = '<p>%s</p>' % _('Your transaction is waiting confirmation.')
743                 if tx.acquirer_id.post_msg:
744                     message += tx.acquirer_id.post_msg
745             else:
746                 message = '<p>%s</p>' % _('Your transaction is waiting confirmation.')
747             validation = tx.acquirer_id.validation
748
749         return {
750             'state': state,
751             'message': message,
752             'validation': validation
753         }
754
755     @http.route('/shop/payment/validate', type='http', auth="public", website=True)
756     def payment_validate(self, transaction_id=None, sale_order_id=None, **post):
757         """ Method that should be called by the server when receiving an update
758         for a transaction. State at this point :
759
760          - UDPATE ME
761         """
762         cr, uid, context = request.cr, request.uid, request.context
763         email_act = None
764         sale_order_obj = request.registry['sale.order']
765
766         if transaction_id is None:
767             tx = request.website.sale_get_transaction()
768         else:
769             tx = request.registry['payment.transaction'].browse(cr, uid, transaction_id, context=context)
770
771         if sale_order_id is None:
772             order = request.website.sale_get_order(context=context)
773         else:
774             order = request.registry['sale.order'].browse(cr, SUPERUSER_ID, sale_order_id, context=context)
775             assert order.id == request.session.get('sale_last_order_id')
776
777         if not order or (order.amount_total and not tx):
778             return request.redirect('/shop')
779
780         if (not order.amount_total and not tx) or tx.state in ['pending', 'done']:
781             if (not order.amount_total and not tx):
782                 # Orders are confirmed by payment transactions, but there is none for free orders,
783                 # (e.g. free events), so confirm immediately
784                 order.action_button_confirm()
785             # send by email
786             email_act = sale_order_obj.action_quotation_send(cr, SUPERUSER_ID, [order.id], context=request.context)
787         elif tx and tx.state == 'cancel':
788             # cancel the quotation
789             sale_order_obj.action_cancel(cr, SUPERUSER_ID, [order.id], context=request.context)
790
791         # send the email
792         if email_act and email_act.get('context'):
793             composer_values = {}
794             email_ctx = email_act['context']
795             public_id = request.website.user_id.id
796             if uid == public_id:
797                 composer_values['email_from'] = request.website.user_id.company_id.email
798             composer_id = request.registry['mail.compose.message'].create(cr, SUPERUSER_ID, composer_values, context=email_ctx)
799             request.registry['mail.compose.message'].send_mail(cr, SUPERUSER_ID, [composer_id], context=email_ctx)
800
801         # clean context and session, then redirect to the confirmation page
802         request.website.sale_reset(context=context)
803
804         return request.redirect('/shop/confirmation')
805
806     @http.route(['/shop/confirmation'], type='http', auth="public", website=True)
807     def payment_confirmation(self, **post):
808         """ End of checkout process controller. Confirmation is basically seing
809         the status of a sale.order. State at this point :
810
811          - should not have any context / session info: clean them
812          - take a sale.order id, because we request a sale.order and are not
813            session dependant anymore
814         """
815         cr, uid, context = request.cr, request.uid, request.context
816
817         sale_order_id = request.session.get('sale_last_order_id')
818         if sale_order_id:
819             order = request.registry['sale.order'].browse(cr, SUPERUSER_ID, sale_order_id, context=context)
820         else:
821             return request.redirect('/shop')
822
823         return request.website.render("website_sale.confirmation", {'order': order})
824
825     #------------------------------------------------------
826     # Edit
827     #------------------------------------------------------
828
829     @http.route(['/shop/add_product'], type='http', auth="user", methods=['POST'], website=True)
830     def add_product(self, name=None, category=0, **post):
831         cr, uid, context, pool = request.cr, request.uid, request.context, request.registry
832         if not name:
833             name = _("New Product")
834         product_obj = request.registry.get('product.product')
835         product_id = product_obj.create(cr, uid, { 'name': name, 'public_categ_ids': category }, context=context)
836         product = product_obj.browse(cr, uid, product_id, context=context)
837
838         return request.redirect("/shop/product/%s?enable_editor=1" % slug(product.product_tmpl_id))
839
840     @http.route(['/shop/change_styles'], type='json', auth="public")
841     def change_styles(self, id, style_id):
842         product_obj = request.registry.get('product.template')
843         product = product_obj.browse(request.cr, request.uid, id, context=request.context)
844
845         remove = []
846         active = False
847         for style in product.website_style_ids:
848             if style.id == style_id:
849                 remove.append(style.id)
850                 active = True
851                 break
852
853         style = request.registry.get('product.style').browse(request.cr, request.uid, style_id, context=request.context)
854
855         if remove:
856             product.write({'website_style_ids': [(3, rid) for rid in remove]})
857         if not active:
858             product.write({'website_style_ids': [(4, style.id)]})
859
860         return not active
861
862     @http.route(['/shop/change_sequence'], type='json', auth="public")
863     def change_sequence(self, id, sequence):
864         product_obj = request.registry.get('product.template')
865         if sequence == "top":
866             product_obj.set_sequence_top(request.cr, request.uid, [id], context=request.context)
867         elif sequence == "bottom":
868             product_obj.set_sequence_bottom(request.cr, request.uid, [id], context=request.context)
869         elif sequence == "up":
870             product_obj.set_sequence_up(request.cr, request.uid, [id], context=request.context)
871         elif sequence == "down":
872             product_obj.set_sequence_down(request.cr, request.uid, [id], context=request.context)
873
874     @http.route(['/shop/change_size'], type='json', auth="public")
875     def change_size(self, id, x, y):
876         product_obj = request.registry.get('product.template')
877         product = product_obj.browse(request.cr, request.uid, id, context=request.context)
878         return product.write({'website_size_x': x, 'website_size_y': y})
879
880     def order_lines_2_google_api(self, order_lines):
881         """ Transforms a list of order lines into a dict for google analytics """
882         ret = []
883         for line in order_lines:
884             ret.append({
885                 'id': line.order_id and line.order_id.id,
886                 'name': line.product_id.categ_id and line.product_id.categ_id.name or '-',
887                 'sku': line.product_id.id,
888                 'quantity': line.product_uom_qty,
889                 'price': line.price_unit,
890             })
891         return ret
892
893     @http.route(['/shop/tracking_last_order'], type='json', auth="public")
894     def tracking_cart(self, **post):
895         """ return data about order in JSON needed for google analytics"""
896         cr, uid, context = request.cr, request.uid, request.context
897         ret = {}
898         sale_order_id = request.session.get('sale_last_order_id')
899         if sale_order_id:
900             order = request.registry['sale.order'].browse(cr, SUPERUSER_ID, sale_order_id, context=context)
901             ret['transaction'] = {
902                 'id': sale_order_id,
903                 'affiliation': order.company_id.name,
904                 'revenue': order.amount_total,
905                 'currency': order.currency_id.name
906             }
907             ret['lines'] = self.order_lines_2_google_api(order.order_line)
908         return ret
909
910 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: