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