[FIX] Bad caculation of the quantity UDM in sale order line
[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     _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=(12,12),
104             fnct_inv=_factor_inv_write,
105             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, 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, name"
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 cannot 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 = [(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. Consumable are product where you don't manage stock."),
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. 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."),
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 UOM.'),
294         'uos_coeff': fields.float('UOM -> UOS Coeff', digits_compute= dp.get_precision('Product UoS'),
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, 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, 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, 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 = False
320         try:
321             res = md.get_object_reference(cr, uid, 'product', 'cat0')[1]
322         except ValueError:
323             res = False
324         return res
325
326     def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
327         if uom_id:
328             return {'value': {'uom_po_id': uom_id}}
329         return False
330
331     _defaults = {
332         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
333         'list_price': lambda *a: 1,
334         'cost_method': lambda *a: 'standard',
335         'supply_method': lambda *a: 'buy',
336         'standard_price': lambda *a: 1,
337         'sale_ok': lambda *a: 1,
338         'sale_delay': lambda *a: 7,
339         'produce_delay': lambda *a: 1,
340         'purchase_ok': lambda *a: 1,
341         'procure_method': lambda *a: 'make_to_stock',
342         'uom_id': _get_uom_id,
343         'uom_po_id': _get_uom_id,
344         'uos_coeff' : lambda *a: 1.0,
345         'mes_type' : lambda *a: 'fixed',
346         'categ_id' : _default_category,
347         'type' : lambda *a: 'consu',
348     }
349
350     def _check_uom(self, cursor, user, ids, context=None):
351         for product in self.browse(cursor, user, ids, context=context):
352             if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
353                 return False
354         return True
355
356     def _check_uos(self, cursor, user, ids, context=None):
357         for product in self.browse(cursor, user, ids, context=context):
358             if product.uos_id \
359                     and product.uos_id.category_id.id \
360                     == product.uom_id.category_id.id:
361                 return False
362         return True
363
364     _constraints = [
365         (_check_uom, 'Error: The default UOM and the purchase UOM must be in the same category.', ['uom_id']),
366     ]
367
368     def name_get(self, cr, user, ids, context=None):
369         if context is None:
370             context = {}
371         if 'partner_id' in context:
372             pass
373         return super(product_template, self).name_get(cr, user, ids, context)
374
375 product_template()
376
377 class product_product(osv.osv):
378     def view_header_get(self, cr, uid, view_id, view_type, context=None):
379         if context is None:
380             context = {}
381         res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
382         if (context.get('categ_id', False)):
383             return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
384         return res
385
386     def _product_price(self, cr, uid, ids, name, arg, context=None):
387         res = {}
388         if context is None:
389             context = {}
390         quantity = context.get('quantity') or 1.0
391         pricelist = context.get('pricelist', False)
392         if pricelist:
393             for id in ids:
394                 try:
395                     price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, context=context)[pricelist]
396                 except:
397                     price = 0.0
398                 res[id] = price
399         for id in ids:
400             res.setdefault(id, 0.0)
401         return res
402
403     def _get_product_available_func(states, what):
404         def _product_available(self, cr, uid, ids, name, arg, context=None):
405             return {}.fromkeys(ids, 0.0)
406         return _product_available
407
408     _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
409     _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
410     _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
411     _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
412
413     def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
414         res = {}
415         product_uom_obj = self.pool.get('product.uom')
416         for id in ids:
417             res.setdefault(id, 0.0)
418         for product in self.browse(cr, uid, ids, context=context):
419             if 'uom' in context:
420                 uom = product.uos_id or product.uom_id
421                 res[product.id] = product_uom_obj._compute_price(cr, uid,
422                         uom.id, product.list_price, context['uom'])
423             else:
424                 res[product.id] = product.list_price
425             res[product.id] =  (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
426         return res
427
428     def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
429         for supinfo in product.seller_ids:
430             if supinfo.name.id == partner_id:
431                 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name, 'variants': ''}
432         res = {'code': product.default_code, 'name': product.name, 'variants': product.variants}
433         return res
434
435     def _product_code(self, cr, uid, ids, name, arg, context=None):
436         res = {}
437         if context is None:
438             context = {}
439         for p in self.browse(cr, uid, ids, context=context):
440             res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
441         return res
442
443     def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
444         res = {}
445         if context is None:
446             context = {}
447         for p in self.browse(cr, uid, ids, context=context):
448             data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
449             if not data['variants']:
450                 data['variants'] = p.variants
451             if not data['code']:
452                 data['code'] = p.code
453             if not data['name']:
454                 data['name'] = p.name
455             res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
456                     (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
457         return res
458
459     _defaults = {
460         'active': lambda *a: 1,
461         'price_extra': lambda *a: 0.0,
462         'price_margin': lambda *a: 1.0,
463         'color': 0,
464     }
465
466     _name = "product.product"
467     _description = "Product"
468     _table = "product_product"
469     _inherits = {'product.template': 'product_tmpl_id'}
470     _order = 'default_code,name_template'
471     _columns = {
472         'qty_available': fields.function(_product_qty_available, type='float', string='Quantity On Hand'),
473         'virtual_available': fields.function(_product_virtual_available, type='float', string='Quantity Available'),
474         'incoming_qty': fields.function(_product_incoming_qty, type='float', string='Incoming'),
475         'outgoing_qty': fields.function(_product_outgoing_qty, type='float', string='Outgoing'),
476         'price': fields.function(_product_price, type='float', string='Pricelist', digits_compute=dp.get_precision('Sale Price')),
477         'lst_price' : fields.function(_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Sale Price')),
478         'code': fields.function(_product_code, type='char', string='Reference'),
479         'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
480         'default_code' : fields.char('Reference', size=64),
481         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the product without removing it."),
482         'variants': fields.char('Variants', size=64),
483         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade"),
484         'ean13': fields.char('EAN13', size=13),
485         '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."),
486         'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Sale Price')),
487         'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Sale Price')),
488         'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
489         'name_template': fields.related('product_tmpl_id', 'name', string="Name", type='char', size=128, store=True),
490         'color': fields.integer('Color Index'),
491         'product_image': fields.binary('Image'),
492     }
493     
494     def unlink(self, cr, uid, ids, context=None):
495         unlink_ids = []
496         unlink_product_tmpl_ids = []
497         for product in self.browse(cr, uid, ids, context=context):
498             tmpl_id = product.product_tmpl_id.id
499             # Check if the product is last product of this template
500             other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
501             if not other_product_ids:
502                  unlink_product_tmpl_ids.append(tmpl_id)
503             unlink_ids.append(product.id)
504         self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
505         return super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
506
507     def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
508         if uom_id and uom_po_id:
509             uom_obj=self.pool.get('product.uom')
510             uom=uom_obj.browse(cursor,user,[uom_id])[0]
511             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
512             if uom.category_id.id != uom_po.category_id.id:
513                 return {'value': {'uom_po_id': uom_id}}
514         return False
515
516     def _check_ean_key(self, cr, uid, ids, context=None):
517         for product in self.browse(cr, uid, ids, context=context):
518             res = check_ean(product.ean13)
519         return res
520
521     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
522
523     def on_order(self, cr, uid, ids, orderline, quantity):
524         pass
525
526     def name_get(self, cr, user, ids, context=None):
527         if context is None:
528             context = {}
529         if not len(ids):
530             return []
531         def _name_get(d):
532             name = d.get('name','')
533             code = d.get('default_code',False)
534             if code:
535                 name = '[%s] %s' % (code,name)
536             if d.get('variants'):
537                 name = name + ' - %s' % (d['variants'],)
538             return (d['id'], name)
539
540         partner_id = context.get('partner_id', False)
541
542         result = []
543         for product in self.browse(cr, user, ids, context=context):
544             sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
545             if sellers:
546                 for s in sellers:
547                     mydict = {
548                               'id': product.id,
549                               'name': s.product_name or product.name,
550                               'default_code': s.product_code or product.default_code,
551                               'variants': product.variants
552                               }
553                     result.append(_name_get(mydict))
554             else:
555                 mydict = {
556                           'id': product.id,
557                           'name': product.name,
558                           'default_code': product.default_code,
559                           'variants': product.variants
560                           }
561                 result.append(_name_get(mydict))
562         return result
563
564     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
565         if not args:
566             args=[]
567         if name:
568             ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
569             if not len(ids):
570                 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
571             if not len(ids):
572                 ids = self.search(cr, user, [('default_code',operator,name)]+ args, limit=limit, context=context)
573                 ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
574             if not len(ids):
575                ptrn=re.compile('(\[(.*?)\])')
576                res = ptrn.search(name)
577                if res:
578                    ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
579         else:
580             ids = self.search(cr, user, args, limit=limit, context=context)
581         result = self.name_get(cr, user, ids, context=context)
582         return result
583
584     #
585     # Could be overrided for variants matrices prices
586     #
587     def price_get(self, cr, uid, ids, ptype='list_price', context=None):
588         if context is None:
589             context = {}
590
591         if 'currency_id' in context:
592             pricetype_obj = self.pool.get('product.price.type')
593             price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
594             price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
595
596         res = {}
597         product_uom_obj = self.pool.get('product.uom')
598         for product in self.browse(cr, uid, ids, context=context):
599             res[product.id] = product[ptype] or 0.0
600             if ptype == 'list_price':
601                 res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
602                         product.price_extra
603             if 'uom' in context:
604                 uom = product.uom_id or product.uos_id
605                 res[product.id] = product_uom_obj._compute_price(cr, uid,
606                         uom.id, res[product.id], context['uom'])
607             # Convert from price_type currency to asked one
608             if 'currency_id' in context:
609                 # Take the price_type currency from the product field
610                 # This is right cause a field cannot be in more than one currency
611                 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
612                     context['currency_id'], res[product.id],context=context)
613
614         return res
615
616     def copy(self, cr, uid, id, default=None, context=None):
617         if context is None:
618             context={}
619
620         product = self.read(cr, uid, id, ['name'], context=context)
621         if not default:
622             default = {}
623         default = default.copy()
624         default['name'] = product['name'] + _(' (copy)')
625
626         if context.get('variant',False):
627             fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
628                     'price_margin', 'price_extra']
629             data = self.read(cr, uid, id, fields=fields, context=context)
630             for f in fields:
631                 if f in default:
632                     data[f] = default[f]
633             data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
634                     and data['product_tmpl_id'][0]
635             del data['id']
636             return self.create(cr, uid, data)
637         else:
638             return super(product_product, self).copy(cr, uid, id, default=default,
639                     context=context)
640 product_product()
641
642 class product_packaging(osv.osv):
643     _name = "product.packaging"
644     _description = "Packaging"
645     _rec_name = 'ean'
646     _order = 'sequence'
647     _columns = {
648         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
649         'name' : fields.text('Description', size=64),
650         'qty' : fields.float('Quantity by Package',
651             help="The total number of products you can put by pallet or box."),
652         'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
653         'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
654         'rows' : fields.integer('Number of Layers', required=True,
655             help='The number of layers on a pallet or box'),
656         'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
657         'ean' : fields.char('EAN', size=14,
658             help="The EAN code of the package unit."),
659         'code' : fields.char('Code', size=14,
660             help="The code of the transport unit."),
661         'weight': fields.float('Total Package Weight',
662             help='The weight of a full package, pallet or box.'),
663         'weight_ul': fields.float('Empty Package Weight',
664             help='The weight of the empty UL'),
665         'height': fields.float('Height', help='The height of the package'),
666         'width': fields.float('Width', help='The width of the package'),
667         'length': fields.float('Length', help='The length of the package'),
668     }
669
670
671     def _check_ean_key(self, cr, uid, ids, context=None):
672         for pack in self.browse(cr, uid, ids, context=context):
673             res = check_ean(pack.ean)
674         return res
675
676     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
677
678     def name_get(self, cr, uid, ids, context=None):
679         if not len(ids):
680             return []
681         res = []
682         for pckg in self.browse(cr, uid, ids, context=context):
683             p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
684             p_name += pckg.ul.name
685             res.append((pckg.id,p_name))
686         return res
687
688     def _get_1st_ul(self, cr, uid, context=None):
689         cr.execute('select id from product_ul order by id asc limit 1')
690         res = cr.fetchone()
691         return (res and res[0]) or False
692
693     _defaults = {
694         'rows' : lambda *a : 3,
695         'sequence' : lambda *a : 1,
696         'ul' : _get_1st_ul,
697     }
698
699     def checksum(ean):
700         salt = '31' * 6 + '3'
701         sum = 0
702         for ean_part, salt_part in zip(ean, salt):
703             sum += int(ean_part) * int(salt_part)
704         return (10 - (sum % 10)) % 10
705     checksum = staticmethod(checksum)
706
707 product_packaging()
708
709
710 class product_supplierinfo(osv.osv):
711     _name = "product.supplierinfo"
712     _description = "Information about a product supplier"
713     def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
714         result = {}
715         product_uom_pool = self.pool.get('product.uom')
716         for supplier_info in self.browse(cr, uid, ids, context=context):
717             for field in fields:
718                 result[supplier_info.id] = {field:False}
719             qty = supplier_info.min_qty
720             result[supplier_info.id]['qty'] = qty
721         return result
722
723     _columns = {
724         'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
725         '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."),
726         '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."),
727         'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
728         '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."),
729         '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."),
730         'qty': fields.function(_calc_qty, store=True, type='float', string='Quantity', multi="qty", help="This is a quantity which is converted into Default Uom."),
731         'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
732         '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."),
733         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
734         'company_id':fields.many2one('res.company','Company',select=1),
735     }
736     _defaults = {
737         'qty': lambda *a: 0.0,
738         'sequence': lambda *a: 1,
739         'delay': lambda *a: 1,
740         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
741     }
742     def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
743         """
744         Calculate price from supplier pricelist.
745         @param supplier_ids: Ids of res.partner object.
746         @param product_id: Id of product.
747         @param product_qty: specify quantity to purchase.
748         """
749         if type(supplier_ids) in (int,long,):
750             supplier_ids = [supplier_ids]
751         res = {}
752         product_pool = self.pool.get('product.product')
753         partner_pool = self.pool.get('res.partner')
754         pricelist_pool = self.pool.get('product.pricelist')
755         currency_pool = self.pool.get('res.currency')
756         currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
757         for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
758             # Compute price from standard price of product
759             price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
760
761             # Compute price from Purchase pricelist of supplier
762             pricelist_id = supplier.property_product_pricelist_purchase.id
763             if pricelist_id:
764                 price = pricelist_pool.price_get(cr, uid, [pricelist_id], product_id, product_qty, context=context).setdefault(pricelist_id, 0)
765                 price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
766
767             # Compute price from supplier pricelist which are in Supplier Information
768             supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_id','=',product_id)])
769             if supplier_info_ids:
770                 cr.execute('SELECT * ' \
771                     'FROM pricelist_partnerinfo ' \
772                     'WHERE suppinfo_id IN %s' \
773                     'AND min_quantity <= %s ' \
774                     'ORDER BY min_quantity DESC LIMIT 1', (tuple(supplier_info_ids),product_qty,))
775                 res2 = cr.dictfetchone()
776                 if res2:
777                     price = res2['price']
778             res[supplier.id] = price
779         return res
780     _order = 'sequence'
781 product_supplierinfo()
782
783
784 class pricelist_partnerinfo(osv.osv):
785     _name = 'pricelist.partnerinfo'
786     _columns = {
787         'name': fields.char('Description', size=64),
788         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
789         '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."),
790         '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"),
791     }
792     _order = 'min_quantity asc'
793 pricelist_partnerinfo()
794 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: