[FIX] Corrected language code for Slovenian and Danish locales and renamed correspond...
[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 from tools.translate import _
31
32 def is_pair(x):
33     return not x%2
34
35 #----------------------------------------------------------
36 # UOM
37 #----------------------------------------------------------
38
39 class product_uom_categ(osv.osv):
40     _name = 'product.uom.categ'
41     _description = 'Product uom categ'
42     _columns = {
43         'name': fields.char('Name', size=64, required=True, translate=True),
44     }
45 product_uom_categ()
46
47 class product_uom(osv.osv):
48     _name = 'product.uom'
49     _description = 'Product Unit of Measure'
50
51     def _factor(self, cursor, user, ids, name, arg, context):
52         res = {}
53         for uom in self.browse(cursor, user, ids, context=context):
54             if uom.factor:
55                 if uom.factor_inv_data:
56                     res[uom.id] = uom.factor_inv_data
57                 else:
58                     res[uom.id] = round(1 / uom.factor, 6)
59             else:
60                 res[uom.id] = 0.0
61         return res
62
63     def _factor_inv(self, cursor, user, id, name, value, arg, context):
64         ctx = context.copy()
65         if 'read_delta' in ctx:
66             del ctx['read_delta']
67         if value:
68             data = 0.0
69             if round(1 / round(1/value, 6), 6) != value:
70                 data = value
71             self.write(cursor, user, id, {
72                 'factor': round(1/value, 6),
73                 'factor_inv_data': data,
74                 }, context=ctx)
75         else:
76             self.write(cursor, user, id, {
77                 'factor': 0.0,
78                 'factor_inv_data': 0.0,
79                 }, context=ctx)
80
81     _columns = {
82         'name': fields.char('Name', size=64, required=True, translate=True),
83         'category_id': fields.many2one('product.uom.categ', 'UoM Category', required=True, ondelete='cascade',
84             help="Unit of Measure of a category can be converted between each others in the same category."),
85         'factor': fields.float('Rate', digits=(12, 6), required=True,
86             help='The coefficient for the formula:\n' \
87                     '1 (base unit) = coeff (this unit). Rate = 1 / Factor.'),
88         'factor_inv': fields.function(_factor, digits=(12, 6),
89             method=True, string='Factor',
90             help='The coefficient for the formula:\n' \
91                     'coeff (base unit) = 1 (this unit). Factor = 1 / Rate.'),
92         'factor_inv_data': fields.float('Factor', digits=(12, 6)),
93         'rounding': fields.float('Rounding Precision', digits=(16, 3), required=True,
94             help="The computed quantity will be a multiple of this value. Use 1.0 for products that can not be split."),
95         'active': fields.boolean('Active'),
96     }
97
98     _defaults = {
99         'factor': lambda *a: 1.0,
100         'factor_inv': lambda *a: 1.0,
101         'active': lambda *a: 1,
102         'rounding': lambda *a: 0.01,
103     }
104
105     def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
106         if not from_uom_id or not qty or not to_uom_id:
107             return qty
108         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
109         if uoms[0].id == from_uom_id:
110             from_unit, to_unit = uoms[0], uoms[-1]
111         else:
112             from_unit, to_unit = uoms[-1], uoms[0]
113         return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit)
114
115     def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context={}):
116         if from_unit.category_id.id <> to_unit.category_id.id:
117             return qty
118         if from_unit.factor_inv_data:
119             amount = qty * from_unit.factor_inv_data
120         else:
121             amount = qty / from_unit.factor
122         if to_unit:
123             if to_unit.factor_inv_data:
124                 amount = rounding(amount / to_unit.factor_inv_data, to_unit.rounding)
125             else:
126                 amount = rounding(amount * to_unit.factor, to_unit.rounding)
127         return amount
128
129     def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
130         if not from_uom_id or not price or not to_uom_id:
131             return price
132         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
133         if uoms[0].id == from_uom_id:
134             from_unit, to_unit = uoms[0], uoms[-1]
135         else:
136             from_unit, to_unit = uoms[-1], uoms[0]
137         if from_unit.category_id.id <> to_unit.category_id.id:
138             return price
139         if from_unit.factor_inv_data:
140             amount = price / from_unit.factor_inv_data
141         else:
142             amount = price * from_unit.factor
143         if to_uom_id:
144             if to_unit.factor_inv_data:
145                 amount = amount * to_unit.factor_inv_data
146             else:
147                 amount = amount / to_unit.factor
148         return amount
149
150     def onchange_factor_inv(self, cursor, user, ids, value):
151         if value == 0.0:
152             return {'value': {'factor': 0}}
153         return {'value': {'factor': round(1/value, 6)}}
154
155     def onchange_factor(self, cursor, user, ids, value):
156         if value == 0.0:
157             return {'value': {'factor_inv': 0}}
158         return {'value': {'factor_inv': round(1/value, 6)}}
159
160 product_uom()
161
162
163 class product_ul(osv.osv):
164     _name = "product.ul"
165     _description = "Shipping Unit"
166     _columns = {
167         'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
168         'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('palet', 'Pallet')], 'Type', required=True),
169     }
170 product_ul()
171
172
173 #----------------------------------------------------------
174 # Categories
175 #----------------------------------------------------------
176 class product_category(osv.osv):
177
178     def name_get(self, cr, uid, ids, context=None):
179         if not len(ids):
180             return []
181         reads = self.read(cr, uid, ids, ['name','parent_id'], context)
182         res = []
183         for record in reads:
184             name = record['name']
185             if record['parent_id']:
186                 name = record['parent_id'][1]+' / '+name
187             res.append((record['id'], name))
188         return res
189
190     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context):
191         res = self.name_get(cr, uid, ids, context)
192         return dict(res)
193
194     _name = "product.category"
195     _description = "Product Category"
196     _columns = {
197         'name': fields.char('Name', size=64, required=True, translate=True),
198         'complete_name': fields.function(_name_get_fnc, method=True, type="char", string='Name'),
199         'parent_id': fields.many2one('product.category','Parent Category', select=True),
200         'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
201         'sequence': fields.integer('Sequence'),
202     }
203     _order = "sequence"
204     def _check_recursion(self, cr, uid, ids):
205         level = 100
206         while len(ids):
207             cr.execute('select distinct parent_id from product_category where id in ('+','.join(map(str,ids))+')')
208             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
209             if not level:
210                 return False
211             level -= 1
212         return True
213
214     _constraints = [
215         (_check_recursion, 'Error ! You can not create recursive categories.', ['parent_id'])
216     ]
217     def child_get(self, cr, uid, ids):
218         return [ids]
219
220 product_category()
221
222
223 #----------------------------------------------------------
224 # Products
225 #----------------------------------------------------------
226 class product_template(osv.osv):
227     _name = "product.template"
228     _description = "Product Template"
229     def _calc_seller_delay(self, cr, uid, ids, name, arg, context={}):
230         result = {}
231         for product in self.browse(cr, uid, ids, context):
232             if product.seller_ids:
233                 result[product.id] = product.seller_ids[0].delay
234             else:
235                 result[product.id] = 1
236         return result
237
238     _columns = {
239         'name': fields.char('Name', size=128, required=True, translate=True, select=True),
240         'product_manager': fields.many2one('res.users','Product Manager'),
241         'description': fields.text('Description',translate=True),
242         'description_purchase': fields.text('Purchase Description',translate=True),
243         'description_sale': fields.text('Sale Description',translate=True),
244         'type': fields.selection([('product','Stockable Product'),('consu', 'Consumable'),('service','Service')], 'Product Type', required=True, help="Will change the way procurements are processed. Consumables are stockable products with infinite stock, or for use when you have no stock management in the system."),
245         '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."),
246         '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 finished products. It's the time you promise to your customers."),
247         '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."),
248         '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 re-supplying. 'Make to Order': When needed, purchase or produce for the procurement request."),
249         'rental': fields.boolean('Rentable Product'),
250         'categ_id': fields.many2one('product.category','Category', required=True, change_default=True),
251         '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."),
252         'standard_price': fields.float('Cost Price', required=True, digits=(16, int(config['price_accuracy'])), help="The cost of the product for accounting stock valuation. It can serves as a base price for supplier price."),
253         'volume': fields.float('Volume', help="The volume in m3."),
254         'weight': fields.float('Gross weight', help="The gross weight in Kg."),
255         'weight_net': fields.float('Net weight', help="The net weight in Kg."),
256         'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
257             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."),
258         'warranty': fields.float('Warranty (months)'),
259         '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."),
260         '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."),
261         '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."),
262         'uom_id': fields.many2one('product.uom', 'Default UoM', required=True, help="Default Unit of Measure used for all stock operation."),
263         '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."),
264         'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
265             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.'),
266         'uos_coeff': fields.float('UOM -> UOS Coeff', digits=(16,4),
267             help='Coefficient to convert UOM to UOS\n'
268             ' uom = uos * coeff'),
269         'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
270         '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."),
271         'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Partners'),
272         'loc_rack': fields.char('Rack', size=16),
273         'loc_row': fields.char('Row', size=16),
274         'loc_case': fields.char('Case', size=16),
275         'company_id': fields.many2one('res.company', 'Company'),
276     }
277
278     def _get_uom_id(self, cr, uid, *args):
279         cr.execute('select id from product_uom order by id limit 1')
280         res = cr.fetchone()
281         return res and res[0] or False
282
283     def _default_category(self, cr, uid, context={}):
284         if 'categ_id' in context and context['categ_id']:
285             return context['categ_id']
286         return False
287
288     def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
289         if uom_id and uom_po_id:
290             uom_obj=self.pool.get('product.uom')
291             uom=uom_obj.browse(cursor,user,[uom_id])[0]
292             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
293             if uom.category_id.id != uom_po.category_id.id:
294                 return {'value': {'uom_po_id': uom_id}}
295         return False
296
297     _defaults = {
298         'company_id': lambda self, cr, uid, context: False, # Visible by all
299         'type': lambda *a: 'product',
300         'list_price': lambda *a: 1,
301         'cost_method': lambda *a: 'standard',
302         'supply_method': lambda *a: 'buy',
303         'standard_price': lambda *a: 1,
304         'sale_ok': lambda *a: 1,
305         'sale_delay': lambda *a: 7,
306         'produce_delay': lambda *a: 1,
307         'purchase_ok': lambda *a: 1,
308         'procure_method': lambda *a: 'make_to_stock',
309         'uom_id': _get_uom_id,
310         'uom_po_id': _get_uom_id,
311         'uos_coeff' : lambda *a: 1.0,
312         'mes_type' : lambda *a: 'fixed',
313         'categ_id' : _default_category,
314     }
315
316     def _check_uom(self, cursor, user, ids):
317         for product in self.browse(cursor, user, ids):
318             if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
319                 return False
320         return True
321
322     def _check_uos(self, cursor, user, ids):
323         for product in self.browse(cursor, user, ids):
324             if product.uos_id \
325                     and product.uos_id.category_id.id \
326                     == product.uom_id.category_id.id:
327                 return False
328         return True
329
330     _constraints = [
331         (_check_uos, 'Error: UOS must be in a different category than the UOM', ['uos_id']),
332         (_check_uom, 'Error: The default UOM and the purchase UOM must be in the same category.', ['uom_id']),
333     ]
334
335     def name_get(self, cr, user, ids, context={}):
336         if 'partner_id' in context:
337             pass
338         return super(product_template, self).name_get(cr, user, ids, context)
339
340 product_template()
341
342 class product_product(osv.osv):
343     def view_header_get(self, cr, uid, view_id, view_type, context):
344         res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
345         if (context.get('categ_id', False)):
346             return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context).name
347         return res
348
349     def _product_price(self, cr, uid, ids, name, arg, context={}):
350         res = {}
351         quantity = context.get('quantity', 1)
352         pricelist = context.get('pricelist', False)
353         if pricelist:
354             for id in ids:
355                 try:
356                     price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, context=context)[pricelist]
357                 except:
358                     price = 0.0
359                 res[id] = price
360         for id in ids:
361             res.setdefault(id, 0.0)
362         return res
363
364     def _get_product_available_func(states, what):
365         def _product_available(self, cr, uid, ids, name, arg, context={}):
366             return {}.fromkeys(ids, 0.0)
367         return _product_available
368
369     _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
370     _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
371     _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
372     _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
373
374     def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
375         res = {}
376         product_uom_obj = self.pool.get('product.uom')
377         for id in ids:
378             res.setdefault(id, 0.0)
379         for product in self.browse(cr, uid, ids, context=context):
380             if 'uom' in context:
381                 uom = product.uos_id or product.uom_id
382                 res[product.id] = product_uom_obj._compute_price(cr, uid,
383                         uom.id, product.list_price, context['uom'])
384             else:
385                 res[product.id] = product.list_price
386             res[product.id] =  (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
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['code'] = 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         if name:
500             ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
501             if not len(ids):
502                 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
503             if not len(ids):
504                 ids = self.search(cr, user, [('default_code',operator,name)]+ args, limit=limit, context=context)
505                 ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
506         else:
507             ids = self.search(cr, user, args, limit=limit, context=context)
508         result = self.name_get(cr, user, ids, context)
509         return result
510
511     #
512     # Could be overrided for variants matrices prices
513     #
514     def price_get(self, cr, uid, ids, ptype='list_price', context={}):
515         res = {}
516         product_uom_obj = self.pool.get('product.uom')
517
518         for product in self.browse(cr, uid, ids, context=context):
519             res[product.id] = product[ptype] or 0.0
520             if ptype == 'list_price':
521                 res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
522                         product.price_extra
523             if 'uom' in context:
524                 uom = product.uos_id or product.uom_id
525                 res[product.id] = product_uom_obj._compute_price(cr, uid,
526                         uom.id, res[product.id], context['uom'])
527         return res
528
529     def copy(self, cr, uid, id, default=None, context=None):
530         if not context:
531             context={}
532
533         if ('variant' in context) and context['variant']:
534             fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
535                     'price_margin', 'price_extra']
536             data = self.read(cr, uid, id, fields=fields, context=context)
537             for f in fields:
538                 if f in default:
539                     data[f] = default[f]
540             data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
541                     and data['product_tmpl_id'][0]
542             del data['id']
543             return self.create(cr, uid, data)
544         else:
545             return super(product_product, self).copy(cr, uid, id, default=default,
546                     context=context)
547 product_product()
548
549 class product_packaging(osv.osv):
550     _name = "product.packaging"
551     _description = "Packaging"
552     _rec_name = 'ean'
553     _columns = {
554         'sequence': fields.integer('Sequence'),
555         'name' : fields.char('Description', size=64),
556         'qty' : fields.float('Quantity by Package',
557             help="The total number of products you can put by palet or box."),
558         'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
559         'ul_qty' : fields.integer('Package by layer'),
560         'rows' : fields.integer('Number of Layer', required=True,
561             help='The number of layer on a palet or box'),
562         'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
563         'ean' : fields.char('EAN', size=14,
564             help="The EAN code of the package unit."),
565         'code' : fields.char('Code', size=14,
566             help="The code of the transport unit."),
567         'weight': fields.float('Total Package Weight',
568             help='The weight of a full of products palet or box.'),
569         'weight_ul': fields.float('Empty Package Weight',
570             help='The weight of the empty UL'),
571         'height': fields.float('Height', help='The height of the package'),
572         'width': fields.float('Width', help='The width of the package'),
573         'length': fields.float('Length', help='The length of the package'),
574     }
575
576     def _get_1st_ul(self, cr, uid, context={}):
577         cr.execute('select id from product_ul order by id asc limit 1')
578         res = cr.fetchone()
579         return (res and res[0]) or False
580
581     _defaults = {
582         'rows' : lambda *a : 3,
583         'sequence' : lambda *a : 1,
584         'ul' : _get_1st_ul,
585     }
586
587     def checksum(ean):
588         salt = '31' * 6 + '3'
589         sum = 0
590         for ean_part, salt_part in zip(ean, salt):
591             sum += int(ean_part) * int(salt_part)
592         return (10 - (sum % 10)) % 10
593     checksum = staticmethod(checksum)
594
595 product_packaging()
596
597
598 class product_supplierinfo(osv.osv):
599     _name = "product.supplierinfo"
600     _description = "Information about a product supplier"
601     _columns = {
602         'name' : fields.many2one('res.partner', 'Partner', required=True, ondelete='cascade', help="Supplier of this product"),
603         '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."),
604         '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."),
605         'sequence' : fields.integer('Priority'),
606         'qty' : fields.float('Minimal Quantity', required=True, help="The minimal quantity to purchase for this supplier, expressed in the default unit of measure."),
607         'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
608         '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."),
609         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
610     }
611     _defaults = {
612         'qty': lambda *a: 0.0,
613         'sequence': lambda *a: 1,
614         'delay': lambda *a: 1,
615     }
616     _order = 'sequence'
617 product_supplierinfo()
618
619
620 class pricelist_partnerinfo(osv.osv):
621     _name = 'pricelist.partnerinfo'
622     _columns = {
623         'name': fields.char('Description', size=64),
624         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
625         'min_quantity': fields.float('Quantity', required=True),
626         'price': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
627     }
628     _order = 'min_quantity asc'
629 pricelist_partnerinfo()
630
631
632
633 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
634