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