34503b5136e7e48b90de4f440ccb90ac714c30b6
[odoo/odoo.git] / addons / product / product.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import math
23 import re
24
25 from _common import ceiling
26
27 from openerp import tools
28 from openerp.osv import osv, fields
29 from openerp.tools.translate import _
30
31 import openerp.addons.decimal_precision as dp
32
33 def ean_checksum(eancode):
34     """returns the checksum of an ean string of length 13, returns -1 if the string has the wrong length"""
35     if len(eancode) <> 13:
36         return -1
37     oddsum=0
38     evensum=0
39     total=0
40     eanvalue=eancode
41     reversevalue = eanvalue[::-1]
42     finalean=reversevalue[1:]
43
44     for i in range(len(finalean)):
45         if i % 2 == 0:
46             oddsum += int(finalean[i])
47         else:
48             evensum += int(finalean[i])
49     total=(oddsum * 3) + evensum
50
51     check = int(10 - math.ceil(total % 10.0)) %10
52     return check
53
54 def check_ean(eancode):
55     """returns True if eancode is a valid ean13 string, or null"""
56     if not eancode:
57         return True
58     if len(eancode) <> 13:
59         return False
60     try:
61         int(eancode)
62     except:
63         return False
64     return ean_checksum(eancode) == int(eancode[-1])
65
66 def sanitize_ean13(ean13):
67     """Creates and returns a valid ean13 from an invalid one"""
68     if not ean13:
69         return "0000000000000"
70     ean13 = re.sub("[A-Za-z]","0",ean13);
71     ean13 = re.sub("[^0-9]","",ean13);
72     ean13 = ean13[:13]
73     if len(ean13) < 13:
74         ean13 = ean13 + '0' * (13-len(ean13))
75     return ean13[:-1] + str(ean_checksum(ean13))
76
77 #----------------------------------------------------------
78 # UOM
79 #----------------------------------------------------------
80
81 class product_uom_categ(osv.osv):
82     _name = 'product.uom.categ'
83     _description = 'Product uom categ'
84     _columns = {
85         'name': fields.char('Name', size=64, required=True, translate=True),
86     }
87 product_uom_categ()
88
89 class product_uom(osv.osv):
90     _name = 'product.uom'
91     _description = 'Product Unit of Measure'
92
93     def _compute_factor_inv(self, factor):
94         return factor and (1.0 / factor) or 0.0
95
96     def _factor_inv(self, cursor, user, ids, name, arg, context=None):
97         res = {}
98         for uom in self.browse(cursor, user, ids, context=context):
99             res[uom.id] = self._compute_factor_inv(uom.factor)
100         return res
101
102     def _factor_inv_write(self, cursor, user, id, name, value, arg, context=None):
103         return self.write(cursor, user, id, {'factor': self._compute_factor_inv(value)}, context=context)
104
105     def name_create(self, cr, uid, name, context=None):
106         """ The UoM category and factor are required, so we'll have to add temporary values
107             for imported UoMs """
108         uom_categ = self.pool.get('product.uom.categ')
109         # look for the category based on the english name, i.e. no context on purpose!
110         # TODO: should find a way to have it translated but not created until actually used
111         categ_misc = 'Unsorted/Imported Units'
112         categ_id = uom_categ.search(cr, uid, [('name', '=', categ_misc)])
113         if categ_id:
114             categ_id = categ_id[0]
115         else:
116             categ_id, _ = uom_categ.name_create(cr, uid, categ_misc)
117         uom_id = self.create(cr, uid, {self._rec_name: name,
118                                        'category_id': categ_id,
119                                        'factor': 1})
120         return self.name_get(cr, uid, [uom_id], context=context)[0]
121
122     def create(self, cr, uid, data, context=None):
123         if 'factor_inv' in data:
124             if data['factor_inv'] <> 1:
125                 data['factor'] = self._compute_factor_inv(data['factor_inv'])
126             del(data['factor_inv'])
127         return super(product_uom, self).create(cr, uid, data, context)
128
129     _order = "name"
130     _columns = {
131         'name': fields.char('Unit of Measure', size=64, required=True, translate=True),
132         'category_id': fields.many2one('product.uom.categ', 'Category', required=True, ondelete='cascade',
133             help="Conversion between Units of Measure can only occur if they belong to the same category. The conversion will be made based on the ratios."),
134         'factor': fields.float('Ratio', required=True,digits=(12, 12),
135             help='How much bigger or smaller this unit is compared to the reference Unit of Measure for this category:\n'\
136                     '1 * (reference unit) = ratio * (this unit)'),
137         'factor_inv': fields.function(_factor_inv, digits=(12,12),
138             fnct_inv=_factor_inv_write,
139             string='Ratio',
140             help='How many times this Unit of Measure is bigger than the reference Unit of Measure in this category:\n'\
141                     '1 * (this unit) = ratio * (reference unit)', required=True),
142         'rounding': fields.float('Rounding Precision', digits_compute=dp.get_precision('Product Unit of Measure'), required=True,
143             help="The computed quantity will be a multiple of this value. "\
144                  "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece."),
145         'active': fields.boolean('Active', help="By unchecking the active field you can disable a unit of measure without deleting it."),
146         'uom_type': fields.selection([('bigger','Bigger than the reference Unit of Measure'),
147                                       ('reference','Reference Unit of Measure for this category'),
148                                       ('smaller','Smaller than the reference Unit of Measure')],'Type', required=1),
149     }
150
151     _defaults = {
152         'active': 1,
153         'rounding': 0.01,
154         'uom_type': 'reference',
155     }
156
157     _sql_constraints = [
158         ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!')
159     ]
160
161     def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
162         if not from_uom_id or not qty or not to_uom_id:
163             return qty
164         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
165         if uoms[0].id == from_uom_id:
166             from_unit, to_unit = uoms[0], uoms[-1]
167         else:
168             from_unit, to_unit = uoms[-1], uoms[0]
169         return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit)
170
171     def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context=None):
172         if context is None:
173             context = {}
174         if from_unit.category_id.id <> to_unit.category_id.id:
175             if context.get('raise-exception', True):
176                 raise osv.except_osv(_('Error!'), _('Conversion from Product UoM %s to Default UoM %s is not possible as they both belong to different Category!.') % (from_unit.name,to_unit.name,))
177             else:
178                 return qty
179         amount = qty / from_unit.factor
180         if to_unit:
181             amount = ceiling(amount * to_unit.factor, to_unit.rounding)
182         return amount
183
184     def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
185         if not from_uom_id or not price or not to_uom_id:
186             return price
187         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
188         if uoms[0].id == from_uom_id:
189             from_unit, to_unit = uoms[0], uoms[-1]
190         else:
191             from_unit, to_unit = uoms[-1], uoms[0]
192         if from_unit.category_id.id <> to_unit.category_id.id:
193             return price
194         amount = price * from_unit.factor
195         if to_uom_id:
196             amount = amount / to_unit.factor
197         return amount
198
199     def onchange_type(self, cursor, user, ids, value):
200         if value == 'reference':
201             return {'value': {'factor': 1, 'factor_inv': 1}}
202         return {}
203
204     def write(self, cr, uid, ids, vals, context=None):
205         if 'category_id' in vals:
206             for uom in self.browse(cr, uid, ids, context=context):
207                 if uom.category_id.id != vals['category_id']:
208                     raise osv.except_osv(_('Warning!'),_("Cannot change the category of existing Unit of Measure '%s'.") % (uom.name,))
209         return super(product_uom, self).write(cr, uid, ids, vals, context=context)
210
211 product_uom()
212
213
214 class product_ul(osv.osv):
215     _name = "product.ul"
216     _description = "Shipping Unit"
217     _columns = {
218         'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
219         'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
220     }
221 product_ul()
222
223
224 #----------------------------------------------------------
225 # Categories
226 #----------------------------------------------------------
227 class product_category(osv.osv):
228
229     def name_get(self, cr, uid, ids, context=None):
230         if isinstance(ids, (list, tuple)) and not len(ids):
231             return []
232         if isinstance(ids, (long, int)):
233             ids = [ids]
234         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
235         res = []
236         for record in reads:
237             name = record['name']
238             if record['parent_id']:
239                 name = record['parent_id'][1]+' / '+name
240             res.append((record['id'], name))
241         return res
242
243     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
244         res = self.name_get(cr, uid, ids, context=context)
245         return dict(res)
246
247     _name = "product.category"
248     _description = "Product Category"
249     _columns = {
250         'name': fields.char('Name', size=64, required=True, translate=True, select=True),
251         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
252         'parent_id': fields.many2one('product.category','Parent Category', select=True, ondelete='cascade'),
253         'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
254         'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of product categories."),
255         'type': fields.selection([('view','View'), ('normal','Normal')], 'Category Type', help="A category of the view type is a virtual category that can be used as the parent of another category to create a hierarchical structure."),
256         'parent_left': fields.integer('Left Parent', select=1),
257         'parent_right': fields.integer('Right Parent', select=1),
258     }
259
260
261     _defaults = {
262         'type' : lambda *a : 'normal',
263     }
264
265     _parent_name = "parent_id"
266     _parent_store = True
267     _parent_order = 'sequence, name'
268     _order = 'parent_left'
269
270     def _check_recursion(self, cr, uid, ids, context=None):
271         level = 100
272         while len(ids):
273             cr.execute('select distinct parent_id from product_category where id IN %s',(tuple(ids),))
274             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
275             if not level:
276                 return False
277             level -= 1
278         return True
279
280     _constraints = [
281         (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
282     ]
283     def child_get(self, cr, uid, ids):
284         return [ids]
285
286 product_category()
287
288
289 #----------------------------------------------------------
290 # Products
291 #----------------------------------------------------------
292 class product_template(osv.osv):
293     _name = "product.template"
294     _description = "Product Template"
295
296     _columns = {
297         'name': fields.char('Name', size=128, required=True, translate=True, select=True),
298         'product_manager': fields.many2one('res.users','Product Manager'),
299         'description': fields.text('Description',translate=True),
300         'description_purchase': fields.text('Purchase Description',translate=True),
301         'description_sale': fields.text('Sale Description',translate=True),
302         '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."),
303         'produce_delay': fields.float('Manufacturing Lead Time', help="Average delay in days to produce this product. In the case of multi-level BOM, the manufacturing lead times of the components will be added."),
304         'rental': fields.boolean('Can be Rent'),
305         'categ_id': fields.many2one('product.category','Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
306         '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."),
307         'standard_price': fields.float('Cost', digits_compute=dp.get_precision('Product Price'), help="Cost price of the product used for standard stock valuation in accounting and used as a base price on purchase orders.", groups="base.group_user"),
308         'volume': fields.float('Volume', help="The volume in m3."),
309         'weight': fields.float('Gross Weight', digits_compute=dp.get_precision('Stock Weight'), help="The gross weight in Kg."),
310         'weight_net': fields.float('Net Weight', digits_compute=dp.get_precision('Stock Weight'), help="The net weight in Kg."),
311         'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
312             help="Standard Price: The cost price is manually updated at the end of a specific period (usually every year). \nAverage Price: The cost price is recomputed at each incoming shipment."),
313         'warranty': fields.float('Warranty'),
314         'sale_ok': fields.boolean('Can be Sold', help="Specify if the product can be selected in a sales order line."),
315         'state': fields.selection([('',''),
316             ('draft', 'In Development'),
317             ('sellable','Normal'),
318             ('end','End of Lifecycle'),
319             ('obsolete','Obsolete')], 'Status'),
320         'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
321         '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."),
322         'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
323             help='Sepcify 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.'),
324         'uos_coeff': fields.float('Unit of Measure -> UOS Coeff', digits_compute= dp.get_precision('Product UoS'),
325             help='Coefficient to convert default Unit of Measure to Unit of Sale\n'
326             ' uos = uom * coeff'),
327         'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type'),
328         'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Supplier'),
329         'company_id': fields.many2one('res.company', 'Company', select=1),
330     }
331
332     def _get_uom_id(self, cr, uid, *args):
333         cr.execute('select id from product_uom order by id limit 1')
334         res = cr.fetchone()
335         return res and res[0] or False
336
337     def _default_category(self, cr, uid, context=None):
338         if context is None:
339             context = {}
340         if 'categ_id' in context and context['categ_id']:
341             return context['categ_id']
342         md = self.pool.get('ir.model.data')
343         res = False
344         try:
345             res = md.get_object_reference(cr, uid, 'product', 'product_category_all')[1]
346         except ValueError:
347             res = False
348         return res
349
350     def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
351         if uom_id:
352             return {'value': {'uom_po_id': uom_id}}
353         return {}
354
355     def write(self, cr, uid, ids, vals, context=None):
356         if 'uom_po_id' in vals:
357             new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_po_id'], context=context)
358             for product in self.browse(cr, uid, ids, context=context):
359                 old_uom = product.uom_po_id
360                 if old_uom.category_id.id != new_uom.category_id.id:
361                     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,))
362         return super(product_template, self).write(cr, uid, ids, vals, context=context)
363
364     _defaults = {
365         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
366         'list_price': 1,
367         'cost_method': 'standard',
368         'standard_price': 0.0,
369         'sale_ok': 1,
370         'produce_delay': 1,
371         'uom_id': _get_uom_id,
372         'uom_po_id': _get_uom_id,
373         'uos_coeff' : 1.0,
374         'mes_type' : 'fixed',
375         'categ_id' : _default_category,
376         'type' : 'consu',
377     }
378
379     def _check_uom(self, cursor, user, ids, context=None):
380         for product in self.browse(cursor, user, ids, context=context):
381             if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
382                 return False
383         return True
384
385     def _check_uos(self, cursor, user, ids, context=None):
386         for product in self.browse(cursor, user, ids, context=context):
387             if product.uos_id \
388                     and product.uos_id.category_id.id \
389                     == product.uom_id.category_id.id:
390                 return False
391         return True
392
393     _constraints = [
394         (_check_uom, 'Error: The default Unit of Measure and the purchase Unit of Measure must be in the same category.', ['uom_id']),
395     ]
396
397     def name_get(self, cr, user, ids, context=None):
398         if context is None:
399             context = {}
400         if 'partner_id' in context:
401             pass
402         return super(product_template, self).name_get(cr, user, ids, context)
403
404 product_template()
405
406 class product_product(osv.osv):
407     def view_header_get(self, cr, uid, view_id, view_type, context=None):
408         if context is None:
409             context = {}
410         res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
411         if (context.get('categ_id', False)):
412             return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
413         return res
414
415     def _product_price(self, cr, uid, ids, name, arg, context=None):
416         res = {}
417         if context is None:
418             context = {}
419         quantity = context.get('quantity') or 1.0
420         pricelist = context.get('pricelist', False)
421         partner = context.get('partner', False)
422         if pricelist:
423             # Support context pricelists specified as display_name or ID for compatibility
424             if isinstance(pricelist, basestring):
425                 pricelist_ids = self.pool.get('product.pricelist').name_search(
426                     cr, uid, pricelist, operator='=', context=context, limit=1)
427                 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
428             for id in ids:
429                 try:
430                     price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, partner=partner, context=context)[pricelist]
431                 except:
432                     price = 0.0
433                 res[id] = price
434         for id in ids:
435             res.setdefault(id, 0.0)
436         return res
437
438     def _get_product_available_func(states, what):
439         def _product_available(self, cr, uid, ids, name, arg, context=None):
440             return {}.fromkeys(ids, 0.0)
441         return _product_available
442
443     _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
444     _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
445     _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
446     _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
447
448     def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
449         res = {}
450         product_uom_obj = self.pool.get('product.uom')
451         for id in ids:
452             res.setdefault(id, 0.0)
453         for product in self.browse(cr, uid, ids, context=context):
454             if 'uom' in context:
455                 uom = product.uos_id or product.uom_id
456                 res[product.id] = product_uom_obj._compute_price(cr, uid,
457                         uom.id, product.list_price, context['uom'])
458             else:
459                 res[product.id] = product.list_price
460             res[product.id] =  (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
461         return res
462
463     def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
464         for supinfo in product.seller_ids:
465             if supinfo.name.id == partner_id:
466                 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name, 'variants': ''}
467         res = {'code': product.default_code, 'name': product.name, 'variants': product.variants}
468         return res
469
470     def _product_code(self, cr, uid, ids, name, arg, context=None):
471         res = {}
472         if context is None:
473             context = {}
474         for p in self.browse(cr, uid, ids, context=context):
475             res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
476         return res
477
478     def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
479         res = {}
480         if context is None:
481             context = {}
482         for p in self.browse(cr, uid, ids, context=context):
483             data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
484             if not data['variants']:
485                 data['variants'] = p.variants
486             if not data['code']:
487                 data['code'] = p.code
488             if not data['name']:
489                 data['name'] = p.name
490             res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
491                     (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
492         return res
493
494
495     def _get_main_product_supplier(self, cr, uid, product, context=None):
496         """Determines the main (best) product supplier for ``product``,
497         returning the corresponding ``supplierinfo`` record, or False
498         if none were found. The default strategy is to select the
499         supplier with the highest priority (i.e. smallest sequence).
500
501         :param browse_record product: product to supply
502         :rtype: product.supplierinfo browse_record or False
503         """
504         sellers = [(seller_info.sequence, seller_info)
505                        for seller_info in product.seller_ids or []
506                        if seller_info and isinstance(seller_info.sequence, (int, long))]
507         return sellers and sellers[0][1] or False
508
509     def _calc_seller(self, cr, uid, ids, fields, arg, context=None):
510         result = {}
511         for product in self.browse(cr, uid, ids, context=context):
512             main_supplier = self._get_main_product_supplier(cr, uid, product, context=context)
513             result[product.id] = {
514                 'seller_info_id': main_supplier and main_supplier.id or False,
515                 'seller_delay': main_supplier.delay if main_supplier else 1,
516                 'seller_qty': main_supplier and main_supplier.qty or 0.0,
517                 'seller_id': main_supplier and main_supplier.name.id or False
518             }
519         return result
520
521
522     def _get_image(self, cr, uid, ids, name, args, context=None):
523         result = dict.fromkeys(ids, False)
524         for obj in self.browse(cr, uid, ids, context=context):
525             result[obj.id] = tools.image_get_resized_images(obj.image, avoid_resize_medium=True)
526         return result
527
528     def _set_image(self, cr, uid, id, name, value, args, context=None):
529         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
530
531     _defaults = {
532         'active': lambda *a: 1,
533         'price_extra': lambda *a: 0.0,
534         'price_margin': lambda *a: 1.0,
535         'color': 0,
536     }
537
538     _name = "product.product"
539     _description = "Product"
540     _table = "product_product"
541     _inherits = {'product.template': 'product_tmpl_id'}
542     _inherit = ['mail.thread']
543     _order = 'default_code,name_template'
544     _columns = {
545         'qty_available': fields.function(_product_qty_available, type='float', string='Quantity On Hand'),
546         'virtual_available': fields.function(_product_virtual_available, type='float', string='Quantity Available'),
547         'incoming_qty': fields.function(_product_incoming_qty, type='float', string='Incoming'),
548         'outgoing_qty': fields.function(_product_outgoing_qty, type='float', string='Outgoing'),
549         'price': fields.function(_product_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
550         'lst_price' : fields.function(_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Product Price')),
551         'code': fields.function(_product_code, type='char', string='Internal Reference'),
552         'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
553         'default_code' : fields.char('Internal Reference', size=64, select=True),
554         'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
555         'variants': fields.char('Variants', size=64),
556         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade", select=True),
557         'ean13': fields.char('EAN13 Barcode', size=13, help="International Article Number used for product identification."),
558         'packaging' : fields.one2many('product.packaging', 'product_id', 'Logistical Units', help="Gives the different ways to package the same product. This has no impact on the picking order and is mainly used if you use the EDI module."),
559         'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Product Price')),
560         'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Product Price')),
561         'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
562         'name_template': fields.related('product_tmpl_id', 'name', string="Template Name", type='char', size=128, store=True, select=True),
563         'color': fields.integer('Color Index'),
564         # image: all image fields are base64 encoded and PIL-supported
565         'image': fields.binary("Image",
566             help="This field holds the image used as image for the product, limited to 1024x1024px."),
567         'image_medium': fields.function(_get_image, fnct_inv=_set_image,
568             string="Medium-sized image", type="binary", multi="_get_image",
569             store={
570                 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
571             },
572             help="Medium-sized image of the product. It is automatically "\
573                  "resized as a 128x128px image, with aspect ratio preserved, "\
574                  "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."),
575         'image_small': fields.function(_get_image, fnct_inv=_set_image,
576             string="Small-sized image", type="binary", multi="_get_image",
577             store={
578                 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
579             },
580             help="Small-sized image of the product. It is automatically "\
581                  "resized as a 64x64px image, with aspect ratio preserved. "\
582                  "Use this field anywhere a small image is required."),
583         'seller_info_id': fields.function(_calc_seller, type='many2one', relation="product.supplierinfo", string="Supplier Info", multi="seller_info"),
584         'seller_delay': fields.function(_calc_seller, type='integer', string='Supplier Lead Time', multi="seller_info", help="This is the average delay in days between the purchase order confirmation and the reception of goods for this product and for the default supplier. It is used by the scheduler to order requests based on reordering delays."),
585         'seller_qty': fields.function(_calc_seller, type='float', string='Supplier Quantity', multi="seller_info", help="This is minimum quantity to purchase from Main Supplier."),
586         'seller_id': fields.function(_calc_seller, type='many2one', relation="res.partner", string='Main Supplier', help="Main Supplier who has highest priority in Supplier List.", multi="seller_info"),
587     }
588     def unlink(self, cr, uid, ids, context=None):
589         unlink_ids = []
590         unlink_product_tmpl_ids = []
591         for product in self.browse(cr, uid, ids, context=context):
592             tmpl_id = product.product_tmpl_id.id
593             # Check if the product is last product of this template
594             other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
595             if not other_product_ids:
596                 unlink_product_tmpl_ids.append(tmpl_id)
597             unlink_ids.append(product.id)
598         res = super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
599         # delete templates after calling super, as deleting template could lead to deleting
600         # products due to ondelete='cascade'
601         self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
602         return res
603
604     def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
605         if uom_id and uom_po_id:
606             uom_obj=self.pool.get('product.uom')
607             uom=uom_obj.browse(cursor,user,[uom_id])[0]
608             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
609             if uom.category_id.id != uom_po.category_id.id:
610                 return {'value': {'uom_po_id': uom_id}}
611         return False
612
613     def _check_ean_key(self, cr, uid, ids, context=None):
614         for product in self.read(cr, uid, ids, ['ean13'], context=context):
615             res = check_ean(product['ean13'])
616         return res
617
618
619     _constraints = [(_check_ean_key, 'You provided an invalid "EAN13 Barcode" reference. You may use the "Internal Reference" field instead.', ['ean13'])]
620
621     def on_order(self, cr, uid, ids, orderline, quantity):
622         pass
623
624     def name_get(self, cr, user, ids, context=None):
625         if context is None:
626             context = {}
627         if isinstance(ids, (int, long)):
628             ids = [ids]
629         if not len(ids):
630             return []
631         def _name_get(d):
632             name = d.get('name','')
633             code = d.get('default_code',False)
634             if code:
635                 name = '[%s] %s' % (code,name)
636             if d.get('variants'):
637                 name = name + ' - %s' % (d['variants'],)
638             return (d['id'], name)
639
640         partner_id = context.get('partner_id', False)
641
642         result = []
643         for product in self.browse(cr, user, ids, context=context):
644             sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
645             if sellers:
646                 for s in sellers:
647                     mydict = {
648                               'id': product.id,
649                               'name': s.product_name or product.name,
650                               'default_code': s.product_code or product.default_code,
651                               'variants': product.variants
652                               }
653                     result.append(_name_get(mydict))
654             else:
655                 mydict = {
656                           'id': product.id,
657                           'name': product.name,
658                           'default_code': product.default_code,
659                           'variants': product.variants
660                           }
661                 result.append(_name_get(mydict))
662         return result
663
664     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
665         if not args:
666             args = []
667         if name:
668             ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
669             if not ids:
670                 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
671             if not ids:
672                 # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
673                 # on a database with thousands of matching products, due to the huge merge+unique needed for the
674                 # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
675                 # Performing a quick memory merge of ids in Python will give much better performance
676                 ids = set()
677                 ids.update(self.search(cr, user, args + [('default_code',operator,name)], limit=limit, context=context))
678                 if not limit or len(ids) < limit:
679                     # we may underrun the limit because of dupes in the results, that's fine
680                     ids.update(self.search(cr, user, args + [('name',operator,name)], limit=(limit and (limit-len(ids)) or False) , context=context))
681                 ids = list(ids)
682             if not ids:
683                 ptrn = re.compile('(\[(.*?)\])')
684                 res = ptrn.search(name)
685                 if res:
686                     ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
687         else:
688             ids = self.search(cr, user, args, limit=limit, context=context)
689         result = self.name_get(cr, user, ids, context=context)
690         return result
691
692     #
693     # Could be overrided for variants matrices prices
694     #
695     def price_get(self, cr, uid, ids, ptype='list_price', context=None):
696         if context is None:
697             context = {}
698
699         if 'currency_id' in context:
700             pricetype_obj = self.pool.get('product.price.type')
701             price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
702             price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
703
704         res = {}
705         product_uom_obj = self.pool.get('product.uom')
706         for product in self.browse(cr, uid, ids, context=context):
707             res[product.id] = product[ptype] or 0.0
708             if ptype == 'list_price':
709                 res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
710                         product.price_extra
711             if 'uom' in context:
712                 uom = product.uom_id or product.uos_id
713                 res[product.id] = product_uom_obj._compute_price(cr, uid,
714                         uom.id, res[product.id], context['uom'])
715             # Convert from price_type currency to asked one
716             if 'currency_id' in context:
717                 # Take the price_type currency from the product field
718                 # This is right cause a field cannot be in more than one currency
719                 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
720                     context['currency_id'], res[product.id],context=context)
721
722         return res
723
724     def copy(self, cr, uid, id, default=None, context=None):
725         if context is None:
726             context={}
727
728         if not default:
729             default = {}
730
731         # Craft our own `<name> (copy)` in en_US (self.copy_translation()
732         # will do the other languages).
733         context_wo_lang = context.copy()
734         context_wo_lang.pop('lang', None)
735         product = self.read(cr, uid, id, ['name'], context=context_wo_lang)
736         default = default.copy()
737         default.update(name=_("%s (copy)") % (product['name']))
738
739         if context.get('variant',False):
740             fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
741                     'price_margin', 'price_extra']
742             data = self.read(cr, uid, id, fields=fields, context=context)
743             for f in fields:
744                 if f in default:
745                     data[f] = default[f]
746             data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
747                     and data['product_tmpl_id'][0]
748             del data['id']
749             return self.create(cr, uid, data)
750         else:
751             return super(product_product, self).copy(cr, uid, id, default=default,
752                     context=context)
753
754     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
755         if context is None:
756             context = {}
757         if context and context.get('search_default_categ_id', False):
758             args.append((('categ_id', 'child_of', context['search_default_categ_id'])))
759         return super(product_product, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
760
761 product_product()
762
763 class product_packaging(osv.osv):
764     _name = "product.packaging"
765     _description = "Packaging"
766     _rec_name = 'ean'
767     _order = 'sequence'
768     _columns = {
769         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
770         'name' : fields.text('Description', size=64),
771         'qty' : fields.float('Quantity by Package',
772             help="The total number of products you can put by pallet or box."),
773         'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
774         'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
775         'rows' : fields.integer('Number of Layers', required=True,
776             help='The number of layers on a pallet or box'),
777         'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
778         'ean' : fields.char('EAN', size=14,
779             help="The EAN code of the package unit."),
780         'code' : fields.char('Code', size=14,
781             help="The code of the transport unit."),
782         'weight': fields.float('Total Package Weight',
783             help='The weight of a full package, pallet or box.'),
784         'weight_ul': fields.float('Empty Package Weight'),
785         'height': fields.float('Height', help='The height of the package'),
786         'width': fields.float('Width', help='The width of the package'),
787         'length': fields.float('Length', help='The length of the package'),
788     }
789
790
791     def _check_ean_key(self, cr, uid, ids, context=None):
792         for pack in self.browse(cr, uid, ids, context=context):
793             res = check_ean(pack.ean)
794         return res
795
796     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
797
798     def name_get(self, cr, uid, ids, context=None):
799         if not len(ids):
800             return []
801         res = []
802         for pckg in self.browse(cr, uid, ids, context=context):
803             p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
804             p_name += pckg.ul.name
805             res.append((pckg.id,p_name))
806         return res
807
808     def _get_1st_ul(self, cr, uid, context=None):
809         cr.execute('select id from product_ul order by id asc limit 1')
810         res = cr.fetchone()
811         return (res and res[0]) or False
812
813     _defaults = {
814         'rows' : lambda *a : 3,
815         'sequence' : lambda *a : 1,
816         'ul' : _get_1st_ul,
817     }
818
819     def checksum(ean):
820         salt = '31' * 6 + '3'
821         sum = 0
822         for ean_part, salt_part in zip(ean, salt):
823             sum += int(ean_part) * int(salt_part)
824         return (10 - (sum % 10)) % 10
825     checksum = staticmethod(checksum)
826
827 product_packaging()
828
829
830 class product_supplierinfo(osv.osv):
831     _name = "product.supplierinfo"
832     _description = "Information about a product supplier"
833     def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
834         result = {}
835         product_uom_pool = self.pool.get('product.uom')
836         for supplier_info in self.browse(cr, uid, ids, context=context):
837             for field in fields:
838                 result[supplier_info.id] = {field:False}
839             qty = supplier_info.min_qty
840             result[supplier_info.id]['qty'] = qty
841         return result
842
843     _columns = {
844         'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
845         'product_name': fields.char('Supplier Product Name', size=128, help="This supplier's product name will be used when printing a request for quotation. Keep empty to use the internal one."),
846         'product_code': fields.char('Supplier Product Code', size=64, help="This supplier's product code will be used when printing a request for quotation. Keep empty to use the internal one."),
847         'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
848         'product_uom': fields.related('product_id', 'uom_po_id', type='many2one', relation='product.uom', string="Supplier Unit of Measure", readonly="1", help="This comes from the product form."),
849         '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."),
850         '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."),
851         'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
852         '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."),
853         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
854         'company_id':fields.many2one('res.company','Company',select=1),
855     }
856     _defaults = {
857         'qty': lambda *a: 0.0,
858         'sequence': lambda *a: 1,
859         'delay': lambda *a: 1,
860         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
861     }
862     def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
863         """
864         Calculate price from supplier pricelist.
865         @param supplier_ids: Ids of res.partner object.
866         @param product_id: Id of product.
867         @param product_qty: specify quantity to purchase.
868         """
869         if type(supplier_ids) in (int,long,):
870             supplier_ids = [supplier_ids]
871         res = {}
872         product_pool = self.pool.get('product.product')
873         partner_pool = self.pool.get('res.partner')
874         pricelist_pool = self.pool.get('product.pricelist')
875         currency_pool = self.pool.get('res.currency')
876         currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
877         for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
878             # Compute price from standard price of product
879             price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
880
881             # Compute price from Purchase pricelist of supplier
882             pricelist_id = supplier.property_product_pricelist_purchase.id
883             if pricelist_id:
884                 price = pricelist_pool.price_get(cr, uid, [pricelist_id], product_id, product_qty, context=context).setdefault(pricelist_id, 0)
885                 price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
886
887             # Compute price from supplier pricelist which are in Supplier Information
888             supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_id','=',product_id)])
889             if supplier_info_ids:
890                 cr.execute('SELECT * ' \
891                     'FROM pricelist_partnerinfo ' \
892                     'WHERE suppinfo_id IN %s' \
893                     'AND min_quantity <= %s ' \
894                     'ORDER BY min_quantity DESC LIMIT 1', (tuple(supplier_info_ids),product_qty,))
895                 res2 = cr.dictfetchone()
896                 if res2:
897                     price = res2['price']
898             res[supplier.id] = price
899         return res
900     _order = 'sequence'
901 product_supplierinfo()
902
903
904 class pricelist_partnerinfo(osv.osv):
905     _name = 'pricelist.partnerinfo'
906     _columns = {
907         'name': fields.char('Description', size=64),
908         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
909         '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."),
910         '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"),
911     }
912     _order = 'min_quantity asc'
913 pricelist_partnerinfo()
914
915 class res_currency(osv.osv):
916     _inherit = 'res.currency'
917
918     def _check_main_currency_rounding(self, cr, uid, ids, context=None):
919         cr.execute('SELECT digits FROM decimal_precision WHERE name like %s',('Account',))
920         digits = cr.fetchone()
921         if digits and len(digits):
922             digits = digits[0]
923             main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
924             for currency_id in ids:
925                 if currency_id == main_currency.id:
926                     if main_currency.rounding < 10 ** -digits:
927                         return False
928         return True
929
930     _constraints = [
931         (_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']),
932     ]
933
934 class decimal_precision(osv.osv):
935     _inherit = 'decimal.precision'
936
937     def _check_main_currency_rounding(self, cr, uid, ids, context=None):
938         cr.execute('SELECT id, digits FROM decimal_precision WHERE name like %s',('Account',))
939         res = cr.fetchone()
940         if res and len(res):
941             account_precision_id, digits = res
942             main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
943             for decimal_precision in ids:
944                 if decimal_precision == account_precision_id:
945                     if main_currency.rounding < 10 ** -digits:
946                         return False
947         return True
948
949     _constraints = [
950         (_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']),
951     ]
952
953 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: