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