fixed bug on duplicate picking
[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=64, 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             res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
390                     (data['name'] or '')
391         return res
392
393     _defaults = {
394         'active': lambda *a: 1,
395         'price_extra': lambda *a: 0.0,
396         'price_margin': lambda *a: 1.0,
397     }
398
399     _name = "product.product"
400     _description = "Product"
401     _table = "product_product"
402     _inherits = {'product.template': 'product_tmpl_id'}
403     _columns = {
404         'qty_available': fields.function(_product_qty_available, method=True, type='float', string='Real Stock'),
405         'virtual_available': fields.function(_product_virtual_available, method=True, type='float', string='Virtual Stock'),
406         'incoming_qty': fields.function(_product_incoming_qty, method=True, type='float', string='Incoming'),
407         'outgoing_qty': fields.function(_product_outgoing_qty, method=True, type='float', string='Outgoing'),
408         'price': fields.function(_product_price, method=True, type='float', string='Customer Price', digits=(16, int(config['price_accuracy']))),
409         'lst_price' : fields.function(_product_lst_price, method=True, type='float', string='List Price', digits=(16, int(config['price_accuracy']))),
410         'code': fields.function(_product_code, method=True, type='char', string='Code'),
411         'partner_ref' : fields.function(_product_partner_ref, method=True, type='char', string='Customer ref'),
412         'default_code' : fields.char('Code', size=64),
413         'active': fields.boolean('Active'),
414         'variants': fields.char('Variants', size=64),
415         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True),
416         'ean13': fields.char('EAN13', size=13),
417         '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."),
418         'price_extra': fields.float('Variant Price Extra', digits=(16, int(config['price_accuracy']))),
419         'price_margin': fields.float('Variant Price Margin', digits=(16, int(config['price_accuracy']))),
420     }
421
422     def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
423         if uom_id and uom_po_id:
424             uom_obj=self.pool.get('product.uom')
425             uom=uom_obj.browse(cursor,user,[uom_id])[0]
426             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
427             if uom.category_id.id != uom_po.category_id.id:
428                 return {'value': {'uom_po_id': uom_id}}
429         return False
430
431     def _check_ean_key(self, cr, uid, ids):
432         for partner in self.browse(cr, uid, ids):
433             if not partner.ean13:
434                 continue
435             if len(partner.ean13) < 12:
436                 return False
437             try:
438                 int(partner.ean13)
439             except:
440                 return False
441             sum=0
442             for i in range(12):
443                 if is_pair(i):
444                     sum += int(partner.ean13[i])
445                 else:
446                     sum += 3 * int(partner.ean13[i])
447             check = int(math.ceil(sum / 10.0) * 10 - sum)
448             if len(partner.ean13) == 12:
449                 self.write(cr, uid, partner.id, {
450                     'ean13': partner.ean13 + str(check)
451                     })
452             elif check != int(partner.ean13[12]):
453                 return False
454         return True
455
456     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
457
458     def on_order(self, cr, uid, ids, orderline, quantity):
459         pass
460
461     def name_get(self, cr, user, ids, context={}):
462         if not len(ids):
463             return []
464         def _name_get(d):
465             #name = self._product_partner_ref(cr, user, [d['id']], '', '', context)[d['id']]
466             #code = self._product_code(cr, user, [d['id']], '', '', context)[d['id']]
467             name = d.get('name','')
468             code = d.get('default_code',False)
469             if code:
470                 name = '[%s] %s' % (code,name)
471             if d['variants']:
472                 name = name + ' - %s' % (d['variants'],)
473             return (d['id'], name)
474         result = map(_name_get, self.read(cr, user, ids, ['variants','name','default_code'], context))
475         return result
476
477     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=80):
478         if not args:
479             args=[]
480         if not context:
481             context={}
482         ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
483         if not len(ids):
484             ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
485         if not len(ids):
486             ids = self.search(cr, user, [('default_code',operator,name)]+ args, limit=limit, context=context)
487             ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
488         result = self.name_get(cr, user, ids, context)
489         return result
490
491     #
492     # Could be overrided for variants matrices prices
493     #
494     def price_get(self, cr, uid, ids, ptype='list_price', context={}):
495         res = {}
496         product_uom_obj = self.pool.get('product.uom')
497
498         for product in self.browse(cr, uid, ids, context=context):
499             res[product.id] = product[ptype] or 0.0
500             if ptype == 'list_price':
501                 res[product.id] = (res[product.id] * product.price_margin) + \
502                         product.price_extra
503             if 'uom' in context:
504                 uom = product.uos_id or product.uom_id
505                 res[product.id] = product_uom_obj._compute_price(cr, uid,
506                         uom.id, res[product.id], context['uom'])
507         return res
508
509     def copy(self, cr, uid, id, default=None, context=None):
510         if not context:
511             context={}
512
513         if ('variant' in context) and context['variant']:
514             fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
515                     'price_margin', 'price_extra']
516             data = self.read(cr, uid, id, fields=fields, context=context)
517             for f in fields:
518                 if f in default:
519                     data[f] = default[f]
520             data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
521                     and data['product_tmpl_id'][0]
522             del data['id']
523             return self.create(cr, uid, data)
524         else:
525             return super(product_product, self).copy(cr, uid, id, default=default,
526                     context=context)
527 product_product()
528
529 class product_packaging(osv.osv):
530     _name = "product.packaging"
531     _description = "Packaging"
532     _rec_name = 'ean'
533     _columns = {
534         'name' : fields.char('Description', size=64),
535         'qty' : fields.float('Quantity by Package',
536             help="The total number of products you can put by palet or box."),
537         'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
538         'ul_qty' : fields.integer('Package by layer'),
539         'rows' : fields.integer('Number of Layer', required=True,
540             help='The number of layer on a palet or box'),
541         'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
542         'ean' : fields.char('EAN', size=14,
543             help="The EAN code of the package unit."),
544         'code' : fields.char('Code', size=14,
545             help="The code of the transport unit."),
546         'weight': fields.float('Total Package Weight',
547             help='The weight of a full of products palet or box.'),
548         'weight_ul': fields.float('Empty Package Weight',
549             help='The weight of the empty UL'),
550         'height': fields.float('Height', help='The height of the package'),
551         'width': fields.float('Width', help='The width of the package'),
552         'length': fields.float('Length', help='The length of the package'),
553     }
554
555     def _get_1st_ul(self, cr, uid, context={}):
556         cr.execute('select id from product_ul order by id asc limit 1')
557         res = cr.fetchone()
558         return (res and res[0]) or False
559
560     _defaults = {
561         'rows' : lambda *a : 3,
562         'ul' : _get_1st_ul,
563     }
564
565     def checksum(ean):
566         salt = '31' * 6 + '3'
567         sum = 0
568         for ean_part, salt_part in zip(ean, salt):
569             sum += int(ean_part) * int(salt_part)
570         return (10 - (sum % 10)) % 10
571     checksum = staticmethod(checksum)
572
573 product_packaging()
574
575
576 class product_supplierinfo(osv.osv):
577     _name = "product.supplierinfo"
578     _description = "Information about a product supplier"
579     _columns = {
580         'name' : fields.many2one('res.partner', 'Partner', required=True, ondelete='cascade', help="Supplier of this product"),
581         '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."),
582         '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."),
583         'sequence' : fields.integer('Priority'),
584         'qty' : fields.float('Minimal Quantity', required=True, help="The minimal quantity to purchase for this supplier, expressed in the default unit of measure."),
585         'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
586         '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."),
587         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
588     }
589     _defaults = {
590         'qty': lambda *a: 0.0,
591         'sequence': lambda *a: 1,
592         'delay': lambda *a: 1,
593     }
594     _order = 'sequence'
595 product_supplierinfo()
596
597
598 class pricelist_partnerinfo(osv.osv):
599     _name = 'pricelist.partnerinfo'
600     _columns = {
601         'name': fields.char('Description', size=64),
602         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
603         'min_quantity': fields.float('Quantity', required=True),
604         'price': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
605     }
606     _order = 'min_quantity asc'
607 pricelist_partnerinfo()
608
609
610
611 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
612