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