[IMP] Add a method half-up for uom conversion in case we want to convert from default...
[odoo/odoo.git] / addons / product / product.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import math
23 import re
24 import time
25 from _common import ceiling
26
27 from openerp import SUPERUSER_ID
28 from openerp import tools
29 from openerp.osv import osv, fields, expression
30 from openerp.tools.translate import _
31 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
32 import psycopg2
33
34 import openerp.addons.decimal_precision as dp
35 from openerp.tools.float_utils import float_round, float_compare
36
37 def ean_checksum(eancode):
38     """returns the checksum of an ean string of length 13, returns -1 if the string has the wrong length"""
39     if len(eancode) != 13:
40         return -1
41     oddsum=0
42     evensum=0
43     total=0
44     eanvalue=eancode
45     reversevalue = eanvalue[::-1]
46     finalean=reversevalue[1:]
47
48     for i in range(len(finalean)):
49         if i % 2 == 0:
50             oddsum += int(finalean[i])
51         else:
52             evensum += int(finalean[i])
53     total=(oddsum * 3) + evensum
54
55     check = int(10 - math.ceil(total % 10.0)) %10
56     return check
57
58 def check_ean(eancode):
59     """returns True if eancode is a valid ean13 string, or null"""
60     if not eancode:
61         return True
62     if len(eancode) != 13:
63         return False
64     try:
65         int(eancode)
66     except:
67         return False
68     return ean_checksum(eancode) == int(eancode[-1])
69
70 def sanitize_ean13(ean13):
71     """Creates and returns a valid ean13 from an invalid one"""
72     if not ean13:
73         return "0000000000000"
74     ean13 = re.sub("[A-Za-z]","0",ean13);
75     ean13 = re.sub("[^0-9]","",ean13);
76     ean13 = ean13[:13]
77     if len(ean13) < 13:
78         ean13 = ean13 + '0' * (13-len(ean13))
79     return ean13[:-1] + str(ean_checksum(ean13))
80
81 #----------------------------------------------------------
82 # UOM
83 #----------------------------------------------------------
84
85 class product_uom_categ(osv.osv):
86     _name = 'product.uom.categ'
87     _description = 'Product uom categ'
88     _columns = {
89         'name': fields.char('Name', required=True, translate=True),
90     }
91
92 class product_uom(osv.osv):
93     _name = 'product.uom'
94     _description = 'Product Unit of Measure'
95
96     def _compute_factor_inv(self, factor):
97         return factor and (1.0 / factor) or 0.0
98
99     def _factor_inv(self, cursor, user, ids, name, arg, context=None):
100         res = {}
101         for uom in self.browse(cursor, user, ids, context=context):
102             res[uom.id] = self._compute_factor_inv(uom.factor)
103         return res
104
105     def _factor_inv_write(self, cursor, user, id, name, value, arg, context=None):
106         return self.write(cursor, user, id, {'factor': self._compute_factor_inv(value)}, context=context)
107
108     def name_create(self, cr, uid, name, context=None):
109         """ The UoM category and factor are required, so we'll have to add temporary values
110             for imported UoMs """
111         uom_categ = self.pool.get('product.uom.categ')
112         # look for the category based on the english name, i.e. no context on purpose!
113         # TODO: should find a way to have it translated but not created until actually used
114         categ_misc = 'Unsorted/Imported Units'
115         categ_id = uom_categ.search(cr, uid, [('name', '=', categ_misc)])
116         if categ_id:
117             categ_id = categ_id[0]
118         else:
119             categ_id, _ = uom_categ.name_create(cr, uid, categ_misc)
120         uom_id = self.create(cr, uid, {self._rec_name: name,
121                                        'category_id': categ_id,
122                                        'factor': 1})
123         return self.name_get(cr, uid, [uom_id], context=context)[0]
124
125     def create(self, cr, uid, data, context=None):
126         if 'factor_inv' in data:
127             if data['factor_inv'] != 1:
128                 data['factor'] = self._compute_factor_inv(data['factor_inv'])
129             del(data['factor_inv'])
130         return super(product_uom, self).create(cr, uid, data, context)
131
132     _order = "name"
133     _columns = {
134         'name': fields.char('Unit of Measure', required=True, translate=True),
135         'category_id': fields.many2one('product.uom.categ', 'Product Category', required=True, ondelete='cascade',
136             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."),
137         'factor': fields.float('Ratio', required=True, digits=0, # force NUMERIC with unlimited precision
138             help='How much bigger or smaller this unit is compared to the reference Unit of Measure for this category:\n'\
139                     '1 * (reference unit) = ratio * (this unit)'),
140         'factor_inv': fields.function(_factor_inv, digits=0, # force NUMERIC with unlimited precision
141             fnct_inv=_factor_inv_write,
142             string='Bigger Ratio',
143             help='How many times this Unit of Measure is bigger than the reference Unit of Measure in this category:\n'\
144                     '1 * (this unit) = ratio * (reference unit)', required=True),
145         'rounding': fields.float('Rounding Precision', digits=0, required=True,
146             help="The computed quantity will be a multiple of this value. "\
147                  "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece."),
148         'active': fields.boolean('Active', help="By unchecking the active field you can disable a unit of measure without deleting it."),
149         'uom_type': fields.selection([('bigger','Bigger than the reference Unit of Measure'),
150                                       ('reference','Reference Unit of Measure for this category'),
151                                       ('smaller','Smaller than the reference Unit of Measure')],'Type', required=1),
152     }
153
154     _defaults = {
155         'active': 1,
156         'rounding': 0.01,
157         'factor': 1,
158         'uom_type': 'reference',
159         'factor': 1.0,
160     }
161
162     _sql_constraints = [
163         ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!')
164     ]
165
166     def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False, round=True, rounding_method='UP'):
167         if not from_uom_id or not qty or not to_uom_id:
168             return qty
169         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
170         if uoms[0].id == from_uom_id:
171             from_unit, to_unit = uoms[0], uoms[-1]
172         else:
173             from_unit, to_unit = uoms[-1], uoms[0]
174         return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit, round=round, rounding_method=rounding_method)
175
176     def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, round=True, rounding_method='UP', context=None):
177         if context is None:
178             context = {}
179         if from_unit.category_id.id != to_unit.category_id.id:
180             if context.get('raise-exception', True):
181                 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,))
182             else:
183                 return qty
184         amount = qty/from_unit.factor
185         if to_unit:
186             amount = amount * to_unit.factor
187             if round:
188                 amount = float_round(amount, precision_rounding=to_unit.rounding, rounding_method=rounding_method)
189         return amount
190
191     def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
192         if not from_uom_id or not price or not to_uom_id:
193             return price
194         from_unit, to_unit = self.browse(cr, uid, [from_uom_id, to_uom_id])
195         if from_unit.category_id.id != to_unit.category_id.id:
196             return price
197         amount = price * from_unit.factor
198         if to_uom_id:
199             amount = amount / to_unit.factor
200         return amount
201
202     def onchange_type(self, cursor, user, ids, value):
203         if value == 'reference':
204             return {'value': {'factor': 1, 'factor_inv': 1}}
205         return {}
206
207     def write(self, cr, uid, ids, vals, context=None):
208         if isinstance(ids, (int, long)):
209             ids = [ids]
210         if 'category_id' in vals:
211             for uom in self.browse(cr, uid, ids, context=context):
212                 if uom.category_id.id != vals['category_id']:
213                     raise osv.except_osv(_('Warning!'),_("Cannot change the category of existing Unit of Measure '%s'.") % (uom.name,))
214         return super(product_uom, self).write(cr, uid, ids, vals, context=context)
215
216
217
218 class product_ul(osv.osv):
219     _name = "product.ul"
220     _description = "Logistic Unit"
221     _columns = {
222         'name' : fields.char('Name', select=True, required=True, translate=True),
223         'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
224         'height': fields.float('Height', help='The height of the package'),
225         'width': fields.float('Width', help='The width of the package'),
226         'length': fields.float('Length', help='The length of the package'),
227         'weight': fields.float('Empty Package Weight'),
228     }
229
230
231 #----------------------------------------------------------
232 # Categories
233 #----------------------------------------------------------
234 class product_category(osv.osv):
235
236     def name_get(self, cr, uid, ids, context=None):
237         if isinstance(ids, (list, tuple)) and not len(ids):
238             return []
239         if isinstance(ids, (long, int)):
240             ids = [ids]
241         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
242         res = []
243         for record in reads:
244             name = record['name']
245             if record['parent_id']:
246                 name = record['parent_id'][1]+' / '+name
247             res.append((record['id'], name))
248         return res
249
250     def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
251         if not args:
252             args = []
253         if not context:
254             context = {}
255         if name:
256             # Be sure name_search is symetric to name_get
257             name = name.split(' / ')[-1]
258             ids = self.search(cr, uid, [('name', operator, name)] + args, limit=limit, context=context)
259         else:
260             ids = self.search(cr, uid, args, limit=limit, context=context)
261         return self.name_get(cr, uid, ids, context)
262
263     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
264         res = self.name_get(cr, uid, ids, context=context)
265         return dict(res)
266
267     _name = "product.category"
268     _description = "Product Category"
269     _columns = {
270         'name': fields.char('Name', required=True, translate=True, select=True),
271         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
272         'parent_id': fields.many2one('product.category','Parent Category', select=True, ondelete='cascade'),
273         'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
274         'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of product categories."),
275         'type': fields.selection([('view','View'), ('normal','Normal')], 'Category Type', help="A category of the view type is a virtual category that can be used as the parent of another category to create a hierarchical structure."),
276         'parent_left': fields.integer('Left Parent', select=1),
277         'parent_right': fields.integer('Right Parent', select=1),
278     }
279
280
281     _defaults = {
282         'type' : 'normal',
283     }
284
285     _parent_name = "parent_id"
286     _parent_store = True
287     _parent_order = 'sequence, name'
288     _order = 'parent_left'
289
290     _constraints = [
291         (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
292     ]
293
294
295 class produce_price_history(osv.osv):
296     """
297     Keep track of the ``product.template`` standard prices as they are changed.
298     """
299
300     _name = 'product.price.history'
301     _rec_name = 'datetime'
302     _order = 'datetime desc'
303
304     _columns = {
305         'company_id': fields.many2one('res.company', required=True),
306         'product_template_id': fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade'),
307         'datetime': fields.datetime('Historization Time'),
308         'cost': fields.float('Historized Cost'),
309     }
310
311     def _get_default_company(self, cr, uid, context=None):
312         if 'force_company' in context:
313             return context['force_company']
314         else:
315             company = self.pool['res.users'].browse(cr, uid, uid,
316                 context=context).company_id
317             return company.id if company else False
318
319     _defaults = {
320         'datetime': fields.datetime.now,
321         'company_id': _get_default_company,
322     }
323
324
325 #----------------------------------------------------------
326 # Product Attributes
327 #----------------------------------------------------------
328 class product_attribute(osv.osv):
329     _name = "product.attribute"
330     _description = "Product Attribute"
331     _columns = {
332         'name': fields.char('Name', translate=True, required=True),
333         'value_ids': fields.one2many('product.attribute.value', 'attribute_id', 'Values', copy=True),
334     }
335
336 class product_attribute_value(osv.osv):
337     _name = "product.attribute.value"
338     _order = 'sequence'
339     def _get_price_extra(self, cr, uid, ids, name, args, context=None):
340         result = dict.fromkeys(ids, 0)
341         if not context.get('active_id'):
342             return result
343
344         for obj in self.browse(cr, uid, ids, context=context):
345             for price_id in obj.price_ids:
346                 if price_id.product_tmpl_id.id == context.get('active_id'):
347                     result[obj.id] = price_id.price_extra
348                     break
349         return result
350
351     def _set_price_extra(self, cr, uid, id, name, value, args, context=None):
352         if context is None:
353             context = {}
354         if 'active_id' not in context:
355             return None
356         p_obj = self.pool['product.attribute.price']
357         p_ids = p_obj.search(cr, uid, [('value_id', '=', id), ('product_tmpl_id', '=', context['active_id'])], context=context)
358         if p_ids:
359             p_obj.write(cr, uid, p_ids, {'price_extra': value}, context=context)
360         else:
361             p_obj.create(cr, uid, {
362                     'product_tmpl_id': context['active_id'],
363                     'value_id': id,
364                     'price_extra': value,
365                 }, context=context)
366
367     def name_get(self, cr, uid, ids, context=None):
368         if context and not context.get('show_attribute', True):
369             return super(product_attribute_value, self).name_get(cr, uid, ids, context=context)
370         res = []
371         for value in self.browse(cr, uid, ids, context=context):
372             res.append([value.id, "%s: %s" % (value.attribute_id.name, value.name)])
373         return res
374
375     _columns = {
376         'sequence': fields.integer('Sequence', help="Determine the display order"),
377         'name': fields.char('Value', translate=True, required=True),
378         'attribute_id': fields.many2one('product.attribute', 'Attribute', required=True, ondelete='cascade'),
379         'product_ids': fields.many2many('product.product', id1='att_id', id2='prod_id', string='Variants', readonly=True),
380         'price_extra': fields.function(_get_price_extra, type='float', string='Attribute Price Extra',
381             fnct_inv=_set_price_extra,
382             digits_compute=dp.get_precision('Product Price'),
383             help="Price Extra: Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200."),
384         'price_ids': fields.one2many('product.attribute.price', 'value_id', string='Attribute Prices', readonly=True),
385     }
386     _sql_constraints = [
387         ('value_company_uniq', 'unique (name,attribute_id)', 'This attribute value already exists !')
388     ]
389     _defaults = {
390         'price_extra': 0.0,
391     }
392     def unlink(self, cr, uid, ids, context=None):
393         ctx = dict(context or {}, active_test=False)
394         product_ids = self.pool['product.product'].search(cr, uid, [('attribute_value_ids', 'in', ids)], context=ctx)
395         if product_ids:
396             raise osv.except_osv(_('Integrity Error!'), _('The operation cannot be completed:\nYou trying to delete an attribute value with a reference on a product variant.'))
397         return super(product_attribute_value, self).unlink(cr, uid, ids, context=context)
398
399 class product_attribute_price(osv.osv):
400     _name = "product.attribute.price"
401     _columns = {
402         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade'),
403         'value_id': fields.many2one('product.attribute.value', 'Product Attribute Value', required=True, ondelete='cascade'),
404         'price_extra': fields.float('Price Extra', digits_compute=dp.get_precision('Product Price')),
405     }
406
407 class product_attribute_line(osv.osv):
408     _name = "product.attribute.line"
409     _rec_name = 'attribute_id'
410     _columns = {
411         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade'),
412         'attribute_id': fields.many2one('product.attribute', 'Attribute', required=True, ondelete='restrict'),
413         'value_ids': fields.many2many('product.attribute.value', id1='line_id', id2='val_id', string='Product Attribute Value'),
414     }
415
416
417 #----------------------------------------------------------
418 # Products
419 #----------------------------------------------------------
420 class product_template(osv.osv):
421     _name = "product.template"
422     _inherit = ['mail.thread']
423     _description = "Product Template"
424     _order = "name"
425
426     def _get_image(self, cr, uid, ids, name, args, context=None):
427         result = dict.fromkeys(ids, False)
428         for obj in self.browse(cr, uid, ids, context=context):
429             result[obj.id] = tools.image_get_resized_images(obj.image, avoid_resize_medium=True)
430         return result
431
432     def _set_image(self, cr, uid, id, name, value, args, context=None):
433         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
434
435     def _is_product_variant(self, cr, uid, ids, name, arg, context=None):
436         return self._is_product_variant_impl(cr, uid, ids, name, arg, context=context)
437
438     def _is_product_variant_impl(self, cr, uid, ids, name, arg, context=None):
439         return dict.fromkeys(ids, False)
440
441     def _product_template_price(self, cr, uid, ids, name, arg, context=None):
442         plobj = self.pool.get('product.pricelist')
443         res = {}
444         quantity = context.get('quantity') or 1.0
445         pricelist = context.get('pricelist', False)
446         partner = context.get('partner', False)
447         if pricelist:
448             # Support context pricelists specified as display_name or ID for compatibility
449             if isinstance(pricelist, basestring):
450                 pricelist_ids = plobj.name_search(
451                     cr, uid, pricelist, operator='=', context=context, limit=1)
452                 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
453
454             if isinstance(pricelist, (int, long)):
455                 products = self.browse(cr, uid, ids, context=context)
456                 qtys = map(lambda x: (x, quantity, partner), products)
457                 pl = plobj.browse(cr, uid, pricelist, context=context)
458                 price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
459                 for id in ids:
460                     res[id] = price.get(id, 0.0)
461         for id in ids:
462             res.setdefault(id, 0.0)
463         return res
464
465     def get_history_price(self, cr, uid, product_tmpl, company_id, date=None, context=None):
466         if context is None:
467             context = {}
468         if date is None:
469             date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
470         price_history_obj = self.pool.get('product.price.history')
471         history_ids = price_history_obj.search(cr, uid, [('company_id', '=', company_id), ('product_template_id', '=', product_tmpl), ('datetime', '<=', date)], limit=1)
472         if history_ids:
473             return price_history_obj.read(cr, uid, history_ids[0], ['cost'], context=context)['cost']
474         return 0.0
475
476     def _set_standard_price(self, cr, uid, product_tmpl_id, value, context=None):
477         ''' Store the standard price change in order to be able to retrieve the cost of a product template for a given date'''
478         if context is None:
479             context = {}
480         price_history_obj = self.pool['product.price.history']
481         user_company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
482         company_id = context.get('force_company', user_company)
483         price_history_obj.create(cr, uid, {
484             'product_template_id': product_tmpl_id,
485             'cost': value,
486             'company_id': company_id,
487         }, context=context)
488
489     def _get_product_variant_count(self, cr, uid, ids, name, arg, context=None):
490         res = {}
491         for product in self.browse(cr, uid, ids):
492             res[product.id] = len(product.product_variant_ids)
493         return res
494
495     _columns = {
496         'name': fields.char('Name', required=True, translate=True, select=True),
497         'product_manager': fields.many2one('res.users','Product Manager'),
498         'description': fields.text('Description',translate=True,
499             help="A precise description of the Product, used only for internal information purposes."),
500         'description_purchase': fields.text('Purchase Description',translate=True,
501             help="A description of the Product that you want to communicate to your suppliers. "
502                  "This description will be copied to every Purchase Order, Receipt and Supplier Invoice/Refund."),
503         'description_sale': fields.text('Sale Description',translate=True,
504             help="A description of the Product that you want to communicate to your customers. "
505                  "This description will be copied to every Sale Order, Delivery Order and Customer Invoice/Refund"),
506         'type': fields.selection([('consu', 'Consumable'),('service','Service')], 'Product Type', required=True, help="Consumable are product where you don't manage stock, a service is a non-material product provided by a company or an individual."),        
507         'rental': fields.boolean('Can be Rent'),
508         'categ_id': fields.many2one('product.category','Internal Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
509         'price': fields.function(_product_template_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
510         'list_price': fields.float('Sale Price', digits_compute=dp.get_precision('Product Price'), help="Base price to compute the customer price. Sometimes called the catalog price."),
511         'lst_price' : fields.related('list_price', type="float", string='Public Price', digits_compute=dp.get_precision('Product Price')),
512         'standard_price': fields.property(type = 'float', digits_compute=dp.get_precision('Product Price'), 
513                                           help="Cost price of the product template used for standard stock valuation in accounting and used as a base price on purchase orders.", 
514                                           groups="base.group_user", string="Cost Price"),
515         'volume': fields.float('Volume', help="The volume in m3."),
516         'weight': fields.float('Gross Weight', digits_compute=dp.get_precision('Stock Weight'), help="The gross weight in Kg."),
517         'weight_net': fields.float('Net Weight', digits_compute=dp.get_precision('Stock Weight'), help="The net weight in Kg."),
518         'warranty': fields.float('Warranty'),
519         'sale_ok': fields.boolean('Can be Sold', help="Specify if the product can be selected in a sales order line."),
520         'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
521         'state': fields.selection([('',''),
522             ('draft', 'In Development'),
523             ('sellable','Normal'),
524             ('end','End of Lifecycle'),
525             ('obsolete','Obsolete')], 'Status'),
526         'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
527         'uom_po_id': fields.many2one('product.uom', 'Purchase Unit of Measure', required=True, help="Default Unit of Measure used for purchase orders. It must be in the same category than the default unit of measure."),
528         'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
529             help='Specify a unit of measure here if invoicing is made in another unit of measure than inventory. Keep empty to use the default unit of measure.'),
530         'uos_coeff': fields.float('Unit of Measure -> UOS Coeff', digits_compute= dp.get_precision('Product UoS'),
531             help='Coefficient to convert default Unit of Measure to Unit of Sale\n'
532             ' uos = uom * coeff'),
533         'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type'),
534         'company_id': fields.many2one('res.company', 'Company', select=1),
535         # image: all image fields are base64 encoded and PIL-supported
536         'image': fields.binary("Image",
537             help="This field holds the image used as image for the product, limited to 1024x1024px."),
538         'image_medium': fields.function(_get_image, fnct_inv=_set_image,
539             string="Medium-sized image", type="binary", multi="_get_image", 
540             store={
541                 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
542             },
543             help="Medium-sized image of the product. It is automatically "\
544                  "resized as a 128x128px image, with aspect ratio preserved, "\
545                  "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."),
546         'image_small': fields.function(_get_image, fnct_inv=_set_image,
547             string="Small-sized image", type="binary", multi="_get_image",
548             store={
549                 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
550             },
551             help="Small-sized image of the product. It is automatically "\
552                  "resized as a 64x64px image, with aspect ratio preserved. "\
553                  "Use this field anywhere a small image is required."),
554         'packaging_ids': fields.one2many(
555             'product.packaging', 'product_tmpl_id', 'Logistical Units',
556             help="Gives the different ways to package the same product. This has no impact on "
557                  "the picking order and is mainly used if you use the EDI module."),
558         'seller_ids': fields.one2many('product.supplierinfo', 'product_tmpl_id', 'Supplier'),
559         'seller_delay': fields.related('seller_ids','delay', type='integer', string='Supplier Lead Time',
560             help="This is the average delay in days between the purchase order confirmation and the receipts for this product and for the default supplier. It is used by the scheduler to order requests based on reordering delays."),
561         'seller_qty': fields.related('seller_ids','qty', type='float', string='Supplier Quantity',
562             help="This is minimum quantity to purchase from Main Supplier."),
563         'seller_id': fields.related('seller_ids','name', type='many2one', relation='res.partner', string='Main Supplier',
564             help="Main Supplier who has highest priority in Supplier List."),
565
566         'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
567         'color': fields.integer('Color Index'),
568         'is_product_variant': fields.function( _is_product_variant, type='boolean', string='Is product variant'),
569
570         'attribute_line_ids': fields.one2many('product.attribute.line', 'product_tmpl_id', 'Product Attributes'),
571         'product_variant_ids': fields.one2many('product.product', 'product_tmpl_id', 'Products', required=True),
572         'product_variant_count': fields.function( _get_product_variant_count, type='integer', string='# of Product Variants'),
573
574         # related to display product product information if is_product_variant
575         'ean13': fields.related('product_variant_ids', 'ean13', type='char', string='EAN13 Barcode'),
576         'default_code': fields.related('product_variant_ids', 'default_code', type='char', string='Internal Reference'),
577     }
578
579     def _price_get_list_price(self, product):
580         return 0.0
581
582     def _price_get(self, cr, uid, products, ptype='list_price', context=None):
583         if context is None:
584             context = {}
585
586         if 'currency_id' in context:
587             pricetype_obj = self.pool.get('product.price.type')
588             price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
589             price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
590
591         res = {}
592         product_uom_obj = self.pool.get('product.uom')
593         for product in products:
594             # standard_price field can only be seen by users in base.group_user
595             # Thus, in order to compute the sale price from the cost price for users not in this group
596             # We fetch the standard price as the superuser
597             if ptype != 'standard_price':
598                 res[product.id] = product[ptype] or 0.0
599             else:
600                 company_id = product.env.user.company_id.id
601                 product = product.with_context(force_company=company_id)
602                 res[product.id] = res[product.id] = product.sudo()[ptype]
603             if ptype == 'list_price':
604                 res[product.id] += product._name == "product.product" and product.price_extra or 0.0
605             if 'uom' in context:
606                 uom = product.uom_id or product.uos_id
607                 res[product.id] = product_uom_obj._compute_price(cr, uid,
608                         uom.id, res[product.id], context['uom'])
609             # Convert from price_type currency to asked one
610             if 'currency_id' in context:
611                 # Take the price_type currency from the product field
612                 # This is right cause a field cannot be in more than one currency
613                 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
614                     context['currency_id'], res[product.id],context=context)
615
616         return res
617
618     def _get_uom_id(self, cr, uid, *args):
619         return self.pool["product.uom"].search(cr, uid, [], limit=1, order='id')[0]
620
621     def _default_category(self, cr, uid, context=None):
622         if context is None:
623             context = {}
624         if 'categ_id' in context and context['categ_id']:
625             return context['categ_id']
626         md = self.pool.get('ir.model.data')
627         res = False
628         try:
629             res = md.get_object_reference(cr, uid, 'product', 'product_category_all')[1]
630         except ValueError:
631             res = False
632         return res
633
634     def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
635         if uom_id:
636             return {'value': {'uom_po_id': uom_id}}
637         return {}
638
639     def create_variant_ids(self, cr, uid, ids, context=None):
640         product_obj = self.pool.get("product.product")
641         ctx = context and context.copy() or {}
642
643         if ctx.get("create_product_variant"):
644             return None
645
646         ctx.update(active_test=False, create_product_variant=True)
647
648         tmpl_ids = self.browse(cr, uid, ids, context=ctx)
649         for tmpl_id in tmpl_ids:
650
651             # list of values combination
652             variant_alone = []
653             all_variants = [[]]
654             for variant_id in tmpl_id.attribute_line_ids:
655                 if len(variant_id.value_ids) == 1:
656                     variant_alone.append(variant_id.value_ids[0])
657                 temp_variants = []
658                 for variant in all_variants:
659                     for value_id in variant_id.value_ids:
660                         temp_variants.append(variant + [int(value_id)])
661                 all_variants = temp_variants
662
663             # adding an attribute with only one value should not recreate product
664             # write this attribute on every product to make sure we don't lose them
665             for variant_id in variant_alone:
666                 product_ids = []
667                 for product_id in tmpl_id.product_variant_ids:
668                     if variant_id.id not in map(int, product_id.attribute_value_ids):
669                         product_ids.append(product_id.id)
670                 product_obj.write(cr, uid, product_ids, {'attribute_value_ids': [(4, variant_id.id)]}, context=ctx)
671
672             # check product
673             variant_ids_to_active = []
674             variants_active_ids = []
675             variants_inactive = []
676             for product_id in tmpl_id.product_variant_ids:
677                 variants = map(int,product_id.attribute_value_ids)
678                 if variants in all_variants:
679                     variants_active_ids.append(product_id.id)
680                     all_variants.pop(all_variants.index(variants))
681                     if not product_id.active:
682                         variant_ids_to_active.append(product_id.id)
683                 else:
684                     variants_inactive.append(product_id)
685             if variant_ids_to_active:
686                 product_obj.write(cr, uid, variant_ids_to_active, {'active': True}, context=ctx)
687
688             # create new product
689             for variant_ids in all_variants:
690                 values = {
691                     'product_tmpl_id': tmpl_id.id,
692                     'attribute_value_ids': [(6, 0, variant_ids)]
693                 }
694                 id = product_obj.create(cr, uid, values, context=ctx)
695                 variants_active_ids.append(id)
696
697             # unlink or inactive product
698             for variant_id in map(int,variants_inactive):
699                 try:
700                     with cr.savepoint():
701                         product_obj.unlink(cr, uid, [variant_id], context=ctx)
702                 except (psycopg2.Error, osv.except_osv):
703                     product_obj.write(cr, uid, [variant_id], {'active': False}, context=ctx)
704                     pass
705         return True
706
707     def create(self, cr, uid, vals, context=None):
708         ''' Store the initial standard price in order to be able to retrieve the cost of a product template for a given date'''
709         product_template_id = super(product_template, self).create(cr, uid, vals, context=context)
710         if not context or "create_product_product" not in context:
711             self.create_variant_ids(cr, uid, [product_template_id], context=context)
712         self._set_standard_price(cr, uid, product_template_id, vals.get('standard_price', 0.0), context=context)
713
714         # TODO: this is needed to set given values to first variant after creation
715         # these fields should be moved to product as lead to confusion
716         related_vals = {}
717         if vals.get('ean13'):
718             related_vals['ean13'] = vals['ean13']
719         if vals.get('default_code'):
720             related_vals['default_code'] = vals['default_code']
721         if related_vals:
722             self.write(cr, uid, product_template_id, related_vals, context=context)
723
724         return product_template_id
725
726     def write(self, cr, uid, ids, vals, context=None):
727         ''' Store the standard price change in order to be able to retrieve the cost of a product template for a given date'''
728         if isinstance(ids, (int, long)):
729             ids = [ids]
730         if 'uom_po_id' in vals:
731             new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_po_id'], context=context)
732             for product in self.browse(cr, uid, ids, context=context):
733                 old_uom = product.uom_po_id
734                 if old_uom.category_id.id != new_uom.category_id.id:
735                     raise osv.except_osv(_('Unit of Measure categories Mismatch!'), _("New Unit of Measure '%s' must belong to same Unit of Measure category '%s' as of old Unit of Measure '%s'. If you need to change the unit of measure, you may deactivate this product from the 'Procurements' tab and create a new one.") % (new_uom.name, old_uom.category_id.name, old_uom.name,))
736         if 'standard_price' in vals:
737             for prod_template_id in ids:
738                 self._set_standard_price(cr, uid, prod_template_id, vals['standard_price'], context=context)
739         res = super(product_template, self).write(cr, uid, ids, vals, context=context)
740         if 'attribute_line_ids' in vals or vals.get('active'):
741             self.create_variant_ids(cr, uid, ids, context=context)
742         if 'active' in vals and not vals.get('active'):
743             ctx = context and context.copy() or {}
744             ctx.update(active_test=False)
745             product_ids = []
746             for product in self.browse(cr, uid, ids, context=ctx):
747                 product_ids = map(int,product.product_variant_ids)
748             self.pool.get("product.product").write(cr, uid, product_ids, {'active': vals.get('active')}, context=ctx)
749         return res
750
751     def copy(self, cr, uid, id, default=None, context=None):
752         if default is None:
753             default = {}
754         template = self.browse(cr, uid, id, context=context)
755         default['name'] = _("%s (copy)") % (template['name'])
756         return super(product_template, self).copy(cr, uid, id, default=default, context=context)
757
758     _defaults = {
759         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
760         'list_price': 1,
761         'standard_price': 0.0,
762         'sale_ok': 1,        
763         'uom_id': _get_uom_id,
764         'uom_po_id': _get_uom_id,
765         'uos_coeff': 1.0,
766         'mes_type': 'fixed',
767         'categ_id' : _default_category,
768         'type' : 'consu',
769         'active': True,
770     }
771
772     def _check_uom(self, cursor, user, ids, context=None):
773         for product in self.browse(cursor, user, ids, context=context):
774             if product.uom_id.category_id.id != product.uom_po_id.category_id.id:
775                 return False
776         return True
777
778     def _check_uos(self, cursor, user, ids, context=None):
779         for product in self.browse(cursor, user, ids, context=context):
780             if product.uos_id \
781                     and product.uos_id.category_id.id \
782                     == product.uom_id.category_id.id:
783                 return False
784         return True
785
786     _constraints = [
787         (_check_uom, 'Error: The default Unit of Measure and the purchase Unit of Measure must be in the same category.', ['uom_id']),
788     ]
789
790     def name_get(self, cr, user, ids, context=None):
791         if context is None:
792             context = {}
793         if 'partner_id' in context:
794             pass
795         return super(product_template, self).name_get(cr, user, ids, context)
796
797
798
799
800
801 class product_product(osv.osv):
802     _name = "product.product"
803     _description = "Product"
804     _inherits = {'product.template': 'product_tmpl_id'}
805     _inherit = ['mail.thread']
806     _order = 'default_code,name_template'
807
808     def _product_price(self, cr, uid, ids, name, arg, context=None):
809         plobj = self.pool.get('product.pricelist')
810         res = {}
811         if context is None:
812             context = {}
813         quantity = context.get('quantity') or 1.0
814         pricelist = context.get('pricelist', False)
815         partner = context.get('partner', False)
816         if pricelist:
817             # Support context pricelists specified as display_name or ID for compatibility
818             if isinstance(pricelist, basestring):
819                 pricelist_ids = plobj.name_search(
820                     cr, uid, pricelist, operator='=', context=context, limit=1)
821                 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
822
823             if isinstance(pricelist, (int, long)):
824                 products = self.browse(cr, uid, ids, context=context)
825                 qtys = map(lambda x: (x, quantity, partner), products)
826                 pl = plobj.browse(cr, uid, pricelist, context=context)
827                 price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
828                 for id in ids:
829                     res[id] = price.get(id, 0.0)
830         for id in ids:
831             res.setdefault(id, 0.0)
832         return res
833
834     def view_header_get(self, cr, uid, view_id, view_type, context=None):
835         if context is None:
836             context = {}
837         res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
838         if (context.get('categ_id', False)):
839             return _('Products: ') + self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
840         return res
841
842     def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
843         product_uom_obj = self.pool.get('product.uom')
844         res = dict.fromkeys(ids, 0.0)
845
846         for product in self.browse(cr, uid, ids, context=context):
847             if 'uom' in context:
848                 uom = product.uos_id or product.uom_id
849                 res[product.id] = product_uom_obj._compute_price(cr, uid,
850                         uom.id, product.list_price, context['uom'])
851             else:
852                 res[product.id] = product.list_price
853             res[product.id] =  res[product.id] + product.price_extra
854
855         return res
856
857     def _set_product_lst_price(self, cr, uid, id, name, value, args, context=None):
858         product_uom_obj = self.pool.get('product.uom')
859
860         product = self.browse(cr, uid, id, context=context)
861         if 'uom' in context:
862             uom = product.uos_id or product.uom_id
863             value = product_uom_obj._compute_price(cr, uid,
864                     context['uom'], value, uom.id)
865         value =  value - product.price_extra
866         
867         return product.write({'list_price': value})
868
869     def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
870         for supinfo in product.seller_ids:
871             if supinfo.name.id == partner_id:
872                 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name}
873         res = {'code': product.default_code, 'name': product.name}
874         return res
875
876     def _product_code(self, cr, uid, ids, name, arg, context=None):
877         res = {}
878         if context is None:
879             context = {}
880         for p in self.browse(cr, uid, ids, context=context):
881             res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
882         return res
883
884     def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
885         res = {}
886         if context is None:
887             context = {}
888         for p in self.browse(cr, uid, ids, context=context):
889             data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
890             if not data['code']:
891                 data['code'] = p.code
892             if not data['name']:
893                 data['name'] = p.name
894             res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + (data['name'] or '')
895         return res
896
897     def _is_product_variant_impl(self, cr, uid, ids, name, arg, context=None):
898         return dict.fromkeys(ids, True)
899
900     def _get_name_template_ids(self, cr, uid, ids, context=None):
901         result = set()
902         template_ids = self.pool.get('product.product').search(cr, uid, [('product_tmpl_id', 'in', ids)])
903         for el in template_ids:
904             result.add(el)
905         return list(result)
906
907     def _get_image_variant(self, cr, uid, ids, name, args, context=None):
908         result = dict.fromkeys(ids, False)
909         for obj in self.browse(cr, uid, ids, context=context):
910             result[obj.id] = obj.image_variant or getattr(obj.product_tmpl_id, name)
911         return result
912
913     def _set_image_variant(self, cr, uid, id, name, value, args, context=None):
914         image = tools.image_resize_image_big(value)
915         res = self.write(cr, uid, [id], {'image_variant': image}, context=context)
916         product = self.browse(cr, uid, id, context=context)
917         if not product.product_tmpl_id.image:
918             product.write({'image_variant': None})
919             product.product_tmpl_id.write({'image': image})
920         return res
921
922     def _get_price_extra(self, cr, uid, ids, name, args, context=None):
923         result = dict.fromkeys(ids, False)
924         for product in self.browse(cr, uid, ids, context=context):
925             price_extra = 0.0
926             for variant_id in product.attribute_value_ids:
927                 for price_id in variant_id.price_ids:
928                     if price_id.product_tmpl_id.id == product.product_tmpl_id.id:
929                         price_extra += price_id.price_extra
930             result[product.id] = price_extra
931         return result
932
933     _columns = {
934         'price': fields.function(_product_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
935         'price_extra': fields.function(_get_price_extra, type='float', string='Variant Extra Price', help="This is the sum of the extra price of all attributes"),
936         'lst_price': fields.function(_product_lst_price, fnct_inv=_set_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Product Price')),
937         'code': fields.function(_product_code, type='char', string='Internal Reference'),
938         'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
939         'default_code' : fields.char('Internal Reference', select=True),
940         'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
941         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade", select=True, auto_join=True),
942         'ean13': fields.char('EAN13 Barcode', size=13, help="International Article Number used for product identification."),
943         'name_template': fields.related('product_tmpl_id', 'name', string="Template Name", type='char', store={
944             'product.template': (_get_name_template_ids, ['name'], 10),
945             'product.product': (lambda self, cr, uid, ids, c=None: ids, [], 10),
946         }, select=True),
947         'attribute_value_ids': fields.many2many('product.attribute.value', id1='prod_id', id2='att_id', string='Attributes', readonly=True, ondelete='restrict'),
948         'is_product_variant': fields.function( _is_product_variant_impl, type='boolean', string='Is product variant'),
949
950         # image: all image fields are base64 encoded and PIL-supported
951         'image_variant': fields.binary("Variant Image",
952             help="This field holds the image used as image for the product variant, limited to 1024x1024px."),
953
954         'image': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
955             string="Big-sized image", type="binary",
956             help="Image of the product variant (Big-sized image of product template if false). It is automatically "\
957                  "resized as a 1024x1024px image, with aspect ratio preserved."),
958         'image_small': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
959             string="Small-sized image", type="binary",
960             help="Image of the product variant (Small-sized image of product template if false)."),
961         'image_medium': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
962             string="Medium-sized image", type="binary",
963             help="Image of the product variant (Medium-sized image of product template if false)."),
964     }
965
966     _defaults = {
967         'active': 1,
968         'color': 0,
969     }
970
971     def unlink(self, cr, uid, ids, context=None):
972         unlink_ids = []
973         unlink_product_tmpl_ids = []
974         for product in self.browse(cr, uid, ids, context=context):
975             # Check if product still exists, in case it has been unlinked by unlinking its template
976             if not product.exists():
977                 continue
978             tmpl_id = product.product_tmpl_id.id
979             # Check if the product is last product of this template
980             other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
981             if not other_product_ids:
982                 unlink_product_tmpl_ids.append(tmpl_id)
983             unlink_ids.append(product.id)
984         res = super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
985         # delete templates after calling super, as deleting template could lead to deleting
986         # products due to ondelete='cascade'
987         self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
988         return res
989
990     def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
991         if uom_id and uom_po_id:
992             uom_obj=self.pool.get('product.uom')
993             uom=uom_obj.browse(cursor,user,[uom_id])[0]
994             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
995             if uom.category_id.id != uom_po.category_id.id:
996                 return {'value': {'uom_po_id': uom_id}}
997         return False
998
999     def _check_ean_key(self, cr, uid, ids, context=None):
1000         for product in self.read(cr, uid, ids, ['ean13'], context=context):
1001             if not check_ean(product['ean13']):
1002                 return False
1003         return True
1004
1005     _constraints = [(_check_ean_key, 'You provided an invalid "EAN13 Barcode" reference. You may use the "Internal Reference" field instead.', ['ean13'])]
1006
1007     def on_order(self, cr, uid, ids, orderline, quantity):
1008         pass
1009
1010     def name_get(self, cr, user, ids, context=None):
1011         if context is None:
1012             context = {}
1013         if isinstance(ids, (int, long)):
1014             ids = [ids]
1015         if not len(ids):
1016             return []
1017
1018         def _name_get(d):
1019             name = d.get('name','')
1020             code = context.get('display_default_code', True) and d.get('default_code',False) or False
1021             if code:
1022                 name = '[%s] %s' % (code,name)
1023             return (d['id'], name)
1024
1025         partner_id = context.get('partner_id', False)
1026         if partner_id:
1027             partner_ids = [partner_id, self.pool['res.partner'].browse(cr, user, partner_id, context=context).commercial_partner_id.id]
1028         else:
1029             partner_ids = []
1030
1031         # all user don't have access to seller and partner
1032         # check access and use superuser
1033         self.check_access_rights(cr, user, "read")
1034         self.check_access_rule(cr, user, ids, "read", context=context)
1035
1036         result = []
1037         for product in self.browse(cr, SUPERUSER_ID, ids, context=context):
1038             variant = ", ".join([v.name for v in product.attribute_value_ids])
1039             name = variant and "%s (%s)" % (product.name, variant) or product.name
1040             sellers = []
1041             if partner_ids:
1042                 sellers = filter(lambda x: x.name.id in partner_ids, product.seller_ids)
1043             if sellers:
1044                 for s in sellers:
1045                     seller_variant = s.product_name and "%s (%s)" % (s.product_name, variant) or False
1046                     mydict = {
1047                               'id': product.id,
1048                               'name': seller_variant or name,
1049                               'default_code': s.product_code or product.default_code,
1050                               }
1051                     result.append(_name_get(mydict))
1052             else:
1053                 mydict = {
1054                           'id': product.id,
1055                           'name': name,
1056                           'default_code': product.default_code,
1057                           }
1058                 result.append(_name_get(mydict))
1059         return result
1060
1061     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
1062         if not args:
1063             args = []
1064         if name:
1065             positive_operators = ['=', 'ilike', '=ilike', 'like', '=like']
1066             ids = []
1067             if operator in positive_operators:
1068                 ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
1069                 if not ids:
1070                     ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
1071             if not ids and operator not in expression.NEGATIVE_TERM_OPERATORS:
1072                 # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
1073                 # on a database with thousands of matching products, due to the huge merge+unique needed for the
1074                 # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
1075                 # Performing a quick memory merge of ids in Python will give much better performance
1076                 ids = set(self.search(cr, user, args + [('default_code', operator, name)], limit=limit, context=context))
1077                 if not limit or len(ids) < limit:
1078                     # we may underrun the limit because of dupes in the results, that's fine
1079                     limit2 = (limit - len(ids)) if limit else False
1080                     ids.update(self.search(cr, user, args + [('name', operator, name), ('id', 'not in', list(ids))], limit=limit2, context=context))
1081                 ids = list(ids)
1082             elif not ids and operator in expression.NEGATIVE_TERM_OPERATORS:
1083                 ids = self.search(cr, user, args + ['&', ('default_code', operator, name), ('name', operator, name)], limit=limit, context=context)
1084             if not ids and operator in positive_operators:
1085                 ptrn = re.compile('(\[(.*?)\])')
1086                 res = ptrn.search(name)
1087                 if res:
1088                     ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
1089         else:
1090             ids = self.search(cr, user, args, limit=limit, context=context)
1091         result = self.name_get(cr, user, ids, context=context)
1092         return result
1093
1094     #
1095     # Could be overrided for variants matrices prices
1096     #
1097     def price_get(self, cr, uid, ids, ptype='list_price', context=None):
1098         products = self.browse(cr, uid, ids, context=context)
1099         return self.pool.get("product.template")._price_get(cr, uid, products, ptype=ptype, context=context)
1100
1101     def copy(self, cr, uid, id, default=None, context=None):
1102         if context is None:
1103             context={}
1104
1105         product = self.browse(cr, uid, id, context)
1106         if context.get('variant'):
1107             # if we copy a variant or create one, we keep the same template
1108             default['product_tmpl_id'] = product.product_tmpl_id.id
1109         elif 'name' not in default:
1110             default['name'] = _("%s (copy)") % (product.name,)
1111
1112         return super(product_product, self).copy(cr, uid, id, default=default, context=context)
1113
1114     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
1115         if context is None:
1116             context = {}
1117         if context.get('search_default_categ_id'):
1118             args.append((('categ_id', 'child_of', context['search_default_categ_id'])))
1119         return super(product_product, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1120
1121     def open_product_template(self, cr, uid, ids, context=None):
1122         """ Utility method used to add an "Open Template" button in product views """
1123         product = self.browse(cr, uid, ids[0], context=context)
1124         return {'type': 'ir.actions.act_window',
1125                 'res_model': 'product.template',
1126                 'view_mode': 'form',
1127                 'res_id': product.product_tmpl_id.id,
1128                 'target': 'new'}
1129
1130     def create(self, cr, uid, vals, context=None):
1131         if context is None:
1132             context = {}
1133         ctx = dict(context or {}, create_product_product=True)
1134         return super(product_product, self).create(cr, uid, vals, context=ctx)
1135
1136
1137
1138     def need_procurement(self, cr, uid, ids, context=None):
1139         return False
1140
1141     def _compute_uos_qty(self, cr, uid, ids, uom, qty, uos, context=None):
1142         '''
1143         Computes product's invoicing quantity in UoS from quantity in UoM.
1144         Takes into account the
1145         :param uom: Source unit
1146         :param qty: Source quantity
1147         :param uos: Target UoS unit.
1148         '''
1149         if not uom or not qty or not uos:
1150             return qty
1151         uom_obj = self.pool['product.uom']
1152         product_id = ids[0] if isinstance(ids, (list, tuple)) else ids
1153         product = self.browse(cr, uid, product_id, context=context)
1154         if isinstance(uos, (int, long)):
1155             uos = uom_obj.browse(cr, uid, uos, context=context)
1156         if isinstance(uom, (int, long)):
1157             uom = uom_obj.browse(cr, uid, uom, context=context)
1158         if product.uos_id:  # Product has UoS defined
1159             # We cannot convert directly between units even if the units are of the same category
1160             # as we need to apply the conversion coefficient which is valid only between quantities
1161             # in product's default UoM/UoS
1162             qty_default_uom = uom_obj._compute_qty_obj(cr, uid, uom, qty, product.uom_id)  # qty in product's default UoM
1163             qty_default_uos = qty_default_uom * product.uos_coeff
1164             return uom_obj._compute_qty_obj(cr, uid, product.uos_id, qty_default_uos, uos)
1165         else:
1166             return uom_obj._compute_qty_obj(cr, uid, uom, qty, uos)
1167
1168
1169
1170 class product_packaging(osv.osv):
1171     _name = "product.packaging"
1172     _description = "Packaging"
1173     _rec_name = 'ean'
1174     _order = 'sequence'
1175     _columns = {
1176         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
1177         'name' : fields.text('Description'),
1178         'qty' : fields.float('Quantity by Package',
1179             help="The total number of products you can put by pallet or box."),
1180         'ul' : fields.many2one('product.ul', 'Package Logistic Unit', required=True),
1181         'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
1182         'ul_container': fields.many2one('product.ul', 'Pallet Logistic Unit'),
1183         'rows' : fields.integer('Number of Layers', required=True,
1184             help='The number of layers on a pallet or box'),
1185         'product_tmpl_id' : fields.many2one('product.template', 'Product', select=1, ondelete='cascade', required=True),
1186         'ean' : fields.char('EAN', size=14, help="The EAN code of the package unit."),
1187         'code' : fields.char('Code', help="The code of the transport unit."),
1188         'weight': fields.float('Total Package Weight',
1189             help='The weight of a full package, pallet or box.'),
1190     }
1191
1192     def _check_ean_key(self, cr, uid, ids, context=None):
1193         for pack in self.browse(cr, uid, ids, context=context):
1194             if not check_ean(pack.ean):
1195                 return False
1196         return True
1197
1198     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
1199
1200     def name_get(self, cr, uid, ids, context=None):
1201         if not len(ids):
1202             return []
1203         res = []
1204         for pckg in self.browse(cr, uid, ids, context=context):
1205             p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
1206             p_name += pckg.ul.name
1207             res.append((pckg.id,p_name))
1208         return res
1209
1210     def _get_1st_ul(self, cr, uid, context=None):
1211         cr.execute('select id from product_ul order by id asc limit 1')
1212         res = cr.fetchone()
1213         return (res and res[0]) or False
1214
1215     _defaults = {
1216         'rows' : 3,
1217         'sequence' : 1,
1218         'ul' : _get_1st_ul,
1219     }
1220
1221     def checksum(ean):
1222         salt = '31' * 6 + '3'
1223         sum = 0
1224         for ean_part, salt_part in zip(ean, salt):
1225             sum += int(ean_part) * int(salt_part)
1226         return (10 - (sum % 10)) % 10
1227     checksum = staticmethod(checksum)
1228
1229
1230
1231 class product_supplierinfo(osv.osv):
1232     _name = "product.supplierinfo"
1233     _description = "Information about a product supplier"
1234     def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
1235         result = {}
1236         for supplier_info in self.browse(cr, uid, ids, context=context):
1237             for field in fields:
1238                 result[supplier_info.id] = {field:False}
1239             qty = supplier_info.min_qty
1240             result[supplier_info.id]['qty'] = qty
1241         return result
1242
1243     _columns = {
1244         'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
1245         '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."),
1246         '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."),
1247         'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
1248         '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."),
1249         '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."),
1250         '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."),
1251         'product_tmpl_id' : fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade', select=True, oldname='product_id'),
1252         'delay' : fields.integer('Delivery Lead Time', required=True, help="Lead time in days between the confirmation of the purchase order and the receipt of the products in your warehouse. Used by the scheduler for automatic computation of the purchase order planning."),
1253         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist', copy=True),
1254         'company_id':fields.many2one('res.company','Company',select=1),
1255     }
1256     _defaults = {
1257         'min_qty': 0.0,
1258         'sequence': 1,
1259         'delay': 1,
1260         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
1261     }
1262
1263     _order = 'sequence'
1264
1265
1266 class pricelist_partnerinfo(osv.osv):
1267     _name = 'pricelist.partnerinfo'
1268     _columns = {
1269         'name': fields.char('Description'),
1270         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
1271         '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."),
1272         '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"),
1273     }
1274     _order = 'min_quantity asc'
1275
1276 class res_currency(osv.osv):
1277     _inherit = 'res.currency'
1278
1279     def _check_main_currency_rounding(self, cr, uid, ids, context=None):
1280         cr.execute('SELECT digits FROM decimal_precision WHERE name like %s',('Account',))
1281         digits = cr.fetchone()
1282         if digits and len(digits):
1283             digits = digits[0]
1284             main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
1285             for currency_id in ids:
1286                 if currency_id == main_currency.id:
1287                     if float_compare(main_currency.rounding, 10 ** -digits, precision_digits=6) == -1:
1288                         return False
1289         return True
1290
1291     _constraints = [
1292         (_check_main_currency_rounding, 'Error! You cannot define a rounding factor for the company\'s main currency that is smaller than the decimal precision of \'Account\'.', ['rounding']),
1293     ]
1294
1295 class decimal_precision(osv.osv):
1296     _inherit = 'decimal.precision'
1297
1298     def _check_main_currency_rounding(self, cr, uid, ids, context=None):
1299         cr.execute('SELECT id, digits FROM decimal_precision WHERE name like %s',('Account',))
1300         res = cr.fetchone()
1301         if res and len(res):
1302             account_precision_id, digits = res
1303             main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
1304             for decimal_precision in ids:
1305                 if decimal_precision == account_precision_id:
1306                     if float_compare(main_currency.rounding, 10 ** -digits, precision_digits=6) == -1:
1307                         return False
1308         return True
1309
1310     _constraints = [
1311         (_check_main_currency_rounding, 'Error! You cannot define the decimal precision of \'Account\' as greater than the rounding factor of the company\'s main currency', ['digits']),
1312     ]
1313
1314 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: