[IMP]improved test case in compute_price_margin.
[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
25 from _common import rounding
26 from lxml import etree
27
28 from openerp.osv.orm import setup_modifiers
29 from openerp import SUPERUSER_ID
30 from openerp import tools
31 from openerp.osv import osv, fields
32 from openerp.tools.translate import _
33
34 import openerp.addons.decimal_precision as dp
35
36 def ean_checksum(eancode):
37     """returns the checksum of an ean string of length 13, returns -1 if the string has the wrong length"""
38     if len(eancode) <> 13:
39         return -1
40     oddsum=0
41     evensum=0
42     total=0
43     eanvalue=eancode
44     reversevalue = eanvalue[::-1]
45     finalean=reversevalue[1:]
46
47     for i in range(len(finalean)):
48         if i % 2 == 0:
49             oddsum += int(finalean[i])
50         else:
51             evensum += int(finalean[i])
52     total=(oddsum * 3) + evensum
53
54     check = int(10 - math.ceil(total % 10.0)) %10
55     return check
56
57 def check_ean(eancode):
58     """returns True if eancode is a valid ean13 string, or null"""
59     if not eancode:
60         return True
61     if len(eancode) <> 13:
62         return False
63     try:
64         int(eancode)
65     except:
66         return False
67     return ean_checksum(eancode) == int(eancode[-1])
68
69 def sanitize_ean13(ean13):
70     """Creates and returns a valid ean13 from an invalid one"""
71     if not ean13:
72         return "0000000000000"
73     ean13 = re.sub("[A-Za-z]","0",ean13);
74     ean13 = re.sub("[^0-9]","",ean13);
75     ean13 = ean13[:13]
76     if len(ean13) < 13:
77         ean13 = ean13 + '0' * (13-len(ean13))
78     return ean13[:-1] + str(ean_checksum(ean13))
79
80 #----------------------------------------------------------
81 # UOM
82 #----------------------------------------------------------
83
84 class product_uom_categ(osv.osv):
85     _name = 'product.uom.categ'
86     _description = 'Product uom categ'
87     _columns = {
88         'name': fields.char('Name', size=64, required=True, translate=True),
89     }
90
91 class product_uom(osv.osv):
92     _name = 'product.uom'
93     _description = 'Product Unit of Measure'
94
95     def _compute_factor_inv(self, factor):
96         return factor and (1.0 / factor) or 0.0
97
98     def _factor_inv(self, cursor, user, ids, name, arg, context=None):
99         res = {}
100         for uom in self.browse(cursor, user, ids, context=context):
101             res[uom.id] = self._compute_factor_inv(uom.factor)
102         return res
103
104     def _factor_inv_write(self, cursor, user, id, name, value, arg, context=None):
105         return self.write(cursor, user, id, {'factor': self._compute_factor_inv(value)}, context=context)
106
107     def name_create(self, cr, uid, name, context=None):
108         """ The UoM category and factor are required, so we'll have to add temporary values
109             for imported UoMs """
110         uom_categ = self.pool.get('product.uom.categ')
111         # look for the category based on the english name, i.e. no context on purpose!
112         # TODO: should find a way to have it translated but not created until actually used
113         categ_misc = 'Unsorted/Imported Units'
114         categ_id = uom_categ.search(cr, uid, [('name', '=', categ_misc)])
115         if categ_id:
116             categ_id = categ_id[0]
117         else:
118             categ_id, _ = uom_categ.name_create(cr, uid, categ_misc)
119         uom_id = self.create(cr, uid, {self._rec_name: name,
120                                        'category_id': categ_id,
121                                        'factor': 1})
122         return self.name_get(cr, uid, [uom_id], context=context)[0]
123
124     def create(self, cr, uid, data, context=None):
125         if 'factor_inv' in data:
126             if data['factor_inv'] <> 1:
127                 data['factor'] = self._compute_factor_inv(data['factor_inv'])
128             del(data['factor_inv'])
129         return super(product_uom, self).create(cr, uid, data, context)
130
131     _order = "name"
132     _columns = {
133         'name': fields.char('Unit of Measure', size=64, required=True, translate=True),
134         'category_id': fields.many2one('product.uom.categ', 'Category', required=True, ondelete='cascade',
135             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."),
136         'factor': fields.float('Ratio', required=True,digits=(12, 12),
137             help='How much bigger or smaller this unit is compared to the reference Unit of Measure for this category:\n'\
138                     '1 * (reference unit) = ratio * (this unit)'),
139         'factor_inv': fields.function(_factor_inv, digits=(12,12),
140             fnct_inv=_factor_inv_write,
141             string='Bigger Ratio',
142             help='How many times this Unit of Measure is bigger than the reference Unit of Measure in this category:\n'\
143                     '1 * (this unit) = ratio * (reference unit)', required=True),
144         'rounding': fields.float('Rounding Precision', digits_compute=dp.get_precision('Product Unit of Measure'), required=True,
145             help="The computed quantity will be a multiple of this value. "\
146                  "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece."),
147         'active': fields.boolean('Active', help="By unchecking the active field you can disable a unit of measure without deleting it."),
148         'uom_type': fields.selection([('bigger','Bigger than the reference Unit of Measure'),
149                                       ('reference','Reference Unit of Measure for this category'),
150                                       ('smaller','Smaller than the reference Unit of Measure')],'Type', required=1),
151     }
152
153     _defaults = {
154         'active': 1,
155         'rounding': 0.01,
156         'uom_type': 'reference',
157     }
158
159     _sql_constraints = [
160         ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!')
161     ]
162
163     def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
164         if not from_uom_id or not qty or not to_uom_id:
165             return qty
166         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
167         if uoms[0].id == from_uom_id:
168             from_unit, to_unit = uoms[0], uoms[-1]
169         else:
170             from_unit, to_unit = uoms[-1], uoms[0]
171         return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit)
172
173     def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context=None):
174         if context is None:
175             context = {}
176         if from_unit.category_id.id <> to_unit.category_id.id:
177             if context.get('raise-exception', True):
178                 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,))
179             else:
180                 return qty
181         amount = qty / from_unit.factor
182         if to_unit:
183             amount = rounding(amount * to_unit.factor, to_unit.rounding)
184         return amount
185
186     def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
187         if not from_uom_id or not price or not to_uom_id:
188             return price
189         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
190         if uoms[0].id == from_uom_id:
191             from_unit, to_unit = uoms[0], uoms[-1]
192         else:
193             from_unit, to_unit = uoms[-1], uoms[0]
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 'category_id' in vals:
208             for uom in self.browse(cr, uid, ids, context=context):
209                 if uom.category_id.id != vals['category_id']:
210                     raise osv.except_osv(_('Warning!'),_("Cannot change the category of existing Unit of Measure '%s'.") % (uom.name,))
211         return super(product_uom, self).write(cr, uid, ids, vals, context=context)
212
213
214
215 class product_ul(osv.osv):
216     _name = "product.ul"
217     _description = "Shipping Unit"
218     _columns = {
219         'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
220         'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
221     }
222
223
224 #----------------------------------------------------------
225 # Categories
226 #----------------------------------------------------------
227 class product_category(osv.osv):
228
229     def name_get(self, cr, uid, ids, context=None):
230         if isinstance(ids, (list, tuple)) and not len(ids):
231             return []
232         if isinstance(ids, (long, int)):
233             ids = [ids]
234         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
235         res = []
236         for record in reads:
237             name = record['name']
238             if record['parent_id']:
239                 name = record['parent_id'][1]+' / '+name
240             res.append((record['id'], name))
241         return res
242
243     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
244         res = self.name_get(cr, uid, ids, context=context)
245         return dict(res)
246
247     _name = "product.category"
248     _description = "Product Category"
249     _columns = {
250         'name': fields.char('Name', size=64, required=True, translate=True, select=True),
251         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
252         'parent_id': fields.many2one('product.category','Parent Category', select=True, ondelete='cascade'),
253         'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
254         'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of product categories."),
255         '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."),
256         'parent_left': fields.integer('Left Parent', select=1),
257         'parent_right': fields.integer('Right Parent', select=1),
258     }
259
260
261     _defaults = {
262         'type' : lambda *a : 'normal',
263     }
264
265     _parent_name = "parent_id"
266     _parent_store = True
267     _parent_order = 'sequence, name'
268     _order = 'parent_left'
269
270     def _check_recursion(self, cr, uid, ids, context=None):
271         level = 100
272         while len(ids):
273             cr.execute('select distinct parent_id from product_category where id IN %s',(tuple(ids),))
274             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
275             if not level:
276                 return False
277             level -= 1
278         return True
279
280     _constraints = [
281         (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
282     ]
283     def child_get(self, cr, uid, ids):
284         return [ids]
285
286
287 class product_public_category(osv.osv):
288     _name = "product.public.category"
289     _description = "Public Category"
290     _order = "sequence, name"
291     def _check_recursion(self, cr, uid, ids, context=None):
292         level = 100
293         while len(ids):
294             cr.execute('select distinct parent_id from product_public_category where id IN %s',(tuple(ids),))
295             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
296             if not level:
297                 return False
298             level -= 1
299         return True
300
301     _constraints = [
302         (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
303     ]
304
305     def name_get(self, cr, uid, ids, context=None):
306         if not len(ids):
307             return []
308         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
309         res = []
310         for record in reads:
311             name = record['name']
312             if record['parent_id']:
313                 name = record['parent_id'][1]+' / '+name
314             res.append((record['id'], name))
315         return res
316
317     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
318         res = self.name_get(cr, uid, ids, context=context)
319         return dict(res)
320
321     def _get_image(self, cr, uid, ids, name, args, context=None):
322         result = dict.fromkeys(ids, False)
323         for obj in self.browse(cr, uid, ids, context=context):
324             result[obj.id] = tools.image_get_resized_images(obj.image)
325         return result
326     
327     def _set_image(self, cr, uid, id, name, value, args, context=None):
328         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
329
330     _columns = {
331         'name': fields.char('Name', size=64, required=True, translate=True),
332         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
333         'parent_id': fields.many2one('product.public.category','Parent Category', select=True),
334         'child_id': fields.one2many('product.public.category', 'parent_id', string='Children Categories'),
335         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
336         
337         # NOTE: there is no 'default image', because by default we don't show thumbnails for categories. However if we have a thumbnail
338         # for at least one category, then we display a default image on the other, so that the buttons have consistent styling.
339         # In this case, the default image is set by the js code.
340         # NOTE2: image: all image fields are base64 encoded and PIL-supported
341         'image': fields.binary("Image",
342             help="This field holds the image used as image for the cateogry, limited to 1024x1024px."),
343         'image_medium': fields.function(_get_image, fnct_inv=_set_image,
344             string="Medium-sized image", type="binary", multi="_get_image",
345             store={
346                 'product.public.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
347             },
348             help="Medium-sized image of the category. It is automatically "\
349                  "resized as a 128x128px image, with aspect ratio preserved. "\
350                  "Use this field in form views or some kanban views."),
351         'image_small': fields.function(_get_image, fnct_inv=_set_image,
352             string="Smal-sized image", type="binary", multi="_get_image",
353             store={
354                 'product.public.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
355             },
356             help="Small-sized image of the category. It is automatically "\
357                  "resized as a 64x64px image, with aspect ratio preserved. "\
358                  "Use this field anywhere a small image is required."),
359     }
360
361
362 #----------------------------------------------------------
363 # Products
364 #----------------------------------------------------------
365 class product_template(osv.osv):
366     _name = "product.template"
367     _inherit = ['mail.thread']
368     _description = "Product Template"
369
370     def _get_image(self, cr, uid, ids, name, args, context=None):
371         result = dict.fromkeys(ids, False)
372         for obj in self.browse(cr, uid, ids, context=context):
373             result[obj.id] = tools.image_get_resized_images(obj.image, avoid_resize_medium=True)
374         return result
375
376     def _set_image(self, cr, uid, id, name, value, args, context=None):
377         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
378
379     _columns = {
380         'name': fields.char('Name', size=128, required=True, translate=True, select=True),
381         'product_manager': fields.many2one('res.users','Product Manager'),
382         'description': fields.text('Description',translate=True,
383             help="A precise description of the Product, used only for internal information purposes."),
384         'description_purchase': fields.text('Purchase Description',translate=True,
385             help="A description of the Product that you want to communicate to your suppliers. "
386                  "This description will be copied to every Purchase Order, Reception and Supplier Invoice/Refund."),
387         'description_sale': fields.text('Sale Description',translate=True,
388             help="A description of the Product that you want to communicate to your customers. "
389                  "This description will be copied to every Sale Order, Delivery Order and Customer Invoice/Refund"),
390         '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."),
391         'produce_delay': fields.float('Manufacturing Lead Time', help="Average delay in days to produce this product. In the case of multi-level BOM, the manufacturing lead times of the components will be added."),
392         'rental': fields.boolean('Can be Rent'),
393         'categ_id': fields.many2one('product.category','Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
394         'public_categ_id': fields.many2one('product.public.category','Public Category', help="Those categories are used to group similar products for public sales (eg.: point of sale, e-commerce)."),
395         '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."),
396         'standard_price': fields.float('Cost Price', digits_compute=dp.get_precision('Product Price'), help="Cost price of the product template used for standard stock valuation in accounting and used as a base price on purchase orders.", groups="base.group_user"),
397         'volume': fields.float('Volume', help="The volume in m3."),
398         'weight': fields.float('Gross Weight', digits_compute=dp.get_precision('Stock Weight'), help="The gross weight in Kg."),
399         'weight_net': fields.float('Net Weight', digits_compute=dp.get_precision('Stock Weight'), help="The net weight in Kg."),
400         'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
401             help="Standard Price: The cost price is manually updated at the end of a specific period (usually every year). \nAverage Price: The cost price is recomputed at each incoming shipment."),
402         'warranty': fields.float('Warranty'),
403         'sale_ok': fields.boolean('Can be Sold', help="Specify if the product can be selected in a sales order line."),
404         'state': fields.selection([('',''),
405             ('draft', 'In Development'),
406             ('sellable','Normal'),
407             ('end','End of Lifecycle'),
408             ('obsolete','Obsolete')], 'Status'),
409         'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
410         '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."),
411         'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
412             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.'),
413         'uos_coeff': fields.float('Unit of Measure -> UOS Coeff', digits_compute= dp.get_precision('Product UoS'),
414             help='Coefficient to convert default Unit of Measure to Unit of Sale\n'
415             ' uos = uom * coeff'),
416         'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type'),
417         'seller_ids': fields.one2many('product.supplierinfo', 'product_tmpl_id', 'Supplier'),
418         'company_id': fields.many2one('res.company', 'Company', select=1),
419         # image: all image fields are base64 encoded and PIL-supported
420         'image': fields.binary("Image",
421             help="This field holds the image used as image for the product, limited to 1024x1024px."),
422         'image_medium': fields.function(_get_image, fnct_inv=_set_image,
423             string="Medium-sized image", type="binary", multi="_get_image",
424             store={
425                 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
426             },
427             help="Medium-sized image of the product. It is automatically "\
428                  "resized as a 128x128px image, with aspect ratio preserved, "\
429                  "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."),
430         'image_small': fields.function(_get_image, fnct_inv=_set_image,
431             string="Small-sized image", type="binary", multi="_get_image",
432             store={
433                 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
434             },
435             help="Small-sized image of the product. It is automatically "\
436                  "resized as a 64x64px image, with aspect ratio preserved. "\
437                  "Use this field anywhere a small image is required."),
438         'product_variant_ids': fields.one2many('product.product', 'product_tmpl_id', 'Product Variants', required=True),
439     }
440
441     def _get_uom_id(self, cr, uid, *args):
442         cr.execute('select id from product_uom order by id limit 1')
443         res = cr.fetchone()
444         return res and res[0] or False
445
446     def _default_category(self, cr, uid, context=None):
447         if context is None:
448             context = {}
449         if 'categ_id' in context and context['categ_id']:
450             return context['categ_id']
451         md = self.pool.get('ir.model.data')
452         res = False
453         try:
454             res = md.get_object_reference(cr, uid, 'product', 'product_category_all')[1]
455         except ValueError:
456             res = False
457         return res
458
459     def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
460         if uom_id:
461             return {'value': {'uom_po_id': uom_id}}
462         return {}
463
464     def write(self, cr, uid, ids, vals, context=None):
465         if 'uom_po_id' in vals:
466             new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_po_id'], context=context)
467             for product in self.browse(cr, uid, ids, context=context):
468                 old_uom = product.uom_po_id
469                 if old_uom.category_id.id != new_uom.category_id.id:
470                     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,))
471         return super(product_template, self).write(cr, uid, ids, vals, context=context)
472
473     def copy(self, cr, uid, id, default=None, context=None):
474         if default is None:
475             default = {}
476         #TOFIX: it should be pass default={'name': _("%s (copy)") % (template['name'])}.
477         template = self.read(cr, uid, id, ['name'], context=context)
478         res = super(product_template, self).copy(cr, uid, id, default=default, context=context)
479         self.write(cr, uid, res, {'name': _("%s (copy)") % (template['name'])}, context=context)
480         return res
481
482     _defaults = {
483         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
484         'list_price': 1,
485         'cost_method': 'standard',
486         'standard_price': 0.0,
487         'sale_ok': 1,
488         'produce_delay': 1,
489         'uom_id': _get_uom_id,
490         'uom_po_id': _get_uom_id,
491         'uos_coeff' : 1.0,
492         'mes_type' : 'fixed',
493         'categ_id' : _default_category,
494         'type' : 'consu',
495     }
496
497     def _check_uom(self, cursor, user, ids, context=None):
498         for product in self.browse(cursor, user, ids, context=context):
499             if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
500                 return False
501         return True
502
503     def _check_uos(self, cursor, user, ids, context=None):
504         for product in self.browse(cursor, user, ids, context=context):
505             if product.uos_id \
506                     and product.uos_id.category_id.id \
507                     == product.uom_id.category_id.id:
508                 return False
509         return True
510
511     _constraints = [
512         (_check_uom, 'Error: The default Unit of Measure and the purchase Unit of Measure must be in the same category.', ['uom_id']),
513     ]
514
515     def name_get(self, cr, user, ids, context=None):
516         if context is None:
517             context = {}
518         if 'partner_id' in context:
519             pass
520         return super(product_template, self).name_get(cr, user, ids, context)
521
522
523 class product_product(osv.osv):
524     def view_header_get(self, cr, uid, view_id, view_type, context=None):
525         if context is None:
526             context = {}
527         res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
528         if (context.get('categ_id', False)):
529             return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
530         return res
531
532     def _product_price(self, cr, uid, ids, name, arg, context=None):
533         plobj = self.pool.get('product.pricelist')
534         res = {}
535         if context is None:
536             context = {}
537         quantity = context.get('quantity') or 1.0
538         pricelist = context.get('pricelist', False)
539         partner = context.get('partner', False)
540         if pricelist:
541             # Support context pricelists specified as display_name or ID for compatibility
542             if isinstance(pricelist, basestring):
543                 pricelist_ids = plobj.name_search(
544                     cr, uid, pricelist, operator='=', context=context, limit=1)
545                 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
546
547             products = self.browse(cr, uid, ids, context=context)
548             qtys = map(lambda x: (x, quantity, partner), products)
549             pl = plobj.browse(cr, uid, pricelist, context=context)
550             price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
551             for id in ids:
552                 res[id] = price.get(id, 0.0)
553         for id in ids:
554             res.setdefault(id, 0.0)
555         return res
556
557     def _get_product_available_func(states, what):
558         def _product_available(self, cr, uid, ids, name, arg, context=None):
559             return {}.fromkeys(ids, 0.0)
560         return _product_available
561
562     _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
563     _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
564     _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
565     _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
566
567     def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
568         res = {}
569         product_uom_obj = self.pool.get('product.uom')
570         for id in ids:
571             res.setdefault(id, 0.0)
572         for product in self.browse(cr, uid, ids, context=context):
573             if 'uom' in context:
574                 uom = product.uos_id or product.uom_id
575                 res[product.id] = product_uom_obj._compute_price(cr, uid,
576                         uom.id, product.list_price, context['uom'])
577             else:
578                 res[product.id] = product.list_price
579             res[product.id] =  (res[product.id] + ((res[product.id] * (product.price_margin)) / 100)) + product.price_extra
580         return res
581
582     def _save_product_lst_price(self, cr, uid, product_id, field_name, field_value, arg, context=None):
583         field_value = field_value or 0.0
584         product = self.browse(cr, uid, product_id, context=context)
585         price = product.list_price
586         if product.price_margin:
587             price = (product.list_price + (product.list_price * (product.price_margin / 100)))
588         price = (field_value - price)
589         price_field = 'price_extra'
590         if product.standard_variants:
591             price_field = 'list_price'
592             price = field_value - product.price_extra
593         return product.write({price_field: price}, context=context)
594
595     def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
596         for supinfo in product.seller_ids:
597             if supinfo.name.id == partner_id:
598                 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name, 'variants': ''}
599         res = {'code': product.default_code, 'name': product.name, 'variants': product.variants}
600         return res
601
602     def _product_code(self, cr, uid, ids, name, arg, context=None):
603         res = {}
604         if context is None:
605             context = {}
606         for p in self.browse(cr, uid, ids, context=context):
607             res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
608         return res
609
610     def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
611         res = {}
612         if context is None:
613             context = {}
614         for p in self.browse(cr, uid, ids, context=context):
615             data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
616             if not data['variants']:
617                 data['variants'] = p.variants
618             if not data['code']:
619                 data['code'] = p.code
620             if not data['name']:
621                 data['name'] = p.name
622             res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
623                     (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
624         return res
625
626
627     def _get_main_product_supplier(self, cr, uid, product, context=None):
628         """Determines the main (best) product supplier for ``product``,
629         returning the corresponding ``supplierinfo`` record, or False
630         if none were found. The default strategy is to select the
631         supplier with the highest priority (i.e. smallest sequence).
632
633         :param browse_record product: product to supply
634         :rtype: product.supplierinfo browse_record or False
635         """
636         sellers = [(seller_info.sequence, seller_info)
637                        for seller_info in product.seller_ids or []
638                        if seller_info and isinstance(seller_info.sequence, (int, long))]
639         return sellers and sellers[0][1] or False
640
641     def _calc_seller(self, cr, uid, ids, fields, arg, context=None):
642         result = {}
643         for product in self.browse(cr, uid, ids, context=context):
644             main_supplier = self._get_main_product_supplier(cr, uid, product, context=context)
645             result[product.id] = {
646                 'seller_info_id': main_supplier and main_supplier.id or False,
647                 'seller_delay': main_supplier.delay if main_supplier else 1,
648                 'seller_qty': main_supplier and main_supplier.qty or 0.0,
649                 'seller_id': main_supplier and main_supplier.name.id or False
650             }
651         return result
652
653
654     def _get_image(self, cr, uid, ids, name, args, context=None):
655         result = dict.fromkeys(ids, False)
656         for obj in self.browse(cr, uid, ids, context=context):
657             img = obj.image or obj.product_tmpl_id.image or False
658             result[obj.id] = tools.image_get_resized_images(img, avoid_resize_medium=True)
659         return result
660
661     def _set_image(self, cr, uid, id, name, value, args, context=None):
662         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
663
664     def _get_name_template_ids(self, cr, uid, ids, context=None):
665         result = set()
666         template_ids = self.pool.get('product.product').search(cr, uid, [('product_tmpl_id', 'in', ids)])
667         for el in template_ids:
668             result.add(el)
669         return list(result)
670
671     def _check_variants(self, cr, uid, ids, fields, args, context=None):
672         res = dict.fromkeys(ids, False)
673         for product in self.browse(cr, uid, ids, context=context):
674             no_varaints = [x for x in product.product_tmpl_id.product_variant_ids if x.variants == False]
675             res[product.id] = len(no_varaints) and True or False
676         return res
677     
678     _defaults = {
679         'active': lambda *a: 1,
680         'price_extra': lambda *a: 0.0,
681         'price_margin': lambda *a: 0.0,
682         'color': 0,
683     }
684
685     _name = "product.product"
686     _description = "Product"
687     _table = "product_product"
688     _inherits = {'product.template': 'product_tmpl_id'}
689     _inherit = ['mail.thread']
690     _order = 'default_code,name_template'
691     _columns = {
692         'qty_available': fields.function(_product_qty_available, type='float', string='Quantity On Hand'),
693         'virtual_available': fields.function(_product_virtual_available, type='float', string='Quantity Available'),
694         'incoming_qty': fields.function(_product_incoming_qty, type='float', string='Incoming'),
695         'outgoing_qty': fields.function(_product_outgoing_qty, type='float', string='Outgoing'),
696         'price': fields.function(_product_price, fnct_inv=_save_product_lst_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
697         'lst_price' : fields.function(_product_lst_price, fnct_inv=_save_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Product Price')),
698         'code': fields.function(_product_code, type='char', string='Internal Reference'),
699         'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
700         'default_code' : fields.char('Internal Reference', size=64, select=True),
701         'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
702         'variants': fields.char('Variants', size=64, translate=True),
703         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade", select=True),
704         'ean13': fields.char('EAN13 Barcode', size=13, help="International Article Number used for product identification."),
705         'packaging' : fields.one2many('product.packaging', 'product_id', 'Logistical Units', help="Gives the different ways to package the same product. This has no impact on the picking order and is mainly used if you use the EDI module."),
706         'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Product Price'), help="Price Extra: Extra price for the variant on sale price. eg. 200 price extra, 1000 + 200 = 1200."),
707         'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Product Price'), help="Price Margin: Margin in percentage on sale price for the variant. eg. 10% price margin, 1000 + 10% = 1100."),
708         'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
709         'name_template': fields.related('product_tmpl_id', 'name', string="Template Name", type='char', size=128, store={
710             'product.template': (_get_name_template_ids, ['name'], 10),
711             'product.product': (lambda self, cr, uid, ids, c=None: ids, [], 10),
712
713             }, select=True),
714         'color': fields.integer('Color Index'),
715         # image: all image fields are base64 encoded and PIL-supported
716         'image': fields.binary("Image",
717             help="This field holds the image used as image for the product, limited to 1024x1024px."),
718         'image_medium': fields.function(_get_image, fnct_inv=_set_image,
719             string="Medium-sized image", type="binary", multi="_get_image",
720             store={
721                 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
722                 'product.template': (_get_name_template_ids, ['image'], 10),
723             },
724             help="Medium-sized image of the product. It is automatically "\
725                  "resized as a 128x128px image, with aspect ratio preserved, "\
726                  "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."),
727         'image_small': fields.function(_get_image, fnct_inv=_set_image,
728             string="Small-sized image", type="binary", multi="_get_image",
729             store={
730                 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
731                 'product.template': (_get_name_template_ids, ['image'], 10),
732             },
733             help="Small-sized image of the product. It is automatically "\
734                  "resized as a 64x64px image, with aspect ratio preserved. "\
735                  "Use this field anywhere a small image is required."),
736         'seller_info_id': fields.function(_calc_seller, type='many2one', relation="product.supplierinfo", string="Supplier Info", multi="seller_info"),
737         'seller_delay': fields.function(_calc_seller, type='integer', string='Supplier Lead Time', multi="seller_info", help="This is the average delay in days between the purchase order confirmation and the reception of goods for this product and for the default supplier. It is used by the scheduler to order requests based on reordering delays."),
738         'seller_qty': fields.function(_calc_seller, type='float', string='Supplier Quantity', multi="seller_info", help="This is minimum quantity to purchase from Main Supplier."),
739         'seller_id': fields.function(_calc_seller, type='many2one', relation="res.partner", string='Main Supplier', help="Main Supplier who has highest priority in Supplier List.", multi="seller_info"),
740         'description': fields.text('Description',translate=True,
741             help="A precise description of the Product, used only for internal information purposes."),
742         'standard_variants': fields.function(_check_variants, type='boolean', string='Standard Variants', store=False),
743     }
744
745
746     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
747         #override of fields_view_get in order to replace the name field to product template
748         if context is None:
749             context = {}
750         res = super(product_product, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
751         #check the current user in group_product_variant
752         if view_type == 'form':
753             doc = etree.XML(res['arch'])
754             if self.pool['res.users'].has_group(cr, uid, 'product.group_product_variant'):
755                 for node in doc.xpath("//field[@name='name']"):
756                     node.set('invisible', '1')
757                     node.set('required', '0')
758                     setup_modifiers(node, res['fields']['name'])
759                 for node in doc.xpath("//label[@name='label_name']"):
760                     node.set('string','Product Template')
761             else:
762                 for node in doc.xpath("//field[@name='product_tmpl_id']"):
763                     node.set('required', '0')
764                     setup_modifiers(node, res['fields']['name'])
765             res['arch'] = etree.tostring(doc)
766         return res
767
768     def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
769         if uom_id and uom_po_id:
770             uom_obj=self.pool.get('product.uom')
771             uom=uom_obj.browse(cursor,user,[uom_id])[0]
772             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
773             if uom.category_id.id != uom_po.category_id.id:
774                 return {'value': {'uom_po_id': uom_id}}
775         return False
776
777     def onchange_product_tmpl_id(self, cr, uid, ids, template_id, lst_price, price_margin, price_extra, context=None):
778         res = {}
779         if template_id:
780             template = self.pool.get('product.template').browse(cr, uid, template_id, context=context)
781             no_varaints = [x for x in template.product_variant_ids if x.variants == False]
782             if not lst_price:
783                 lst_price = (template.list_price + ((template.list_price * (price_margin)) / 100)) + price_extra
784             res['value'] = {
785                 'standard_variants': len(no_varaints) and True or False, 
786                 'name': template.name,
787                 'lst_price': lst_price,
788             }
789         return res
790
791     def _check_ean_key(self, cr, uid, ids, context=None):
792         for product in self.read(cr, uid, ids, ['ean13'], context=context):
793             res = check_ean(product['ean13'])
794         return res
795
796
797     _constraints = [(_check_ean_key, 'You provided an invalid "EAN13 Barcode" reference. You may use the "Internal Reference" field instead.', ['ean13'])]
798
799     def on_order(self, cr, uid, ids, orderline, quantity):
800         pass
801
802     def name_get(self, cr, user, ids, context=None):
803         if context is None:
804             context = {}
805         if isinstance(ids, (int, long)):
806             ids = [ids]
807         if not len(ids):
808             return []
809         def _name_get(d):
810             name = d.get('name','')
811             code = d.get('default_code',False)
812             if code:
813                 name = '[%s] %s' % (code,name)
814             if d.get('variants'):
815                 name = name + ' - %s' % (d['variants'],)
816             return (d['id'], name)
817
818         partner_id = context.get('partner_id', False)
819
820         # all user don't have access to seller and partner
821         # check access and use superuser
822         self.check_access_rights(cr, user, "read")
823         self.check_access_rule(cr, user, ids, "read", context=context)
824
825         result = []
826         for product in self.browse(cr, SUPERUSER_ID, ids, context=context):
827             sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
828             if sellers:
829                 for s in sellers:
830                     mydict = {
831                               'id': product.id,
832                               'name': s.product_name or product.name,
833                               'default_code': s.product_code or product.default_code,
834                               'variants': product.variants
835                               }
836                     result.append(_name_get(mydict))
837             else:
838                 mydict = {
839                           'id': product.id,
840                           'name': product.name,
841                           'default_code': product.default_code,
842                           'variants': product.variants
843                           }
844                 result.append(_name_get(mydict))
845         return result
846
847     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
848         if not args:
849             args = []
850         if name:
851             ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
852             if not ids:
853                 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
854             if not ids:
855                 # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
856                 # on a database with thousands of matching products, due to the huge merge+unique needed for the
857                 # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
858                 # Performing a quick memory merge of ids in Python will give much better performance
859                 ids = set()
860                 ids.update(self.search(cr, user, args + ['|',('default_code',operator,name),('variants',operator,name)], limit=limit, context=context))
861                 if not limit or len(ids) < limit:
862                     # we may underrun the limit because of dupes in the results, that's fine
863                     ids.update(self.search(cr, user, args + [('name',operator,name)], limit=(limit and (limit-len(ids)) or False) , context=context))
864                 ids = list(ids)
865             if not ids:
866                 ptrn = re.compile('(\[(.*?)\])')
867                 res = ptrn.search(name)
868                 if res:
869                     ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
870         else:
871             ids = self.search(cr, user, args, limit=limit, context=context)
872         result = self.name_get(cr, user, ids, context=context)
873         return result
874
875     #
876     # Could be overrided for variants matrices prices
877     #
878     def price_get(self, cr, uid, ids, ptype='list_price', context=None):
879         products = self.browse(cr, uid, ids, context=context)
880         return self._price_get(cr, uid, products, ptype=ptype, context=context)
881
882     def _price_get(self, cr, uid, products, ptype='list_price', context=None):
883         if context is None:
884             context = {}
885
886         if 'currency_id' in context:
887             pricetype_obj = self.pool.get('product.price.type')
888             price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
889             price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
890
891         res = {}
892         product_uom_obj = self.pool.get('product.uom')
893         for product in products:
894             res[product.id] = product[ptype] or 0.0
895             if ptype == 'list_price':
896                 res[product.id] = (res[product.id] + ((res[product.id] * (product.price_margin)) / 100)) + \
897                         product.price_extra
898             if 'uom' in context:
899                 uom = product.uom_id or product.uos_id
900                 res[product.id] = product_uom_obj._compute_price(cr, uid,
901                         uom.id, res[product.id], context['uom'])
902             # Convert from price_type currency to asked one
903             if 'currency_id' in context:
904                 # Take the price_type currency from the product field
905                 # This is right cause a field cannot be in more than one currency
906                 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
907                     context['currency_id'], res[product.id],context=context)
908
909         return res
910
911     def copy(self, cr, uid, id, default=None, context=None):
912         if context is None:
913             context={}
914
915         if not default:
916             default = {}
917
918         # Craft our own `<name> (copy)` in en_US (self.copy_translation()
919         # will do the other languages).
920         context_wo_lang = context.copy()
921         context_wo_lang.pop('lang', None)
922         product = self.read(cr, uid, id, ['name', 'list_price', 'standard_price', 'categ_id', 'variants', 'product_tmpl_id'], context=context_wo_lang)
923         default = default.copy()
924         if product['variants']:
925             default.update(variants=_("%s (copy)") % (product['variants']), product_tmpl_id=product['product_tmpl_id'][0])
926         else:
927             default.update(name=_("%s (copy)") % (product['name']), list_price=product['list_price'], standard_price=product['standard_price'], categ_id=product['categ_id'][0], product_tmpl_id=None)
928
929         if context.get('variant',False):
930             fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
931                     'price_margin', 'price_extra']
932             data = self.read(cr, uid, id, fields=fields, context=context)
933             for f in fields:
934                 if f in default:
935                     data[f] = default[f]
936             data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
937                     and data['product_tmpl_id'][0]
938             del data['id']
939             return self.create(cr, uid, data)
940         else:
941             return super(product_product, self).copy(cr, uid, id, default=default,
942                     context=context)
943
944     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
945         if context is None:
946             context = {}
947         if context.get('search_default_categ_id'):
948             args.append((('categ_id', 'child_of', context['search_default_categ_id'])))
949         if context.get('search_variants'):
950             args.append(('variants', '!=', ''))
951         return super(product_product, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
952
953
954 class product_packaging(osv.osv):
955     _name = "product.packaging"
956     _description = "Packaging"
957     _rec_name = 'ean'
958     _order = 'sequence'
959     _columns = {
960         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
961         'name' : fields.text('Description', size=64),
962         'qty' : fields.float('Quantity by Package',
963             help="The total number of products you can put by pallet or box."),
964         'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
965         'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
966         'rows' : fields.integer('Number of Layers', required=True,
967             help='The number of layers on a pallet or box'),
968         'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
969         'ean' : fields.char('EAN', size=14,
970             help="The EAN code of the package unit."),
971         'code' : fields.char('Code', size=14,
972             help="The code of the transport unit."),
973         'weight': fields.float('Total Package Weight',
974             help='The weight of a full package, pallet or box.'),
975         'weight_ul': fields.float('Empty Package Weight'),
976         'height': fields.float('Height', help='The height of the package'),
977         'width': fields.float('Width', help='The width of the package'),
978         'length': fields.float('Length', help='The length of the package'),
979     }
980
981
982     def _check_ean_key(self, cr, uid, ids, context=None):
983         for pack in self.browse(cr, uid, ids, context=context):
984             res = check_ean(pack.ean)
985         return res
986
987     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
988
989     def name_get(self, cr, uid, ids, context=None):
990         if not len(ids):
991             return []
992         res = []
993         for pckg in self.browse(cr, uid, ids, context=context):
994             p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
995             p_name += pckg.ul.name
996             res.append((pckg.id,p_name))
997         return res
998
999     def _get_1st_ul(self, cr, uid, context=None):
1000         cr.execute('select id from product_ul order by id asc limit 1')
1001         res = cr.fetchone()
1002         return (res and res[0]) or False
1003
1004     _defaults = {
1005         'rows' : lambda *a : 3,
1006         'sequence' : lambda *a : 1,
1007         'ul' : _get_1st_ul,
1008     }
1009
1010     def checksum(ean):
1011         salt = '31' * 6 + '3'
1012         sum = 0
1013         for ean_part, salt_part in zip(ean, salt):
1014             sum += int(ean_part) * int(salt_part)
1015         return (10 - (sum % 10)) % 10
1016     checksum = staticmethod(checksum)
1017
1018
1019
1020 class product_supplierinfo(osv.osv):
1021     _name = "product.supplierinfo"
1022     _description = "Information about a product supplier"
1023     def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
1024         result = {}
1025         for supplier_info in self.browse(cr, uid, ids, context=context):
1026             for field in fields:
1027                 result[supplier_info.id] = {field:False}
1028             qty = supplier_info.min_qty
1029             result[supplier_info.id]['qty'] = qty
1030         return result
1031
1032     _columns = {
1033         'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
1034         'product_name': fields.char('Supplier Product Name', size=128, help="This supplier's product name will be used when printing a request for quotation. Keep empty to use the internal one."),
1035         'product_code': fields.char('Supplier Product Code', size=64, help="This supplier's product code will be used when printing a request for quotation. Keep empty to use the internal one."),
1036         'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
1037         '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."),
1038         '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."),
1039         '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."),
1040         'product_tmpl_id' : fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade', select=True),
1041         'delay' : fields.integer('Delivery Lead Time', required=True, help="Lead time in days between the confirmation of the purchase order and the reception of the products in your warehouse. Used by the scheduler for automatic computation of the purchase order planning."),
1042         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
1043         'company_id':fields.many2one('res.company','Company',select=1),
1044     }
1045     _defaults = {
1046         'qty': lambda *a: 0.0,
1047         'sequence': lambda *a: 1,
1048         'delay': lambda *a: 1,
1049         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
1050     }
1051     def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
1052         """
1053         Calculate price from supplier pricelist.
1054         @param supplier_ids: Ids of res.partner object.
1055         @param product_id: Id of product.
1056         @param product_qty: specify quantity to purchase.
1057         """
1058         if type(supplier_ids) in (int,long,):
1059             supplier_ids = [supplier_ids]
1060         res = {}
1061         product_pool = self.pool.get('product.product')
1062         partner_pool = self.pool.get('res.partner')
1063         pricelist_pool = self.pool.get('product.pricelist')
1064         currency_pool = self.pool.get('res.currency')
1065         currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
1066         # Compute price from standard price of product
1067         product_price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
1068         product = product_pool.browse(cr, uid, product_id, context=context)
1069         for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
1070             price = product_price
1071             # Compute price from Purchase pricelist of supplier
1072             pricelist_id = supplier.property_product_pricelist_purchase.id
1073             if pricelist_id:
1074                 price = pricelist_pool.price_get(cr, uid, [pricelist_id], product_id, product_qty, context=context).setdefault(pricelist_id, 0)
1075                 price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
1076
1077             # Compute price from supplier pricelist which are in Supplier Information
1078             supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_tmpl_id','=',product.product_tmpl_id.id)])
1079             if supplier_info_ids:
1080                 cr.execute('SELECT * ' \
1081                     'FROM pricelist_partnerinfo ' \
1082                     'WHERE suppinfo_id IN %s' \
1083                     'AND min_quantity <= %s ' \
1084                     'ORDER BY min_quantity DESC LIMIT 1', (tuple(supplier_info_ids),product_qty,))
1085                 res2 = cr.dictfetchone()
1086                 if res2:
1087                     price = res2['price']
1088             res[supplier.id] = price
1089         return res
1090     _order = 'sequence'
1091
1092
1093 class pricelist_partnerinfo(osv.osv):
1094     _name = 'pricelist.partnerinfo'
1095     _columns = {
1096         'name': fields.char('Description', size=64),
1097         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
1098         '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."),
1099         '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"),
1100     }
1101     _order = 'min_quantity asc'
1102
1103 class res_currency(osv.osv):
1104     _inherit = 'res.currency'
1105
1106     def _check_main_currency_rounding(self, cr, uid, ids, context=None):
1107         cr.execute('SELECT digits FROM decimal_precision WHERE name like %s',('Account',))
1108         digits = cr.fetchone()
1109         if digits and len(digits):
1110             digits = digits[0]
1111             main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
1112             for currency_id in ids:
1113                 if currency_id == main_currency.id:
1114                     if main_currency.rounding < 10 ** -digits:
1115                         return False
1116         return True
1117
1118     _constraints = [
1119         (_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']),
1120     ]
1121
1122 class decimal_precision(osv.osv):
1123     _inherit = 'decimal.precision'
1124
1125     def _check_main_currency_rounding(self, cr, uid, ids, context=None):
1126         cr.execute('SELECT id, digits FROM decimal_precision WHERE name like %s',('Account',))
1127         res = cr.fetchone()
1128         if res and len(res):
1129             account_precision_id, digits = res
1130             main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
1131             for decimal_precision in ids:
1132                 if decimal_precision == account_precision_id:
1133                     if main_currency.rounding < 10 ** -digits:
1134                         return False
1135         return True
1136
1137     _constraints = [
1138         (_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']),
1139     ]
1140
1141 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: