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