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