[MERGE] merged from lp:~openerp/openobject-addons/trunk/
[odoo/odoo.git] / addons / product / product.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from osv import osv, fields
23 import decimal_precision as dp
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('Ratio', digits=(12, 6), required=True,
86             help='The coefficient for the formula:\n' \
87                     '1 (base unit) = coeff (this unit). Ratio = 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', help="If the active field is set to true, it will allow you to hide the unit of measure without removing it."),
96         'uom_factor': fields.selection([('bigger','Bigger than the Default'),
97                                         ('smaller','Smaller than the Default'),
98                                         ('','')],'UoM Factor'),
99     }
100
101     _defaults = {
102         'factor': lambda *a: 1.0,
103         'factor_inv': lambda *a: 1.0,
104         'active': lambda *a: 1,
105         'rounding': lambda *a: 0.01,
106         'uom_factor': lambda *a: 'smaller',
107     }
108
109     _sql_constraints = [
110         ('factor_gt_zero', 'CHECK (factor!=0)', 'Value of the factor can never be 0 !'),
111         ('factor_inv_data_gt_zero', 'CHECK (factor_inv_data!=0)', 'Value of the factor_inv_data can never be 0 !'),
112     ]
113
114     def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
115         if not from_uom_id or not qty or not to_uom_id:
116             return qty
117         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
118         if uoms[0].id == from_uom_id:
119             from_unit, to_unit = uoms[0], uoms[-1]
120         else:
121             from_unit, to_unit = uoms[-1], uoms[0]
122         return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit)
123
124     def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context={}):
125         if from_unit.category_id.id <> to_unit.category_id.id:
126             return qty
127         if from_unit.factor_inv_data:
128             amount = qty * from_unit.factor_inv_data
129         else:
130             amount = qty / from_unit.factor
131         if to_unit:
132             if to_unit.factor_inv_data:
133                 amount = rounding(amount / to_unit.factor_inv_data, to_unit.rounding)
134             else:
135                 amount = rounding(amount * to_unit.factor, to_unit.rounding)
136         return amount
137
138     def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
139         if not from_uom_id or not price or not to_uom_id:
140             return price
141         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
142         if uoms[0].id == from_uom_id:
143             from_unit, to_unit = uoms[0], uoms[-1]
144         else:
145             from_unit, to_unit = uoms[-1], uoms[0]
146         if from_unit.category_id.id <> to_unit.category_id.id:
147             return price
148         if from_unit.factor_inv_data:
149             amount = price / from_unit.factor_inv_data
150         else:
151             amount = price * from_unit.factor
152         if to_uom_id:
153             if to_unit.factor_inv_data:
154                 amount = amount * to_unit.factor_inv_data
155             else:
156                 amount = amount / to_unit.factor
157         return amount
158
159     def onchange_factor_inv(self, cursor, user, ids, value):
160         if value == 0.0:
161             return {'value': {'factor': 0}}
162         return {'value': {'factor': round(1/value, 6)}}
163
164     def onchange_factor(self, cursor, user, ids, value):
165         if value == 0.0:
166             return {'value': {'factor_inv': 0}}
167         return {'value': {'factor_inv': round(1/value, 6)}}
168
169 product_uom()
170
171
172 class product_ul(osv.osv):
173     _name = "product.ul"
174     _description = "Shipping Unit"
175     _columns = {
176         'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
177         'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
178     }
179 product_ul()
180
181
182 #----------------------------------------------------------
183 # Categories
184 #----------------------------------------------------------
185 class product_category(osv.osv):
186
187     def name_get(self, cr, uid, ids, context=None):
188         if not len(ids):
189             return []
190         reads = self.read(cr, uid, ids, ['name','parent_id'], context)
191         res = []
192         for record in reads:
193             name = record['name']
194             if record['parent_id']:
195                 name = record['parent_id'][1]+' / '+name
196             res.append((record['id'], name))
197         return res
198
199     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context):
200         res = self.name_get(cr, uid, ids, context)
201         return dict(res)
202
203     _name = "product.category"
204     _description = "Product Category"
205     _columns = {
206         'name': fields.char('Name', size=64, required=True, translate=True),
207         'complete_name': fields.function(_name_get_fnc, method=True, type="char", string='Name'),
208         'parent_id': fields.many2one('product.category','Parent Category', select=True),
209         'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
210         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
211         'type': fields.selection([('view','View'), ('normal','Normal')], 'Category Type'),
212     }
213     
214     _defaults = {
215         'type' : lambda *a : 'normal',
216     }
217     
218     _order = "sequence"
219     def _check_recursion(self, cr, uid, ids):
220         level = 100
221         while len(ids):
222             cr.execute('select distinct parent_id from product_category where id =ANY(%s)',(ids,))
223             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
224             if not level:
225                 return False
226             level -= 1
227         return True
228
229     _constraints = [
230         (_check_recursion, 'Error ! You can not create recursive categories.', ['parent_id'])
231     ]
232     def child_get(self, cr, uid, ids):
233         return [ids]
234
235 product_category()
236
237
238 #----------------------------------------------------------
239 # Products
240 #----------------------------------------------------------
241 class product_template(osv.osv):
242     _name = "product.template"
243     _description = "Product Template"
244     def _calc_seller_delay(self, cr, uid, ids, name, arg, context={}):
245         result = {}
246         for product in self.browse(cr, uid, ids, context):
247             if product.seller_ids:
248                 result[product.id] = product.seller_ids[0].delay
249             else:
250                 result[product.id] = 1
251         return result
252
253     _columns = {
254         'name': fields.char('Name', size=128, required=True, translate=True, select=True),
255         'product_manager': fields.many2one('res.users','Product Manager'),
256         'description': fields.text('Description',translate=True),
257         'description_purchase': fields.text('Purchase Description',translate=True),
258         'description_sale': fields.text('Sale Description',translate=True),
259         'type': fields.selection([('product','Stockable Product'),('consu', 'Consumable'),('service','Service')], 'Product Type', required=True, help="Will change the way requisitions are processed. Consumables are stockable products with infinite stock, or for use when you have no inventory management in the system."),
260         '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."),
261         '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."),
262         '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 lead times will be summed for all levels and purchase orders."),
263         'procure_method': fields.selection([('make_to_stock','Make to Stock'),('make_to_order','Make to Order')], 'Requisition 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 requisition request."),
264         'rental': fields.boolean('Can be Rent'),
265         'categ_id': fields.many2one('product.category','Category', required=True, change_default=True, domain="[('type','=','normal')]"),
266         'list_price': fields.float('Sale Price', digits_compute=dp.get_precision('Sale Price'), help="Base price for computing the customer price. Sometimes called the catalog price."),
267         'standard_price': fields.float('Cost Price', required=True, digits_compute=dp.get_precision('Account'), help="Product's cost for accounting stock valuation. It is the base price for the supplier price."),
268         'volume': fields.float('Volume', help="The volume in m3."),
269         'weight': fields.float('Gross weight', help="The gross weight in Kg."),
270         'weight_net': fields.float('Net weight', help="The net weight in Kg."),
271         'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
272             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."),
273         'warranty': fields.float('Warranty (months)'),
274         'sale_ok': fields.boolean('Can be sold', help="Determines if the product can be visible in the list of product within a selection from a sale order line."),
275         '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."),
276         'state': fields.selection([('',''),('draft', 'In Development'),('sellable','In Production'),('end','End of Lifecycle'),('obsolete','Obsolete')], 'State', help="Tells the user if he can use the product or not."),
277         'uom_id': fields.many2one('product.uom', 'Default UoM', required=True, help="Default Unit of Measure used for all stock operation."),
278         'uom_po_id': fields.many2one('product.uom', 'Purchase UoM', required=True, help="Default Unit of Measure used for purchase orders. It must be in the same category than the default unit of measure."),
279         'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
280             help='Used by companies that manage two units of measure: invoicing and inventory management. For example, in food industries, you will manage a stock of ham but invoice in Kg. Keep empty to use the default UOM.'),
281         'uos_coeff': fields.float('UOM -> UOS Coeff', digits=(16,4),
282             help='Coefficient to convert UOM to UOS\n'
283             ' uom = uos * coeff'),
284         'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
285         '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."),
286         'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Partners'),
287         'loc_rack': fields.char('Rack', size=16),
288         'loc_row': fields.char('Row', size=16),
289         'loc_case': fields.char('Case', size=16),
290         'company_id': fields.many2one('res.company', 'Company',select=1),
291     }
292
293     def _get_uom_id(self, cr, uid, *args):
294         cr.execute('select id from product_uom order by id limit 1')
295         res = cr.fetchone()
296         return res and res[0] or False
297
298     def _default_category(self, cr, uid, context={}):
299         if 'categ_id' in context and context['categ_id']:
300             return context['categ_id']
301         return False
302
303     def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
304         if uom_id and uom_po_id:
305             uom_obj=self.pool.get('product.uom')
306             uom=uom_obj.browse(cursor,user,[uom_id])[0]
307             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
308             if uom.category_id.id != uom_po.category_id.id:
309                 return {'value': {'uom_po_id': uom_id}}
310         return False
311
312     _defaults = {
313         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
314 #        'company_id': lambda self, cr, uid, context: False, # Visible by all
315         'type': lambda *a: 'product',
316         'list_price': lambda *a: 1,
317         'cost_method': lambda *a: 'standard',
318         'supply_method': lambda *a: 'buy',
319         'standard_price': lambda *a: 1,
320         'sale_ok': lambda *a: 1,
321         'sale_delay': lambda *a: 7,
322         'produce_delay': lambda *a: 1,
323         'purchase_ok': lambda *a: 1,
324         'procure_method': lambda *a: 'make_to_stock',
325         'uom_id': _get_uom_id,
326         'uom_po_id': _get_uom_id,
327         'uos_coeff' : lambda *a: 1.0,
328         'mes_type' : lambda *a: 'fixed',
329         'categ_id' : _default_category,
330     }
331
332     def _check_uom(self, cursor, user, ids):
333         for product in self.browse(cursor, user, ids):
334             if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
335                 return False
336         return True
337
338     def _check_uos(self, cursor, user, ids):
339         for product in self.browse(cursor, user, ids):
340             if product.uos_id \
341                     and product.uos_id.category_id.id \
342                     == product.uom_id.category_id.id:
343                 return False
344         return True
345
346     _constraints = [
347         (_check_uos, 'Error: UOS must be in a different category than the UOM', ['uos_id']),
348         (_check_uom, 'Error: The default UOM and the purchase UOM must be in the same category.', ['uom_id']),
349     ]
350
351     def name_get(self, cr, user, ids, context={}):
352         if 'partner_id' in context:
353             pass
354         return super(product_template, self).name_get(cr, user, ids, context)
355
356 product_template()
357
358 class product_product(osv.osv):
359     def view_header_get(self, cr, uid, view_id, view_type, context):
360         res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
361         if (context.get('categ_id', False)):
362             return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context).name
363         return res
364
365     def _product_price(self, cr, uid, ids, name, arg, context={}):
366         res = {}
367         quantity = context.get('quantity', 1)
368         pricelist = context.get('pricelist', False)
369         if pricelist:
370             for id in ids:
371                 try:
372                     price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, context=context)[pricelist]
373                 except:
374                     price = 0.0
375                 res[id] = price
376         for id in ids:
377             res.setdefault(id, 0.0)
378         return res
379
380     def _get_product_available_func(states, what):
381         def _product_available(self, cr, uid, ids, name, arg, context={}):
382             return {}.fromkeys(ids, 0.0)
383         return _product_available
384
385     _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
386     _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
387     _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
388     _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
389
390     def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
391         res = {}
392         product_uom_obj = self.pool.get('product.uom')
393         for id in ids:
394             res.setdefault(id, 0.0)
395         for product in self.browse(cr, uid, ids, context=context):
396             if 'uom' in context:
397                 uom = product.uos_id or product.uom_id
398                 res[product.id] = product_uom_obj._compute_price(cr, uid,
399                         uom.id, product.list_price, context['uom'])
400             else:
401                 res[product.id] = product.list_price
402             res[product.id] =  (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
403         return res
404
405     def _get_partner_code_name(self, cr, uid, ids, product_id, partner_id, context={}):
406         product = self.browse(cr, uid, [product_id], context)[0]
407         for supinfo in product.seller_ids:
408             if supinfo.name.id == partner_id:
409                 return {'code': supinfo.product_code, 'name': supinfo.product_name, 'variants': ''}
410         return {'code' : product.default_code, 'name' : product.name, 'variants': product.variants}
411
412     def _product_code(self, cr, uid, ids, name, arg, context={}):
413         res = {}
414         for p in self.browse(cr, uid, ids, context):
415             res[p.id] = self._get_partner_code_name(cr, uid, [], p.id, context.get('partner_id', None), context)['code']
416         return res
417
418     def _product_partner_ref(self, cr, uid, ids, name, arg, context={}):
419         res = {}
420         for p in self.browse(cr, uid, ids, context):
421             data = self._get_partner_code_name(cr, uid, [], p.id, context.get('partner_id', None), context)
422             if not data['variants']:
423                 data['variants'] = p.variants
424             if not data['code']:
425                 data['code'] = p.code
426             if not data['name']:
427                 data['name'] = p.name
428             res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
429                     (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
430         return res
431
432     _defaults = {
433         'active': lambda *a: 1,
434         'price_extra': lambda *a: 0.0,
435         'price_margin': lambda *a: 1.0,
436     }
437
438     _name = "product.product"
439     _description = "Product"
440     _table = "product_product"
441     _inherits = {'product.template': 'product_tmpl_id'}
442     _columns = {
443         'qty_available': fields.function(_product_qty_available, method=True, type='float', string='Real Stock'),
444         'virtual_available': fields.function(_product_virtual_available, method=True, type='float', string='Virtual Stock'),
445         'incoming_qty': fields.function(_product_incoming_qty, method=True, type='float', string='Incoming'),
446         'outgoing_qty': fields.function(_product_outgoing_qty, method=True, type='float', string='Outgoing'),
447         'price': fields.function(_product_price, method=True, type='float', string='Customer Price', digits_compute=dp.get_precision('Sale Price')),
448         'lst_price' : fields.function(_product_lst_price, method=True, type='float', string='List Price', digits_compute=dp.get_precision('Sale Price')),
449         'code': fields.function(_product_code, method=True, type='char', string='Code'),
450         'partner_ref' : fields.function(_product_partner_ref, method=True, type='char', string='Customer ref'),
451         'default_code' : fields.char('Code', size=64),
452         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the product without removing it."),
453         'variants': fields.char('Variants', size=64),
454         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=False),
455         'ean13': fields.char('EAN13', size=13),
456         '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 picking order and is mainly used if you use the EDI module."),
457         'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Sale Price')),
458         'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Sale Price')),
459         'pricelist_id': fields.dummy(string='Pricelist',relation='product.pricelist', type='many2one'),
460     }
461
462     def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
463         if uom_id and uom_po_id:
464             uom_obj=self.pool.get('product.uom')
465             uom=uom_obj.browse(cursor,user,[uom_id])[0]
466             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
467             if uom.category_id.id != uom_po.category_id.id:
468                 return {'value': {'uom_po_id': uom_id}}
469         return False
470
471     def _check_ean_key(self, cr, uid, ids):
472         for partner in self.browse(cr, uid, ids):
473             if not partner.ean13:
474                 continue
475             if len(partner.ean13) <> 13:
476                 return False
477             try:
478                 int(partner.ean13)
479             except:
480                 return False
481             sum=0
482             for i in range(12):
483                 if is_pair(i):
484                     sum += int(partner.ean13[i])
485                 else:
486                     sum += 3 * int(partner.ean13[i])
487             check = int(math.ceil(sum / 10.0) * 10 - sum)
488             if check != int(partner.ean13[12]):
489                 return False
490         return True
491
492     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
493
494     def on_order(self, cr, uid, ids, orderline, quantity):
495         pass
496
497     def name_get(self, cr, user, ids, context={}):
498         if not len(ids):
499             return []
500         def _name_get(d):
501             #name = self._product_partner_ref(cr, user, [d['id']], '', '', context)[d['id']]
502             #code = self._product_code(cr, user, [d['id']], '', '', context)[d['id']]
503             name = d.get('name','')
504             code = d.get('default_code',False)
505             if code:
506                 name = '[%s] %s' % (code,name)
507             if d['variants']:
508                 name = name + ' - %s' % (d['variants'],)
509             return (d['id'], name)
510         result = map(_name_get, self.read(cr, user, ids, ['variants','name','default_code'], context))
511         return result
512
513     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
514         if not args:
515             args=[]
516         if not context:
517             context={}
518         if name:
519             ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
520             if not len(ids):
521                 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
522             if not len(ids):
523                 ids = self.search(cr, user, [('default_code',operator,name)]+ args, limit=limit, context=context)
524                 ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
525         else:
526             ids = self.search(cr, user, args, limit=limit, context=context)
527         result = self.name_get(cr, user, ids, context)
528         return result
529
530     #
531     # Could be overrided for variants matrices prices
532     #
533     def price_get(self, cr, uid, ids, ptype='list_price', context={}):
534         res = {}
535         product_uom_obj = self.pool.get('product.uom')
536         for product in self.browse(cr, uid, ids, context=context):
537             res[product.id] = product[ptype] or 0.0
538             if ptype == 'list_price':
539                 res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
540                         product.price_extra
541             if 'uom' in context:
542                 uom = product.uos_id or product.uom_id
543                 res[product.id] = product_uom_obj._compute_price(cr, uid,
544                         uom.id, res[product.id], context['uom'])
545             # Convert from price_type currency to asked one
546             if 'currency_id' in context:
547                 pricetype_obj = self.pool.get('product.price.type')
548                 # Take the price_type currency from the product field
549                 # This is right cause a field cannot be in more than one currency
550                 price_type_id = pricetype_obj.search(cr,ui,['field','=',ptype])[0]
551                 price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
552                 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
553                     context['currency_id'], res[product.id],context=context)
554                 
555         return res
556
557     def copy(self, cr, uid, id, default=None, context=None):
558         if not context:
559             context={}
560
561         product = self.read(cr, uid, id, ['name'], context=context)
562         if not default:
563             default = {}
564         default = default.copy()
565         default['name'] = product['name'] + _(' (copy)')
566
567         if context.get('variant',False):
568             fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
569                     'price_margin', 'price_extra']
570             data = self.read(cr, uid, id, fields=fields, context=context)
571             for f in fields:
572                 if f in default:
573                     data[f] = default[f]
574             data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
575                     and data['product_tmpl_id'][0]
576             del data['id']
577             return self.create(cr, uid, data)
578         else:
579             return super(product_product, self).copy(cr, uid, id, default=default,
580                     context=context)
581 product_product()
582
583 class product_packaging(osv.osv):
584     _name = "product.packaging"
585     _description = "Packaging"
586     _rec_name = 'ean'
587     _columns = {
588         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
589         'name' : fields.char('Description', size=64),
590         'qty' : fields.float('Quantity by Package',
591             help="The total number of products you can put by pallet or box."),
592         'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
593         'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
594         'rows' : fields.integer('Number of Layers', required=True,
595             help='The number of layers on a pallet or box'),
596         'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
597         'ean' : fields.char('EAN', size=14,
598             help="The EAN code of the package unit."),
599         'code' : fields.char('Code', size=14,
600             help="The code of the transport unit."),
601         'weight': fields.float('Total Package Weight',
602             help='The weight of a full package, pallet or box.'),
603         'weight_ul': fields.float('Empty Package Weight',
604             help='The weight of the empty UL'),
605         'height': fields.float('Height', help='The height of the package'),
606         'width': fields.float('Width', help='The width of the package'),
607         'length': fields.float('Length', help='The length of the package'),
608     }
609
610     _order = 'sequence'
611
612     def name_get(self, cr, uid, ids, context={}):
613         if not len(ids):
614             return []
615         res = []
616         for pckg in self.browse(cr, uid, ids,context=context):
617             p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
618             p_name += pckg.ul.name
619             res.append((pckg.id,p_name))
620         return res
621
622     def _get_1st_ul(self, cr, uid, context={}):
623         cr.execute('select id from product_ul order by id asc limit 1')
624         res = cr.fetchone()
625         return (res and res[0]) or False
626
627     _defaults = {
628         'rows' : lambda *a : 3,
629         'sequence' : lambda *a : 1,
630         'ul' : _get_1st_ul,
631     }
632
633     def checksum(ean):
634         salt = '31' * 6 + '3'
635         sum = 0
636         for ean_part, salt_part in zip(ean, salt):
637             sum += int(ean_part) * int(salt_part)
638         return (10 - (sum % 10)) % 10
639     checksum = staticmethod(checksum)
640
641 product_packaging()
642
643
644 class product_supplierinfo(osv.osv):
645     _name = "product.supplierinfo"
646     _description = "Information about a product supplier"
647     _columns = {
648         'name' : fields.many2one('res.partner', 'Partner', required=True, ondelete='cascade', help="Supplier of this product"),
649         'product_name': fields.char('Partner Product Name', size=128, help="This partner's product name will be used when printing a request for quotation. Keep empty to use the internal one."),
650         'product_code': fields.char('Partner Product Code', size=64, help="This partner's product code will be used when printing a request for quotation. Keep empty to use the internal one."),
651         'sequence' : fields.integer('Priority', help="Assigns the priority to the list of product supplier."),
652         'qty' : fields.float('Minimal Quantity', required=True, help="The minimal quantity to purchase to this supplier, expressed in the default unit of measure."),
653         'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
654         'delay' : fields.integer('Delivery Lead Time', required=True, help="Lead time 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."),
655         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
656         'company_id':fields.many2one('res.company','Company',select=1),
657     }
658     _defaults = {
659         'qty': lambda *a: 0.0,
660         'sequence': lambda *a: 1,
661         'delay': lambda *a: 1,
662         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c)
663     }
664     _order = 'sequence'
665 product_supplierinfo()
666
667
668 class pricelist_partnerinfo(osv.osv):
669     _name = 'pricelist.partnerinfo'
670     _columns = {
671         'name': fields.char('Description', size=64),
672         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
673         'min_quantity': fields.float('Quantity', required=True),
674         'price': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Purchase Price')),
675     }
676     _order = 'min_quantity asc'
677 pricelist_partnerinfo()
678
679
680
681 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
682