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