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