[MERGE] forward port of branch 7.0 up to 5035c76
[odoo/odoo.git] / addons / product / product.py
index 255b844..2110f24 100644 (file)
 import math
 import re
 
-from _common import rounding
-from lxml import etree
+from _common import ceiling
 
-from openerp.osv.orm import setup_modifiers
 from openerp import SUPERUSER_ID
 from openerp import tools
-from openerp.osv import osv, fields
+from openerp.osv import osv, orm, fields, expression
 from openerp.tools.translate import _
 
 import openerp.addons.decimal_precision as dp
+from openerp.tools.float_utils import float_round
 
 def ean_checksum(eancode):
     """returns the checksum of an ean string of length 13, returns -1 if the string has the wrong length"""
-    if len(eancode) <> 13:
+    if len(eancode) != 13:
         return -1
     oddsum=0
     evensum=0
@@ -58,7 +57,7 @@ def check_ean(eancode):
     """returns True if eancode is a valid ean13 string, or null"""
     if not eancode:
         return True
-    if len(eancode) <> 13:
+    if len(eancode) != 13:
         return False
     try:
         int(eancode)
@@ -85,7 +84,7 @@ class product_uom_categ(osv.osv):
     _name = 'product.uom.categ'
     _description = 'Product uom categ'
     _columns = {
-        'name': fields.char('Name', size=64, required=True, translate=True),
+        'name': fields.char('Name', required=True, translate=True),
     }
 
 class product_uom(osv.osv):
@@ -123,20 +122,20 @@ class product_uom(osv.osv):
 
     def create(self, cr, uid, data, context=None):
         if 'factor_inv' in data:
-            if data['factor_inv'] <> 1:
+            if data['factor_inv'] != 1:
                 data['factor'] = self._compute_factor_inv(data['factor_inv'])
             del(data['factor_inv'])
         return super(product_uom, self).create(cr, uid, data, context)
 
     _order = "name"
     _columns = {
-        'name': fields.char('Unit of Measure', size=64, required=True, translate=True),
+        'name': fields.char('Unit of Measure', required=True, translate=True),
         'category_id': fields.many2one('product.uom.categ', 'Category', required=True, ondelete='cascade',
             help="Conversion between Units of Measure can only occur if they belong to the same category. The conversion will be made based on the ratios."),
-        'factor': fields.float('Ratio', required=True,digits=(12, 12),
+        'factor': fields.float('Ratio', required=True, digits=0, # force NUMERIC with unlimited precision
             help='How much bigger or smaller this unit is compared to the reference Unit of Measure for this category:\n'\
                     '1 * (reference unit) = ratio * (this unit)'),
-        'factor_inv': fields.function(_factor_inv, digits=(12,12),
+        'factor_inv': fields.function(_factor_inv, digits=0, # force NUMERIC with unlimited precision
             fnct_inv=_factor_inv_write,
             string='Bigger Ratio',
             help='How many times this Unit of Measure is bigger than the reference Unit of Measure in this category:\n'\
@@ -173,14 +172,17 @@ class product_uom(osv.osv):
     def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context=None):
         if context is None:
             context = {}
-        if from_unit.category_id.id <> to_unit.category_id.id:
+        if from_unit.category_id.id != to_unit.category_id.id:
             if context.get('raise-exception', True):
                 raise osv.except_osv(_('Error!'), _('Conversion from Product UoM %s to Default UoM %s is not possible as they both belong to different Category!.') % (from_unit.name,to_unit.name,))
             else:
                 return qty
-        amount = qty / from_unit.factor
+        # First round to the precision of the original unit, so that
+        # float representation errors do not bias the following ceil()
+        # e.g. with 1 / (1/12) we could get 12.0000048, ceiling to 13! 
+        amount = float_round(qty/from_unit.factor, precision_rounding=from_unit.rounding)
         if to_unit:
-            amount = rounding(amount * to_unit.factor, to_unit.rounding)
+            amount = ceiling(amount * to_unit.factor, to_unit.rounding)
         return amount
 
     def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
@@ -191,7 +193,7 @@ class product_uom(osv.osv):
             from_unit, to_unit = uoms[0], uoms[-1]
         else:
             from_unit, to_unit = uoms[-1], uoms[0]
-        if from_unit.category_id.id <> to_unit.category_id.id:
+        if from_unit.category_id.id != to_unit.category_id.id:
             return price
         amount = price * from_unit.factor
         if to_uom_id:
@@ -216,7 +218,7 @@ class product_ul(osv.osv):
     _name = "product.ul"
     _description = "Shipping Unit"
     _columns = {
-        'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
+        'name' : fields.char('Name', select=True, required=True, translate=True),
         'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
     }
 
@@ -247,7 +249,7 @@ class product_category(osv.osv):
     _name = "product.category"
     _description = "Product Category"
     _columns = {
-        'name': fields.char('Name', size=64, required=True, translate=True, select=True),
+        'name': fields.char('Name', required=True, translate=True, select=True),
         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
         'parent_id': fields.many2one('product.category','Parent Category', select=True, ondelete='cascade'),
         'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
@@ -267,18 +269,8 @@ class product_category(osv.osv):
     _parent_order = 'sequence, name'
     _order = 'parent_left'
 
-    def _check_recursion(self, cr, uid, ids, context=None):
-        level = 100
-        while len(ids):
-            cr.execute('select distinct parent_id from product_category where id IN %s',(tuple(ids),))
-            ids = filter(None, map(lambda x:x[0], cr.fetchall()))
-            if not level:
-                return False
-            level -= 1
-        return True
-
     _constraints = [
-        (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
+        (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
     ]
     def child_get(self, cr, uid, ids):
         return [ids]
@@ -288,18 +280,9 @@ class product_public_category(osv.osv):
     _name = "product.public.category"
     _description = "Public Category"
     _order = "sequence, name"
-    def _check_recursion(self, cr, uid, ids, context=None):
-        level = 100
-        while len(ids):
-            cr.execute('select distinct parent_id from product_public_category where id IN %s',(tuple(ids),))
-            ids = filter(None, map(lambda x:x[0], cr.fetchall()))
-            if not level:
-                return False
-            level -= 1
-        return True
 
     _constraints = [
-        (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
+        (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
     ]
 
     def name_get(self, cr, uid, ids, context=None):
@@ -328,7 +311,7 @@ class product_public_category(osv.osv):
         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
 
     _columns = {
-        'name': fields.char('Name', size=64, required=True, translate=True),
+        'name': fields.char('Name', required=True, translate=True),
         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
         'parent_id': fields.many2one('product.public.category','Parent Category', select=True),
         'child_id': fields.one2many('product.public.category', 'parent_id', string='Children Categories'),
@@ -377,7 +360,7 @@ class product_template(osv.osv):
         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
 
     _columns = {
-        'name': fields.char('Name', size=128, required=True, translate=True, select=True),
+        'name': fields.char('Name', required=True, translate=True, select=True),
         'product_manager': fields.many2one('res.users','Product Manager'),
         'description': fields.text('Description',translate=True,
             help="A precise description of the Product, used only for internal information purposes."),
@@ -473,11 +456,9 @@ class product_template(osv.osv):
     def copy(self, cr, uid, id, default=None, context=None):
         if default is None:
             default = {}
-        #TOFIX: it should be pass default={'name': _("%s (copy)") % (template['name'])}.
-        template = self.read(cr, uid, id, ['name'], context=context)
-        res = super(product_template, self).copy(cr, uid, id, default=default, context=context)
-        self.write(cr, uid, res, {'name': _("%s (copy)") % (template['name'])}, context=context)
-        return res
+        template = self.browse(cr, uid, id, context=context)
+        default['name'] = _("%s (copy)") % (template['name'])
+        return super(product_template, self).copy(cr, uid, id, default=default, context=context)
 
     _defaults = {
         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
@@ -496,7 +477,7 @@ class product_template(osv.osv):
 
     def _check_uom(self, cursor, user, ids, context=None):
         for product in self.browse(cursor, user, ids, context=context):
-            if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
+            if product.uom_id.category_id.id != product.uom_po_id.category_id.id:
                 return False
         return True
 
@@ -521,6 +502,12 @@ class product_template(osv.osv):
 
 
 class product_product(osv.osv):
+    _name = "product.product"
+    _description = "Product"
+    _inherits = {'product.template': 'product_tmpl_id'}
+    _inherit = ['mail.thread']
+    _order = 'default_code,name_template'
+
     def view_header_get(self, cr, uid, view_id, view_type, context=None):
         if context is None:
             context = {}
@@ -544,12 +531,13 @@ class product_product(osv.osv):
                     cr, uid, pricelist, operator='=', context=context, limit=1)
                 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
 
-            products = self.browse(cr, uid, ids, context=context)
-            qtys = map(lambda x: (x, quantity, partner), products)
-            pl = plobj.browse(cr, uid, pricelist, context=context)
-            price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
-            for id in ids:
-                res[id] = price.get(id, 0.0)
+            if isinstance(pricelist, (int, long)):
+                products = self.browse(cr, uid, ids, context=context)
+                qtys = map(lambda x: (x, quantity, partner), products)
+                pl = plobj.browse(cr, uid, pricelist, context=context)
+                price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
+                for id in ids:
+                    res[id] = price.get(id, 0.0)
         for id in ids:
             res.setdefault(id, 0.0)
         return res
@@ -576,7 +564,7 @@ class product_product(osv.osv):
                         uom.id, product.list_price, context['uom'])
             else:
                 res[product.id] = product.list_price
-            res[product.id] =  (res[product.id] + ((res[product.id] * (product.price_margin)) / 100)) + product.price_extra
+            res[product.id] =  (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
         return res
 
     def _save_product_lst_price(self, cr, uid, product_id, field_name, field_value, arg, context=None):
@@ -617,6 +605,12 @@ class product_product(osv.osv):
                     (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
         return res
 
+    def _is_only_child(self, cr, uid, ids, name, arg, context=None):
+        res = dict.fromkeys(ids, True)
+        for product in self.browse(cr, uid, ids, context=context):
+            if product.product_tmpl_id and len(product.product_tmpl_id.product_variant_ids) > 1:
+                res[product.id] = False
+        return res
 
     def _get_main_product_supplier(self, cr, uid, product, context=None):
         """Determines the main (best) product supplier for ``product``,
@@ -644,7 +638,6 @@ class product_product(osv.osv):
             }
         return result
 
-
     def _get_name_template_ids(self, cr, uid, ids, context=None):
         result = set()
         template_ids = self.pool.get('product.product').search(cr, uid, [('product_tmpl_id', 'in', ids)])
@@ -652,19 +645,6 @@ class product_product(osv.osv):
             result.add(el)
         return list(result)
 
-    _defaults = {
-        'active': lambda *a: 1,
-        'price_extra': lambda *a: 0.0,
-        'price_margin': lambda *a: 0.0,
-        'color': 0,
-    }
-
-    _name = "product.product"
-    _description = "Product"
-    _table = "product_product"
-    _inherits = {'product.template': 'product_tmpl_id'}
-    _inherit = ['mail.thread']
-    _order = 'default_code,name_template'
     _columns = {
         'qty_available': fields.function(_product_qty_available, type='float', string='Quantity On Hand'),
         'virtual_available': fields.function(_product_virtual_available, type='float', string='Quantity Available'),
@@ -674,20 +654,21 @@ class product_product(osv.osv):
         'lst_price' : fields.function(_product_lst_price, fnct_inv=_save_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Product Price')),
         'code': fields.function(_product_code, type='char', string='Internal Reference'),
         'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
-        'default_code' : fields.char('Internal Reference', size=64, select=True),
+        'default_code' : fields.char('Internal Reference', select=True),
         'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
-        'variants': fields.char('Variants', size=64, translate=True),
+        'variants': fields.char('Variants', translate=True),
         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade", select=True),
+        'is_only_child': fields.function(
+            _is_only_child, type='boolean', string='Sole child of the parent template'),
         'ean13': fields.char('EAN13 Barcode', size=13, help="International Article Number used for product identification."),
         'packaging' : fields.one2many('product.packaging', 'product_id', 'Logistical Units', help="Gives the different ways to package the same product. This has no impact on the picking order and is mainly used if you use the EDI module."),
         'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Product Price'), help="Price Extra: Extra price for the variant on sale price. eg. 200 price extra, 1000 + 200 = 1200."),
-        'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Product Price'), help="Price Margin: Margin in percentage on sale price for the variant. eg. 10% price margin, 1000 + 10% = 1100."),
+        'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Product Price'), help="Price Margin: Margin in percentage amount on sale price for the variant. eg. 10 price margin, 1000 * 1.1 = 1100."),
         'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
-        'name_template': fields.related('product_tmpl_id', 'name', string="Template Name", type='char', size=128, store={
+        'name_template': fields.related('product_tmpl_id', 'name', string="Template Name", type='char', store={
             'product.template': (_get_name_template_ids, ['name'], 10),
             'product.product': (lambda self, cr, uid, ids, c=None: ids, [], 10),
-
-            }, select=True),
+        }, select=True),
         'color': fields.integer('Color Index'),
         'seller_info_id': fields.function(_calc_seller, type='many2one', relation="product.supplierinfo", string="Supplier Info", multi="seller_info"),
         'seller_delay': fields.function(_calc_seller, type='integer', string='Supplier Lead Time', multi="seller_info", help="This is the average delay in days between the purchase order confirmation and the reception of goods for this product and for the default supplier. It is used by the scheduler to order requests based on reordering delays."),
@@ -695,27 +676,31 @@ class product_product(osv.osv):
         'seller_id': fields.function(_calc_seller, type='many2one', relation="res.partner", string='Main Supplier', help="Main Supplier who has highest priority in Supplier List.", multi="seller_info"),
     }
 
+    _defaults = {
+        'active': lambda *a: 1,
+        'price_extra': lambda *a: 0.0,
+        'price_margin': lambda *a: 1.0,
+        'color': 0,
+        'is_only_child': True,
+    }
 
-    def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
-        #override of fields_view_get in order to replace the name field to product template
-        if context is None:
-            context = {}
-        res = super(product_product, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
-        #check the current user in group_product_variant
-        if view_type == 'form':
-            doc = etree.XML(res['arch'])
-            if self.pool['res.users'].has_group(cr, uid, 'product.group_product_variant'):
-                for node in doc.xpath("//field[@name='name']"):
-                    node.set('invisible', '1')
-                    node.set('required', '0')
-                    setup_modifiers(node, res['fields']['name'])
-                for node in doc.xpath("//label[@name='label_name']"):
-                    node.set('string','Product Template')
-            else:
-                for node in doc.xpath("//field[@name='product_tmpl_id']"):
-                    node.set('required', '0')
-                    setup_modifiers(node, res['fields']['name'])
-            res['arch'] = etree.tostring(doc)
+    def unlink(self, cr, uid, ids, context=None):
+        unlink_ids = []
+        unlink_product_tmpl_ids = []
+        for product in self.browse(cr, uid, ids, context=context):
+            # Check if product still exists, in case it has been unlinked by unlinking its template
+            if not product.exists():
+                continue
+            tmpl_id = product.product_tmpl_id.id
+            # Check if the product is last product of this template
+            other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
+            if not other_product_ids:
+                unlink_product_tmpl_ids.append(tmpl_id)
+            unlink_ids.append(product.id)
+        res = super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
+        # delete templates after calling super, as deleting template could lead to deleting
+        # products due to ondelete='cascade'
+        self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
         return res
 
     def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
@@ -727,24 +712,11 @@ class product_product(osv.osv):
                 return {'value': {'uom_po_id': uom_id}}
         return False
 
-    def onchange_product_tmpl_id(self, cr, uid, ids, template_id, lst_price, price_margin, price_extra, context=None):
-        res = {}
-        if template_id:
-            template = self.pool.get('product.template').browse(cr, uid, template_id, context=context)
-            if not lst_price:
-                lst_price = (template.list_price + ((template.list_price * (price_margin)) / 100)) + price_extra
-            res['value'] = {
-                'name': template.name,
-                'lst_price': lst_price,
-            }
-        return res
-
     def _check_ean_key(self, cr, uid, ids, context=None):
         for product in self.read(cr, uid, ids, ['ean13'], context=context):
             res = check_ean(product['ean13'])
         return res
 
-
     _constraints = [(_check_ean_key, 'You provided an invalid "EAN13 Barcode" reference. You may use the "Internal Reference" field instead.', ['ean13'])]
 
     def on_order(self, cr, uid, ids, orderline, quantity):
@@ -767,6 +739,10 @@ class product_product(osv.osv):
             return (d['id'], name)
 
         partner_id = context.get('partner_id', False)
+        if partner_id:
+            partner_ids = [partner_id, self.pool['res.partner'].browse(cr, user, partner_id, context=context).commercial_partner_id.id]
+        else:
+            partner_ids = []
 
         # all user don't have access to seller and partner
         # check access and use superuser
@@ -775,7 +751,7 @@ class product_product(osv.osv):
 
         result = []
         for product in self.browse(cr, SUPERUSER_ID, ids, context=context):
-            sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
+            sellers = partner_ids and filter(lambda x: x.name.id in partner_ids, product.seller_ids) or []
             if sellers:
                 for s in sellers:
                     mydict = {
@@ -799,21 +775,26 @@ class product_product(osv.osv):
         if not args:
             args = []
         if name:
-            ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
-            if not ids:
-                ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
-            if not ids:
+            positive_operators = ['=', 'ilike', '=ilike', 'like', '=like']
+            ids = []
+            if operator in positive_operators:
+                ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
+                if not ids:
+                    ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
+            if not ids and operator not in expression.NEGATIVE_TERM_OPERATORS:
                 # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
                 # on a database with thousands of matching products, due to the huge merge+unique needed for the
                 # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
                 # Performing a quick memory merge of ids in Python will give much better performance
-                ids = set()
-                ids.update(self.search(cr, user, args + ['|',('default_code',operator,name),('variants',operator,name)], limit=limit, context=context))
+                ids = set(self.search(cr, user, args + [('default_code', operator, name)], limit=limit, context=context))
                 if not limit or len(ids) < limit:
                     # we may underrun the limit because of dupes in the results, that's fine
-                    ids.update(self.search(cr, user, args + [('name',operator,name)], limit=(limit and (limit-len(ids)) or False) , context=context))
+                    limit2 = (limit - len(ids)) if limit else False
+                    ids.update(self.search(cr, user, args + [('name', operator, name)], limit=limit2, context=context))
                 ids = list(ids)
-            if not ids:
+            elif not ids and operator in expression.NEGATIVE_TERM_OPERATORS:
+                ids = self.search(cr, user, args + ['&', ('default_code', operator, name), ('name', operator, name)], limit=limit, context=context)
+            if not ids and operator in positive_operators:
                 ptrn = re.compile('(\[(.*?)\])')
                 res = ptrn.search(name)
                 if res:
@@ -840,11 +821,20 @@ class product_product(osv.osv):
             price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
 
         res = {}
+        company_id = self.pool['res.users'].read(cr, uid, uid, ['company_id'], context=context)['company_id'][0]
+        # standard_price field can only be seen by users in base.group_user
+        # Thus, in order to compute the sale price from the cost price for users not in this group
+        # We fetch the standard price as the superuser
+        for product in products:
+            if ptype != 'standard_price':
+                res[product.id] = product[ptype] or 0.0
+            else: 
+                res[product.id] = self.read(cr, SUPERUSER_ID, product.id, [ptype], context=dict(context, force_company=company_id))[ptype] or 0.0
+
         product_uom_obj = self.pool.get('product.uom')
         for product in products:
-            res[product.id] = product[ptype] or 0.0
             if ptype == 'list_price':
-                res[product.id] = (res[product.id] + ((res[product.id] * (product.price_margin)) / 100)) + \
+                res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
                         product.price_extra
             if 'uom' in context:
                 uom = product.uom_id or product.uos_id
@@ -863,44 +853,59 @@ class product_product(osv.osv):
         if context is None:
             context={}
 
-        if not default:
-            default = {}
+        product = self.browse(cr, uid, id, context)
+        if context.get('variant'):
+            # if we copy a variant or create one, we keep the same template
+            default['product_tmpl_id'] = product.product_tmpl_id.id
+        elif 'name' not in default:
+            default['name'] = _("%s (copy)") % (product.name,)
 
-        # Craft our own `<name> (copy)` in en_US (self.copy_translation()
-        # will do the other languages).
-        context_wo_lang = context.copy()
-        context_wo_lang.pop('lang', None)
-        product = self.read(cr, uid, id, ['name', 'list_price', 'standard_price', 'categ_id', 'variants', 'product_tmpl_id'], context=context_wo_lang)
-        default = default.copy()
-        if product['variants']:
-            default.update(variants=_("%s (copy)") % (product['variants']), product_tmpl_id=product['product_tmpl_id'][0])
-        else:
-            default.update(name=_("%s (copy)") % (product['name']), list_price=product['list_price'], standard_price=product['standard_price'], categ_id=product['categ_id'][0], product_tmpl_id=None)
-
-        if context.get('variant',False):
-            fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
-                    'price_margin', 'price_extra']
-            data = self.read(cr, uid, id, fields=fields, context=context)
-            for f in fields:
-                if f in default:
-                    data[f] = default[f]
-            data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
-                    and data['product_tmpl_id'][0]
-            del data['id']
-            return self.create(cr, uid, data)
-        else:
-            return super(product_product, self).copy(cr, uid, id, default=default,
-                    context=context)
+        return super(product_product, self).copy(cr, uid, id, default=default, context=context)
 
     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
         if context is None:
             context = {}
         if context.get('search_default_categ_id'):
             args.append((('categ_id', 'child_of', context['search_default_categ_id'])))
-        if context.get('search_variants'):
-            args.append(('variants', '!=', ''))
         return super(product_product, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
 
+    def open_product_template(self, cr, uid, ids, context=None):
+        """ Utility method used to add an "Open Template" button in product views """
+        product = self.browse(cr, uid, ids[0], context=context)
+        return {'type': 'ir.actions.act_window',
+                'res_model': 'product.template',
+                'view_mode': 'form',
+                'res_id': product.product_tmpl_id.id,
+                'target': 'new'}
+
+    def _compute_uos_qty(self, cr, uid, ids, uom, qty, uos, context=None):
+        '''
+        Computes product's invoicing quantity in UoS from quantity in UoM.
+        Takes into account the
+        :param uom: Source unit
+        :param qty: Source quantity
+        :param uos: Target UoS unit.
+        '''
+        if not uom or not qty or not uos:
+            return qty
+        uom_obj = self.pool['product.uom']
+        product_id = ids[0] if isinstance(ids, (list, tuple)) else ids
+        product = self.browse(cr, uid, product_id, context=context)
+        if isinstance(uos, (int, long)):
+            uos = uom_obj.browse(cr, uid, uos, context=context)
+        if isinstance(uom, (int, long)):
+            uom = uom_obj.browse(cr, uid, uom, context=context)
+        if product.uos_id:  # Product has UoS defined
+            # We cannot convert directly between units even if the units are of the same category
+            # as we need to apply the conversion coefficient which is valid only between quantities
+            # in product's default UoM/UoS
+            qty_default_uom = uom_obj._compute_qty_obj(cr, uid, uom, qty, product.uom_id)  # qty in product's default UoM
+            qty_default_uos = qty_default_uom * product.uos_coeff
+            return uom_obj._compute_qty_obj(cr, uid, product.uos_id, qty_default_uos, uos)
+        else:
+            return uom_obj._compute_qty_obj(cr, uid, uom, qty, uos)
+
+
 
 class product_packaging(osv.osv):
     _name = "product.packaging"
@@ -909,7 +914,7 @@ class product_packaging(osv.osv):
     _order = 'sequence'
     _columns = {
         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
-        'name' : fields.text('Description', size=64),
+        'name' : fields.text('Description'),
         'qty' : fields.float('Quantity by Package',
             help="The total number of products you can put by pallet or box."),
         'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
@@ -917,10 +922,8 @@ class product_packaging(osv.osv):
         'rows' : fields.integer('Number of Layers', required=True,
             help='The number of layers on a pallet or box'),
         'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
-        'ean' : fields.char('EAN', size=14,
-            help="The EAN code of the package unit."),
-        'code' : fields.char('Code', size=14,
-            help="The code of the transport unit."),
+        'ean' : fields.char('EAN', size=14, help="The EAN code of the package unit."),
+        'code' : fields.char('Code', help="The code of the transport unit."),
         'weight': fields.float('Total Package Weight',
             help='The weight of a full package, pallet or box.'),
         'weight_ul': fields.float('Empty Package Weight'),
@@ -982,13 +985,13 @@ class product_supplierinfo(osv.osv):
 
     _columns = {
         'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
-        'product_name': fields.char('Supplier Product Name', size=128, help="This supplier's product name will be used when printing a request for quotation. Keep empty to use the internal one."),
-        'product_code': fields.char('Supplier Product Code', size=64, help="This supplier's product code will be used when printing a request for quotation. Keep empty to use the internal one."),
+        'product_name': fields.char('Supplier Product Name', help="This supplier's product name will be used when printing a request for quotation. Keep empty to use the internal one."),
+        'product_code': fields.char('Supplier Product Code', help="This supplier's product code will be used when printing a request for quotation. Keep empty to use the internal one."),
         'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
         'product_uom': fields.related('product_tmpl_id', 'uom_po_id', type='many2one', relation='product.uom', string="Supplier Unit of Measure", readonly="1", help="This comes from the product form."),
         'min_qty': fields.float('Minimal Quantity', required=True, help="The minimal quantity to purchase to this supplier, expressed in the supplier Product Unit of Measure if not empty, in the default unit of measure of the product otherwise."),
         'qty': fields.function(_calc_qty, store=True, type='float', string='Quantity', multi="qty", help="This is a quantity which is converted into Default Unit of Measure."),
-        'product_tmpl_id' : fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade', select=True),
+        'product_tmpl_id' : fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade', select=True, oldname='product_id'),
         'delay' : fields.integer('Delivery Lead Time', required=True, help="Lead time in days between the confirmation of the purchase order and the reception of the products in your warehouse. Used by the scheduler for automatic computation of the purchase order planning."),
         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
         'company_id':fields.many2one('res.company','Company',select=1),
@@ -1044,7 +1047,7 @@ class product_supplierinfo(osv.osv):
 class pricelist_partnerinfo(osv.osv):
     _name = 'pricelist.partnerinfo'
     _columns = {
-        'name': fields.char('Description', size=64),
+        'name': fields.char('Description'),
         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
         'min_quantity': fields.float('Quantity', required=True, help="The minimal quantity to trigger this rule, expressed in the supplier Unit of Measure if any or in the default Unit of Measure of the product otherrwise."),
         'price': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Product Price'), help="This price will be considered as a price for the supplier Unit of Measure if any or the default Unit of Measure of the product otherwise"),