[FIX] product: fix _set_price_extra method
[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 import time
25 from _common import ceiling
26
27 from openerp import SUPERUSER_ID
28 from openerp import tools
29 from openerp.osv import osv, fields
30 from openerp.tools.translate import _
31 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
32 import psycopg2
33
34 import openerp.addons.decimal_precision as dp
35
36 def ean_checksum(eancode):
37     """returns the checksum of an ean string of length 13, returns -1 if the string has the wrong length"""
38     if len(eancode) != 13:
39         return -1
40     oddsum=0
41     evensum=0
42     total=0
43     eanvalue=eancode
44     reversevalue = eanvalue[::-1]
45     finalean=reversevalue[1:]
46
47     for i in range(len(finalean)):
48         if i % 2 == 0:
49             oddsum += int(finalean[i])
50         else:
51             evensum += int(finalean[i])
52     total=(oddsum * 3) + evensum
53
54     check = int(10 - math.ceil(total % 10.0)) %10
55     return check
56
57 def check_ean(eancode):
58     """returns True if eancode is a valid ean13 string, or null"""
59     if not eancode:
60         return True
61     if len(eancode) != 13:
62         return False
63     try:
64         int(eancode)
65     except:
66         return False
67     return ean_checksum(eancode) == int(eancode[-1])
68
69 def sanitize_ean13(ean13):
70     """Creates and returns a valid ean13 from an invalid one"""
71     if not ean13:
72         return "0000000000000"
73     ean13 = re.sub("[A-Za-z]","0",ean13);
74     ean13 = re.sub("[^0-9]","",ean13);
75     ean13 = ean13[:13]
76     if len(ean13) < 13:
77         ean13 = ean13 + '0' * (13-len(ean13))
78     return ean13[:-1] + str(ean_checksum(ean13))
79
80 #----------------------------------------------------------
81 # UOM
82 #----------------------------------------------------------
83
84 class product_uom_categ(osv.osv):
85     _name = 'product.uom.categ'
86     _description = 'Product uom categ'
87     _columns = {
88         'name': fields.char('Name', required=True, translate=True),
89     }
90
91 class product_uom(osv.osv):
92     _name = 'product.uom'
93     _description = 'Product Unit of Measure'
94
95     def _compute_factor_inv(self, factor):
96         return factor and (1.0 / factor) or 0.0
97
98     def _factor_inv(self, cursor, user, ids, name, arg, context=None):
99         res = {}
100         for uom in self.browse(cursor, user, ids, context=context):
101             res[uom.id] = self._compute_factor_inv(uom.factor)
102         return res
103
104     def _factor_inv_write(self, cursor, user, id, name, value, arg, context=None):
105         return self.write(cursor, user, id, {'factor': self._compute_factor_inv(value)}, context=context)
106
107     def name_create(self, cr, uid, name, context=None):
108         """ The UoM category and factor are required, so we'll have to add temporary values
109             for imported UoMs """
110         uom_categ = self.pool.get('product.uom.categ')
111         # look for the category based on the english name, i.e. no context on purpose!
112         # TODO: should find a way to have it translated but not created until actually used
113         categ_misc = 'Unsorted/Imported Units'
114         categ_id = uom_categ.search(cr, uid, [('name', '=', categ_misc)])
115         if categ_id:
116             categ_id = categ_id[0]
117         else:
118             categ_id, _ = uom_categ.name_create(cr, uid, categ_misc)
119         uom_id = self.create(cr, uid, {self._rec_name: name,
120                                        'category_id': categ_id,
121                                        'factor': 1})
122         return self.name_get(cr, uid, [uom_id], context=context)[0]
123
124     def create(self, cr, uid, data, context=None):
125         if 'factor_inv' in data:
126             if data['factor_inv'] != 1:
127                 data['factor'] = self._compute_factor_inv(data['factor_inv'])
128             del(data['factor_inv'])
129         return super(product_uom, self).create(cr, uid, data, context)
130
131     _order = "name"
132     _columns = {
133         'name': fields.char('Unit of Measure', required=True, translate=True),
134         'category_id': fields.many2one('product.uom.categ', 'Product Category', required=True, ondelete='cascade',
135             help="Conversion between Units of Measure can only occur if they belong to the same category. The conversion will be made based on the ratios."),
136         'factor': fields.float('Ratio', required=True,digits=(12, 12),
137             help='How much bigger or smaller this unit is compared to the reference Unit of Measure for this category:\n'\
138                     '1 * (reference unit) = ratio * (this unit)'),
139         'factor_inv': fields.function(_factor_inv, digits=(12,12),
140             fnct_inv=_factor_inv_write,
141             string='Bigger Ratio',
142             help='How many times this Unit of Measure is bigger than the reference Unit of Measure in this category:\n'\
143                     '1 * (this unit) = ratio * (reference unit)', required=True),
144         'rounding': fields.float('Rounding Precision', digits_compute=dp.get_precision('Product Unit of Measure'), required=True,
145             help="The computed quantity will be a multiple of this value. "\
146                  "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece."),
147         'active': fields.boolean('Active', help="By unchecking the active field you can disable a unit of measure without deleting it."),
148         'uom_type': fields.selection([('bigger','Bigger than the reference Unit of Measure'),
149                                       ('reference','Reference Unit of Measure for this category'),
150                                       ('smaller','Smaller than the reference Unit of Measure')],'Type', required=1),
151     }
152
153     _defaults = {
154         'active': 1,
155         'rounding': 0.01,
156         'uom_type': 'reference',
157     }
158
159     _sql_constraints = [
160         ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!')
161     ]
162
163     def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False, round=True):
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, round=round)
172
173     def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, round=True, 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 = amount * to_unit.factor
184             if round:
185                 amount = ceiling(amount, to_unit.rounding)
186         return amount
187
188     def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
189         if not from_uom_id or not price or not to_uom_id:
190             return price
191         from_unit, to_unit = self.browse(cr, uid, [from_uom_id, to_uom_id])
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 = "Logistic 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         'height': fields.float('Height', help='The height of the package'),
220         'width': fields.float('Width', help='The width of the package'),
221         'length': fields.float('Length', help='The length of the package'),
222         'weight': fields.float('Empty Package Weight'),
223     }
224
225
226 #----------------------------------------------------------
227 # Categories
228 #----------------------------------------------------------
229 class product_category(osv.osv):
230
231     def name_get(self, cr, uid, ids, context=None):
232         if isinstance(ids, (list, tuple)) and not len(ids):
233             return []
234         if isinstance(ids, (long, int)):
235             ids = [ids]
236         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
237         res = []
238         for record in reads:
239             name = record['name']
240             if record['parent_id']:
241                 name = record['parent_id'][1]+' / '+name
242             res.append((record['id'], name))
243         return res
244
245     def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
246         if not args:
247             args = []
248         if not context:
249             context = {}
250         if name:
251             # Be sure name_search is symetric to name_get
252             name = name.split(' / ')[-1]
253             ids = self.search(cr, uid, [('name', operator, name)] + args, limit=limit, context=context)
254         else:
255             ids = self.search(cr, uid, args, limit=limit, context=context)
256         return self.name_get(cr, uid, ids, context)
257
258     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
259         res = self.name_get(cr, uid, ids, context=context)
260         return dict(res)
261
262     _name = "product.category"
263     _description = "Product Category"
264     _columns = {
265         'name': fields.char('Name', required=True, translate=True, select=True),
266         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
267         'parent_id': fields.many2one('product.category','Parent Category', select=True, ondelete='cascade'),
268         'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
269         'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of product categories."),
270         '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."),
271         'parent_left': fields.integer('Left Parent', select=1),
272         'parent_right': fields.integer('Right Parent', select=1),
273     }
274
275
276     _defaults = {
277         'type' : 'normal',
278     }
279
280     _parent_name = "parent_id"
281     _parent_store = True
282     _parent_order = 'sequence, name'
283     _order = 'parent_left'
284
285     _constraints = [
286         (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
287     ]
288
289
290 class produce_price_history(osv.osv):
291     """
292     Keep track of the ``product.template`` standard prices as they are changed.
293     """
294
295     _name = 'product.price.history'
296     _rec_name = 'datetime'
297     _order = 'datetime desc'
298
299     _columns = {
300         'company_id': fields.many2one('res.company', required=True),
301         'product_template_id': fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade'),
302         'datetime': fields.datetime('Historization Time'),
303         'cost': fields.float('Historized Cost'),
304     }
305
306     def _get_default_company(self, cr, uid, context=None):
307         if 'force_company' in context:
308             return context['force_company']
309         else:
310             company = self.pool['res.users'].browse(cr, uid, uid,
311                 context=context).company_id
312             return company.id if company else False
313
314     _defaults = {
315         'datetime': fields.datetime.now,
316         'company_id': _get_default_company,
317     }
318
319
320 #----------------------------------------------------------
321 # Product Attributes
322 #----------------------------------------------------------
323 class product_attribute(osv.osv):
324     _name = "product.attribute"
325     _description = "Product Attribute"
326     _columns = {
327         'name': fields.char('Name', translate=True, required=True),
328         'value_ids': fields.one2many('product.attribute.value', 'attribute_id', 'Values'),
329     }
330
331 class product_attribute_value(osv.osv):
332     _name = "product.attribute.value"
333     _order = 'sequence'
334     def _get_price_extra(self, cr, uid, ids, name, args, context=None):
335         result = dict.fromkeys(ids, 0)
336         if not context.get('active_id'):
337             return result
338
339         for obj in self.browse(cr, uid, ids, context=context):
340             for price_id in obj.price_ids:
341                 if price_id.product_tmpl_id.id == context.get('active_id'):
342                     result[obj.id] = price_id.price_extra
343                     break
344         return result
345
346     def _set_price_extra(self, cr, uid, id, name, value, args, context=None):
347         if context is None:
348             context = {}
349         if 'active_id' not in context:
350             return None
351         p_obj = self.pool['product.attribute.price']
352         p_ids = p_obj.search(cr, uid, [('value_id', '=', id), ('product_tmpl_id', '=', context['active_id'])], context=context)
353         if p_ids:
354             p_obj.write(cr, uid, p_ids, {'price_extra': value}, context=context)
355         else:
356             p_obj.create(cr, uid, {
357                     'product_tmpl_id': context['active_id'],
358                     'value_id': id,
359                     'price_extra': value,
360                 }, context=context)
361
362     _columns = {
363         'sequence': fields.integer('Sequence', help="Determine the display order"),
364         'name': fields.char('Value', translate=True, required=True),
365         'attribute_id': fields.many2one('product.attribute', 'Attribute', required=True),
366         'product_ids': fields.many2many('product.product', id1='att_id', id2='prod_id', string='Variants', readonly=True),
367         'price_extra': fields.function(_get_price_extra, type='float', string='Attribute Price Extra',
368             fnct_inv=_set_price_extra,
369             digits_compute=dp.get_precision('Product Price'),
370             help="Price Extra: Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200."),
371         'price_ids': fields.one2many('product.attribute.price', 'value_id', string='Attribute Prices', readonly=True),
372     }
373     _sql_constraints = [
374         ('value_company_uniq', 'unique (name,attribute_id)', 'This attribute value already exists !')
375     ]
376     _defaults = {
377         'price_extra': 0.0,
378     }
379
380 class product_attribute_price(osv.osv):
381     _name = "product.attribute.price"
382     _columns = {
383         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True),
384         'value_id': fields.many2one('product.attribute.value', 'Product Attribute Value', required=True),
385         'price_extra': fields.float('Price Extra', digits_compute=dp.get_precision('Product Price')),
386     }
387
388 class product_attribute_line(osv.osv):
389     _name = "product.attribute.line"
390     _columns = {
391         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True),
392         'attribute_id': fields.many2one('product.attribute', 'Attribute', required=True),
393         'value_ids': fields.many2many('product.attribute.value', id1='line_id', id2='val_id', string='Product Attribute Value'),
394     }
395
396
397 #----------------------------------------------------------
398 # Products
399 #----------------------------------------------------------
400 class product_template(osv.osv):
401     _name = "product.template"
402     _inherit = ['mail.thread']
403     _description = "Product Template"
404
405     def _get_image(self, cr, uid, ids, name, args, context=None):
406         result = dict.fromkeys(ids, False)
407         for obj in self.browse(cr, uid, ids, context=context):
408             result[obj.id] = tools.image_get_resized_images(obj.image, avoid_resize_medium=True)
409         return result
410
411     def _set_image(self, cr, uid, id, name, value, args, context=None):
412         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
413
414     def _is_product_variant(self, cr, uid, ids, name, arg, context=None):
415         return self.is_product_variant(cr, uid, ids, name, arg, context=context)
416
417     def is_product_variant(self, cr, uid, ids, name, arg, context=None):
418         prod = self.pool.get('product.product')
419         res = dict.fromkeys(ids, False)
420         ctx = dict(context, active_test=True)
421         for product in self.browse(cr, uid, ids, context=context):
422             res[product.id] = prod.search(cr, uid, [('product_tmpl_id','=',product.id)], context=ctx, count=True) == 1
423         return res
424
425
426     def _product_template_price(self, cr, uid, ids, name, arg, context=None):
427         plobj = self.pool.get('product.pricelist')
428         res = {}
429         quantity = context.get('quantity') or 1.0
430         pricelist = context.get('pricelist', False)
431         partner = context.get('partner', False)
432         if pricelist:
433             # Support context pricelists specified as display_name or ID for compatibility
434             if isinstance(pricelist, basestring):
435                 pricelist_ids = plobj.name_search(
436                     cr, uid, pricelist, operator='=', context=context, limit=1)
437                 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
438
439             if isinstance(pricelist, (int, long)):
440                 products = self.browse(cr, uid, ids, context=context)
441                 qtys = map(lambda x: (x, quantity, partner), products)
442                 pl = plobj.browse(cr, uid, pricelist, context=context)
443                 price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
444                 for id in ids:
445                     res[id] = price.get(id, 0.0)
446         for id in ids:
447             res.setdefault(id, 0.0)
448         return res
449
450     def get_history_price(self, cr, uid, product_tmpl, company_id, date=None, context=None):
451         if context is None:
452             context = {}
453         if date is None:
454             date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
455         price_history_obj = self.pool.get('product.price.history')
456         history_ids = price_history_obj.search(cr, uid, [('company_id', '=', company_id), ('product_template_id', '=', product_tmpl), ('datetime', '<=', date)], limit=1)
457         if history_ids:
458             return price_history_obj.read(cr, uid, history_ids[0], ['cost'], context=context)['cost']
459         return 0.0
460
461     def _set_standard_price(self, cr, uid, product_tmpl_id, value, context=None):
462         ''' Store the standard price change in order to be able to retrieve the cost of a product template for a given date'''
463         if context is None:
464             context = {}
465         price_history_obj = self.pool['product.price.history']
466         user_company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
467         company_id = context.get('force_company', user_company)
468         price_history_obj.create(cr, uid, {
469             'product_template_id': product_tmpl_id,
470             'cost': value,
471             'company_id': company_id,
472         }, context=context)
473
474     def _get_product_variant_count(self, cr, uid, ids, name, arg, context=None):
475         res = {}
476         for product in self.browse(cr, uid, ids):
477             res[product.id] = len(product.product_variant_ids)
478         return res
479
480     _columns = {
481         'name': fields.char('Name', required=True, translate=True, select=True),
482         'product_manager': fields.many2one('res.users','Product Manager'),
483         'description': fields.text('Description',translate=True,
484             help="A precise description of the Product, used only for internal information purposes."),
485         'description_purchase': fields.text('Purchase Description',translate=True,
486             help="A description of the Product that you want to communicate to your suppliers. "
487                  "This description will be copied to every Purchase Order, Reception and Supplier Invoice/Refund."),
488         'description_sale': fields.text('Sale Description',translate=True,
489             help="A description of the Product that you want to communicate to your customers. "
490                  "This description will be copied to every Sale Order, Delivery Order and Customer Invoice/Refund"),
491         '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."),        
492         'rental': fields.boolean('Can be Rent'),
493         'categ_id': fields.many2one('product.category','Internal Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
494         'price': fields.function(_product_template_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
495         '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."),
496         'lst_price' : fields.related('list_price', type="float", string='Public Price', digits_compute=dp.get_precision('Product Price')),
497         'standard_price': fields.property(type = 'float', digits_compute=dp.get_precision('Product Price'), 
498                                           help="Cost price of the product template used for standard stock valuation in accounting and used as a base price on purchase orders.", 
499                                           groups="base.group_user", string="Cost Price"),
500         'volume': fields.float('Volume', help="The volume in m3."),
501         'weight': fields.float('Gross Weight', digits_compute=dp.get_precision('Stock Weight'), help="The gross weight in Kg."),
502         'weight_net': fields.float('Net Weight', digits_compute=dp.get_precision('Stock Weight'), help="The net weight in Kg."),
503         'warranty': fields.float('Warranty'),
504         'sale_ok': fields.boolean('Can be Sold', help="Specify if the product can be selected in a sales order line."),
505         'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
506         'state': fields.selection([('',''),
507             ('draft', 'In Development'),
508             ('sellable','Normal'),
509             ('end','End of Lifecycle'),
510             ('obsolete','Obsolete')], 'Status'),
511         'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
512         '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."),
513         'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
514             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.'),
515         'uos_coeff': fields.float('Unit of Measure -> UOS Coeff', digits_compute= dp.get_precision('Product UoS'),
516             help='Coefficient to convert default Unit of Measure to Unit of Sale\n'
517             ' uos = uom * coeff'),
518         'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type'),
519         'company_id': fields.many2one('res.company', 'Company', select=1),
520         # image: all image fields are base64 encoded and PIL-supported
521         'image': fields.binary("Image",
522             help="This field holds the image used as image for the product, limited to 1024x1024px."),
523         'image_medium': fields.function(_get_image, fnct_inv=_set_image,
524             string="Medium-sized image", type="binary", multi="_get_image", 
525             store={
526                 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
527             },
528             help="Medium-sized image of the product. It is automatically "\
529                  "resized as a 128x128px image, with aspect ratio preserved, "\
530                  "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."),
531         'image_small': fields.function(_get_image, fnct_inv=_set_image,
532             string="Small-sized image", type="binary", multi="_get_image",
533             store={
534                 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
535             },
536             help="Small-sized image of the product. It is automatically "\
537                  "resized as a 64x64px image, with aspect ratio preserved. "\
538                  "Use this field anywhere a small image is required."),
539
540         'seller_ids': fields.one2many('product.supplierinfo', 'product_tmpl_id', 'Supplier'),
541         'seller_delay': fields.related('seller_ids','delay', type='integer', string='Supplier Lead Time',
542             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."),
543         'seller_qty': fields.related('seller_ids','qty', type='float', string='Supplier Quantity',
544             help="This is minimum quantity to purchase from Main Supplier."),
545         'seller_id': fields.related('seller_ids','name', type='many2one', relation='res.partner', string='Main Supplier',
546             help="Main Supplier who has highest priority in Supplier List."),
547
548         'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
549         'color': fields.integer('Color Index'),
550         'is_product_variant': fields.function( _is_product_variant, type='boolean', string='Only one product variant'),
551
552         'attribute_line_ids': fields.one2many('product.attribute.line', 'product_tmpl_id', 'Product Attributes'),
553         'product_variant_ids': fields.one2many('product.product', 'product_tmpl_id', 'Products', required=True),
554         'product_variant_count': fields.function( _get_product_variant_count, type='integer', string='# of Product Variants'),
555
556         # related to display product product information if is_product_variant
557         'ean13': fields.related('product_variant_ids', 'ean13', type='char', string='EAN13 Barcode'),
558         'default_code': fields.related('product_variant_ids', 'default_code', type='char', string='Internal Reference'),
559     }
560
561     def _price_get_list_price(self, product):
562         return 0.0
563
564     def _price_get(self, cr, uid, products, ptype='list_price', context=None):
565         if context is None:
566             context = {}
567
568         if 'currency_id' in context:
569             pricetype_obj = self.pool.get('product.price.type')
570             price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
571             price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
572
573         res = {}
574         product_uom_obj = self.pool.get('product.uom')
575         for product in products:
576             res[product.id] = product[ptype] or 0.0
577             if ptype == 'list_price':
578                 res[product.id] += product._name == "product.product" and product.price_extra or 0.0
579             if 'uom' in context:
580                 uom = product.uom_id or product.uos_id
581                 res[product.id] = product_uom_obj._compute_price(cr, uid,
582                         uom.id, res[product.id], context['uom'])
583             # Convert from price_type currency to asked one
584             if 'currency_id' in context:
585                 # Take the price_type currency from the product field
586                 # This is right cause a field cannot be in more than one currency
587                 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
588                     context['currency_id'], res[product.id],context=context)
589
590         return res
591
592     def _get_uom_id(self, cr, uid, *args):
593         return self.pool["product.uom"].search(cr, uid, [], limit=1, order='id')[0]
594
595     def _default_category(self, cr, uid, context=None):
596         if context is None:
597             context = {}
598         if 'categ_id' in context and context['categ_id']:
599             return context['categ_id']
600         md = self.pool.get('ir.model.data')
601         res = False
602         try:
603             res = md.get_object_reference(cr, uid, 'product', 'product_category_all')[1]
604         except ValueError:
605             res = False
606         return res
607
608     def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
609         if uom_id:
610             return {'value': {'uom_po_id': uom_id}}
611         return {}
612
613     def create_variant_ids(self, cr, uid, ids, context=None):
614         product_obj = self.pool.get("product.product")
615         ctx = context and context.copy() or {}
616
617         if ctx.get("create_product_variant"):
618             return None
619
620         ctx.update(active_test=False, create_product_variant=True)
621
622         tmpl_ids = self.browse(cr, uid, ids, context=ctx)
623         for tmpl_id in tmpl_ids:
624
625             # list of values combination
626             all_variants = [[]]
627             for variant_id in tmpl_id.attribute_line_ids:
628                 if len(variant_id.value_ids) > 1:
629                     temp_variants = []
630                     for value_id in variant_id.value_ids:
631                         for variant in all_variants:
632                             temp_variants.append(variant + [int(value_id)])
633                     all_variants = temp_variants
634
635             # check product
636             variant_ids_to_active = []
637             variants_active_ids = []
638             variants_inactive = []
639             for product_id in tmpl_id.product_variant_ids:
640                 variants = map(int,product_id.attribute_value_ids)
641                 if variants in all_variants:
642                     variants_active_ids.append(product_id.id)
643                     all_variants.pop(all_variants.index(variants))
644                     if not product_id.active:
645                         variant_ids_to_active.append(product_id.id)
646                 else:
647                     variants_inactive.append(product_id)
648             if variant_ids_to_active:
649                 product_obj.write(cr, uid, variant_ids_to_active, {'active': True}, context=ctx)
650
651             # create new product
652             for variant_ids in all_variants:
653                 values = {
654                     'product_tmpl_id': tmpl_id.id,
655                     'attribute_value_ids': [(6, 0, variant_ids)]
656                 }
657                 id = product_obj.create(cr, uid, values, context=ctx)
658                 variants_active_ids.append(id)
659
660             # unlink or inactive product
661             for variant_id in map(int,variants_inactive):
662                 try:
663                     with cr.savepoint():
664                         product_obj.unlink(cr, uid, [variant_id], context=ctx)
665                 except (psycopg2.Error, osv.except_osv):
666                     product_obj.write(cr, uid, [variant_id], {'active': False}, context=ctx)
667                     pass
668         return True
669
670     def create(self, cr, uid, vals, context=None):
671         ''' Store the initial standard price in order to be able to retrieve the cost of a product template for a given date'''
672         product_template_id = super(product_template, self).create(cr, uid, vals, context=context)
673         if not context or "create_product_product" not in context:
674             self.create_variant_ids(cr, uid, [product_template_id], context=context)
675         self._set_standard_price(cr, uid, product_template_id, vals.get('standard_price', 0.0), context=context)
676         return product_template_id
677
678     def write(self, cr, uid, ids, vals, context=None):
679         ''' Store the standard price change in order to be able to retrieve the cost of a product template for a given date'''
680         if isinstance(id, (int, long)):
681             ids = [ids]
682         if 'uom_po_id' in vals:
683             new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_po_id'], context=context)
684             for product in self.browse(cr, uid, ids, context=context):
685                 old_uom = product.uom_po_id
686                 if old_uom.category_id.id != new_uom.category_id.id:
687                     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,))
688         if 'standard_price' in vals:
689             for prod_template_id in ids:
690                 self._set_standard_price(cr, uid, prod_template_id, vals['standard_price'], context=context)
691         res = super(product_template, self).write(cr, uid, ids, vals, context=context)
692         if 'attribute_line_ids' in vals or vals.get('active'):
693             self.create_variant_ids(cr, uid, ids, context=context)
694         if 'active' in vals and not vals.get('active'):
695             ctx = context and context.copy() or {}
696             ctx.update(active_test=False)
697             product_ids = []
698             for product in self.browse(cr, uid, ids, context=ctx):
699                 product_ids = map(int,product.product_variant_ids)
700             self.pool.get("product.product").write(cr, uid, product_ids, {'active': vals.get('active')}, context=ctx)
701         return res
702
703     def copy(self, cr, uid, id, default=None, context=None):
704         if default is None:
705             default = {}
706         template = self.browse(cr, uid, id, context=context)
707         default['name'] = _("%s (copy)") % (template['name'])
708         return super(product_template, self).copy(cr, uid, id, default=default, context=context)
709
710     _defaults = {
711         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
712         'list_price': 1,
713         'standard_price': 0.0,
714         'sale_ok': 1,        
715         'uom_id': _get_uom_id,
716         'uom_po_id': _get_uom_id,
717         'uos_coeff': 1.0,
718         'mes_type': 'fixed',
719         'categ_id' : _default_category,
720         'type' : 'consu',
721         'active': True,
722     }
723
724     def _check_uom(self, cursor, user, ids, context=None):
725         for product in self.browse(cursor, user, ids, context=context):
726             if product.uom_id.category_id.id != product.uom_po_id.category_id.id:
727                 return False
728         return True
729
730     def _check_uos(self, cursor, user, ids, context=None):
731         for product in self.browse(cursor, user, ids, context=context):
732             if product.uos_id \
733                     and product.uos_id.category_id.id \
734                     == product.uom_id.category_id.id:
735                 return False
736         return True
737
738     _constraints = [
739         (_check_uom, 'Error: The default Unit of Measure and the purchase Unit of Measure must be in the same category.', ['uom_id']),
740     ]
741
742     def name_get(self, cr, user, ids, context=None):
743         if context is None:
744             context = {}
745         if 'partner_id' in context:
746             pass
747         return super(product_template, self).name_get(cr, user, ids, context)
748
749
750 class product_product(osv.osv):
751     _name = "product.product"
752     _description = "Product"
753     _inherits = {'product.template': 'product_tmpl_id'}
754     _inherit = ['mail.thread']
755     _order = 'default_code,name_template'
756
757     def _product_price(self, cr, uid, ids, name, arg, context=None):
758         plobj = self.pool.get('product.pricelist')
759         res = {}
760         if context is None:
761             context = {}
762         quantity = context.get('quantity') or 1.0
763         pricelist = context.get('pricelist', False)
764         partner = context.get('partner', False)
765         if pricelist:
766             # Support context pricelists specified as display_name or ID for compatibility
767             if isinstance(pricelist, basestring):
768                 pricelist_ids = plobj.name_search(
769                     cr, uid, pricelist, operator='=', context=context, limit=1)
770                 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
771
772             if isinstance(pricelist, (int, long)):
773                 products = self.browse(cr, uid, ids, context=context)
774                 qtys = map(lambda x: (x, quantity, partner), products)
775                 pl = plobj.browse(cr, uid, pricelist, context=context)
776                 price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
777                 for id in ids:
778                     res[id] = price.get(id, 0.0)
779         for id in ids:
780             res.setdefault(id, 0.0)
781         return res
782
783     def view_header_get(self, cr, uid, view_id, view_type, context=None):
784         if context is None:
785             context = {}
786         res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
787         if (context.get('categ_id', False)):
788             return _('Products: ') + self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
789         return res
790
791     def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
792         res = {}
793         product_uom_obj = self.pool.get('product.uom')
794         res = dict.fromkeys(ids, 0.0)
795
796         for product in self.browse(cr, uid, ids, context=context):
797             if 'uom' in context:
798                 uom = product.uos_id or product.uom_id
799                 res[product.id] = product_uom_obj._compute_price(cr, uid,
800                         uom.id, product.list_price, context['uom'])
801             else:
802                 res[product.id] = product.list_price
803             price_extra = 0.0
804             for variant_id in product.attribute_value_ids:
805                 price_extra += variant_id.price_extra
806             res[product.id] =  (res[product.id] or 0.0) + price_extra
807         return res
808
809     def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
810         for supinfo in product.seller_ids:
811             if supinfo.name.id == partner_id:
812                 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name}
813         res = {'code': product.default_code, 'name': product.name}
814         return res
815
816     def _product_code(self, cr, uid, ids, name, arg, context=None):
817         res = {}
818         if context is None:
819             context = {}
820         for p in self.browse(cr, uid, ids, context=context):
821             res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
822         return res
823
824     def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
825         res = {}
826         if context is None:
827             context = {}
828         for p in self.browse(cr, uid, ids, context=context):
829             data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
830             if not data['code']:
831                 data['code'] = p.code
832             if not data['name']:
833                 data['name'] = p.name
834             res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + (data['name'] or '')
835         return res
836
837     def is_product_variant(self, cr, uid, ids, name, arg, context=None):
838         return dict.fromkeys(ids, True)
839
840     def _get_name_template_ids(self, cr, uid, ids, context=None):
841         result = set()
842         template_ids = self.pool.get('product.product').search(cr, uid, [('product_tmpl_id', 'in', ids)])
843         for el in template_ids:
844             result.add(el)
845         return list(result)
846
847     def _get_image_variant(self, cr, uid, ids, name, args, context=None):
848         result = dict.fromkeys(ids, False)
849         for obj in self.browse(cr, uid, ids, context=context):
850             result[obj.id] = obj.image_variant or getattr(obj.product_tmpl_id, name)
851         return result
852
853     def _set_image_variant(self, cr, uid, id, name, value, args, context=None):
854         image = tools.image_resize_image_big(value)
855         res = self.write(cr, uid, [id], {'image_variant': image}, context=context)
856         product = self.browse(cr, uid, id, context=context)
857         if not product.product_tmpl_id.image:
858             product.write({'image_variant': None}, context=context)
859             product.product_tmpl_id.write({'image': image}, context=context)
860         return res
861
862     def _get_price_extra(self, cr, uid, ids, name, args, context=None):
863         result = dict.fromkeys(ids, False)
864         for product in self.browse(cr, uid, ids, context=context):
865             price_extra = 0.0
866             for variant_id in product.attribute_value_ids:
867                 for price_id in variant_id.price_ids:
868                     if price_id.product_tmpl_id.id == product.product_tmpl_id.id:
869                         price_extra += price_id.price_extra
870             result[product.id] = price_extra
871         return result
872
873     _columns = {
874         'price': fields.function(_product_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
875         'price_extra': fields.function(_get_price_extra, type='float', string='Variant Extra Price', help="This is le sum of the extra price of all attributes"),
876         'lst_price': fields.function(_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Product Price')),
877         'code': fields.function(_product_code, type='char', string='Internal Reference'),
878         'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
879         'default_code' : fields.char('Internal Reference', select=True),
880         'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
881         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade", select=True),
882         'ean13': fields.char('EAN13 Barcode', size=13, help="International Article Number used for product identification."),
883         'packaging': fields.one2many('product.packaging', 'product_id', 'Packaging', 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."),
884         'name_template': fields.related('product_tmpl_id', 'name', string="Template Name", type='char', store={
885             'product.template': (_get_name_template_ids, ['name'], 10),
886             'product.product': (lambda self, cr, uid, ids, c=None: ids, [], 10),
887         }, select=True),
888         'attribute_value_ids': fields.many2many('product.attribute.value', id1='prod_id', id2='att_id', string='Attributes', readonly=True),
889
890         # image: all image fields are base64 encoded and PIL-supported
891         'image_variant': fields.binary("Variant Image",
892             help="This field holds the image used as image for the product variant, limited to 1024x1024px."),
893
894         'image': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
895             string="Big-sized image", type="binary",
896             help="Image of the product variant (Big-sized image of product template if false). It is automatically "\
897                  "resized as a 1024x1024px image, with aspect ratio preserved."),
898         'image_small': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
899             string="Small-sized image", type="binary",
900             help="Image of the product variant (Small-sized image of product template if false)."),
901         'image_medium': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
902             string="Medium-sized image", type="binary",
903             help="Image of the product variant (Medium-sized image of product template if false)."),
904     }
905
906     _defaults = {
907         'active': 1,
908         'color': 0,
909     }
910
911     def unlink(self, cr, uid, ids, context=None):
912         unlink_ids = []
913         unlink_product_tmpl_ids = []
914         for product in self.browse(cr, uid, ids, context=context):
915             tmpl_id = product.product_tmpl_id.id
916             # Check if the product is last product of this template
917             other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
918             if not other_product_ids:
919                 unlink_product_tmpl_ids.append(tmpl_id)
920             unlink_ids.append(product.id)
921         res = super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
922         # delete templates after calling super, as deleting template could lead to deleting
923         # products due to ondelete='cascade'
924         self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
925         return res
926
927     def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
928         if uom_id and uom_po_id:
929             uom_obj=self.pool.get('product.uom')
930             uom=uom_obj.browse(cursor,user,[uom_id])[0]
931             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
932             if uom.category_id.id != uom_po.category_id.id:
933                 return {'value': {'uom_po_id': uom_id}}
934         return False
935
936     def _check_ean_key(self, cr, uid, ids, context=None):
937         for product in self.read(cr, uid, ids, ['ean13'], context=context):
938             if not check_ean(product['ean13']):
939                 return False
940         return True
941
942     _constraints = [(_check_ean_key, 'You provided an invalid "EAN13 Barcode" reference. You may use the "Internal Reference" field instead.', ['ean13'])]
943
944     def on_order(self, cr, uid, ids, orderline, quantity):
945         pass
946
947     def name_get(self, cr, user, ids, context=None):
948         if context is None:
949             context = {}
950         if isinstance(ids, (int, long)):
951             ids = [ids]
952         if not len(ids):
953             return []
954
955         def _name_get(d):
956             name = d.get('name','')
957             code = d.get('default_code',False)
958             if code:
959                 name = '[%s] %s' % (code,name)
960             return (d['id'], name)
961
962         partner_id = context.get('partner_id', False)
963
964         # all user don't have access to seller and partner
965         # check access and use superuser
966         self.check_access_rights(cr, user, "read")
967         self.check_access_rule(cr, user, ids, "read", context=context)
968
969         result = []
970         for product in self.browse(cr, SUPERUSER_ID, ids, context=context):
971             variant = ", ".join([v.name for v in product.attribute_value_ids])
972             name = variant and "%s (%s)" % (product.name, variant) or product.name
973             sellers = []
974             if partner_id:
975                 sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
976             if sellers:
977                 for s in sellers:
978                     mydict = {
979                               'id': product.id,
980                               'name': s.product_name or name,
981                               'default_code': s.product_code or product.default_code,
982                               }
983                     result.append(_name_get(mydict))
984             else:
985                 mydict = {
986                           'id': product.id,
987                           'name': name,
988                           'default_code': product.default_code,
989                           }
990                 result.append(_name_get(mydict))
991         return result
992
993     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
994         if not args:
995             args = []
996         if name:
997             ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
998             if not ids:
999                 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
1000             if not ids:
1001                 # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
1002                 # on a database with thousands of matching products, due to the huge merge+unique needed for the
1003                 # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
1004                 # Performing a quick memory merge of ids in Python will give much better performance
1005                 ids = set(self.search(cr, user, args + [('default_code', operator, name)], limit=limit, context=context))
1006                 if not limit or len(ids) < limit:
1007                     # we may underrun the limit because of dupes in the results, that's fine
1008                     limit2 = (limit - len(ids)) if limit else False
1009                     ids.update(self.search(cr, user, args + [('name', operator, name)], limit=limit2, context=context))
1010                 ids = list(ids)
1011             if not ids:
1012                 ptrn = re.compile('(\[(.*?)\])')
1013                 res = ptrn.search(name)
1014                 if res:
1015                     ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
1016         else:
1017             ids = self.search(cr, user, args, limit=limit, context=context)
1018         result = self.name_get(cr, user, ids, context=context)
1019         return result
1020
1021     #
1022     # Could be overrided for variants matrices prices
1023     #
1024     def price_get(self, cr, uid, ids, ptype='list_price', context=None):
1025         products = self.browse(cr, uid, ids, context=context)
1026         return self.pool.get("product.template")._price_get(cr, uid, products, ptype=ptype, context=context)
1027
1028     def copy(self, cr, uid, id, default=None, context=None):
1029         if context is None:
1030             context={}
1031
1032         product = self.browse(cr, uid, id, context)
1033         if context.get('variant'):
1034             # if we copy a variant or create one, we keep the same template
1035             default['product_tmpl_id'] = product.product_tmpl_id.id
1036         elif 'name' not in default:
1037             default['name'] = _("%s (copy)") % (product.name,)
1038
1039         return super(product_product, self).copy(cr, uid, id, default=default, context=context)
1040
1041     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
1042         if context is None:
1043             context = {}
1044         if context.get('search_default_categ_id'):
1045             args.append((('categ_id', 'child_of', context['search_default_categ_id'])))
1046         return super(product_product, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1047
1048     def open_product_template(self, cr, uid, ids, context=None):
1049         """ Utility method used to add an "Open Template" button in product views """
1050         product = self.browse(cr, uid, ids[0], context=context)
1051         return {'type': 'ir.actions.act_window',
1052                 'res_model': 'product.template',
1053                 'view_mode': 'form',
1054                 'res_id': product.product_tmpl_id.id,
1055                 'target': 'new'}
1056
1057     def create(self, cr, uid, vals, context=None):
1058         if context is None:
1059             context = {}
1060         ctx = dict(context or {}, create_product_product=True)
1061         return super(product_product, self).create(cr, uid, vals, context=ctx)
1062
1063
1064 class product_packaging(osv.osv):
1065     _name = "product.packaging"
1066     _description = "Packaging"
1067     _rec_name = 'ean'
1068     _order = 'sequence'
1069     _columns = {
1070         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
1071         'name' : fields.text('Description'),
1072         'qty' : fields.float('Quantity by Package',
1073             help="The total number of products you can put by pallet or box."),
1074         'ul' : fields.many2one('product.ul', 'Package Logistic Unit', required=True),
1075         'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
1076         'ul_container': fields.many2one('product.ul', 'Pallet Logistic Unit'),
1077         'rows' : fields.integer('Number of Layers', required=True,
1078             help='The number of layers on a pallet or box'),
1079         'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
1080         'ean' : fields.char('EAN', size=14, help="The EAN code of the package unit."),
1081         'code' : fields.char('Code', help="The code of the transport unit."),
1082         'weight': fields.float('Total Package Weight',
1083             help='The weight of a full package, pallet or box.'),
1084     }
1085
1086     def _check_ean_key(self, cr, uid, ids, context=None):
1087         for pack in self.browse(cr, uid, ids, context=context):
1088             if not check_ean(pack.ean):
1089                 return False
1090         return True
1091
1092     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
1093
1094     def name_get(self, cr, uid, ids, context=None):
1095         if not len(ids):
1096             return []
1097         res = []
1098         for pckg in self.browse(cr, uid, ids, context=context):
1099             p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
1100             p_name += pckg.ul.name
1101             res.append((pckg.id,p_name))
1102         return res
1103
1104     def _get_1st_ul(self, cr, uid, context=None):
1105         cr.execute('select id from product_ul order by id asc limit 1')
1106         res = cr.fetchone()
1107         return (res and res[0]) or False
1108
1109     _defaults = {
1110         'rows' : 3,
1111         'sequence' : 1,
1112         'ul' : _get_1st_ul,
1113     }
1114
1115     def checksum(ean):
1116         salt = '31' * 6 + '3'
1117         sum = 0
1118         for ean_part, salt_part in zip(ean, salt):
1119             sum += int(ean_part) * int(salt_part)
1120         return (10 - (sum % 10)) % 10
1121     checksum = staticmethod(checksum)
1122
1123
1124
1125 class product_supplierinfo(osv.osv):
1126     _name = "product.supplierinfo"
1127     _description = "Information about a product supplier"
1128     def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
1129         result = {}
1130         for supplier_info in self.browse(cr, uid, ids, context=context):
1131             for field in fields:
1132                 result[supplier_info.id] = {field:False}
1133             qty = supplier_info.min_qty
1134             result[supplier_info.id]['qty'] = qty
1135         return result
1136
1137     _columns = {
1138         'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
1139         '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."),
1140         '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."),
1141         'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
1142         '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."),
1143         '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."),
1144         '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."),
1145         'product_tmpl_id' : fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade', select=True, oldname='product_id'),
1146         '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."),
1147         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
1148         'company_id':fields.many2one('res.company','Company',select=1),
1149     }
1150     _defaults = {
1151         'qty': 0.0,
1152         'sequence': 1,
1153         'delay': 1,
1154         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
1155     }
1156     def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
1157         """
1158         Calculate price from supplier pricelist.
1159         @param supplier_ids: Ids of res.partner object.
1160         @param product_id: Id of product.
1161         @param product_qty: specify quantity to purchase.
1162         """
1163         if type(supplier_ids) in (int,long,):
1164             supplier_ids = [supplier_ids]
1165         res = {}
1166         product_pool = self.pool.get('product.product')
1167         partner_pool = self.pool.get('res.partner')
1168         pricelist_pool = self.pool.get('product.pricelist')
1169         currency_pool = self.pool.get('res.currency')
1170         currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
1171         # Compute price from standard price of product
1172         product_price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
1173         product = product_pool.browse(cr, uid, product_id, context=context)
1174         for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
1175             price = product_price
1176             # Compute price from Purchase pricelist of supplier
1177             pricelist_id = supplier.property_product_pricelist_purchase.id
1178             if pricelist_id:
1179                 price = pricelist_pool.price_get(cr, uid, [pricelist_id], product_id, product_qty, context=context).setdefault(pricelist_id, 0)
1180                 price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
1181
1182             # Compute price from supplier pricelist which are in Supplier Information
1183             supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_tmpl_id','=',product.product_tmpl_id.id)])
1184             if supplier_info_ids:
1185                 cr.execute('SELECT * ' \
1186                     'FROM pricelist_partnerinfo ' \
1187                     'WHERE suppinfo_id IN %s' \
1188                     'AND min_quantity <= %s ' \
1189                     'ORDER BY min_quantity DESC LIMIT 1', (tuple(supplier_info_ids),product_qty,))
1190                 res2 = cr.dictfetchone()
1191                 if res2:
1192                     price = res2['price']
1193             res[supplier.id] = price
1194         return res
1195     _order = 'sequence'
1196
1197
1198 class pricelist_partnerinfo(osv.osv):
1199     _name = 'pricelist.partnerinfo'
1200     _columns = {
1201         'name': fields.char('Description'),
1202         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
1203         '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."),
1204         '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"),
1205     }
1206     _order = 'min_quantity asc'
1207
1208 class res_currency(osv.osv):
1209     _inherit = 'res.currency'
1210
1211     def _check_main_currency_rounding(self, cr, uid, ids, context=None):
1212         cr.execute('SELECT digits FROM decimal_precision WHERE name like %s',('Account',))
1213         digits = cr.fetchone()
1214         if digits and len(digits):
1215             digits = digits[0]
1216             main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
1217             for currency_id in ids:
1218                 if currency_id == main_currency.id:
1219                     if main_currency.rounding < 10 ** -digits:
1220                         return False
1221         return True
1222
1223     _constraints = [
1224         (_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']),
1225     ]
1226
1227 class decimal_precision(osv.osv):
1228     _inherit = 'decimal.precision'
1229
1230     def _check_main_currency_rounding(self, cr, uid, ids, context=None):
1231         cr.execute('SELECT id, digits FROM decimal_precision WHERE name like %s',('Account',))
1232         res = cr.fetchone()
1233         if res and len(res):
1234             account_precision_id, digits = res
1235             main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
1236             for decimal_precision in ids:
1237                 if decimal_precision == account_precision_id:
1238                     if main_currency.rounding < 10 ** -digits:
1239                         return False
1240         return True
1241
1242     _constraints = [
1243         (_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']),
1244     ]
1245
1246 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: