merge
[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         'factor': fields.float('Rate', digits=(12, 6), required=True,
92             help='The coefficient for the formula:\n' \
93                     '1 (base unit) = coef (this unit)'),
94         'factor_inv': fields.function(_factor, fnct_inv=_factor_inv, digits=(12, 6),
95             method=True, string='Factor',
96             help='The coefficient for the formula:\n' \
97                     'coef (base unit) = 1 (this unit)'),
98         'factor_inv_data': fields.float('Factor', digits=(12, 6)),
99         'rounding': fields.float('Rounding Precision', digits=(16, 3), required=True),
100         'active': fields.boolean('Active'),
101     }
102
103     _defaults = {
104         'factor': lambda *a: 1.0,
105         'factor_inv': lambda *a: 1.0,
106         'active': lambda *a: 1,
107         'rounding': lambda *a: 0.01,
108     }
109
110     def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
111         if not from_uom_id or not qty or not to_uom_id:
112             return qty
113         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
114         if uoms[0].id == from_uom_id:
115             from_unit, to_unit = uoms[0], uoms[-1]
116         else:
117             from_unit, to_unit = uoms[-1], uoms[0]
118         if from_unit.category_id.id <> to_unit.category_id.id:
119             return qty
120         if from_unit['factor_inv_data']:
121             amount = qty * from_unit['factor_inv_data']
122         else:
123             amount = qty / from_unit['factor']
124         if to_uom_id:
125             if to_unit['factor_inv_data']:
126                 amount = rounding(amount / to_unit['factor_inv_data'], to_unit['rounding'])
127             else:
128                 amount = rounding(amount * to_unit['factor'], to_unit['rounding'])
129         return amount
130
131     def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
132         if not from_uom_id or not price or not to_uom_id:
133             return price
134         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
135         if uoms[0].id == from_uom_id:
136             from_unit, to_unit = uoms[0], uoms[-1]
137         else:
138             from_unit, to_unit = uoms[-1], uoms[0]
139         if from_unit.category_id.id <> to_unit.category_id.id:
140             return price
141         if from_unit.factor_inv_data:
142             amount = price / from_unit.factor_inv_data
143         else:
144             amount = price * from_unit.factor
145         if to_uom_id:
146             if to_unit.factor_inv_data:
147                 amount = amount * to_unit.factor_inv_data
148             else:
149                 amount = amount / to_unit.factor
150         return amount
151
152     def onchange_factor_inv(self, cursor, user, ids, value):
153         if value == 0.0:
154             return {'value': {'factor': 0}}
155         return {'value': {'factor': round(1/value, 6)}}
156
157     def onchange_factor(self, cursor, user, ids, value):
158         if value == 0.0:
159             return {'value': {'factor_inv': 0}}
160         return {'value': {'factor_inv': round(1/value, 6)}}
161
162 product_uom()
163
164
165 class product_ul(osv.osv):
166     _name = "product.ul"
167     _description = "Shipping Unit"
168     _columns = {
169         'name' : fields.char('Name', size=64,select=True),
170         'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('palet', 'Palet')], 'Type', required=True),
171     }
172 product_ul()
173
174
175 #----------------------------------------------------------
176 # Categories
177 #----------------------------------------------------------
178 class product_category(osv.osv):
179
180     def name_get(self, cr, uid, ids, context={}):
181         if not len(ids):
182             return []
183         reads = self.read(cr, uid, ids, ['name','parent_id'], context)
184         res = []
185         for record in reads:
186             name = record['name']
187             if record['parent_id']:
188                 name = record['parent_id'][1]+' / '+name
189             res.append((record['id'], name))
190         return res
191
192     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context):
193         res = self.name_get(cr, uid, ids)
194         return dict(res)
195
196     _name = "product.category"
197     _description = "Product Category"
198     _columns = {
199         'name': fields.char('Name', size=64, required=True, translate=True),
200         'complete_name': fields.function(_name_get_fnc, method=True, type="char", string='Name'),
201         'parent_id': fields.many2one('product.category','Parent Category', select=True),
202         'child_id': fields.one2many('product.category', 'parent_id', string='Childs Categories'),
203         'sequence': fields.integer('Sequence'),
204     }
205     _order = "sequence"
206     def _check_recursion(self, cr, uid, ids):
207         level = 100
208         while len(ids):
209             cr.execute('select distinct parent_id from product_category where id in ('+','.join(map(str,ids))+')')
210             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
211             if not level:
212                 return False
213             level -= 1
214         return True
215
216     _constraints = [
217         (_check_recursion, 'Error ! You can not create recursive categories.', ['parent_id'])
218     ]
219     def child_get(self, cr, uid, ids):
220         return [ids]
221
222 product_category()
223
224
225 #----------------------------------------------------------
226 # Products
227 #----------------------------------------------------------
228 class product_template(osv.osv):
229     _name = "product.template"
230     _description = "Product Template"
231
232     def _calc_seller_delay(self, cr, uid, ids, name, arg, context={}):
233         result = {}
234         for product in self.browse(cr, uid, ids, context):
235             if product.seller_ids:
236                 result[product.id] = product.seller_ids[0].delay
237             else:
238                 result[product.id] = 1
239         return result
240
241     _columns = {
242         'name': fields.char('Name', size=64, required=True, translate=True, select=True),
243         'product_manager': fields.many2one('res.users','Product Manager'),
244         'description': fields.text('Description'),
245         'description_purchase': fields.text('Purchase Description'),
246         'description_sale': fields.text('Sale Description'),
247         '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."),
248         '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."),
249         '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."),
250         '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."),
251         '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."),
252         'rental': fields.boolean('Rentable product'),
253         'categ_id': fields.many2one('product.category','Category', required=True, change_default=True),
254         'list_price': fields.float('Sale Price', digits=(16, int(config['price_accuracy']))),
255         'standard_price': fields.float('Cost Price', required=True, digits=(16, int(config['price_accuracy']))),
256         'volume': fields.float('Volume', help="The weight in Kg."),
257         'weight': fields.float('Gross weight'),
258         'weight_net': fields.float('Net weight'),
259         'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True),
260         'warranty': fields.float('Warranty (months)'),
261         '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."),
262         '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."),
263         'uom_id': fields.many2one('product.uom', 'Default UoM', required=True, help="This is the default Unit of Measure used for all stock operation."),
264         'uom_po_id': fields.many2one('product.uom', 'Purchase UoM', required=True),
265         '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."),
266         'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
267             help='Keep empty to use the default UOM'),
268         'uos_coeff': fields.float('UOM -> UOS Coeff', digits=(16,4),
269             help='Coefficient to convert UOM to UOS\n'
270             ' uom = uos * coeff'),
271         'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
272         'tracking': fields.boolean('Track Lots', help="Force to use a Production Lot number during stock operations for traceability."),
273         '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."),
274         'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Partners'),
275         'loc_rack': fields.char('Rack', size=16),
276         'loc_row': fields.char('Row', size=16),
277         'loc_case': fields.char('Case', size=16),
278     }
279
280     def _get_uom_id(self, cr, uid, *args):
281         cr.execute('select id from product_uom order by id limit 1')
282         res = cr.fetchone()
283         return res and res[0] or False
284
285     _defaults = {
286         'type': lambda *a: 'product',
287         'list_price': lambda *a: 1,
288         'cost_method': lambda *a: 'standard',
289         'supply_method': lambda *a: 'buy',
290         'standard_price': lambda *a: 1,
291         'sale_ok': lambda *a: 1,
292         'sale_delay': lambda *a: 7,
293         'produce_delay': lambda *a: 1,
294         'purchase_ok': lambda *a: 1,
295         'procure_method': lambda *a: 'make_to_stock',
296         'uom_id': _get_uom_id,
297         'uom_po_id': _get_uom_id,
298         'uos_coeff' : lambda *a: 1.0,
299         'mes_type' : lambda *a: 'fixed',
300     }
301
302     def _check_uom(self, cursor, user, ids):
303         for product in self.browse(cursor, user, ids):
304             if product.uom_id.id <> product.uom_po_id.id:
305                 return False
306         return True
307
308     def _check_uos(self, cursor, user, ids):
309         for product in self.browse(cursor, user, ids):
310             if product.uos_id \
311                     and product.uos_id.category_id.id \
312                     == product.uom_id.category_id.id:
313                 return False
314         return True
315
316     _constraints = [
317         (_check_uos, 'Error: UOS must be in a different category than the UOM', ['uos_id']),
318         (_check_uom, 'Error: The default UOM and the purchase UOM must be in the same category.', ['uom_id']),
319     ]
320
321     def name_get(self, cr, user, ids, context={}):
322         if 'partner_id' in context:
323             pass
324         return super(product_template, self).name_get(cr, user, ids, context)
325
326 product_template()
327
328 class product_product(osv.osv):
329     def view_header_get(self, cr, uid, view_id, view_type, context):
330         res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
331         if res: return res
332         if (not context.get('categ_id', False)):
333             return False
334         return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context).name
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', 'Palettization', 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             print uom.category_id.id , uom_po.category_id.id
428             if uom.category_id.id != uom_po.category_id.id:
429                 return {'value': {'uom_po_id': uom_id}}
430         return False
431
432     def _check_ean_key(self, cr, uid, ids):
433         for partner in self.browse(cr, uid, ids):
434             if not partner.ean13:
435                 continue
436             if len(partner.ean13) < 12:
437                 return False
438             try:
439                 int(partner.ean13)
440             except:
441                 return False
442             sum=0
443             for i in range(12):
444                 if is_pair(i):
445                     sum += int(partner.ean13[i])
446                 else:
447                     sum += 3 * int(partner.ean13[i])
448             check = int(math.ceil(sum / 10.0) * 10 - sum)
449             if len(partner.ean13) == 12:
450                 self.write(cr, uid, partner.id, {
451                     'ean13': partner.ean13 + str(check)
452                     })
453             elif check != int(partner.ean13[12]):
454                 return False
455         return True
456
457     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
458
459     def on_order(self, cr, uid, ids, orderline, quantity):
460         pass
461
462     def name_get(self, cr, user, ids, context={}):
463         if not len(ids):
464             return []
465         def _name_get(d):
466             #name = self._product_partner_ref(cr, user, [d['id']], '', '', context)[d['id']]
467             #code = self._product_code(cr, user, [d['id']], '', '', context)[d['id']]
468             name = d.get('name','')
469             code = d.get('default_code',False)
470             if code:
471                 name = '[%s] %s' % (code,name)
472             if d['variants']:
473                 name = name + ' - %s' % (d['variants'],)
474             return (d['id'], name)
475         result = map(_name_get, self.read(cr, user, ids, ['variants','name','default_code'], context))
476         return result
477
478     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=80):
479         if not args:
480             args=[]
481         if not context:
482             context={}
483         ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
484         if not len(ids):
485             ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
486         if not len(ids):
487             ids = self.search(cr, user, [('default_code',operator,name)]+ args, limit=limit, context=context)
488             ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
489         result = self.name_get(cr, user, ids, context)
490         return result
491
492     #
493     # Could be overrided for variants matrices prices
494     #
495     def price_get(self, cr, uid, ids, ptype='list_price', context={}):
496         res = {}
497         product_uom_obj = self.pool.get('product.uom')
498
499         for product in self.browse(cr, uid, ids, context=context):
500             res[product.id] = product[ptype] or 0.0
501             if ptype == 'list_price':
502                 res[product.id] = (res[product.id] * product.price_margin) + \
503                         product.price_extra
504             if 'uom' in context:
505                 uom = product.uos_id or product.uom_id
506                 res[product.id] = product_uom_obj._compute_price(cr, uid,
507                         uom.id, res[product.id], context['uom'])
508         return res
509
510     def copy(self, cr, uid, id, default=None, context=None):
511         if not context:
512             context={}
513
514         if ('variant' in context) and context['variant']:
515             fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
516                     'price_margin', 'price_extra']
517             data = self.read(cr, uid, id, fields=fields, context=context)
518             for f in fields:
519                 if f in default:
520                     data[f] = default[f]
521             data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
522                     and data['product_tmpl_id'][0]
523             del data['id']
524             return self.create(cr, uid, data)
525         else:
526             return super(product_product, self).copy(cr, uid, id, default=default,
527                     context=context)
528 product_product()
529
530 class product_packaging(osv.osv):
531     _name = "product.packaging"
532     _description = "Conditionnement"
533     _rec_name = 'ean'
534     _columns = {
535         'name' : fields.char('Description', size=64),
536         'qty' : fields.float('Quantity by UL',
537             help="The total number of products you can put by UL."),
538         'ul' : fields.many2one('product.ul', 'Type of UL', required=True),
539         'ul_qty' : fields.integer('UL by layer'),
540         'rows' : fields.integer('Number of layer', required=True,
541             help='The number of layer on palette'),
542         'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
543         'ean' : fields.char('EAN', size=14,
544             help="The EAN code of the transport unit."),
545         'code' : fields.char('Code', size=14,
546             help="The code of the transport unit."),
547         'weight': fields.float('Palette Weight',
548             help='The weight of the empty palette'),
549         'weight_ul': fields.float('UL Weight',
550             help='The weight of the empty UL'),
551         'height': fields.float('Height', help='The height of the palette'),
552         'width': fields.float('Width', help='The width of the palette'),
553         'length': fields.float('Length', help='The length of the palette'),
554     }
555
556     def _get_1st_ul(self, cr, uid, context={}):
557         cr.execute('select id from product_ul order by id asc limit 1')
558         res = cr.fetchone()
559         return (res and res[0]) or False
560
561     _defaults = {
562         'rows' : lambda *a : 3,
563         'ul' : _get_1st_ul,
564     }
565
566     def checksum(ean):
567         salt = '31' * 6 + '3'
568         sum = 0
569         for ean_part, salt_part in zip(ean, salt):
570             sum += int(ean_part) * int(salt_part)
571         return (10 - (sum % 10)) % 10
572     checksum = staticmethod(checksum)
573
574 product_packaging()
575
576
577 class product_supplierinfo(osv.osv):
578     _name = "product.supplierinfo"
579     _description = "Information about a product supplier"
580     _columns = {
581         'name' : fields.many2one('res.partner', 'Partner', required=True, ondelete='cascade'),
582         'product_name': fields.char('Partner product name', size=128),
583         'product_code': fields.char('Partner product reference', size=64),
584         'sequence' : fields.integer('Priority'),
585         'qty' : fields.float('Minimal quantity', required=True),
586         'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
587         'delay' : fields.integer('Delivery delay', required=True),
588         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
589     }
590     _defaults = {
591         'qty': lambda *a: 0.0,
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