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