4c205bb29d51e841cfb676f4fd512763c2c9d648
[odoo/odoo.git] / addons / product / product.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 # Copyright (c) 2004-2008 TINY SPRL. (http://tiny.be) All Rights Reserved.
5 #
6 # $Id$
7 #
8 # WARNING: This program as such is intended to be used by professional
9 # programmers who take the whole responsability of assessing all potential
10 # consequences resulting from its eventual inadequacies and bugs
11 # End users who are looking for a ready-to-use solution with commercial
12 # garantees and support are strongly adviced to contract a Free Software
13 # Service Company
14 #
15 # This program is Free Software; you can redistribute it and/or
16 # modify it under the terms of the GNU General Public License
17 # as published by the Free Software Foundation; either version 2
18 # of the License, or (at your option) any later version.
19 #
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with this program; if not, write to the Free Software
27 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
28 #
29 ##############################################################################
30
31 from osv import osv, fields
32 import pooler
33
34 import math
35 from _common import rounding
36
37 from tools import config
38
39 def is_pair(x):
40     return not x%2
41
42 #----------------------------------------------------------
43 # UOM
44 #----------------------------------------------------------
45
46 class product_uom_categ(osv.osv):
47     _name = 'product.uom.categ'
48     _description = 'Product uom categ'
49     _columns = {
50         'name': fields.char('Name', size=64, required=True),
51     }
52 product_uom_categ()
53
54 class product_uom(osv.osv):
55     _name = 'product.uom'
56     _description = 'Product Unit of Measure'
57
58     def _factor(self, cursor, user, ids, name, arg, context):
59         res = {}
60         for uom in self.browse(cursor, user, ids, context=context):
61             if uom.factor:
62                 if uom.factor_inv_data:
63                     res[uom.id] = uom.factor_inv_data
64                 else:
65                     res[uom.id] = round(1 / uom.factor, 6)
66             else:
67                 res[uom.id] = 0.0
68         return res
69
70     def _factor_inv(self, cursor, user, id, name, value, arg, context):
71         ctx = context.copy()
72         if 'read_delta' in ctx:
73             del ctx['read_delta']
74         if value:
75             data = 0.0
76             if round(1 / round(1/value, 6), 6) != value:
77                 data = value
78             self.write(cursor, user, id, {
79                 'factor': round(1/value, 6),
80                 'factor_inv_data': data,
81                 }, context=ctx)
82         else:
83             self.write(cursor, user, id, {
84                 'factor': 0.0,
85                 'factor_inv_data': 0.0,
86                 }, context=ctx)
87
88     _columns = {
89         'name': fields.char('Name', size=64, required=True),
90         'category_id': fields.many2one('product.uom.categ', 'UoM Category', required=True, ondelete='cascade',
91             help="Unit of Measure of the same category can be converted between each others."),
92         'factor': fields.float('Rate', digits=(12, 6), required=True,
93             help='The coefficient for the formula:\n' \
94                     '1 (base unit) = coef (this unit)'),
95         'factor_inv': fields.function(_factor, fnct_inv=_factor_inv, digits=(12, 6),
96             method=True, string='Factor',
97             help='The coefficient for the formula:\n' \
98                     'coef (base unit) = 1 (this unit)'),
99         'factor_inv_data': fields.float('Factor', digits=(12, 6)),
100         'rounding': fields.float('Rounding Precision', digits=(16, 3), required=True,
101             help="The computed quantity will be a multiple of this value. Use 1.0 for products that can not be splitted."),
102         'active': fields.boolean('Active'),
103     }
104
105     _defaults = {
106         'factor': lambda *a: 1.0,
107         'factor_inv': lambda *a: 1.0,
108         'active': lambda *a: 1,
109         'rounding': lambda *a: 0.01,
110     }
111
112     def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
113         if not from_uom_id or not qty or not to_uom_id:
114             return qty
115         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
116         if uoms[0].id == from_uom_id:
117             from_unit, to_unit = uoms[0], uoms[-1]
118         else:
119             from_unit, to_unit = uoms[-1], uoms[0]
120         if from_unit.category_id.id <> to_unit.category_id.id:
121             return qty
122         if from_unit['factor_inv_data']:
123             amount = qty * from_unit['factor_inv_data']
124         else:
125             amount = qty / from_unit['factor']
126         if to_uom_id:
127             if to_unit['factor_inv_data']:
128                 amount = rounding(amount / to_unit['factor_inv_data'], to_unit['rounding'])
129             else:
130                 amount = rounding(amount * to_unit['factor'], to_unit['rounding'])
131         return amount
132
133     def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
134         if not from_uom_id or not price or not to_uom_id:
135             return price
136         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
137         if uoms[0].id == from_uom_id:
138             from_unit, to_unit = uoms[0], uoms[-1]
139         else:
140             from_unit, to_unit = uoms[-1], uoms[0]
141         if from_unit.category_id.id <> to_unit.category_id.id:
142             return price
143         if from_unit.factor_inv_data:
144             amount = price / from_unit.factor_inv_data
145         else:
146             amount = price * from_unit.factor
147         if to_uom_id:
148             if to_unit.factor_inv_data:
149                 amount = amount * to_unit.factor_inv_data
150             else:
151                 amount = amount / to_unit.factor
152         return amount
153
154     def onchange_factor_inv(self, cursor, user, ids, value):
155         if value == 0.0:
156             return {'value': {'factor': 0}}
157         return {'value': {'factor': round(1/value, 6)}}
158
159     def onchange_factor(self, cursor, user, ids, value):
160         if value == 0.0:
161             return {'value': {'factor_inv': 0}}
162         return {'value': {'factor_inv': round(1/value, 6)}}
163
164 product_uom()
165
166
167 class product_ul(osv.osv):
168     _name = "product.ul"
169     _description = "Shipping Unit"
170     _columns = {
171         'name' : fields.char('Name', size=64,select=True),
172         'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('palet', 'Palet')], 'Type', required=True),
173     }
174 product_ul()
175
176
177 #----------------------------------------------------------
178 # Categories
179 #----------------------------------------------------------
180 class product_category(osv.osv):
181
182     def name_get(self, cr, uid, ids, context={}):
183         if not len(ids):
184             return []
185         reads = self.read(cr, uid, ids, ['name','parent_id'], context)
186         res = []
187         for record in reads:
188             name = record['name']
189             if record['parent_id']:
190                 name = record['parent_id'][1]+' / '+name
191             res.append((record['id'], name))
192         return res
193
194     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context):
195         res = self.name_get(cr, uid, ids)
196         return dict(res)
197
198     _name = "product.category"
199     _description = "Product Category"
200     _columns = {
201         'name': fields.char('Name', size=64, required=True, translate=True),
202         'complete_name': fields.function(_name_get_fnc, method=True, type="char", string='Name'),
203         'parent_id': fields.many2one('product.category','Parent Category', select=True),
204         'child_id': fields.one2many('product.category', 'parent_id', string='Childs Categories'),
205         'sequence': fields.integer('Sequence'),
206     }
207     _order = "sequence"
208     def _check_recursion(self, cr, uid, ids):
209         level = 100
210         while len(ids):
211             cr.execute('select distinct parent_id from product_category where id in ('+','.join(map(str,ids))+')')
212             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
213             if not level:
214                 return False
215             level -= 1
216         return True
217
218     _constraints = [
219         (_check_recursion, 'Error ! You can not create recursive categories.', ['parent_id'])
220     ]
221     def child_get(self, cr, uid, ids):
222         return [ids]
223
224 product_category()
225
226
227 #----------------------------------------------------------
228 # Products
229 #----------------------------------------------------------
230 class product_template(osv.osv):
231     _name = "product.template"
232     _description = "Product Template"
233     def _calc_seller_delay(self, cr, uid, ids, name, arg, context={}):
234         result = {}
235         for product in self.browse(cr, uid, ids, context):
236             if product.seller_ids:
237                 result[product.id] = product.seller_ids[0].delay
238             else:
239                 result[product.id] = 1
240         return result
241
242     _columns = {
243         'name': fields.char('Name', size=128, required=True, translate=True, select=True),
244         'product_manager': fields.many2one('res.users','Product Manager'),
245         'description': fields.text('Description',translate=True),
246         'description_purchase': fields.text('Purchase Description',translate=True),
247         'description_sale': fields.text('Sale Description',translate=True),
248         'type': fields.selection([('product','Stockable Product'),('consu', 'Consumable'),('service','Service')], 'Product Type', required=True, help="Will change the way procurements are processed, consumable are stockable products with infinite stock, or without a stock management in the system."),
249         'supply_method': fields.selection([('produce','Produce'),('buy','Buy')], 'Supply method', required=True, help="Produce will generate production order or tasks, according to the product type. Purchase will trigger purchase orders when requested."),
250         'sale_delay': fields.float('Customer Lead Time', help="This is the average time between the confirmation of the customer order and the delivery of the finnished products. It's the time you promise to your customers."),
251         'produce_delay': fields.float('Manufacturing Lead Time', help="Average time 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 delays will be summed for all levels and purchase orders."),
252         'procure_method': fields.selection([('make_to_stock','Make to Stock'),('make_to_order','Make to Order')], 'Procure Method', required=True, help="'Make to Stock': When needed, take from the stock or wait until refurnishing. 'Make to Order': When needed, purchase or produce for the procurement request."),
253         'rental': fields.boolean('Rentable product'),
254         'categ_id': fields.many2one('product.category','Category', required=True, change_default=True),
255         'list_price': fields.float('Sale Price', digits=(16, int(config['price_accuracy'])), help="Base price for computing the customer price. Sometimes called the catalog price."),
256         'standard_price': fields.float('Cost Price', required=True, digits=(16, int(config['price_accuracy'])), help="The cost of the product for accounting stock valorisation. It can serves as a base price for supplier price."),
257         'volume': fields.float('Volume', help="The weight in Kg."),
258         'weight': fields.float('Gross weight'),
259         'weight_net': fields.float('Net weight'),
260         'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
261             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."),
262         'warranty': fields.float('Warranty (months)'),
263         'sale_ok': fields.boolean('Can be sold', help="Determine if the product can be visible in the list of product within a selection from a sale order line."),
264         '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."),
265         'uom_id': fields.many2one('product.uom', 'Default UoM', required=True, help="Default Unit of Measure used for all stock operation."),
266         'uom_po_id': fields.many2one('product.uom', 'Purchase UoM', required=True, help="Default Unit of Measure used for purchase orders. It must in the same category than the default unit of measure."),
267         'state': fields.selection([('',''),('draft', 'In Development'),('sellable','In Production'),('end','End of Lifecycle'),('obsolete','Obsolete')], 'Status', help="Tells the user if he can use the product or not."),
268         'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
269             help='Used by companies that manages two unit of measure: invoicing and stock management. For example, in food industries, you will manage a stock of ham but invoice in Kg. Keep empty to use the default UOM.'),
270         'uos_coeff': fields.float('UOM -> UOS Coeff', digits=(16,4),
271             help='Coefficient to convert UOM to UOS\n'
272             ' uom = uos * coeff'),
273         'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
274         'seller_delay': fields.function(_calc_seller_delay, method=True, type='integer', string='Supplier Lead Time', 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."),
275         'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Partners'),
276         'loc_rack': fields.char('Rack', size=16),
277         'loc_row': fields.char('Row', size=16),
278         'loc_case': fields.char('Case', size=16),
279     }
280
281     def _get_uom_id(self, cr, uid, *args):
282         cr.execute('select id from product_uom order by id limit 1')
283         res = cr.fetchone()
284         return res and res[0] or False
285
286     _defaults = {
287         'type': lambda *a: 'product',
288         'list_price': lambda *a: 1,
289         'cost_method': lambda *a: 'standard',
290         'supply_method': lambda *a: 'buy',
291         'standard_price': lambda *a: 1,
292         'sale_ok': lambda *a: 1,
293         'sale_delay': lambda *a: 7,
294         'produce_delay': lambda *a: 1,
295         'purchase_ok': lambda *a: 1,
296         'procure_method': lambda *a: 'make_to_stock',
297         'uom_id': _get_uom_id,
298         'uom_po_id': _get_uom_id,
299         'uos_coeff' : lambda *a: 1.0,
300         'mes_type' : lambda *a: 'fixed',
301     }
302
303     def _check_uom(self, cursor, user, ids):
304         for product in self.browse(cursor, user, ids):
305             if product.uom_id.id <> product.uom_po_id.id:
306                 return False
307         return True
308
309     def _check_uos(self, cursor, user, ids):
310         for product in self.browse(cursor, user, ids):
311             if product.uos_id \
312                     and product.uos_id.category_id.id \
313                     == product.uom_id.category_id.id:
314                 return False
315         return True
316
317     _constraints = [
318         (_check_uos, 'Error: UOS must be in a different category than the UOM', ['uos_id']),
319         (_check_uom, 'Error: The default UOM and the purchase UOM must be in the same category.', ['uom_id']),
320     ]
321
322     def name_get(self, cr, user, ids, context={}):
323         if 'partner_id' in context:
324             pass
325         return super(product_template, self).name_get(cr, user, ids, context)
326
327 product_template()
328
329 class product_product(osv.osv):
330     def view_header_get(self, cr, uid, view_id, view_type, context):
331         res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
332         if (context.get('categ_id', False)):
333             return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context).name
334         return res
335
336     def _product_price(self, cr, uid, ids, name, arg, context={}):
337         res = {}
338         quantity = context.get('quantity', 1)
339         pricelist = context.get('pricelist', False)
340         if pricelist:
341             for id in ids:
342                 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, context=context)[pricelist]
343                 res[id] = price
344         for id in ids:
345             res.setdefault(id, 0.0)
346         return res
347
348     def _get_product_available_func(states, what):
349         def _product_available(self, cr, uid, ids, name, arg, context={}):
350             return {}.fromkeys(ids, 0.0)
351         return _product_available
352
353     _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
354     _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
355     _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
356     _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
357
358     def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
359         res = {}
360         product_uom_obj = self.pool.get('product.uom')
361         for id in ids:
362             res.setdefault(id, 0.0)
363         for product in self.browse(cr, uid, ids, context=context):
364             if 'uom' in context:
365                 uom = product.uos_id or product.uom_id
366                 res[product.id] = product_uom_obj._compute_price(cr, uid,
367                         uom.id, product.list_price, context['uom'])
368             else:
369                 res[product.id] = product.list_price
370         return res
371
372     def _get_partner_code_name(self, cr, uid, ids, product_id, partner_id, context={}):
373         product = self.browse(cr, uid, [product_id], context)[0]
374         for supinfo in product.seller_ids:
375             if supinfo.name.id == partner_id:
376                 return {'code': supinfo.product_code, 'name': supinfo.product_name}
377         return {'code' : product.default_code, 'name' : product.name}
378
379     def _product_code(self, cr, uid, ids, name, arg, context={}):
380         res = {}
381         for p in self.browse(cr, uid, ids, context):
382             res[p.id] = self._get_partner_code_name(cr, uid, [], p.id, context.get('partner_id', None), context)['code']
383         return res
384
385     def _product_partner_ref(self, cr, uid, ids, name, arg, context={}):
386         res = {}
387         for p in self.browse(cr, uid, ids, context):
388             data = self._get_partner_code_name(cr, uid, [], p.id, context.get('partner_id', None), context)
389             if not data['code']:
390                 data['name'] = p.code
391             if not data['name']:
392                 data['name'] = p.name
393             res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
394                     (data['name'] or '')
395         return res
396
397     _defaults = {
398         'active': lambda *a: 1,
399         'price_extra': lambda *a: 0.0,
400         'price_margin': lambda *a: 1.0,
401     }
402
403     _name = "product.product"
404     _description = "Product"
405     _table = "product_product"
406     _inherits = {'product.template': 'product_tmpl_id'}
407     _columns = {
408         'qty_available': fields.function(_product_qty_available, method=True, type='float', string='Real Stock'),
409         'virtual_available': fields.function(_product_virtual_available, method=True, type='float', string='Virtual Stock'),
410         'incoming_qty': fields.function(_product_incoming_qty, method=True, type='float', string='Incoming'),
411         'outgoing_qty': fields.function(_product_outgoing_qty, method=True, type='float', string='Outgoing'),
412         'price': fields.function(_product_price, method=True, type='float', string='Customer Price', digits=(16, int(config['price_accuracy']))),
413         'lst_price' : fields.function(_product_lst_price, method=True, type='float', string='List Price', digits=(16, int(config['price_accuracy']))),
414         'code': fields.function(_product_code, method=True, type='char', string='Code'),
415         'partner_ref' : fields.function(_product_partner_ref, method=True, type='char', string='Customer ref'),
416         'default_code' : fields.char('Code', size=64),
417         'active': fields.boolean('Active'),
418         'variants': fields.char('Variants', size=64),
419         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True),
420         'ean13': fields.char('EAN13', size=13),
421         '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 packing order and is mainly used if you use the EDI module."),
422         'price_extra': fields.float('Variant Price Extra', digits=(16, int(config['price_accuracy']))),
423         'price_margin': fields.float('Variant Price Margin', digits=(16, int(config['price_accuracy']))),
424     }
425
426     def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
427         if uom_id and uom_po_id:
428             uom_obj=self.pool.get('product.uom')
429             uom=uom_obj.browse(cursor,user,[uom_id])[0]
430             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
431             if uom.category_id.id != uom_po.category_id.id:
432                 return {'value': {'uom_po_id': uom_id}}
433         return False
434
435     def _check_ean_key(self, cr, uid, ids):
436         for partner in self.browse(cr, uid, ids):
437             if not partner.ean13:
438                 continue
439             if len(partner.ean13) < 12:
440                 return False
441             try:
442                 int(partner.ean13)
443             except:
444                 return False
445             sum=0
446             for i in range(12):
447                 if is_pair(i):
448                     sum += int(partner.ean13[i])
449                 else:
450                     sum += 3 * int(partner.ean13[i])
451             check = int(math.ceil(sum / 10.0) * 10 - sum)
452             if len(partner.ean13) == 12:
453                 self.write(cr, uid, partner.id, {
454                     'ean13': partner.ean13 + str(check)
455                     })
456             elif check != int(partner.ean13[12]):
457                 return False
458         return True
459
460     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
461
462     def on_order(self, cr, uid, ids, orderline, quantity):
463         pass
464
465     def name_get(self, cr, user, ids, context={}):
466         if not len(ids):
467             return []
468         def _name_get(d):
469             #name = self._product_partner_ref(cr, user, [d['id']], '', '', context)[d['id']]
470             #code = self._product_code(cr, user, [d['id']], '', '', context)[d['id']]
471             name = d.get('name','')
472             code = d.get('default_code',False)
473             if code:
474                 name = '[%s] %s' % (code,name)
475             if d['variants']:
476                 name = name + ' - %s' % (d['variants'],)
477             return (d['id'], name)
478         result = map(_name_get, self.read(cr, user, ids, ['variants','name','default_code'], context))
479         return result
480
481     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=80):
482         if not args:
483             args=[]
484         if not context:
485             context={}
486         ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
487         if not len(ids):
488             ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
489         if not len(ids):
490             ids = self.search(cr, user, [('default_code',operator,name)]+ args, limit=limit, context=context)
491             ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
492         result = self.name_get(cr, user, ids, context)
493         return result
494
495     #
496     # Could be overrided for variants matrices prices
497     #
498     def price_get(self, cr, uid, ids, ptype='list_price', context={}):
499         res = {}
500         product_uom_obj = self.pool.get('product.uom')
501
502         for product in self.browse(cr, uid, ids, context=context):
503             res[product.id] = product[ptype] or 0.0
504             if ptype == 'list_price':
505                 res[product.id] = (res[product.id] * product.price_margin) + \
506                         product.price_extra
507             if 'uom' in context:
508                 uom = product.uos_id or product.uom_id
509                 res[product.id] = product_uom_obj._compute_price(cr, uid,
510                         uom.id, res[product.id], context['uom'])
511         return res
512
513     def copy(self, cr, uid, id, default=None, context=None):
514         if not context:
515             context={}
516
517         if ('variant' in context) and context['variant']:
518             fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
519                     'price_margin', 'price_extra']
520             data = self.read(cr, uid, id, fields=fields, context=context)
521             for f in fields:
522                 if f in default:
523                     data[f] = default[f]
524             data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
525                     and data['product_tmpl_id'][0]
526             del data['id']
527             return self.create(cr, uid, data)
528         else:
529             return super(product_product, self).copy(cr, uid, id, default=default,
530                     context=context)
531 product_product()
532
533 class product_packaging(osv.osv):
534     _name = "product.packaging"
535     _description = "Packaging"
536     _rec_name = 'ean'
537     _columns = {
538         'sequence': fields.integer('Sequence'),
539         'name' : fields.char('Description', size=64),
540         'qty' : fields.float('Quantity by Package',
541             help="The total number of products you can put by palet or box."),
542         'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
543         'ul_qty' : fields.integer('Package by layer'),
544         'rows' : fields.integer('Number of Layer', required=True,
545             help='The number of layer on a palet or box'),
546         'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
547         'ean' : fields.char('EAN', size=14,
548             help="The EAN code of the package unit."),
549         'code' : fields.char('Code', size=14,
550             help="The code of the transport unit."),
551         'weight': fields.float('Total Package Weight',
552             help='The weight of a full of products palet or box.'),
553         'weight_ul': fields.float('Empty Package Weight',
554             help='The weight of the empty UL'),
555         'height': fields.float('Height', help='The height of the package'),
556         'width': fields.float('Width', help='The width of the package'),
557         'length': fields.float('Length', help='The length of the package'),
558     }
559
560     def _get_1st_ul(self, cr, uid, context={}):
561         cr.execute('select id from product_ul order by id asc limit 1')
562         res = cr.fetchone()
563         return (res and res[0]) or False
564
565     _defaults = {
566         'rows' : lambda *a : 3,
567         'sequence' : lambda *a : 1,
568         'ul' : _get_1st_ul,
569     }
570
571     def checksum(ean):
572         salt = '31' * 6 + '3'
573         sum = 0
574         for ean_part, salt_part in zip(ean, salt):
575             sum += int(ean_part) * int(salt_part)
576         return (10 - (sum % 10)) % 10
577     checksum = staticmethod(checksum)
578
579 product_packaging()
580
581
582 class product_supplierinfo(osv.osv):
583     _name = "product.supplierinfo"
584     _description = "Information about a product supplier"
585     _columns = {
586         'name' : fields.many2one('res.partner', 'Partner', required=True, ondelete='cascade', help="Supplier of this product"),
587         'product_name': fields.char('Partner Product Name', size=128, help="Name of the product for this partner, will be used when printing a request for quotation. Keep empty to use the internal one."),
588         'product_code': fields.char('Partner Product Code', size=64, help="Code of the product for this partner, will be used when printing a request for quotation. Keep empty to use the internal one."),
589         'sequence' : fields.integer('Priority'),
590         'qty' : fields.float('Minimal Quantity', required=True, help="The minimal quantity to purchase for this supplier, expressed in the default unit of measure."),
591         'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
592         'delay' : fields.integer('Delivery Delay', required=True, help="Delay 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."),
593         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
594     }
595     _defaults = {
596         'qty': lambda *a: 0.0,
597         'sequence': lambda *a: 1,
598         'delay': lambda *a: 1,
599     }
600     _order = 'sequence'
601 product_supplierinfo()
602
603
604 class pricelist_partnerinfo(osv.osv):
605     _name = 'pricelist.partnerinfo'
606     _columns = {
607         'name': fields.char('Description', size=64),
608         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
609         'min_quantity': fields.float('Quantity', required=True),
610         'price': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
611     }
612     _order = 'min_quantity asc'
613 pricelist_partnerinfo()
614
615
616
617 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
618