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