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