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