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