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