83e57e364894961e03cca2330c49575b34187fc5
[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', 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', 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', 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', 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     _constraints = [
269         (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
270     ]
271     def child_get(self, cr, uid, ids):
272         return [ids]
273
274
275 class product_public_category(osv.osv):
276     _name = "product.public.category"
277     _description = "Public Category"
278     _order = "sequence, name"
279
280     _constraints = [
281         (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
282     ]
283
284     def name_get(self, cr, uid, ids, context=None):
285         if not len(ids):
286             return []
287         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
288         res = []
289         for record in reads:
290             name = record['name']
291             if record['parent_id']:
292                 name = record['parent_id'][1]+' / '+name
293             res.append((record['id'], name))
294         return res
295
296     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
297         res = self.name_get(cr, uid, ids, context=context)
298         return dict(res)
299
300     def _get_image(self, cr, uid, ids, name, args, context=None):
301         result = dict.fromkeys(ids, False)
302         for obj in self.browse(cr, uid, ids, context=context):
303             result[obj.id] = tools.image_get_resized_images(obj.image)
304         return result
305     
306     def _set_image(self, cr, uid, id, name, value, args, context=None):
307         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
308
309     _columns = {
310         'name': fields.char('Name', required=True, translate=True),
311         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
312         'parent_id': fields.many2one('product.public.category','Parent Category', select=True),
313         'child_id': fields.one2many('product.public.category', 'parent_id', string='Children Categories'),
314         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
315         
316         # NOTE: there is no 'default image', because by default we don't show thumbnails for categories. However if we have a thumbnail
317         # for at least one category, then we display a default image on the other, so that the buttons have consistent styling.
318         # In this case, the default image is set by the js code.
319         # NOTE2: image: all image fields are base64 encoded and PIL-supported
320         'image': fields.binary("Image",
321             help="This field holds the image used as image for the cateogry, limited to 1024x1024px."),
322         'image_medium': fields.function(_get_image, fnct_inv=_set_image,
323             string="Medium-sized image", type="binary", multi="_get_image",
324             store={
325                 'product.public.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
326             },
327             help="Medium-sized image of the category. It is automatically "\
328                  "resized as a 128x128px image, with aspect ratio preserved. "\
329                  "Use this field in form views or some kanban views."),
330         'image_small': fields.function(_get_image, fnct_inv=_set_image,
331             string="Smal-sized image", type="binary", multi="_get_image",
332             store={
333                 'product.public.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
334             },
335             help="Small-sized image of the category. It is automatically "\
336                  "resized as a 64x64px image, with aspect ratio preserved. "\
337                  "Use this field anywhere a small image is required."),
338     }
339
340
341 #----------------------------------------------------------
342 # Products
343 #----------------------------------------------------------
344 class product_template(osv.osv):
345     _name = "product.template"
346     _inherit = ['mail.thread']
347     _description = "Product Template"
348
349     def _get_image(self, cr, uid, ids, name, args, context=None):
350         result = dict.fromkeys(ids, False)
351         for obj in self.browse(cr, uid, ids, context=context):
352             result[obj.id] = tools.image_get_resized_images(obj.image, avoid_resize_medium=True)
353         return result
354
355     def _set_image(self, cr, uid, id, name, value, args, context=None):
356         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
357
358     _columns = {
359         'name': fields.char('Name', required=True, translate=True, select=True),
360         'product_manager': fields.many2one('res.users','Product Manager'),
361         'description': fields.text('Description',translate=True,
362             help="A precise description of the Product, used only for internal information purposes."),
363         'description_purchase': fields.text('Purchase Description',translate=True,
364             help="A description of the Product that you want to communicate to your suppliers. "
365                  "This description will be copied to every Purchase Order, Reception and Supplier Invoice/Refund."),
366         'description_sale': fields.text('Sale Description',translate=True,
367             help="A description of the Product that you want to communicate to your customers. "
368                  "This description will be copied to every Sale Order, Delivery Order and Customer Invoice/Refund"),
369         '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."),
370         '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."),
371         'rental': fields.boolean('Can be Rent'),
372         'categ_id': fields.many2one('product.category','Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
373         '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)."),
374         '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."),
375         '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"),
376         'volume': fields.float('Volume', help="The volume in m3."),
377         'weight': fields.float('Gross Weight', digits_compute=dp.get_precision('Stock Weight'), help="The gross weight in Kg."),
378         'weight_net': fields.float('Net Weight', digits_compute=dp.get_precision('Stock Weight'), help="The net weight in Kg."),
379         'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
380             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."),
381         'warranty': fields.float('Warranty'),
382         'sale_ok': fields.boolean('Can be Sold', help="Specify if the product can be selected in a sales order line."),
383         'state': fields.selection([('',''),
384             ('draft', 'In Development'),
385             ('sellable','Normal'),
386             ('end','End of Lifecycle'),
387             ('obsolete','Obsolete')], 'Status'),
388         'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
389         '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."),
390         'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
391             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.'),
392         'uos_coeff': fields.float('Unit of Measure -> UOS Coeff', digits_compute= dp.get_precision('Product UoS'),
393             help='Coefficient to convert default Unit of Measure to Unit of Sale\n'
394             ' uos = uom * coeff'),
395         'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type'),
396         'seller_ids': fields.one2many('product.supplierinfo', 'product_tmpl_id', 'Supplier'),
397         'company_id': fields.many2one('res.company', 'Company', select=1),
398         # image: all image fields are base64 encoded and PIL-supported
399         'image': fields.binary("Image",
400             help="This field holds the image used as image for the product, limited to 1024x1024px."),
401         'image_medium': fields.function(_get_image, fnct_inv=_set_image,
402             string="Medium-sized image", type="binary", multi="_get_image",
403             store={
404                 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
405             },
406             help="Medium-sized image of the product. It is automatically "\
407                  "resized as a 128x128px image, with aspect ratio preserved, "\
408                  "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."),
409         'image_small': fields.function(_get_image, fnct_inv=_set_image,
410             string="Small-sized image", type="binary", multi="_get_image",
411             store={
412                 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
413             },
414             help="Small-sized image of the product. It is automatically "\
415                  "resized as a 64x64px image, with aspect ratio preserved. "\
416                  "Use this field anywhere a small image is required."),
417         'product_variant_ids': fields.one2many('product.product', 'product_tmpl_id', 'Product Variants', required=True),
418     }
419
420     def _get_uom_id(self, cr, uid, *args):
421         cr.execute('select id from product_uom order by id limit 1')
422         res = cr.fetchone()
423         return res and res[0] or False
424
425     def _default_category(self, cr, uid, context=None):
426         if context is None:
427             context = {}
428         if 'categ_id' in context and context['categ_id']:
429             return context['categ_id']
430         md = self.pool.get('ir.model.data')
431         res = False
432         try:
433             res = md.get_object_reference(cr, uid, 'product', 'product_category_all')[1]
434         except ValueError:
435             res = False
436         return res
437
438     def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
439         if uom_id:
440             return {'value': {'uom_po_id': uom_id}}
441         return {}
442
443     def write(self, cr, uid, ids, vals, context=None):
444         if 'uom_po_id' in vals:
445             new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_po_id'], context=context)
446             for product in self.browse(cr, uid, ids, context=context):
447                 old_uom = product.uom_po_id
448                 if old_uom.category_id.id != new_uom.category_id.id:
449                     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,))
450         return super(product_template, self).write(cr, uid, ids, vals, context=context)
451
452     def copy(self, cr, uid, id, default=None, context=None):
453         if default is None:
454             default = {}
455         template = self.browse(cr, uid, id, context=context)
456         default['name'] = _("%s (copy)") % (template['name'])
457         return super(product_template, self).copy(cr, uid, id, default=default, context=context)
458
459     _defaults = {
460         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
461         'list_price': 1,
462         'cost_method': 'standard',
463         'standard_price': 0.0,
464         'sale_ok': 1,
465         'produce_delay': 1,
466         'uom_id': _get_uom_id,
467         'uom_po_id': _get_uom_id,
468         'uos_coeff' : 1.0,
469         'mes_type' : 'fixed',
470         'categ_id' : _default_category,
471         'type' : 'consu',
472     }
473
474     def _check_uom(self, cursor, user, ids, context=None):
475         for product in self.browse(cursor, user, ids, context=context):
476             if product.uom_id.category_id.id != product.uom_po_id.category_id.id:
477                 return False
478         return True
479
480     def _check_uos(self, cursor, user, ids, context=None):
481         for product in self.browse(cursor, user, ids, context=context):
482             if product.uos_id \
483                     and product.uos_id.category_id.id \
484                     == product.uom_id.category_id.id:
485                 return False
486         return True
487
488     _constraints = [
489         (_check_uom, 'Error: The default Unit of Measure and the purchase Unit of Measure must be in the same category.', ['uom_id']),
490     ]
491
492     def name_get(self, cr, user, ids, context=None):
493         if context is None:
494             context = {}
495         if 'partner_id' in context:
496             pass
497         return super(product_template, self).name_get(cr, user, ids, context)
498
499
500 class product_product(osv.osv):
501     _name = "product.product"
502     _description = "Product"
503     _inherits = {'product.template': 'product_tmpl_id'}
504     _inherit = ['mail.thread']
505     _order = 'default_code,name_template'
506
507     def view_header_get(self, cr, uid, view_id, view_type, context=None):
508         if context is None:
509             context = {}
510         res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
511         if (context.get('categ_id', False)):
512             return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
513         return res
514
515     def _product_price(self, cr, uid, ids, name, arg, context=None):
516         plobj = self.pool.get('product.pricelist')
517         res = {}
518         if context is None:
519             context = {}
520         quantity = context.get('quantity') or 1.0
521         pricelist = context.get('pricelist', False)
522         partner = context.get('partner', False)
523         if pricelist:
524             # Support context pricelists specified as display_name or ID for compatibility
525             if isinstance(pricelist, basestring):
526                 pricelist_ids = plobj.name_search(
527                     cr, uid, pricelist, operator='=', context=context, limit=1)
528                 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
529
530             products = self.browse(cr, uid, ids, context=context)
531             qtys = map(lambda x: (x, quantity, partner), products)
532             pl = plobj.browse(cr, uid, pricelist, context=context)
533             price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
534             for id in ids:
535                 res[id] = price.get(id, 0.0)
536         for id in ids:
537             res.setdefault(id, 0.0)
538         return res
539
540     def _get_product_available_func(states, what):
541         def _product_available(self, cr, uid, ids, name, arg, context=None):
542             return {}.fromkeys(ids, 0.0)
543         return _product_available
544
545     _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
546     _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
547     _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
548     _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
549
550     def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
551         res = dict.fromkeys(ids, 0.0)
552         product_uom_obj = self.pool.get('product.uom')
553
554         # retrieve pricelist
555         pricelist = None
556         if context.get('pricelist'):
557             pricelist = self.pool['product.pricelist'].browse(cr, uid, context.get('pricelist'), context=context)
558             base_currency = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id
559
560         for product in self.browse(cr, uid, ids, context=context):
561             if 'uom' in context:
562                 uom = product.uos_id or product.uom_id
563                 res[product.id] = product_uom_obj._compute_price(cr, uid,
564                         uom.id, product.list_price, context['uom'])
565             else:
566                 res[product.id] = product.list_price
567             res[product.id] = (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
568             # update the result, according to the eventual pricelist currency
569             if pricelist and pricelist.currency_id:
570                 res[product.id] = self.pool['res.currency'].compute(
571                     cr, uid, base_currency.id, pricelist.currency_id.id, res[product.id], round=False, context=context)
572         return res
573
574     def _save_product_lst_price(self, cr, uid, product_id, field_name, field_value, arg, context=None):
575         field_value = field_value or 0.0
576         product = self.browse(cr, uid, product_id, context=context)
577         list_price = (field_value - product.price_extra) / (product.price_margin or 1.0)
578         return self.write(cr, uid, [product_id], {'list_price': list_price}, context=context)
579
580
581     def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
582         for supinfo in product.seller_ids:
583             if supinfo.name.id == partner_id:
584                 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name, 'variants': ''}
585         res = {'code': product.default_code, 'name': product.name, 'variants': product.variants}
586         return res
587
588     def _product_code(self, cr, uid, ids, name, arg, context=None):
589         res = {}
590         if context is None:
591             context = {}
592         for p in self.browse(cr, uid, ids, context=context):
593             res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
594         return res
595
596     def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
597         res = {}
598         if context is None:
599             context = {}
600         for p in self.browse(cr, uid, ids, context=context):
601             data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
602             if not data['variants']:
603                 data['variants'] = p.variants
604             if not data['code']:
605                 data['code'] = p.code
606             if not data['name']:
607                 data['name'] = p.name
608             res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
609                     (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
610         return res
611
612     def _is_only_child(self, cr, uid, ids, name, arg, context=None):
613         res = dict.fromkeys(ids, True)
614         for product in self.browse(cr, uid, ids, context=context):
615             if product.product_tmpl_id and len(product.product_tmpl_id.product_variant_ids) > 1:
616                 res[product.id] = False
617         return res
618
619     def _get_main_product_supplier(self, cr, uid, product, context=None):
620         """Determines the main (best) product supplier for ``product``,
621         returning the corresponding ``supplierinfo`` record, or False
622         if none were found. The default strategy is to select the
623         supplier with the highest priority (i.e. smallest sequence).
624
625         :param browse_record product: product to supply
626         :rtype: product.supplierinfo browse_record or False
627         """
628         sellers = [(seller_info.sequence, seller_info)
629                        for seller_info in product.seller_ids or []
630                        if seller_info and isinstance(seller_info.sequence, (int, long))]
631         return sellers and sellers[0][1] or False
632
633     def _calc_seller(self, cr, uid, ids, fields, arg, context=None):
634         result = {}
635         for product in self.browse(cr, uid, ids, context=context):
636             main_supplier = self._get_main_product_supplier(cr, uid, product, context=context)
637             result[product.id] = {
638                 'seller_info_id': main_supplier and main_supplier.id or False,
639                 'seller_delay': main_supplier.delay if main_supplier else 1,
640                 'seller_qty': main_supplier and main_supplier.qty or 0.0,
641                 'seller_id': main_supplier and main_supplier.name.id or False
642             }
643         return result
644
645     def _get_name_template_ids(self, cr, uid, ids, context=None):
646         result = set()
647         template_ids = self.pool.get('product.product').search(cr, uid, [('product_tmpl_id', 'in', ids)])
648         for el in template_ids:
649             result.add(el)
650         return list(result)
651
652     _columns = {
653         'qty_available': fields.function(_product_qty_available, type='float', string='Quantity On Hand'),
654         'virtual_available': fields.function(_product_virtual_available, type='float', string='Quantity Available'),
655         'incoming_qty': fields.function(_product_incoming_qty, type='float', string='Incoming'),
656         'outgoing_qty': fields.function(_product_outgoing_qty, type='float', string='Outgoing'),
657         'price': fields.function(_product_price, fnct_inv=_save_product_lst_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
658         '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')),
659         'code': fields.function(_product_code, type='char', string='Internal Reference'),
660         'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
661         'default_code' : fields.char('Internal Reference', select=True),
662         'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
663         'variants': fields.char('Variants', translate=True),
664         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade", select=True),
665         'is_only_child': fields.function(
666             _is_only_child, type='boolean', string='Sole child of the parent template'),
667         'ean13': fields.char('EAN13 Barcode', size=13, help="International Article Number used for product identification."),
668         '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."),
669         '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."),
670         '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."),
671         'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
672         'name_template': fields.related('product_tmpl_id', 'name', string="Template Name", type='char', store={
673             'product.template': (_get_name_template_ids, ['name'], 10),
674             'product.product': (lambda self, cr, uid, ids, c=None: ids, [], 10),
675         }, select=True),
676         'color': fields.integer('Color Index'),
677         'seller_info_id': fields.function(_calc_seller, type='many2one', relation="product.supplierinfo", string="Supplier Info", multi="seller_info"),
678         '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."),
679         'seller_qty': fields.function(_calc_seller, type='float', string='Supplier Quantity', multi="seller_info", help="This is minimum quantity to purchase from Main Supplier."),
680         '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"),
681     }
682
683     _defaults = {
684         'active': lambda *a: 1,
685         'price_extra': lambda *a: 0.0,
686         'price_margin': lambda *a: 1.0,
687         'color': 0,
688         'is_only_child': True,
689     }
690
691     def unlink(self, cr, uid, ids, context=None):
692         unlink_ids = []
693         unlink_product_tmpl_ids = []
694         for product in self.browse(cr, uid, ids, context=context):
695             tmpl_id = product.product_tmpl_id.id
696             # Check if the product is last product of this template
697             other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
698             if not other_product_ids:
699                 unlink_product_tmpl_ids.append(tmpl_id)
700             unlink_ids.append(product.id)
701         res = super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
702         # delete templates after calling super, as deleting template could lead to deleting
703         # products due to ondelete='cascade'
704         self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
705         return res
706
707     def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
708         if uom_id and uom_po_id:
709             uom_obj=self.pool.get('product.uom')
710             uom=uom_obj.browse(cursor,user,[uom_id])[0]
711             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
712             if uom.category_id.id != uom_po.category_id.id:
713                 return {'value': {'uom_po_id': uom_id}}
714         return False
715
716     def _check_ean_key(self, cr, uid, ids, context=None):
717         for product in self.read(cr, uid, ids, ['ean13'], context=context):
718             res = check_ean(product['ean13'])
719         return res
720
721     _constraints = [(_check_ean_key, 'You provided an invalid "EAN13 Barcode" reference. You may use the "Internal Reference" field instead.', ['ean13'])]
722
723     def on_order(self, cr, uid, ids, orderline, quantity):
724         pass
725
726     def name_get(self, cr, user, ids, context=None):
727         if context is None:
728             context = {}
729         if isinstance(ids, (int, long)):
730             ids = [ids]
731         if not len(ids):
732             return []
733         def _name_get(d):
734             name = d.get('name','')
735             code = d.get('default_code',False)
736             if code:
737                 name = '[%s] %s' % (code,name)
738             if d.get('variants'):
739                 name = name + ' - %s' % (d['variants'],)
740             return (d['id'], name)
741
742         partner_id = context.get('partner_id', False)
743
744         # all user don't have access to seller and partner
745         # check access and use superuser
746         self.check_access_rights(cr, user, "read")
747         self.check_access_rule(cr, user, ids, "read", context=context)
748
749         result = []
750         for product in self.browse(cr, SUPERUSER_ID, ids, context=context):
751             sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
752             if sellers:
753                 for s in sellers:
754                     mydict = {
755                               'id': product.id,
756                               'name': s.product_name or product.name,
757                               'default_code': s.product_code or product.default_code,
758                               'variants': product.variants
759                               }
760                     result.append(_name_get(mydict))
761             else:
762                 mydict = {
763                           'id': product.id,
764                           'name': product.name,
765                           'default_code': product.default_code,
766                           'variants': product.variants
767                           }
768                 result.append(_name_get(mydict))
769         return result
770
771     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
772         if not args:
773             args = []
774         if name:
775             ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
776             if not ids:
777                 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
778             if not ids:
779                 # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
780                 # on a database with thousands of matching products, due to the huge merge+unique needed for the
781                 # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
782                 # Performing a quick memory merge of ids in Python will give much better performance
783                 ids = set(self.search(cr, user, args + [('default_code', operator, name)], limit=limit, context=context))
784                 if not limit or len(ids) < limit:
785                     # we may underrun the limit because of dupes in the results, that's fine
786                     limit2 = (limit - len(ids)) if limit else False
787                     ids.update(self.search(cr, user, args + [('name', operator, name)], limit=limit2, context=context))
788                 ids = list(ids)
789             if not ids:
790                 ptrn = re.compile('(\[(.*?)\])')
791                 res = ptrn.search(name)
792                 if res:
793                     ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
794         else:
795             ids = self.search(cr, user, args, limit=limit, context=context)
796         result = self.name_get(cr, user, ids, context=context)
797         return result
798
799     #
800     # Could be overrided for variants matrices prices
801     #
802     def price_get(self, cr, uid, ids, ptype='list_price', context=None):
803         products = self.browse(cr, uid, ids, context=context)
804         return self._price_get(cr, uid, products, ptype=ptype, context=context)
805
806     def _price_get(self, cr, uid, products, ptype='list_price', context=None):
807         if context is None:
808             context = {}
809
810         if 'currency_id' in context:
811             pricetype_obj = self.pool.get('product.price.type')
812             price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
813             price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
814
815         res = {}
816         product_uom_obj = self.pool.get('product.uom')
817         for product in products:
818             res[product.id] = product[ptype] or 0.0
819             if ptype == 'list_price':
820                 res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
821                         product.price_extra
822             if 'uom' in context:
823                 uom = product.uom_id or product.uos_id
824                 res[product.id] = product_uom_obj._compute_price(cr, uid,
825                         uom.id, res[product.id], context['uom'])
826             # Convert from price_type currency to asked one
827             if 'currency_id' in context:
828                 # Take the price_type currency from the product field
829                 # This is right cause a field cannot be in more than one currency
830                 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
831                     context['currency_id'], res[product.id],context=context)
832
833         return res
834
835     def copy(self, cr, uid, id, default=None, context=None):
836         context = context or {}
837         default = dict(default or {})
838
839         # Craft our own `<name> (copy)` in en_US (self.copy_translation()
840         # will do the other languages).
841         context_wo_lang = dict(context or {})
842         context_wo_lang.pop('lang', None)
843         product = self.browse(cr, uid, id, context_wo_lang)
844         if context.get('variant'):
845             # if we copy a variant or create one, we keep the same template
846             default['product_tmpl_id'] = product.product_tmpl_id.id
847         elif 'name' not in default:
848             default['name'] = _("%s (copy)") % (product.name,)
849
850         return super(product_product, self).copy(cr, uid, id, default=default, context=context)
851
852     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
853         if context is None:
854             context = {}
855         if context.get('search_default_categ_id'):
856             args.append((('categ_id', 'child_of', context['search_default_categ_id'])))
857         return super(product_product, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
858
859     def open_product_template(self, cr, uid, ids, context=None):
860         """ Utility method used to add an "Open Template" button in product views """
861         product = self.browse(cr, uid, ids[0], context=context)
862         return {'type': 'ir.actions.act_window',
863                 'res_model': 'product.template',
864                 'view_mode': 'form',
865                 'res_id': product.product_tmpl_id.id,
866                 'target': 'new'}
867
868
869 class product_packaging(osv.osv):
870     _name = "product.packaging"
871     _description = "Packaging"
872     _rec_name = 'ean'
873     _order = 'sequence'
874     _columns = {
875         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
876         'name' : fields.text('Description'),
877         'qty' : fields.float('Quantity by Package',
878             help="The total number of products you can put by pallet or box."),
879         'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
880         'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
881         'rows' : fields.integer('Number of Layers', required=True,
882             help='The number of layers on a pallet or box'),
883         'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
884         'ean' : fields.char('EAN', size=14, help="The EAN code of the package unit."),
885         'code' : fields.char('Code', help="The code of the transport unit."),
886         'weight': fields.float('Total Package Weight',
887             help='The weight of a full package, pallet or box.'),
888         'weight_ul': fields.float('Empty Package Weight'),
889         'height': fields.float('Height', help='The height of the package'),
890         'width': fields.float('Width', help='The width of the package'),
891         'length': fields.float('Length', help='The length of the package'),
892     }
893
894
895     def _check_ean_key(self, cr, uid, ids, context=None):
896         for pack in self.browse(cr, uid, ids, context=context):
897             res = check_ean(pack.ean)
898         return res
899
900     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
901
902     def name_get(self, cr, uid, ids, context=None):
903         if not len(ids):
904             return []
905         res = []
906         for pckg in self.browse(cr, uid, ids, context=context):
907             p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
908             p_name += pckg.ul.name
909             res.append((pckg.id,p_name))
910         return res
911
912     def _get_1st_ul(self, cr, uid, context=None):
913         cr.execute('select id from product_ul order by id asc limit 1')
914         res = cr.fetchone()
915         return (res and res[0]) or False
916
917     _defaults = {
918         'rows' : lambda *a : 3,
919         'sequence' : lambda *a : 1,
920         'ul' : _get_1st_ul,
921     }
922
923     def checksum(ean):
924         salt = '31' * 6 + '3'
925         sum = 0
926         for ean_part, salt_part in zip(ean, salt):
927             sum += int(ean_part) * int(salt_part)
928         return (10 - (sum % 10)) % 10
929     checksum = staticmethod(checksum)
930
931
932
933 class product_supplierinfo(osv.osv):
934     _name = "product.supplierinfo"
935     _description = "Information about a product supplier"
936     def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
937         result = {}
938         for supplier_info in self.browse(cr, uid, ids, context=context):
939             for field in fields:
940                 result[supplier_info.id] = {field:False}
941             qty = supplier_info.min_qty
942             result[supplier_info.id]['qty'] = qty
943         return result
944
945     _columns = {
946         'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
947         'product_name': fields.char('Supplier Product Name', help="This supplier's product name will be used when printing a request for quotation. Keep empty to use the internal one."),
948         'product_code': fields.char('Supplier Product Code', help="This supplier's product code will be used when printing a request for quotation. Keep empty to use the internal one."),
949         'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
950         '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."),
951         '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."),
952         '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."),
953         'product_tmpl_id' : fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade', select=True, oldname='product_id'),
954         '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."),
955         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
956         'company_id':fields.many2one('res.company','Company',select=1),
957     }
958     _defaults = {
959         'qty': lambda *a: 0.0,
960         'sequence': lambda *a: 1,
961         'delay': lambda *a: 1,
962         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
963     }
964     def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
965         """
966         Calculate price from supplier pricelist.
967         @param supplier_ids: Ids of res.partner object.
968         @param product_id: Id of product.
969         @param product_qty: specify quantity to purchase.
970         """
971         if type(supplier_ids) in (int,long,):
972             supplier_ids = [supplier_ids]
973         res = {}
974         product_pool = self.pool.get('product.product')
975         partner_pool = self.pool.get('res.partner')
976         pricelist_pool = self.pool.get('product.pricelist')
977         currency_pool = self.pool.get('res.currency')
978         currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
979         # Compute price from standard price of product
980         product_price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
981         product = product_pool.browse(cr, uid, product_id, context=context)
982         for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
983             price = product_price
984             # Compute price from Purchase pricelist of supplier
985             pricelist_id = supplier.property_product_pricelist_purchase.id
986             if pricelist_id:
987                 price = pricelist_pool.price_get(cr, uid, [pricelist_id], product_id, product_qty, context=context).setdefault(pricelist_id, 0)
988                 price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
989
990             # Compute price from supplier pricelist which are in Supplier Information
991             supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_tmpl_id','=',product.product_tmpl_id.id)])
992             if supplier_info_ids:
993                 cr.execute('SELECT * ' \
994                     'FROM pricelist_partnerinfo ' \
995                     'WHERE suppinfo_id IN %s' \
996                     'AND min_quantity <= %s ' \
997                     'ORDER BY min_quantity DESC LIMIT 1', (tuple(supplier_info_ids),product_qty,))
998                 res2 = cr.dictfetchone()
999                 if res2:
1000                     price = res2['price']
1001             res[supplier.id] = price
1002         return res
1003     _order = 'sequence'
1004
1005
1006 class pricelist_partnerinfo(osv.osv):
1007     _name = 'pricelist.partnerinfo'
1008     _columns = {
1009         'name': fields.char('Description'),
1010         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
1011         '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."),
1012         '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"),
1013     }
1014     _order = 'min_quantity asc'
1015
1016 class res_currency(osv.osv):
1017     _inherit = 'res.currency'
1018
1019     def _check_main_currency_rounding(self, cr, uid, ids, context=None):
1020         cr.execute('SELECT digits FROM decimal_precision WHERE name like %s',('Account',))
1021         digits = cr.fetchone()
1022         if digits and len(digits):
1023             digits = digits[0]
1024             main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
1025             for currency_id in ids:
1026                 if currency_id == main_currency.id:
1027                     if main_currency.rounding < 10 ** -digits:
1028                         return False
1029         return True
1030
1031     _constraints = [
1032         (_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']),
1033     ]
1034
1035 class decimal_precision(osv.osv):
1036     _inherit = 'decimal.precision'
1037
1038     def _check_main_currency_rounding(self, cr, uid, ids, context=None):
1039         cr.execute('SELECT id, digits FROM decimal_precision WHERE name like %s',('Account',))
1040         res = cr.fetchone()
1041         if res and len(res):
1042             account_precision_id, digits = res
1043             main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
1044             for decimal_precision in ids:
1045                 if decimal_precision == account_precision_id:
1046                     if main_currency.rounding < 10 ** -digits:
1047                         return False
1048         return True
1049
1050     _constraints = [
1051         (_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']),
1052     ]
1053
1054 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: