[MERGE]: merge with lp:~openerp-dev/openobject-addons/ron-dev-addons2
[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
25 import math
26 from _common import rounding
27 import re
28 from tools.translate import _
29
30 def is_pair(x):
31     return not x%2
32
33 #----------------------------------------------------------
34 # UOM
35 #----------------------------------------------------------
36
37 class product_uom_categ(osv.osv):
38     _name = 'product.uom.categ'
39     _description = 'Product uom categ'
40     _columns = {
41         'name': fields.char('Name', size=64, required=True, translate=True),
42     }
43 product_uom_categ()
44
45 class product_uom(osv.osv):
46     _name = 'product.uom'
47     _description = 'Product Unit of Measure'
48
49     def _compute_factor_inv(self, factor):
50         return factor and round(1.0 / factor, 6) or 0.0
51
52     def _factor_inv(self, cursor, user, ids, name, arg, context):
53         res = {}
54         for uom in self.browse(cursor, user, ids, context=context):
55             res[uom.id] = self._compute_factor_inv(uom.factor)
56         return res
57
58     def _factor_inv_write(self, cursor, user, id, name, value, arg, context):
59         return self.write(cursor, user, id, {'factor': self._compute_factor_inv(value)}, context=context)
60
61     def create(self, cr, uid, data, context={}):
62         if 'factor_inv' in data:
63             if data['factor_inv'] <> 1:
64                 data['factor'] = self._compute_factor_inv(data['factor_inv'])
65             del(data['factor_inv'])
66         return super(product_uom, self).create(cr, uid, data, context)
67
68     _columns = {
69         'name': fields.char('Name', size=64, required=True, translate=True),
70         'category_id': fields.many2one('product.uom.categ', 'UoM Category', required=True, ondelete='cascade',
71             help="Quantity conversions may happen automatically between Units of Measure in the same category, according to their respective ratios."),
72         'factor': fields.float('Ratio', required=True,digits=(12, 12),
73             help='How many times this UoM is smaller than the reference UoM in this category:\n'\
74                     '1 * (reference unit) = ratio * (this unit)'),
75         'factor_inv': fields.function(_factor_inv, digits_compute=dp.get_precision('Product UoM'),
76             fnct_inv=_factor_inv_write,
77             method=True, string='Ratio',
78             help='How many times this UoM is bigger than the reference UoM in this category:\n'\
79                     '1 * (this unit) = ratio * (reference unit)', required=True),
80         'rounding': fields.float('Rounding Precision', digits_compute=dp.get_precision('Product UoM'), required=True,
81             help="The computed quantity will be a multiple of this value. "\
82                  "Use 1.0 for a UoM that cannot be further split, such as a piece."),
83         'active': fields.boolean('Active', help="By unchecking the active field you can disable a unit of measure without deleting it."),
84         'uom_type': fields.selection([('bigger','Bigger than the reference UoM'),
85                                       ('reference','Reference UoM for this category'),
86                                       ('smaller','Smaller than the reference UoM')],'UoM Type', required=1),
87     }
88
89     _defaults = {
90         'active': 1,
91         'rounding': 0.01,
92         'uom_type': 'reference',
93     }
94
95     _sql_constraints = [
96         ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!'),
97     ]
98
99     def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
100         if not from_uom_id or not qty or not to_uom_id:
101             return qty
102         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
103         if uoms[0].id == from_uom_id:
104             from_unit, to_unit = uoms[0], uoms[-1]
105         else:
106             from_unit, to_unit = uoms[-1], uoms[0]
107         return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit)
108
109     def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context=None):
110         if context is None:
111             context = {}
112         if from_unit.category_id.id <> to_unit.category_id.id:
113             if context.get('raise-exception', True):
114                 raise osv.except_osv(_('Error !'), _('Conversion from Product UoM m to Default UoM PCE is not possible as they both belong to different Category!.'))
115             else:
116                 return qty
117         amount = qty / from_unit.factor
118         if to_unit:
119             amount = rounding(amount * to_unit.factor, to_unit.rounding)
120         return amount
121
122     def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
123         if not from_uom_id or not price or not to_uom_id:
124             return price
125         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
126         if uoms[0].id == from_uom_id:
127             from_unit, to_unit = uoms[0], uoms[-1]
128         else:
129             from_unit, to_unit = uoms[-1], uoms[0]
130         if from_unit.category_id.id <> to_unit.category_id.id:
131             return price
132         amount = price * from_unit.factor
133         if to_uom_id:
134             amount = amount / to_unit.factor
135         return amount
136
137     def onchange_type(self, cursor, user, ids, value):
138         if value == 'reference':
139             return {'value': {'factor': 1, 'factor_inv': 1}}
140         return {}
141
142 product_uom()
143
144
145 class product_ul(osv.osv):
146     _name = "product.ul"
147     _description = "Shipping Unit"
148     _columns = {
149         'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
150         'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
151     }
152 product_ul()
153
154
155 #----------------------------------------------------------
156 # Categories
157 #----------------------------------------------------------
158 class product_category(osv.osv):
159
160     def name_get(self, cr, uid, ids, context=None):
161         if not len(ids):
162             return []
163         reads = self.read(cr, uid, ids, ['name','parent_id'], context)
164         res = []
165         for record in reads:
166             name = record['name']
167             if record['parent_id']:
168                 name = record['parent_id'][1]+' / '+name
169             res.append((record['id'], name))
170         return res
171
172     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context):
173         res = self.name_get(cr, uid, ids, context)
174         return dict(res)
175
176     _name = "product.category"
177     _description = "Product Category"
178     _columns = {
179         'name': fields.char('Name', size=64, required=True, translate=True),
180         'complete_name': fields.function(_name_get_fnc, method=True, type="char", string='Name'),
181         'parent_id': fields.many2one('product.category','Parent Category', select=True),
182         'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
183         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
184         'type': fields.selection([('view','View'), ('normal','Normal')], 'Category Type'),
185     }
186
187
188     _defaults = {
189         'type' : lambda *a : 'normal',
190     }
191
192     _order = "sequence"
193     def _check_recursion(self, cr, uid, ids):
194         level = 100
195         while len(ids):
196             cr.execute('select distinct parent_id from product_category where id IN %s',(tuple(ids),))
197             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
198             if not level:
199                 return False
200             level -= 1
201         return True
202
203     _constraints = [
204         (_check_recursion, 'Error ! You can not create recursive categories.', ['parent_id'])
205     ]
206     def child_get(self, cr, uid, ids):
207         return [ids]
208
209 product_category()
210
211
212 #----------------------------------------------------------
213 # Products
214 #----------------------------------------------------------
215 class product_template(osv.osv):
216     _name = "product.template"
217     _description = "Product Template"
218     def _calc_seller(self, cr, uid, ids, fields, arg, context={}):
219         result = {}
220         for product in self.browse(cr, uid, ids, context):
221             for field in fields:
222                 result[product.id] = {field:False}
223             result[product.id]['seller_delay'] = 1
224             if product.seller_ids:
225                 partner_list = sorted([(partner_id.sequence, partner_id) for partner_id in  product.seller_ids if partner_id and partner_id.sequence])
226                 main_supplier = partner_list and partner_list[0] and partner_list[0][1] or False
227                 result[product.id]['seller_delay'] =  main_supplier and main_supplier.delay or 1
228                 result[product.id]['seller_qty'] =  main_supplier and main_supplier.qty or 0.0
229                 result[product.id]['seller_id'] = main_supplier and main_supplier.name.id or False
230         return result
231
232     _columns = {
233         'name': fields.char('Name', size=128, required=True, translate=True, select=True),
234         'product_manager': fields.many2one('res.users','Product Manager',help="This is use as task responsible"),
235         'description': fields.text('Description',translate=True),
236         'description_purchase': fields.text('Purchase Description',translate=True),
237         'description_sale': fields.text('Sale Description',translate=True),
238         '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 inventory management in the system."),
239         '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."),
240         'sale_delay': fields.float('Customer Lead Time', help="This is the average delay in days between the confirmation of the customer order and the delivery of the finished products. It's the time you promise to your customers."),
241         'produce_delay': fields.float('Manufacturing Lead Time', help="Average delay in days 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."),
242         'procure_method': fields.selection([('make_to_stock','Make to Stock'),('make_to_order','Make to Order')], 'Procurement 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."),
243         'rental': fields.boolean('Can be Rent'),
244         'categ_id': fields.many2one('product.category','Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
245         '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."),
246         '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."),
247         'volume': fields.float('Volume', help="The volume in m3."),
248         'weight': fields.float('Gross weight', help="The gross weight in Kg."),
249         'weight_net': fields.float('Net weight', help="The net weight in Kg."),
250         'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
251             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."),
252         'warranty': fields.float('Warranty (months)'),
253         '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."),
254         '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."),
255         'state': fields.selection([('',''),
256             ('draft', 'In Development'),
257             ('sellable','Normal'),
258             ('end','End of Lifecycle'),
259             ('obsolete','Obsolete')], 'Status', help="Tells the user if he can use the product or not."),
260         'uom_id': fields.many2one('product.uom', 'Default Unit Of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
261         'uom_po_id': fields.many2one('product.uom', 'Purchase Unit of Measure', required=True, help="Default Unit of Measure used for purchase orders. It must be in the same category than the default unit of measure."),
262         'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
263             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.'),
264         'uos_coeff': fields.float('UOM -> UOS Coeff', digits=(16,4),
265             help='Coefficient to convert UOM to UOS\n'
266             ' uos = uom * coeff'),
267         'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
268         'seller_delay': fields.function(_calc_seller, method=True, type='integer', string='Supplier Lead Time', multi="seller_delay", 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."),
269         'seller_qty': fields.function(_calc_seller, method=True, type='float', string='Supplier Quantity', multi="seller_qty", help="This is minimum quantity to purchase from Main Supplier."),
270         'seller_id': fields.function(_calc_seller, method=True, type='many2one', relation="res.partner", string='Main Supplier', help="Main Supplier who has highest priority in Supplier List.", multi="seller_id"),
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',select=1),
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         md = self.pool.get('ir.model.data')
287         res = md.get_object_reference(cr, uid, 'product', 'cat0') or False
288         return res and res[1] or False
289
290     def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
291         if uom_id:
292             return {'value': {'uom_po_id': uom_id}}
293         return False
294
295     _defaults = {
296         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
297         'type': lambda *a: 'product',
298         'list_price': lambda *a: 1,
299         'cost_method': lambda *a: 'standard',
300         'supply_method': lambda *a: 'buy',
301         'standard_price': lambda *a: 1,
302         'sale_ok': lambda *a: 1,
303         'sale_delay': lambda *a: 7,
304         'produce_delay': lambda *a: 1,
305         'purchase_ok': lambda *a: 1,
306         'procure_method': lambda *a: 'make_to_stock',
307         'uom_id': _get_uom_id,
308         'uom_po_id': _get_uom_id,
309         'uos_coeff' : lambda *a: 1.0,
310         'mes_type' : lambda *a: 'fixed',
311         'categ_id' : _default_category,
312         'type' : lambda *a: 'consu',
313     }
314
315     def _check_uom(self, cursor, user, ids):
316         for product in self.browse(cursor, user, ids):
317             if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
318                 return False
319         return True
320
321     def _check_uos(self, cursor, user, ids):
322         for product in self.browse(cursor, user, ids):
323             if product.uos_id \
324                     and product.uos_id.category_id.id \
325                     == product.uom_id.category_id.id:
326                 return False
327         return True
328
329     _constraints = [
330         (_check_uos, 'Error: UOS must be in a different category than the UOM', ['uos_id']),
331         (_check_uom, 'Error: The default UOM and the purchase UOM must be in the same category.', ['uom_id']),
332     ]
333
334     def name_get(self, cr, user, ids, context={}):
335         if 'partner_id' in context:
336             pass
337         return super(product_template, self).name_get(cr, user, ids, context)
338
339 product_template()
340
341 class product_product(osv.osv):
342     def view_header_get(self, cr, uid, view_id, view_type, context):
343         res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
344         if (context.get('categ_id', False)):
345             return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context).name
346         return res
347
348     def _product_price(self, cr, uid, ids, name, arg, context={}):
349         res = {}
350         quantity = context.get('quantity') or 1.0
351         pricelist = context.get('pricelist', False)
352         if pricelist:
353             for id in ids:
354                 try:
355                     price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, context=context)[pricelist]
356                 except:
357                     price = 0.0
358                 res[id] = price
359         for id in ids:
360             res.setdefault(id, 0.0)
361         return res
362
363     def _get_product_available_func(states, what):
364         def _product_available(self, cr, uid, ids, name, arg, context={}):
365             return {}.fromkeys(ids, 0.0)
366         return _product_available
367
368     _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
369     _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
370     _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
371     _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
372
373     def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
374         res = {}
375         product_uom_obj = self.pool.get('product.uom')
376         for id in ids:
377             res.setdefault(id, 0.0)
378         for product in self.browse(cr, uid, ids, context=context):
379             if 'uom' in context:
380                 uom = product.uos_id or product.uom_id
381                 res[product.id] = product_uom_obj._compute_price(cr, uid,
382                         uom.id, product.list_price, context['uom'])
383             else:
384                 res[product.id] = product.list_price
385             res[product.id] =  (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
386         return res
387
388     def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context={}):
389         for supinfo in product.seller_ids:
390             if supinfo.name.id == partner_id:
391                 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name, 'variants': ''}
392         res = {'code': product.default_code, 'name': product.name, 'variants': product.variants}
393         return res
394
395     def _product_code(self, cr, uid, ids, name, arg, context={}):
396         res = {}
397         for p in self.browse(cr, uid, ids, context):
398             res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context)['code']
399         return res
400
401     def _product_partner_ref(self, cr, uid, ids, name, arg, context={}):
402         res = {}
403         for p in self.browse(cr, uid, ids, context):
404             data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context)
405             if not data['variants']:
406                 data['variants'] = p.variants
407             if not data['code']:
408                 data['code'] = p.code
409             if not data['name']:
410                 data['name'] = p.name
411             res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
412                     (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
413         return res
414
415     _defaults = {
416         'active': lambda *a: 1,
417         'price_extra': lambda *a: 0.0,
418         'price_margin': lambda *a: 1.0,
419     }
420
421     _name = "product.product"
422     _description = "Product"
423     _table = "product_product"
424     _inherits = {'product.template': 'product_tmpl_id'}
425     _columns = {
426         'qty_available': fields.function(_product_qty_available, method=True, type='float', string='Real Stock'),
427         'virtual_available': fields.function(_product_virtual_available, method=True, type='float', string='Virtual Stock'),
428         'incoming_qty': fields.function(_product_incoming_qty, method=True, type='float', string='Incoming'),
429         'outgoing_qty': fields.function(_product_outgoing_qty, method=True, type='float', string='Outgoing'),
430         'price': fields.function(_product_price, method=True, type='float', string='Pricelist', digits_compute=dp.get_precision('Sale Price')),
431         'lst_price' : fields.function(_product_lst_price, method=True, type='float', string='List Price', digits_compute=dp.get_precision('Sale Price')),
432         'code': fields.function(_product_code, method=True, type='char', string='Reference'),
433         'partner_ref' : fields.function(_product_partner_ref, method=True, type='char', string='Customer ref'),
434         'default_code' : fields.char('Reference', size=64),
435         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the product without removing it."),
436         'variants': fields.char('Variants', size=64),
437         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade"),
438         'ean13': fields.char('EAN13', size=13),
439         '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."),
440         'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Sale Price')),
441         'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Sale Price')),
442         'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
443     }
444
445     def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
446         if uom_id and uom_po_id:
447             uom_obj=self.pool.get('product.uom')
448             uom=uom_obj.browse(cursor,user,[uom_id])[0]
449             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
450             if uom.category_id.id != uom_po.category_id.id:
451                 return {'value': {'uom_po_id': uom_id}}
452         return False
453
454     def _check_ean_key(self, cr, uid, ids):
455         for partner in self.browse(cr, uid, ids):
456             if not partner.ean13:
457                 continue
458             if len(partner.ean13) <> 13:
459                 return False
460             try:
461                 int(partner.ean13)
462             except:
463                 return False
464             sum=0
465             for i in range(12):
466                 if is_pair(i):
467                     sum += int(partner.ean13[i])
468                 else:
469                     sum += 3 * int(partner.ean13[i])
470             check = int(math.ceil(sum / 10.0) * 10 - sum)
471             if check != int(partner.ean13[12]):
472                 return False
473         return True
474
475     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
476
477     def on_order(self, cr, uid, ids, orderline, quantity):
478         pass
479
480     def name_get(self, cr, user, ids, context={}):
481         if not len(ids):
482             return []
483         def _name_get(d):
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=100):
495         if not args:
496             args=[]
497         if not context:
498             context={}
499         if name:
500             ids = self.search(cr, user, [('default_code',operator,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             if not len(ids):
507                ptrn=re.compile('(\[(.*?)\])')
508                res = ptrn.search(str(name))
509                if res:
510                    ids = self.search(cr, user, [('default_code','ilike',res.group(2))]+ args, limit=limit, context=context)
511         else:
512             ids = self.search(cr, user, args, limit=limit, context=context)
513         result = self.name_get(cr, user, ids, context)
514         return result
515
516     #
517     # Could be overrided for variants matrices prices
518     #
519     def price_get(self, cr, uid, ids, ptype='list_price', context=None):
520         if context is None:
521             context = {}
522
523         if 'currency_id' in context:
524             pricetype_obj = self.pool.get('product.price.type')
525             price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
526             price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
527
528         res = {}
529         product_uom_obj = self.pool.get('product.uom')
530         for product in self.browse(cr, uid, ids, context=context):
531             res[product.id] = product[ptype] or 0.0
532             if ptype == 'list_price':
533                 res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
534                         product.price_extra
535             if 'uom' in context:
536                 uom = product.uos_id or product.uom_id
537                 res[product.id] = product_uom_obj._compute_price(cr, uid,
538                         uom.id, res[product.id], context['uom'])
539             # Convert from price_type currency to asked one
540             if 'currency_id' in context:
541                 # Take the price_type currency from the product field
542                 # This is right cause a field cannot be in more than one currency
543                 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
544                     context['currency_id'], res[product.id],context=context)
545
546         return res
547
548     def copy(self, cr, uid, id, default=None, context=None):
549         if not context:
550             context={}
551
552         product = self.read(cr, uid, id, ['name'], context=context)
553         if not default:
554             default = {}
555         default = default.copy()
556         default['name'] = product['name'] + _(' (copy)')
557
558         if context.get('variant',False):
559             fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
560                     'price_margin', 'price_extra']
561             data = self.read(cr, uid, id, fields=fields, context=context)
562             for f in fields:
563                 if f in default:
564                     data[f] = default[f]
565             data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
566                     and data['product_tmpl_id'][0]
567             del data['id']
568             return self.create(cr, uid, data)
569         else:
570             return super(product_product, self).copy(cr, uid, id, default=default,
571                     context=context)
572 product_product()
573
574 class product_packaging(osv.osv):
575     _name = "product.packaging"
576     _description = "Packaging"
577     _rec_name = 'ean'
578     _order = 'sequence'
579     _columns = {
580         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
581         'name' : fields.text('Description', size=64),
582         'qty' : fields.float('Quantity by Package',
583             help="The total number of products you can put by pallet or box."),
584         'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
585         'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
586         'rows' : fields.integer('Number of Layers', required=True,
587             help='The number of layers on a pallet or box'),
588         'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
589         'ean' : fields.char('EAN', size=14,
590             help="The EAN code of the package unit."),
591         'code' : fields.char('Code', size=14,
592             help="The code of the transport unit."),
593         'weight': fields.float('Total Package Weight',
594             help='The weight of a full package, pallet or box.'),
595         'weight_ul': fields.float('Empty Package Weight',
596             help='The weight of the empty UL'),
597         'height': fields.float('Height', help='The height of the package'),
598         'width': fields.float('Width', help='The width of the package'),
599         'length': fields.float('Length', help='The length of the package'),
600     }
601
602
603     def name_get(self, cr, uid, ids, context={}):
604         if not len(ids):
605             return []
606         res = []
607         for pckg in self.browse(cr, uid, ids,context=context):
608             p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
609             p_name += pckg.ul.name
610             res.append((pckg.id,p_name))
611         return res
612
613     def _get_1st_ul(self, cr, uid, context={}):
614         cr.execute('select id from product_ul order by id asc limit 1')
615         res = cr.fetchone()
616         return (res and res[0]) or False
617
618     _defaults = {
619         'rows' : lambda *a : 3,
620         'sequence' : lambda *a : 1,
621         'ul' : _get_1st_ul,
622     }
623
624     def checksum(ean):
625         salt = '31' * 6 + '3'
626         sum = 0
627         for ean_part, salt_part in zip(ean, salt):
628             sum += int(ean_part) * int(salt_part)
629         return (10 - (sum % 10)) % 10
630     checksum = staticmethod(checksum)
631
632 product_packaging()
633
634
635 class product_supplierinfo(osv.osv):
636     _name = "product.supplierinfo"
637     _description = "Information about a product supplier"
638     def _calc_qty(self, cr, uid, ids, fields, arg, context={}):
639         result = {}
640         product_uom_pool = self.pool.get('product.uom')
641         for supplier_info in self.browse(cr, uid, ids, context):
642             for field in fields:
643                 result[supplier_info.id] = {field:False}
644             if supplier_info.product_uom.id:
645                 qty = product_uom_pool._compute_qty(cr, uid, supplier_info.product_uom.id, supplier_info.min_qty, to_uom_id=supplier_info.product_id.uom_id.id)
646             else:
647                 qty = supplier_info.min_qty
648             result[supplier_info.id]['qty'] = qty
649         return result
650
651     def _get_uom_id(self, cr, uid, *args):
652         cr.execute('select id from product_uom order by id limit 1')
653         res = cr.fetchone()
654         return res and res[0] or False
655
656     _columns = {
657         'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
658         'product_name': fields.char('Supplier Product Name', size=128, help="This supplier's product name will be used when printing a request for quotation. Keep empty to use the internal one."),
659         'product_code': fields.char('Supplier Product Code', size=64, help="This supplier's product code will be used when printing a request for quotation. Keep empty to use the internal one."),
660         'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
661         'product_uom': fields.many2one('product.uom', string="UOM", help="Supplier Product UoM."),
662         'min_qty': fields.float('Minimal Quantity', required=True, help="The minimal quantity to purchase to this supplier, expressed in the default unit of measure."),
663         'qty': fields.function(_calc_qty, method=True, store=True, type='float', string='Quantity', multi="qty", help="This is a quantity which is converted into Default Uom."),
664         'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
665         '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."),
666         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
667         'company_id':fields.many2one('res.company','Company',select=1),
668     }
669     _defaults = {
670         'qty': lambda *a: 0.0,
671         'sequence': lambda *a: 1,
672         'delay': lambda *a: 1,
673         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c)
674     }
675     def _check_uom(self, cr, uid, ids):
676         for supplier_info in self.browse(cr, uid, ids):
677             if supplier_info.product_uom and supplier_info.product_uom.category_id.id <> supplier_info.product_id.uom_id.category_id.id:
678                 return False
679         return True
680
681     _constraints = [
682         (_check_uom, 'Error: The default UOM and the Supplier Product UOM must be in the same category.', ['product_uom']),
683     ]
684     def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
685         """
686         Calculate price from supplier pricelist.
687         @param supplier_ids: Ids of res.partner object.
688         @param product_id: Id of product.
689         @param product_qty: specify quantity to purchase.
690         """
691         if not context:
692             context = {}
693         if type(supplier_ids) in (int,long,):
694             supplier_ids = [supplier_ids]
695         res = {}
696         product_pool = self.pool.get('product.product')
697         partner_pool = self.pool.get('res.partner')
698         pricelist_pool = self.pool.get('product.pricelist')
699         currency_pool = self.pool.get('res.currency')
700         currency_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.currency_id.id
701         for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
702             # Compute price from standard price of product
703             price = product_pool.price_get(cr, uid, [product_id], 'standard_price')[product_id]
704
705             # Compute price from Purchase pricelist of supplier
706             pricelist_id = supplier.property_product_pricelist_purchase.id
707             if pricelist_id:
708                 price = pricelist_pool.price_get(cr, uid, [pricelist_id], product_id, product_qty).setdefault(pricelist_id, 0)
709                 price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
710
711             # Compute price from supplier pricelist which are in Supplier Information
712             supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_id','=',product_id)])
713             if supplier_info_ids:
714                 cr.execute('SELECT * ' \
715                     'FROM pricelist_partnerinfo ' \
716                     'WHERE suppinfo_id IN %s' \
717                     'AND min_quantity <= %s ' \
718                     'ORDER BY min_quantity DESC LIMIT 1', (tuple(supplier_info_ids),product_qty,))
719                 res2 = cr.dictfetchone()
720                 if res2:
721                     price = res2['price']
722             res[supplier.id] = price
723         return res
724     _order = 'sequence'
725 product_supplierinfo()
726
727
728 class pricelist_partnerinfo(osv.osv):
729     _name = 'pricelist.partnerinfo'
730     _columns = {
731         'name': fields.char('Description', size=64),
732         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
733         'min_quantity': fields.float('Quantity', required=True),
734         'price': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Purchase Price')),
735     }
736     _order = 'min_quantity asc'
737 pricelist_partnerinfo()
738 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
739
740 class res_users(osv.osv):
741     _inherit = 'res.users'
742     def _get_group(self, cr, uid, context):
743         result = super(res_users, self)._get_group(cr, uid, context)
744         dataobj = self.pool.get('ir.model.data')
745         try:
746             dummy,group_id = dataobj.get_object_reference(cr, 1, 'product', 'group_product_manager')
747             result.append(group_id)
748         except ValueError:
749             # If these groups does not exists anymore
750             pass
751         return result
752
753     _defaults = {
754         'groups_id': _get_group,
755     }
756
757 res_users()