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