From bcb395616b6d865ab2315c730a9af76132b66493 Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Fri, 24 Apr 2015 17:38:21 +0200 Subject: [PATCH] [ADD] website_sale: manage pricelist by website. Allow to specify which pricelist can be used on wich website for which countries --- addons/website_sale/controllers/main.py | 30 +-- addons/website_sale/data/data.xml | 7 +- addons/website_sale/data/demo.xml | 65 +++++++ addons/website_sale/models/sale_order.py | 182 ++++++++++++++++-- .../website_sale/security/ir.model.access.csv | 2 + addons/website_sale/tests/__init__.py | 1 + .../tests/test_website_sale_pricelist.py | 82 ++++++++ addons/website_sale/views/templates.xml | 21 ++ addons/website_sale/views/views.xml | 50 ++++- 9 files changed, 413 insertions(+), 27 deletions(-) create mode 100644 addons/website_sale/tests/test_website_sale_pricelist.py diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 7e9bdbd71aa..ba2b1172b52 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -105,14 +105,7 @@ class QueryURL(object): def get_pricelist(): - cr, uid, context, pool = request.cr, request.uid, request.context, request.registry - sale_order = context.get('sale_order') - if sale_order: - pricelist = sale_order.pricelist_id - else: - partner = pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id - pricelist = partner.property_product_pricelist - return pricelist + return request.website.get_current_pricelist() class website_sale(http.Controller): @@ -138,6 +131,13 @@ class website_sale(http.Controller): return attribute_value_ids + @http.route(['/shop/change_pricelist/'], type='http', auth="public", website=True) + def pricelist_change(self, pl_id, **post): + if request.website.is_pricelist_available(pl_id.id, context=request.context): + request.session['website_sale_current_pl'] = pl_id.id + request.website.sale_get_order(force_pricelist=pl_id.id, context=request.context) + return request.redirect(request.httprequest.referrer or '/shop') + @http.route(['/shop', '/shop/page/', '/shop/category/', @@ -305,7 +305,12 @@ class website_sale(http.Controller): @http.route(['/shop/pricelist'], type='http', auth="public", website=True) def pricelist(self, promo, **post): - cr, uid, context = request.cr, request.uid, request.context + cr, uid, pool, context = request.cr, request.uid, request.registry, request.context + pl = pool['product.pricelist'].search(cr, SUPERUSER_ID, [('code', '=', promo)], context=context) + if pl: + if not request.website.is_pricelist_available(pl[0], context=context): + return request.redirect("/shop/cart?code_not_available=1") + request.website.sale_get_order(code=promo, context=context) return request.redirect("/shop/cart") @@ -331,11 +336,13 @@ class website_sale(http.Controller): _order = order.with_context(pricelist=order.pricelist_id.id) values['suggested_products'] = _order._cart_accessories() + if post.get('code_not_available'): + values['code_not_available'] = post.get('code_not_available') + return request.website.render("website_sale.cart", values) @http.route(['/shop/cart/update'], type='http', auth="public", methods=['POST'], website=True) def cart_update(self, product_id, add_qty=1, set_qty=0, **kw): - cr, uid, context = request.cr, request.uid, request.context 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)) return request.redirect("/shop/cart") @@ -660,7 +667,6 @@ class website_sale(http.Controller): request.session['sale_last_order_id'] = order.id request.website.sale_get_order(update_pricelist=True, context=context) - return request.redirect("/shop/payment") #------------------------------------------------------ @@ -993,6 +999,6 @@ class website_sale(http.Controller): cr, uid, context, pool = request.cr, request.uid, request.context, request.registry products = pool['product.product'].browse(cr, uid, product_ids, context=context) partner = pool['res.users'].browse(cr, uid, uid, context=context).partner_id - pricelist_id = request.session.get('sale_order_code_pricelist_id') or partner.property_product_pricelist.id + pricelist_id = request.website.get_current_pricelist(context=context).id prices = pool['product.pricelist'].price_rule_get_multi(cr, uid, [], [(product, add_qty, partner) for product in products], context=context) return {product_id: prices[product_id][pricelist_id][0] for product_id in product_ids} diff --git a/addons/website_sale/data/data.xml b/addons/website_sale/data/data.xml index 84b7969fa1a..8a77cd46d58 100644 --- a/addons/website_sale/data/data.xml +++ b/addons/website_sale/data/data.xml @@ -33,9 +33,12 @@ + + + + + - - diff --git a/addons/website_sale/data/demo.xml b/addons/website_sale/data/demo.xml index 57ebfa4fe91..62988f7d43b 100644 --- a/addons/website_sale/data/demo.xml +++ b/addons/website_sale/data/demo.xml @@ -785,5 +785,70 @@ Weight: 1.1 ounces + + + Christmas Pricelist + sale + + + + Christmas Pricelist Version + + + + + -0.2 + 10 + Christmas Pricelist Line + + + + Benelux Pricelist + sale + + + + Benelux Pricelist Version + + + + + -0.1 + 10 + Benelux Pricelist Line + + + + BeNeLux + + + + + US-CA + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 23e2563829e..2a52f9c2732 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- import random -from openerp import SUPERUSER_ID +from openerp import SUPERUSER_ID, tools from openerp.osv import osv, orm, fields from openerp.addons.web.http import request +from openerp.tools.translate import _ class sale_order(osv.Model): @@ -129,15 +130,110 @@ class website(orm.Model): type='many2one', relation='res.currency', string='Default Currency'), 'salesperson_id': fields.many2one('res.users', 'Salesperson'), 'salesteam_id': fields.many2one('crm.team', 'Sales Team'), + 'website_pricelist_ids': fields.one2many('website_pricelist', 'website_id', + string='Price list available for this Ecommerce/Website'), } + @tools.ormcache() + # No context, else no cache + def _get_pl(self, cr, uid, country_code, show_visible, website_pl, current_pl, all_pl): + """ Return the list of pricelists that can be used on website for the current user. + + :param str country_code: code iso or False, If set, we search only price list available for this country + :param bool show_visible: if True, we don't display pricelist where selectable is False (Eg: Code promo) + :param int website_pl: The default pricelist used on this website + :param int current_pl: The current pricelist used on the website + (If not selectable but the current pricelist we had this pricelist anyway) + :param list all_pl: List of all pricelist available for this website + + :returns: list of pricelist + """ + pcs = [] + + if country_code: + groups = self.pool['res.country.group'].search(cr, uid, [('country_ids.code', '=', country_code)]) + for cgroup in self.pool['res.country.group'].browse(cr, uid, groups): + for pll in cgroup.website_pricelist_ids: + if not show_visible or pll.selectable or pll.pricelist_id.id == current_pl: + pcs.append(pll.pricelist_id) + + if not pcs: # no pricelist for this country, or no GeoIP + pcs = [pll.pricelist_id for pll in all_pl + if not show_visible or pll.selectable or pll.pricelist_id.id == current_pl] + + partner = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid).partner_id + if not pcs or partner.property_product_pricelist.id != website_pl: + pcs.append(partner.property_product_pricelist) + pcs = list(set(pcs)) # remove duplicate + pcs.sort(key=lambda x: x.name) # sort by name + return pcs + + def get_pricelist_available(self, cr, uid, show_visible=False, context=None): + """ Return the list of pricelists that can be used on website for the current user. + Country restrictions will be detected with GeoIP (if installed). + + :param str country_code: code iso or False, If set, we search only price list available for this country + :param bool show_visible: if True, we don't display pricelist where selectable is False (Eg: Code promo) + + :returns: list of pricelist + """ + isocountry = request.session.geoip and request.session.geoip.get('country_code') or False + return self._get_pl( + cr, + uid, + isocountry, + show_visible, + request.website.pricelist_id.id, + request.session.get('website_sale_current_pl'), + request.website.website_pricelist_ids + ) + + def is_pricelist_available(self, cr, uid, pl_id, context=None): + """ Return a boolean to specify if a specific pricelist can be manually set on the website. + Warning: It check only if pricelist is in the 'selectable' pricelists or the current pricelist. + + :param int pl_id: The pricelist id to check + + :returns: Boolean, True if valid / available + """ + return pl_id in [ppl.id for ppl in self.get_pricelist_available(cr, uid, show_visible=False, context=context)] + + def get_current_pricelist(self, cr, uid, context=None): + """ + :returns: The current pricelist record + """ + pl_id = request.session.get('website_sale_current_pl') + if pl_id: + return self.pool['product.pricelist'].browse(cr, uid, [pl_id], context=context)[0] + else: + pl = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.property_product_pricelist + request.session['website_sale_current_pl'] = pl.id + return pl + def sale_product_domain(self, cr, uid, ids, context=None): return [("sale_ok", "=", True)] - def sale_get_order(self, cr, uid, ids, force_create=False, code=None, update_pricelist=None, context=None): + def sale_get_order(self, cr, uid, ids, force_create=False, code=None, update_pricelist=False, force_pricelist=False, context=None): + """ Return the current sale order after mofications specified by params. + + :param bool force_create: Create sale order if not already existing + :param str code: Code to force a pricelist (promo code) + If empty, it's a special case to reset the pricelist with the first available else the default. + :param bool update_pricelist: Force to recompute all the lines from sale order to adapt the price with the current pricelist. + :param int force_pricelist: pricelist_id - if set, we change the pricelist with this one + + :returns: browse record for the current sale order + """ sale_order_obj = self.pool['sale.order'] sale_order_id = request.session.get('sale_order_id') sale_order = None + pricelist_id = request.session.get('website_sale_current_pl') + + if force_pricelist and self.pool['product.pricelist'].search_count(cr, uid, [('id', '=', force_pricelist)], context=context): + pricelist_id = force_pricelist + request.session['website_sale_current_pl'] = pricelist_id + update_pricelist = True + # create so if needed if not sale_order_id and (force_create or code): # TODO cache partner_id session @@ -148,14 +244,16 @@ class website(orm.Model): for w in self.browse(cr, uid, ids): values = { 'partner_id': partner.id, - 'pricelist_id': partner.property_product_pricelist.id, + 'pricelist_id': pricelist_id, 'team_id': w.salesteam_id.id, } sale_order_id = sale_order_obj.create(cr, SUPERUSER_ID, values, context=context) values = sale_order_obj.onchange_partner_id(cr, SUPERUSER_ID, [], partner.id, context=context)['value'] values.update({'user_id': salesperson_id or w.salesperson_id.id}) + sale_order_obj.write(cr, SUPERUSER_ID, [sale_order_id], values, context=context) request.session['sale_order_id'] = sale_order_id + if sale_order_id: # TODO cache partner_id session partner = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id @@ -166,14 +264,7 @@ class website(orm.Model): return None # check for change of pricelist with a coupon - if code and code != sale_order.pricelist_id.code: - pricelist_ids = self.pool['product.pricelist'].search(cr, SUPERUSER_ID, [('code', '=', code)], context=context) - if pricelist_ids: - pricelist_id = pricelist_ids[0] - request.session['sale_order_code_pricelist_id'] = pricelist_id - update_pricelist = True - - pricelist_id = request.session.get('sale_order_code_pricelist_id') or partner.property_product_pricelist.id + pricelist_id = pricelist_id or partner.property_product_pricelist.id # check for change of partner_id ie after signup if sale_order.partner_id.id != partner.id and request.website.partner_id.id != partner.id: @@ -183,6 +274,11 @@ class website(orm.Model): fiscal_position = sale_order.fiscal_position_id and sale_order.fiscal_position_id.id or False values = sale_order_obj.onchange_partner_id(cr, SUPERUSER_ID, [sale_order_id], partner.id, context=context)['value'] + if values.get('pricelist_id'): + if values['pricelist_id'] != pricelist_id: + values['pricelist_id'] = pricelist_id + update_pricelist = True + if values.get('fiscal_position_id'): order_lines = map(int, sale_order.order_line) values.update(sale_order_obj.onchange_fiscal_position( @@ -195,6 +291,19 @@ class website(orm.Model): if flag_pricelist or values.get('fiscal_position_id') != fiscal_position: update_pricelist = True + if (code and code != sale_order.pricelist_id.code) or \ + (code is not None and code == '' and request.session.get('sale_order_code_pricelist_id') and request.session.get('sale_order_code_pricelist_id') != ''): # empty code so reset + pricelist_ids = self.pool['product.pricelist'].search(cr, uid, [('code', '=', code)], limit=1, context=context) + if pricelist_ids: + pricelist_id = pricelist_ids[0] + request.session['sale_order_code_pricelist_id'] = pricelist_id + request.session['website_sale_current_pl'] = pricelist_id + update_pricelist = True + elif code == '' and request.session['website_sale_current_pl'] == request.session['sale_order_code_pricelist_id']: + request.session['website_sale_current_pl'] = partner.property_product_pricelist.id + request.session['sale_order_code_pricelist_id'] = False + update_pricelist = True + # update the pricelist if update_pricelist: values = {'pricelist_id': pricelist_id} @@ -204,7 +313,7 @@ class website(orm.Model): sale_order._cart_update(product_id=line.product_id.id, line_id=line.id, add_qty=0) # update browse record - if (code and code != sale_order.pricelist_id.code) or sale_order.partner_id.id != partner.id: + if (code and code != sale_order.pricelist_id.code) or sale_order.partner_id.id != partner.id or force_pricelist: sale_order = sale_order_obj.browse(cr, SUPERUSER_ID, sale_order.id, context=context) return sale_order @@ -225,4 +334,53 @@ class website(orm.Model): 'sale_order_id': False, 'sale_transaction_id': False, 'sale_order_code_pricelist_id': False, + 'website_sale_current_pl': False, }) + + +class website_pricelist(osv.Model): + _name = 'website_pricelist' + _description = 'Website Pricelist' + + def _get_display_name(self, cr, uid, ids, name, arg, context=None): + result = {} + for o in self.browse(cr, uid, ids, context=context): + result[o.id] = _("Website Pricelist for %s") % o.pricelist_id.name + return result + + _columns = { + 'name': fields.function(_get_display_name, string='Pricelist Name', type="char"), + 'website_id': fields.many2one('website', string="Website", required=True), + 'selectable': fields.boolean('Selectable', help="Allow the end user to choose this price list"), + 'pricelist_id': fields.many2one('product.pricelist', string='Pricelist'), + 'country_group_ids': fields.many2many('res.country.group', 'res_country_group_website_pricelist_rel', + 'website_pricelist_id', 'res_country_group_id', string='Country Groups'), + } + + # _get_pl is cached to avoid to recompute at each request the list of pricelists availables. + # So, we need to invalidate the cache when we change the config of website price list to force to recompute. + def clear_cache(self): + self.pool['website']._get_pl.clear_cache(self.pool['website']) + + def create(self, cr, uid, data, context=None): + res = super(website_pricelist, self).create(cr, uid, data, context=context) + self.clear_cache() + return res + + def write(self, cr, uid, ids, data, context=None): + res = super(website_pricelist, self).write(cr, uid, ids, data, context=context) + self.clear_cache() + return res + + def unlink(self, cr, uid, ids, context=None): + res = super(website_pricelist, self).unlink(cr, uid, ids, context=context) + self.clear_cache() + return res + + +class CountryGroup(osv.Model): + _inherit = 'res.country.group' + _columns = { + 'website_pricelist_ids': fields.many2many('website_pricelist', 'res_country_group_website_pricelist_rel', + 'res_country_group_id', 'website_pricelist_id', string='Website Price Lists'), + } diff --git a/addons/website_sale/security/ir.model.access.csv b/addons/website_sale/security/ir.model.access.csv index 2101397bf60..110de4a75b0 100644 --- a/addons/website_sale/security/ir.model.access.csv +++ b/addons/website_sale/security/ir.model.access.csv @@ -14,3 +14,5 @@ access_product_attribute_public,product.attribute public,product.model_product_a access_product_attribute_value_public,product.attribute value public,product.model_product_attribute_value,,1,0,0,0 access_product_attribute_price_public,product.attribute price public,product.model_product_attribute_price,,1,0,0,0 access_product_attribute_line_public,product.attribute line public,product.model_product_attribute_line,,1,0,0,0 +access_website_pricelist,website_pricelist,model_website_pricelist,,1,0,0,0 +access_website_pricelist_sale_manager,website_pricelist,model_website_pricelist,,base.group_sale_manager,1,1,1,1 \ No newline at end of file diff --git a/addons/website_sale/tests/__init__.py b/addons/website_sale/tests/__init__.py index f2f75d42d2c..044f1c71a65 100644 --- a/addons/website_sale/tests/__init__.py +++ b/addons/website_sale/tests/__init__.py @@ -1 +1,2 @@ import test_sale_process +import test_website_sale_pricelist diff --git a/addons/website_sale/tests/test_website_sale_pricelist.py b/addons/website_sale/tests/test_website_sale_pricelist.py new file mode 100644 index 00000000000..e6b485b777a --- /dev/null +++ b/addons/website_sale/tests/test_website_sale_pricelist.py @@ -0,0 +1,82 @@ +from openerp.tests.common import TransactionCase +from mock import patch + + +class TestWebsitePriceList(TransactionCase): + + # Mock nedded because request.session doesn't exist during test + def _get_pricelist_available(self, cr, uid, show_visible=False, context=None): + return self.get_pl(context.get('show'), context.get('current_pl'), context.get('country')) + + def setUp(self): + super(TestWebsitePriceList, self).setUp() + self.website = self.registry('website').browse(self.cr, self.uid, 1) + self.patcher = patch('openerp.addons.website_sale.models.sale_order.website.get_pricelist_available', wraps=self._get_pricelist_available) + self.mock_get_pricelist_available = self.patcher.start() + + def get_pl(self, show, current_pl, country): + pls = self.website._get_pl( + country, + show, + self.website.pricelist_id.id, + current_pl, + self.website.website_pricelist_ids + ) + return pls + + def test_get_pricelist_available_show(self): + show = True + current_pl = False + + country_list = { + False: 2, + 'BE': 2, + 'IT': 1, + 'US': 1, + 'AF': 2 + } + for country, result in country_list.items(): + pls = self.get_pl(show, current_pl, country) + self.assertEquals(len(pls), result) + + def test_get_pricelist_available_not_show(self): + show = False + current_pl = False + + country_list = { + False: 3, + 'BE': 3, + 'IT': 1, + 'US': 1, + 'AF': 3 + } + + for country, result in country_list.items(): + pls = self.get_pl(show, current_pl, country) + self.assertEquals(len(pls), result) + + def test_get_pricelist_available_promocode(self): + christmas_pl = self.registry('ir.model.data').xmlid_to_res_id(self.cr, self.uid, 'website_sale.list_christmas') + context = { + 'show': True, + 'current_pl': christmas_pl, + } + + country_list = { + False: True, + 'BE': True, + 'IT': False, + 'US': False, + } + + for country, result in country_list.items(): + context['country'] = country + available = self.website.with_context(context).is_pricelist_available(christmas_pl) + if result: + self.assertTrue(available) + else: + self.assertFalse(available) + + def tearDown(self): + self.patcher.stop() + super(TestWebsitePriceList, self).tearDown() diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index ea608d3b307..702aa97e43f 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -770,6 +770,9 @@

Have a coupon code? Fill in this field and apply.

+ +

This promo code is not available

+
@@ -1519,5 +1522,23 @@ + + diff --git a/addons/website_sale/views/views.xml b/addons/website_sale/views/views.xml index ebd8c6bb27d..924a1b02f12 100644 --- a/addons/website_sale/views/views.xml +++ b/addons/website_sale/views/views.xml @@ -122,7 +122,55 @@ - + + + Website Pricelist + ir.actions.act_window + website_pricelist + form + tree,form + + + + + + + website_pricelist.form + website_pricelist + + + + + + + + + + + + + + website_pricelist.tree + website_pricelist + + + + + + + + + + + website_sale.country_group.form + res.country.group + + + + + + +