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