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